added additional plugins
This commit is contained in:
parent
c85895d306
commit
00e60ec1b7
132 changed files with 27514 additions and 0 deletions
1
native/wordpress/learndash-start-button/.gitignore
vendored
Normal file
1
native/wordpress/learndash-start-button/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
.DS_Store
|
||||||
135
native/wordpress/learndash-start-button/LICENSE
Normal file
135
native/wordpress/learndash-start-button/LICENSE
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 2, June 1991
|
||||||
|
|
||||||
|
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
|
||||||
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The licenses for most software are designed to take away your
|
||||||
|
freedom to share and change it. By contrast, the GNU General Public
|
||||||
|
License is intended to guarantee your freedom to share and change free
|
||||||
|
software--to make sure the software is free for all its users. This
|
||||||
|
General Public License applies to most of the Free Software
|
||||||
|
Foundation's software and to any other program whose authors commit to
|
||||||
|
using it. (Some other Free Software Foundation software is covered by
|
||||||
|
the GNU Lesser General Public License instead.) You can apply it to
|
||||||
|
your programs, too.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
this service if you wish), that you receive source code or can get it
|
||||||
|
if you want it, that you can change the software or use pieces of it
|
||||||
|
in new free programs; and that you know you can do these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to make restrictions that forbid
|
||||||
|
anyone to deny you these rights or to ask you to surrender the rights.
|
||||||
|
These restrictions translate to certain responsibilities for you if
|
||||||
|
you distribute copies of the software, or if you modify it.
|
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether
|
||||||
|
gratis or for a fee, you must give the recipients all the rights that
|
||||||
|
you have. You must make sure that they, too, receive or can get the
|
||||||
|
source code. And you must show them these terms so they know their
|
||||||
|
rights.
|
||||||
|
|
||||||
|
We protect your rights with two steps: (1) copyright the software,
|
||||||
|
and (2) offer you this license which gives you legal permission to
|
||||||
|
copy, distribute and/or modify the software.
|
||||||
|
|
||||||
|
Also, for each author's protection and ours, we want to make certain
|
||||||
|
that everyone understands that there is no warranty for this free
|
||||||
|
software. If the software is modified by someone else and passed on,
|
||||||
|
we want its recipients to know that what they have is not the
|
||||||
|
original, so that any problems introduced by others will not reflect
|
||||||
|
on the original authors' reputations.
|
||||||
|
|
||||||
|
Finally, any free program is threatened constantly by software
|
||||||
|
patents. We wish to avoid the danger that redistributors of a free
|
||||||
|
program will individually obtain patent licenses, in effect making the
|
||||||
|
program proprietary. To prevent this, we have made it clear that any
|
||||||
|
patent must be licensed for everyone's free use or not licensed at
|
||||||
|
all.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||||
|
|
||||||
|
0. This License applies to any program or other work which contains
|
||||||
|
a notice placed by the copyright holder saying it may be distributed
|
||||||
|
under the terms of this General Public License. The "Program", below,
|
||||||
|
refers to any such program or work, and a "work based on the Program"
|
||||||
|
means either the Program or any derivative work under copyright law:
|
||||||
|
that is to say, a work containing the Program or a portion of it,
|
||||||
|
either verbatim or with modifications and/or translated into another
|
||||||
|
language. (Hereinafter, translation is included without limitation in
|
||||||
|
the term "modification".) Each licensee is addressed as "you".
|
||||||
|
|
||||||
|
[... full license continues unchanged until end ...]
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these
|
||||||
|
terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
convey the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation; either version 2 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program; if not, write to the Free Software
|
||||||
|
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If the program is interactive, make it output a short notice like this
|
||||||
|
when it starts in an interactive mode:
|
||||||
|
|
||||||
|
Gnomovision version 69, Copyright (C) year name of author
|
||||||
|
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
|
This is free software, and you are welcome to redistribute it
|
||||||
|
under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
|
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||||
|
parts of the General Public License. Of course, the commands you use may
|
||||||
|
be called something other than `show w' and `show c'; they could even be
|
||||||
|
mouse-clicks or menu items--whatever suits your program.
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or your
|
||||||
|
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||||
|
necessary. Here is a sample; alter the names:
|
||||||
|
|
||||||
|
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||||
|
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||||
|
|
||||||
|
<signature of Ty Coon>, 1 April 1989
|
||||||
|
Ty Coon, President of Vice
|
||||||
|
|
||||||
|
This General Public License does not permit incorporating your program into
|
||||||
|
proprietary programs. If your program is a subroutine library, you may
|
||||||
|
consider it more useful to permit linking proprietary applications with the
|
||||||
|
library. If this is what you want to do, use the GNU Lesser General
|
||||||
|
Public License instead of this License.
|
||||||
7
native/wordpress/learndash-start-button/README.md
Normal file
7
native/wordpress/learndash-start-button/README.md
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
# learndash-start-button
|
||||||
|
|
||||||
|
This is a plugin to add simple Start buttons to improve the learndash experience for the end user. This adds shortcodes and Gutenberg blocks to make placement easy. This is intended for Wordpress Gutenberg/FSE sites.
|
||||||
|
|
||||||
|
Intended usage: Add a "Start First Course" to group pages, and add a "Start Course Now" to course pages.
|
||||||
|
|
||||||
|
Why I made this: because users opened their learndash pages and were lost. I literally had users sitting staring at a screen unsure as to how to proceed. I have requested this functionality from Learndash time and again, so I decided to make it myself.
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
/**
|
||||||
|
* LearnDash Start Course/Group Buttons - Block Editor Styles
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Block wrapper in editor */
|
||||||
|
.wp-block-ldsb-start-button {
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px dashed #ddd;
|
||||||
|
background: #f9f9f9;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Prevent button interaction in editor */
|
||||||
|
.wp-block-ldsb-start-button .ldsb-start-button {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Selected block styling */
|
||||||
|
.wp-block-ldsb-start-button.is-selected {
|
||||||
|
border-color: #007cba;
|
||||||
|
background: #f0f8ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button preview in editor - import main styles */
|
||||||
|
.wp-block-ldsb-start-button .ldsb-button-wrapper {
|
||||||
|
margin: 10px 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-block-ldsb-start-button .ldsb-start-button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px 32px;
|
||||||
|
background-color: #2d7a2d;
|
||||||
|
color: #f6f6f6;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
box-shadow: 0 4px 12px rgba(45, 122, 45, 0.3);
|
||||||
|
border: none;
|
||||||
|
font-family:
|
||||||
|
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu,
|
||||||
|
Cantarell, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-block-ldsb-start-button .ldsb-button-text {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-block-ldsb-start-button .ldsb-button-arrow {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Block placeholder when no course selected */
|
||||||
|
.wp-block-ldsb-start-button.no-course::before {
|
||||||
|
content: "LearnDash Start Course Button: Configure course ID in block settings";
|
||||||
|
display: block;
|
||||||
|
color: #757575;
|
||||||
|
font-size: 13px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Alignment in editor preview */
|
||||||
|
.wp-block-ldsb-start-button.align-left .ldsb-button-wrapper {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-block-ldsb-start-button.align-center .ldsb-button-wrapper {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-block-ldsb-start-button.align-right .ldsb-button-wrapper {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
<?php
|
||||||
|
// Silence is golden.
|
||||||
173
native/wordpress/learndash-start-button/assets/css/style.css
Normal file
173
native/wordpress/learndash-start-button/assets/css/style.css
Normal file
|
|
@ -0,0 +1,173 @@
|
||||||
|
/**
|
||||||
|
* LearnDash Start Course/Group Buttons - Frontend Styles
|
||||||
|
*
|
||||||
|
* WCAG AAA Compliant Colors:
|
||||||
|
* Background: #2D7A2D (Brightest green that passes AAA with #f6f6f6)
|
||||||
|
* Text: #f6f6f6
|
||||||
|
* Contrast Ratio: 7:1
|
||||||
|
*/
|
||||||
|
|
||||||
|
.ldsb-button-wrapper {
|
||||||
|
margin: 20px 0;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ldsb-start-button {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px 32px;
|
||||||
|
background-color: #2d7a2d; /* Brightest green that passes WCAG AAA with #f6f6f6 */
|
||||||
|
color: #f6f6f6;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
box-shadow: 0 4px 12px rgba(45, 122, 45, 0.3);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family:
|
||||||
|
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu,
|
||||||
|
Cantarell, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ldsb-start-button:hover {
|
||||||
|
background-color: #245f24;
|
||||||
|
color: #ffffff;
|
||||||
|
box-shadow: 0 6px 20px rgba(45, 122, 45, 0.4);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ldsb-start-button:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
box-shadow: 0 2px 8px rgba(45, 122, 45, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ldsb-start-button:focus {
|
||||||
|
outline: 3px solid #5ea85e;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ldsb-button-text {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ldsb-button-arrow {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ldsb-start-button:hover .ldsb-button-arrow {
|
||||||
|
transform: translateX(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure proper visibility in different contexts */
|
||||||
|
.entry-content .ldsb-start-button,
|
||||||
|
.learndash-wrapper .ldsb-start-button {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Alignment options - using both text-align and flex for better compatibility */
|
||||||
|
.ldsb-button-wrapper.align-left {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ldsb-button-wrapper.align-center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ldsb-button-wrapper.align-center .ldsb-start-button {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ldsb-button-wrapper.align-right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ldsb-button-wrapper.align-right .ldsb-start-button {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Full width option */
|
||||||
|
.ldsb-button-wrapper.align-full .ldsb-start-button,
|
||||||
|
.ldsb-button-wrapper.full-width .ldsb-start-button {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Wide alignment */
|
||||||
|
.ldsb-button-wrapper.align-wide {
|
||||||
|
max-width: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ldsb-button-wrapper.align-wide .ldsb-start-button {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile responsive styles */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.ldsb-start-button {
|
||||||
|
padding: 14px 24px;
|
||||||
|
font-size: 16px;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ldsb-button-text {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ldsb-button-arrow {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tablet styles */
|
||||||
|
@media (min-width: 481px) and (max-width: 768px) {
|
||||||
|
.ldsb-start-button {
|
||||||
|
padding: 15px 28px;
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ldsb-button-text {
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* High contrast mode support */
|
||||||
|
@media (prefers-contrast: high) {
|
||||||
|
.ldsb-start-button {
|
||||||
|
border: 2px solid currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reduced motion support */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.ldsb-start-button,
|
||||||
|
.ldsb-button-arrow {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ldsb-start-button:hover {
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ldsb-start-button:hover .ldsb-button-arrow {
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
2
native/wordpress/learndash-start-button/assets/index.php
Normal file
2
native/wordpress/learndash-start-button/assets/index.php
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
<?php
|
||||||
|
// Silence is golden.
|
||||||
566
native/wordpress/learndash-start-button/assets/js/block.js
Normal file
566
native/wordpress/learndash-start-button/assets/js/block.js
Normal file
|
|
@ -0,0 +1,566 @@
|
||||||
|
/**
|
||||||
|
* LearnDash Start Course Button - Gutenberg Blocks
|
||||||
|
*
|
||||||
|
* Registers custom Gutenberg blocks for Start Course and Start Group buttons
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function (wp) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
// Destructure required WordPress packages
|
||||||
|
const { __ } = wp.i18n;
|
||||||
|
const { createElement: el } = wp.element;
|
||||||
|
const { registerBlockType } = wp.blocks;
|
||||||
|
const {
|
||||||
|
TextControl,
|
||||||
|
ToggleControl,
|
||||||
|
PanelBody,
|
||||||
|
PanelRow,
|
||||||
|
SelectControl,
|
||||||
|
Notice,
|
||||||
|
} = wp.components;
|
||||||
|
const { InspectorControls, BlockControls, AlignmentToolbar, useBlockProps } =
|
||||||
|
wp.blockEditor;
|
||||||
|
const { Fragment } = wp.element;
|
||||||
|
|
||||||
|
// SVG Arrow Icon
|
||||||
|
const arrowIcon = el(
|
||||||
|
"svg",
|
||||||
|
{
|
||||||
|
className: "ldsb-button-arrow",
|
||||||
|
width: "24",
|
||||||
|
height: "24",
|
||||||
|
viewBox: "0 0 24 24",
|
||||||
|
fill: "none",
|
||||||
|
xmlns: "http://www.w3.org/2000/svg",
|
||||||
|
},
|
||||||
|
el("path", {
|
||||||
|
d: "M5 12H19M19 12L12 5M19 12L12 19",
|
||||||
|
stroke: "currentColor",
|
||||||
|
strokeWidth: "2",
|
||||||
|
strokeLinecap: "round",
|
||||||
|
strokeLinejoin: "round",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Course block icon
|
||||||
|
const courseBlockIcon = el(
|
||||||
|
"svg",
|
||||||
|
{
|
||||||
|
width: "24",
|
||||||
|
height: "24",
|
||||||
|
viewBox: "0 0 24 24",
|
||||||
|
fill: "none",
|
||||||
|
},
|
||||||
|
el("path", {
|
||||||
|
d: "M13 7L18 12M18 12L13 17M18 12H6",
|
||||||
|
stroke: "currentColor",
|
||||||
|
strokeWidth: "2",
|
||||||
|
strokeLinecap: "round",
|
||||||
|
strokeLinejoin: "round",
|
||||||
|
}),
|
||||||
|
el("rect", {
|
||||||
|
x: "3",
|
||||||
|
y: "3",
|
||||||
|
width: "18",
|
||||||
|
height: "18",
|
||||||
|
rx: "2",
|
||||||
|
stroke: "currentColor",
|
||||||
|
strokeWidth: "2",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Group block icon
|
||||||
|
const groupBlockIcon = el(
|
||||||
|
"svg",
|
||||||
|
{
|
||||||
|
width: "24",
|
||||||
|
height: "24",
|
||||||
|
viewBox: "0 0 24 24",
|
||||||
|
fill: "none",
|
||||||
|
},
|
||||||
|
el("path", {
|
||||||
|
d: "M17 21V19C17 17.9391 16.5786 16.9217 15.8284 16.1716C15.0783 15.4214 14.0609 15 13 15H5C3.93913 15 2.92172 15.4214 2.17157 16.1716C1.42143 16.9217 1 17.9391 1 19V21",
|
||||||
|
stroke: "currentColor",
|
||||||
|
strokeWidth: "2",
|
||||||
|
strokeLinecap: "round",
|
||||||
|
strokeLinejoin: "round",
|
||||||
|
}),
|
||||||
|
el("circle", {
|
||||||
|
cx: "9",
|
||||||
|
cy: "7",
|
||||||
|
r: "4",
|
||||||
|
stroke: "currentColor",
|
||||||
|
strokeWidth: "2",
|
||||||
|
}),
|
||||||
|
el("path", {
|
||||||
|
d: "M23 21V19C22.9993 18.1137 22.7044 17.2528 22.1614 16.5523C21.6184 15.8519 20.8581 15.3516 20 15.13",
|
||||||
|
stroke: "currentColor",
|
||||||
|
strokeWidth: "2",
|
||||||
|
strokeLinecap: "round",
|
||||||
|
strokeLinejoin: "round",
|
||||||
|
}),
|
||||||
|
el("path", {
|
||||||
|
d: "M16 3.13C16.8604 3.35031 17.623 3.85071 18.1676 4.55232C18.7122 5.25392 19.0078 6.11683 19.0078 7.005C19.0078 7.89318 18.7122 8.75608 18.1676 9.45769C17.623 10.1593 16.8604 10.6597 16 10.88",
|
||||||
|
stroke: "currentColor",
|
||||||
|
strokeWidth: "2",
|
||||||
|
strokeLinecap: "round",
|
||||||
|
strokeLinejoin: "round",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Register the Course Start button block
|
||||||
|
registerBlockType("ldsb/start-button", {
|
||||||
|
title: __("LearnDash Start Course Button", "learndash-start-button"),
|
||||||
|
description: __(
|
||||||
|
"A prominent button to start LearnDash courses",
|
||||||
|
"learndash-start-button",
|
||||||
|
),
|
||||||
|
icon: courseBlockIcon,
|
||||||
|
category: "common",
|
||||||
|
keywords: [
|
||||||
|
__("learndash", "learndash-start-button"),
|
||||||
|
__("start", "learndash-start-button"),
|
||||||
|
__("course", "learndash-start-button"),
|
||||||
|
__("button", "learndash-start-button"),
|
||||||
|
],
|
||||||
|
attributes: {
|
||||||
|
courseId: {
|
||||||
|
type: "number",
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
buttonText: {
|
||||||
|
type: "string",
|
||||||
|
default: "Start Course",
|
||||||
|
},
|
||||||
|
newTab: {
|
||||||
|
type: "boolean",
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
alignment: {
|
||||||
|
type: "string",
|
||||||
|
default: "left",
|
||||||
|
enum: ["left", "center", "right"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
supports: {
|
||||||
|
align: false, // We handle alignment internally
|
||||||
|
className: true,
|
||||||
|
html: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Edit function - what you see in the editor
|
||||||
|
edit: function (props) {
|
||||||
|
const {
|
||||||
|
attributes: { courseId, buttonText, newTab, alignment },
|
||||||
|
setAttributes,
|
||||||
|
className,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const blockProps = useBlockProps({
|
||||||
|
className: `align-${alignment} ${courseId === 0 ? "no-course" : ""}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update alignment
|
||||||
|
function onChangeAlignment(newAlignment) {
|
||||||
|
setAttributes({ alignment: newAlignment || "left" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return el(
|
||||||
|
Fragment,
|
||||||
|
{},
|
||||||
|
// Block Controls (toolbar)
|
||||||
|
el(
|
||||||
|
BlockControls,
|
||||||
|
{},
|
||||||
|
el(AlignmentToolbar, {
|
||||||
|
value: alignment,
|
||||||
|
onChange: onChangeAlignment,
|
||||||
|
alignmentControls: [
|
||||||
|
{
|
||||||
|
icon: "editor-alignleft",
|
||||||
|
title: __("Align left", "learndash-start-button"),
|
||||||
|
align: "left",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "editor-aligncenter",
|
||||||
|
title: __("Align center", "learndash-start-button"),
|
||||||
|
align: "center",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "editor-alignright",
|
||||||
|
title: __("Align right", "learndash-start-button"),
|
||||||
|
align: "right",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Inspector Controls (sidebar)
|
||||||
|
el(
|
||||||
|
InspectorControls,
|
||||||
|
{},
|
||||||
|
el(
|
||||||
|
PanelBody,
|
||||||
|
{
|
||||||
|
title: __("Button Settings", "learndash-start-button"),
|
||||||
|
initialOpen: true,
|
||||||
|
},
|
||||||
|
el(
|
||||||
|
PanelRow,
|
||||||
|
{},
|
||||||
|
el(TextControl, {
|
||||||
|
label: __("Course ID", "learndash-start-button"),
|
||||||
|
help: __(
|
||||||
|
"Enter the LearnDash course ID. Leave empty to use current course.",
|
||||||
|
"learndash-start-button",
|
||||||
|
),
|
||||||
|
value: courseId || "",
|
||||||
|
onChange: function (value) {
|
||||||
|
var parsed = parseInt(value, 10);
|
||||||
|
setAttributes({ courseId: isNaN(parsed) || parsed < 0 ? 0 : parsed });
|
||||||
|
},
|
||||||
|
type: "number",
|
||||||
|
min: 0,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
el(
|
||||||
|
PanelRow,
|
||||||
|
{},
|
||||||
|
el(TextControl, {
|
||||||
|
label: __("Button Text", "learndash-start-button"),
|
||||||
|
help: __(
|
||||||
|
"The text to display on the button",
|
||||||
|
"learndash-start-button",
|
||||||
|
),
|
||||||
|
value: buttonText,
|
||||||
|
onChange: function (value) {
|
||||||
|
setAttributes({ buttonText: value || "Start Course" });
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
el(
|
||||||
|
PanelRow,
|
||||||
|
{},
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: __("Open in New Tab", "learndash-start-button"),
|
||||||
|
help: __(
|
||||||
|
"Open the course in a new browser tab",
|
||||||
|
"learndash-start-button",
|
||||||
|
),
|
||||||
|
checked: newTab,
|
||||||
|
onChange: function (value) {
|
||||||
|
setAttributes({ newTab: value });
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Usage instructions panel
|
||||||
|
el(
|
||||||
|
PanelBody,
|
||||||
|
{
|
||||||
|
title: __("Usage Instructions", "learndash-start-button"),
|
||||||
|
initialOpen: false,
|
||||||
|
},
|
||||||
|
el(
|
||||||
|
"div",
|
||||||
|
{ className: "ldsb-help-text" },
|
||||||
|
el(
|
||||||
|
"p",
|
||||||
|
{},
|
||||||
|
__("This button will automatically:", "learndash-start-button"),
|
||||||
|
),
|
||||||
|
el(
|
||||||
|
"ul",
|
||||||
|
{},
|
||||||
|
el(
|
||||||
|
"li",
|
||||||
|
{},
|
||||||
|
__(
|
||||||
|
"• Link to the first lesson if available",
|
||||||
|
"learndash-start-button",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
el(
|
||||||
|
"li",
|
||||||
|
{},
|
||||||
|
__(
|
||||||
|
'• Show "Login to Start" for logged-out users',
|
||||||
|
"learndash-start-button",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
el(
|
||||||
|
"li",
|
||||||
|
{},
|
||||||
|
__(
|
||||||
|
'• Show "Enroll to Start" for non-enrolled users',
|
||||||
|
"learndash-start-button",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Block Preview
|
||||||
|
el(
|
||||||
|
"div",
|
||||||
|
blockProps,
|
||||||
|
el(
|
||||||
|
"div",
|
||||||
|
{
|
||||||
|
className: `ldsb-button-wrapper align-${alignment}`,
|
||||||
|
style: { textAlign: alignment },
|
||||||
|
},
|
||||||
|
el(
|
||||||
|
"a",
|
||||||
|
{
|
||||||
|
className: "ldsb-start-button",
|
||||||
|
href: "#",
|
||||||
|
onClick: function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
el("span", { className: "ldsb-button-text" }, buttonText),
|
||||||
|
arrowIcon,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Save function - null because this is a dynamic block
|
||||||
|
save: function () {
|
||||||
|
return null; // Dynamic block rendered by PHP
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register the Group Start button block
|
||||||
|
registerBlockType("ldsb/start-group", {
|
||||||
|
title: __("LearnDash Start Group Work", "learndash-start-button"),
|
||||||
|
description: __(
|
||||||
|
"A button to start the first course in a LearnDash group",
|
||||||
|
"learndash-start-button",
|
||||||
|
),
|
||||||
|
icon: groupBlockIcon,
|
||||||
|
category: "common",
|
||||||
|
keywords: [
|
||||||
|
__("learndash", "learndash-start-button"),
|
||||||
|
__("group", "learndash-start-button"),
|
||||||
|
__("start", "learndash-start-button"),
|
||||||
|
__("button", "learndash-start-button"),
|
||||||
|
],
|
||||||
|
attributes: {
|
||||||
|
groupId: {
|
||||||
|
type: "number",
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
buttonText: {
|
||||||
|
type: "string",
|
||||||
|
default: "Start First Course",
|
||||||
|
},
|
||||||
|
newTab: {
|
||||||
|
type: "boolean",
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
alignment: {
|
||||||
|
type: "string",
|
||||||
|
default: "left",
|
||||||
|
enum: ["left", "center", "right"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
supports: {
|
||||||
|
align: false,
|
||||||
|
className: true,
|
||||||
|
html: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Edit function
|
||||||
|
edit: function (props) {
|
||||||
|
const {
|
||||||
|
attributes: { groupId, buttonText, newTab, alignment },
|
||||||
|
setAttributes,
|
||||||
|
className,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const blockProps = useBlockProps({
|
||||||
|
className: `align-${alignment} ${groupId === 0 ? "no-group" : ""}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
function onChangeAlignment(newAlignment) {
|
||||||
|
setAttributes({ alignment: newAlignment || "left" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return el(
|
||||||
|
Fragment,
|
||||||
|
{},
|
||||||
|
// Block Controls
|
||||||
|
el(
|
||||||
|
BlockControls,
|
||||||
|
{},
|
||||||
|
el(AlignmentToolbar, {
|
||||||
|
value: alignment,
|
||||||
|
onChange: onChangeAlignment,
|
||||||
|
alignmentControls: [
|
||||||
|
{
|
||||||
|
icon: "editor-alignleft",
|
||||||
|
title: __("Align left", "learndash-start-button"),
|
||||||
|
align: "left",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "editor-aligncenter",
|
||||||
|
title: __("Align center", "learndash-start-button"),
|
||||||
|
align: "center",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: "editor-alignright",
|
||||||
|
title: __("Align right", "learndash-start-button"),
|
||||||
|
align: "right",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Inspector Controls
|
||||||
|
el(
|
||||||
|
InspectorControls,
|
||||||
|
{},
|
||||||
|
el(
|
||||||
|
PanelBody,
|
||||||
|
{
|
||||||
|
title: __("Group Button Settings", "learndash-start-button"),
|
||||||
|
initialOpen: true,
|
||||||
|
},
|
||||||
|
el(
|
||||||
|
PanelRow,
|
||||||
|
{},
|
||||||
|
el(TextControl, {
|
||||||
|
label: __("Group ID", "learndash-start-button"),
|
||||||
|
help: __(
|
||||||
|
"Enter the LearnDash group ID. Leave empty to use current group.",
|
||||||
|
"learndash-start-button",
|
||||||
|
),
|
||||||
|
value: groupId || "",
|
||||||
|
onChange: function (value) {
|
||||||
|
var parsed = parseInt(value, 10);
|
||||||
|
setAttributes({ groupId: isNaN(parsed) || parsed < 0 ? 0 : parsed });
|
||||||
|
},
|
||||||
|
type: "number",
|
||||||
|
min: 0,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
el(
|
||||||
|
PanelRow,
|
||||||
|
{},
|
||||||
|
el(TextControl, {
|
||||||
|
label: __("Button Text", "learndash-start-button"),
|
||||||
|
help: __(
|
||||||
|
"The text to display on the button",
|
||||||
|
"learndash-start-button",
|
||||||
|
),
|
||||||
|
value: buttonText,
|
||||||
|
onChange: function (value) {
|
||||||
|
setAttributes({ buttonText: value || "Start First Course" });
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
el(
|
||||||
|
PanelRow,
|
||||||
|
{},
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: __("Open in New Tab", "learndash-start-button"),
|
||||||
|
help: __(
|
||||||
|
"Open the course in a new browser tab",
|
||||||
|
"learndash-start-button",
|
||||||
|
),
|
||||||
|
checked: newTab,
|
||||||
|
onChange: function (value) {
|
||||||
|
setAttributes({ newTab: value });
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
el(
|
||||||
|
PanelBody,
|
||||||
|
{
|
||||||
|
title: __("How It Works", "learndash-start-button"),
|
||||||
|
initialOpen: false,
|
||||||
|
},
|
||||||
|
el(
|
||||||
|
"div",
|
||||||
|
{ className: "ldsb-help-text" },
|
||||||
|
el("p", {}, __("This button will:", "learndash-start-button")),
|
||||||
|
el(
|
||||||
|
"ul",
|
||||||
|
{},
|
||||||
|
el(
|
||||||
|
"li",
|
||||||
|
{},
|
||||||
|
__(
|
||||||
|
"• Link to the first course in the group",
|
||||||
|
"learndash-start-button",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
el(
|
||||||
|
"li",
|
||||||
|
{},
|
||||||
|
__(
|
||||||
|
"• Go to the first lesson if available",
|
||||||
|
"learndash-start-button",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
el(
|
||||||
|
"li",
|
||||||
|
{},
|
||||||
|
__(
|
||||||
|
'• Show "Login to Start" for logged-out users',
|
||||||
|
"learndash-start-button",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
el(
|
||||||
|
"li",
|
||||||
|
{},
|
||||||
|
__(
|
||||||
|
'• Show "Join Group to Start" for non-members',
|
||||||
|
"learndash-start-button",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Block Preview
|
||||||
|
el(
|
||||||
|
"div",
|
||||||
|
blockProps,
|
||||||
|
el(
|
||||||
|
"div",
|
||||||
|
{
|
||||||
|
className: `ldsb-button-wrapper align-${alignment}`,
|
||||||
|
style: { textAlign: alignment },
|
||||||
|
},
|
||||||
|
el(
|
||||||
|
"a",
|
||||||
|
{
|
||||||
|
className: "ldsb-start-button ldsb-group-button",
|
||||||
|
href: "#",
|
||||||
|
onClick: function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
el("span", { className: "ldsb-button-text" }, buttonText),
|
||||||
|
arrowIcon,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
save: function () {
|
||||||
|
return null; // Dynamic block
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})(window.wp);
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
<?php
|
||||||
|
// Silence is golden.
|
||||||
2
native/wordpress/learndash-start-button/index.php
Normal file
2
native/wordpress/learndash-start-button/index.php
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
<?php
|
||||||
|
// Silence is golden.
|
||||||
|
|
@ -0,0 +1,677 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: LearnDash Start Course Button
|
||||||
|
* Plugin URI: https://sspmedia.ca/wordpress/
|
||||||
|
* Description: Adds prominent "Start Course" and "Start Group Work" buttons for LearnDash with shortcode and Gutenberg block support
|
||||||
|
* Version: 1.0.0
|
||||||
|
* Author: SSP Media
|
||||||
|
* License: GPL v2 or later
|
||||||
|
* Text Domain: learndash-start-button
|
||||||
|
* Domain Path: /languages
|
||||||
|
* Requires at least: 5.0
|
||||||
|
* Requires PHP: 7.2
|
||||||
|
* WC requires at least: 7.0
|
||||||
|
* WC tested up to: 9.0
|
||||||
|
*
|
||||||
|
* @package LearnDashStartCourseButton
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Prevent direct access - Wordfence compatible
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit; // Exit if accessed directly
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define plugin constants
|
||||||
|
if (!defined('LDSB_VERSION')) {
|
||||||
|
define('LDSB_VERSION', '1.0.0');
|
||||||
|
}
|
||||||
|
if (!defined('LDSB_PLUGIN_URL')) {
|
||||||
|
define('LDSB_PLUGIN_URL', plugin_dir_url(__FILE__));
|
||||||
|
}
|
||||||
|
if (!defined('LDSB_PLUGIN_PATH')) {
|
||||||
|
define('LDSB_PLUGIN_PATH', plugin_dir_path(__FILE__));
|
||||||
|
}
|
||||||
|
if (!defined('LDSB_PLUGIN_BASENAME')) {
|
||||||
|
define('LDSB_PLUGIN_BASENAME', plugin_basename(__FILE__));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check plugin dependencies
|
||||||
|
*
|
||||||
|
* @since 1.0.0
|
||||||
|
* @return bool True if dependencies are met, false otherwise
|
||||||
|
*/
|
||||||
|
function ldsb_check_dependencies() {
|
||||||
|
$deps_met = true;
|
||||||
|
$notices = array();
|
||||||
|
|
||||||
|
// Check for LearnDash
|
||||||
|
if (!defined('LEARNDASH_VERSION')) {
|
||||||
|
$deps_met = false;
|
||||||
|
$notices[] = __('LearnDash Start Course Button requires LearnDash LMS to be installed and activated.', 'learndash-start-button');
|
||||||
|
} elseif (version_compare(LEARNDASH_VERSION, '3.0', '<')) {
|
||||||
|
$deps_met = false;
|
||||||
|
$notices[] = __('LearnDash Start Course Button requires LearnDash version 3.0 or higher.', 'learndash-start-button');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display admin notices if dependencies not met
|
||||||
|
if (!$deps_met && is_admin() && !empty($notices)) {
|
||||||
|
add_action('admin_notices', function() use ($notices) {
|
||||||
|
foreach ($notices as $notice) {
|
||||||
|
echo '<div class="notice notice-error"><p>' . esc_html($notice) . '</p></div>';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
add_action('plugins_loaded', 'ldsb_check_dependencies');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Declare WooCommerce HPOS compatibility
|
||||||
|
*
|
||||||
|
* @since 1.0.0
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
add_action('before_woocommerce_init', function() {
|
||||||
|
if (class_exists('\Automattic\WooCommerce\Utilities\FeaturesUtil')) {
|
||||||
|
\Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility('custom_order_tables', __FILE__, true);
|
||||||
|
\Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility('cart_checkout_blocks', __FILE__, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enqueue plugin styles - Only on LearnDash pages
|
||||||
|
*
|
||||||
|
* @since 1.0.0
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
function ldsb_enqueue_styles() {
|
||||||
|
// Only load on singular pages or LearnDash archives
|
||||||
|
if (!is_singular() && !is_post_type_archive('sfwd-courses') && !is_post_type_archive('groups') && !is_tax('ld_course_category') && !is_tax('ld_course_tag') && !is_tax('ld_group_category') && !is_tax('ld_group_tag')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if current post is LearnDash content or has our shortcode
|
||||||
|
if (is_singular()) {
|
||||||
|
global $post;
|
||||||
|
|
||||||
|
// Validate post object exists
|
||||||
|
if (!$post || !is_object($post) || !isset($post->post_content)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check post type
|
||||||
|
$ld_post_types = array('sfwd-courses', 'sfwd-lessons', 'sfwd-topic', 'sfwd-quiz', 'groups');
|
||||||
|
$is_ld_content = in_array(get_post_type(), $ld_post_types);
|
||||||
|
|
||||||
|
// Check for shortcodes
|
||||||
|
$has_course_shortcode = has_shortcode($post->post_content, 'learndash_start_button');
|
||||||
|
$has_group_shortcode = has_shortcode($post->post_content, 'learndash_start_group');
|
||||||
|
|
||||||
|
// Check for blocks
|
||||||
|
$has_course_block = has_block('ldsb/start-button', $post);
|
||||||
|
$has_group_block = has_block('ldsb/start-group', $post);
|
||||||
|
|
||||||
|
if (!$is_ld_content && !$has_course_shortcode && !$has_group_shortcode && !$has_course_block && !$has_group_block) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_enqueue_style(
|
||||||
|
'ldsb-button-styles',
|
||||||
|
LDSB_PLUGIN_URL . 'assets/css/style.css',
|
||||||
|
array(),
|
||||||
|
LDSB_VERSION,
|
||||||
|
'all'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
add_action('wp_enqueue_scripts', 'ldsb_enqueue_styles');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get first lesson URL with compatibility checks
|
||||||
|
*
|
||||||
|
* @since 1.0.0
|
||||||
|
* @param int $course_id The course ID
|
||||||
|
* @return string|false The first lesson URL or course URL, false on failure
|
||||||
|
*/
|
||||||
|
function ldsb_get_first_lesson_url($course_id) {
|
||||||
|
$course_url = get_permalink($course_id);
|
||||||
|
|
||||||
|
// If we can't get the course URL, return false
|
||||||
|
if (!$course_url) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try new LearnDash 4.0+ method first
|
||||||
|
if (function_exists('learndash_course_get_children_of_step')) {
|
||||||
|
$lessons = learndash_course_get_children_of_step($course_id, $course_id, 'sfwd-lessons');
|
||||||
|
if (!empty($lessons)) {
|
||||||
|
$lesson_url = get_permalink($lessons[0]);
|
||||||
|
return $lesson_url ? $lesson_url : $course_url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to older method
|
||||||
|
if (function_exists('learndash_get_course_lessons_list')) {
|
||||||
|
$lessons = learndash_get_course_lessons_list($course_id);
|
||||||
|
if (!empty($lessons) && is_array($lessons)) {
|
||||||
|
$first_lesson = reset($lessons);
|
||||||
|
if (isset($first_lesson['post']->ID)) {
|
||||||
|
$lesson_url = get_permalink($first_lesson['post']->ID);
|
||||||
|
return $lesson_url ? $lesson_url : $course_url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $course_url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitize multiple HTML classes
|
||||||
|
*
|
||||||
|
* @since 1.0.0
|
||||||
|
* @param string $classes Space-separated list of class names
|
||||||
|
* @return string Sanitized space-separated list of class names
|
||||||
|
*/
|
||||||
|
function ldsb_sanitize_html_classes($classes) {
|
||||||
|
if (empty($classes)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split by spaces and sanitize each class individually
|
||||||
|
$classes_array = explode(' ', $classes);
|
||||||
|
$sanitized_classes = array();
|
||||||
|
|
||||||
|
foreach ($classes_array as $class) {
|
||||||
|
$class = trim($class);
|
||||||
|
if (!empty($class)) {
|
||||||
|
$sanitized = sanitize_html_class($class);
|
||||||
|
if (!empty($sanitized)) {
|
||||||
|
$sanitized_classes[] = $sanitized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode(' ', $sanitized_classes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check user access with compatibility
|
||||||
|
*
|
||||||
|
* @since 1.0.0
|
||||||
|
* @param int $course_id The course ID to check access for
|
||||||
|
* @param int|null $user_id The user ID to check, defaults to current user
|
||||||
|
* @return bool True if user has access, false otherwise
|
||||||
|
*/
|
||||||
|
function ldsb_check_user_access($course_id, $user_id = null) {
|
||||||
|
if (null === $user_id) {
|
||||||
|
$user_id = get_current_user_id();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try newer method first (LearnDash 3.0+)
|
||||||
|
if (function_exists('sfwd_lms_has_access')) {
|
||||||
|
return sfwd_lms_has_access($course_id, $user_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback for older versions
|
||||||
|
if (function_exists('ld_course_check_user_access')) {
|
||||||
|
return ld_course_check_user_access($course_id, $user_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last resort - check if user is logged in
|
||||||
|
return is_user_logged_in();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get first course URL from a group
|
||||||
|
*
|
||||||
|
* @since 1.0.0
|
||||||
|
* @param int $group_id The group ID
|
||||||
|
* @return string|false The first course/lesson URL or group URL, false on failure
|
||||||
|
*/
|
||||||
|
function ldsb_get_first_group_course_url($group_id) {
|
||||||
|
// Get group courses
|
||||||
|
$group_courses = array();
|
||||||
|
|
||||||
|
// Try LearnDash 3.2+ method
|
||||||
|
if (function_exists('learndash_group_enrolled_courses')) {
|
||||||
|
$group_courses = learndash_group_enrolled_courses($group_id);
|
||||||
|
} elseif (function_exists('learndash_get_group_enrolled_courses')) {
|
||||||
|
$group_courses = learndash_get_group_enrolled_courses($group_id);
|
||||||
|
} else {
|
||||||
|
// Fallback to direct meta query
|
||||||
|
$group_courses = get_post_meta($group_id, 'learndash_group_enrolled_courses', true);
|
||||||
|
if (!is_array($group_courses)) {
|
||||||
|
$group_courses = array();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have courses, get the first one
|
||||||
|
if (!empty($group_courses)) {
|
||||||
|
$first_course_id = is_array($group_courses) ? reset($group_courses) : $group_courses;
|
||||||
|
|
||||||
|
// Check if it's a valid course
|
||||||
|
if (get_post_type($first_course_id) === 'sfwd-courses') {
|
||||||
|
// Try to get first lesson of this course
|
||||||
|
$course_url = ldsb_get_first_lesson_url($first_course_id);
|
||||||
|
if ($course_url) {
|
||||||
|
return $course_url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to group URL
|
||||||
|
$group_url = get_permalink($group_id);
|
||||||
|
return $group_url ? $group_url : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register shortcode for the Start Group Work button
|
||||||
|
*
|
||||||
|
* @since 1.0.0
|
||||||
|
* @param array $atts Shortcode attributes
|
||||||
|
* @return string Button HTML or empty comment
|
||||||
|
*/
|
||||||
|
function ldsb_start_group_shortcode($atts) {
|
||||||
|
// Verify LearnDash is active
|
||||||
|
if (!defined('LEARNDASH_VERSION')) {
|
||||||
|
return '<!-- LearnDash not active -->';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse and sanitize attributes
|
||||||
|
$atts = shortcode_atts(array(
|
||||||
|
'group_id' => '',
|
||||||
|
'text' => __('Start First Course', 'learndash-start-button'),
|
||||||
|
'new_tab' => 'no',
|
||||||
|
'class' => '',
|
||||||
|
'alignment' => ''
|
||||||
|
), $atts, 'learndash_start_group');
|
||||||
|
|
||||||
|
// Sanitize inputs
|
||||||
|
$group_id = !empty($atts['group_id']) ? absint($atts['group_id']) : get_the_ID();
|
||||||
|
$button_text = sanitize_text_field($atts['text']);
|
||||||
|
$new_tab = in_array($atts['new_tab'], array('yes', 'true', '1'), true);
|
||||||
|
$custom_class = ldsb_sanitize_html_classes($atts['class']);
|
||||||
|
$alignment = !empty($atts['alignment']) ? sanitize_key($atts['alignment']) : '';
|
||||||
|
|
||||||
|
// Validate group exists and is correct post type
|
||||||
|
$group_post = get_post($group_id);
|
||||||
|
if (!$group_post || get_post_type($group_post) !== 'groups') {
|
||||||
|
return '<!-- Invalid group ID or not a group page -->';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the first course URL from the group
|
||||||
|
$course_url = ldsb_get_first_group_course_url($group_id);
|
||||||
|
|
||||||
|
// If we can't get a valid URL, return empty
|
||||||
|
if (!$course_url) {
|
||||||
|
return '<!-- Could not generate valid URL for group -->';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine button text - SIMPLIFIED VERSION
|
||||||
|
$access_text = esc_html($button_text); // Default text for everyone
|
||||||
|
|
||||||
|
// ONLY override for logged-out users
|
||||||
|
if (!is_user_logged_in()) {
|
||||||
|
$access_text = __('Login to Start', 'learndash-start-button');
|
||||||
|
$course_url = wp_login_url($course_url);
|
||||||
|
}
|
||||||
|
// Admins and all logged-in users see the default button text
|
||||||
|
// No "Join Group to Start" text anywhere!
|
||||||
|
|
||||||
|
// Build wrapper classes
|
||||||
|
$wrapper_classes = array('ldsb-button-wrapper');
|
||||||
|
if ($alignment) {
|
||||||
|
$wrapper_classes[] = 'align-' . $alignment;
|
||||||
|
}
|
||||||
|
if ($custom_class) {
|
||||||
|
$wrapper_classes[] = $custom_class;
|
||||||
|
}
|
||||||
|
$wrapper_class_string = implode(' ', $wrapper_classes);
|
||||||
|
|
||||||
|
// Build link attributes
|
||||||
|
$link_attrs = array(
|
||||||
|
'href' => esc_url($course_url),
|
||||||
|
'class' => 'ldsb-start-button ldsb-group-button',
|
||||||
|
'role' => 'button',
|
||||||
|
'data-group-id' => $group_id
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($new_tab) {
|
||||||
|
$link_attrs['target'] = '_blank';
|
||||||
|
$link_attrs['rel'] = 'noopener noreferrer';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build attributes string
|
||||||
|
$attrs_string = '';
|
||||||
|
foreach ($link_attrs as $key => $value) {
|
||||||
|
$attrs_string .= sprintf(' %s="%s"', $key, esc_attr($value));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the button HTML
|
||||||
|
$button_html = sprintf(
|
||||||
|
'<div class="%s">
|
||||||
|
<a%s>
|
||||||
|
<span class="ldsb-button-text">%s</span>
|
||||||
|
<svg class="ldsb-button-arrow" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||||
|
<path d="M5 12H19M19 12L12 5M19 12L12 19" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>',
|
||||||
|
esc_attr($wrapper_class_string),
|
||||||
|
$attrs_string,
|
||||||
|
$access_text
|
||||||
|
);
|
||||||
|
|
||||||
|
// Allow filtering of output
|
||||||
|
return apply_filters('ldsb_group_button_html', $button_html, $atts);
|
||||||
|
}
|
||||||
|
add_shortcode('learndash_start_group', 'ldsb_start_group_shortcode');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register shortcode for the Start Now button
|
||||||
|
*
|
||||||
|
* @since 1.0.0
|
||||||
|
* @param array $atts Shortcode attributes
|
||||||
|
* @return string Button HTML or empty comment
|
||||||
|
*/
|
||||||
|
function ldsb_start_button_shortcode($atts) {
|
||||||
|
// Verify LearnDash is active
|
||||||
|
if (!defined('LEARNDASH_VERSION')) {
|
||||||
|
return '<!-- LearnDash not active -->';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse and sanitize attributes
|
||||||
|
$atts = shortcode_atts(array(
|
||||||
|
'course_id' => '',
|
||||||
|
'text' => __('Start Course', 'learndash-start-button'),
|
||||||
|
'new_tab' => 'no',
|
||||||
|
'class' => '',
|
||||||
|
'alignment' => ''
|
||||||
|
), $atts, 'learndash_start_button');
|
||||||
|
|
||||||
|
// Sanitize inputs
|
||||||
|
$course_id = !empty($atts['course_id']) ? absint($atts['course_id']) : get_the_ID();
|
||||||
|
$button_text = sanitize_text_field($atts['text']);
|
||||||
|
$new_tab = in_array($atts['new_tab'], array('yes', 'true', '1'), true);
|
||||||
|
$custom_class = ldsb_sanitize_html_classes($atts['class']);
|
||||||
|
$alignment = !empty($atts['alignment']) ? sanitize_key($atts['alignment']) : '';
|
||||||
|
|
||||||
|
// Validate course exists and get post type
|
||||||
|
$course_post = get_post($course_id);
|
||||||
|
if (!$course_post) {
|
||||||
|
return '<!-- Invalid course ID -->';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get proper course ID if we're on a lesson/topic
|
||||||
|
$course_post_type = get_post_type($course_post);
|
||||||
|
if ($course_post_type !== 'sfwd-courses') {
|
||||||
|
if (function_exists('learndash_get_course_id')) {
|
||||||
|
$course_id = learndash_get_course_id($course_id);
|
||||||
|
if (!$course_id) {
|
||||||
|
return '<!-- No associated course found -->';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the course URL
|
||||||
|
$course_url = ldsb_get_first_lesson_url($course_id);
|
||||||
|
|
||||||
|
// If we can't get a valid URL, return empty
|
||||||
|
if (!$course_url) {
|
||||||
|
return '<!-- Could not generate valid URL for course -->';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check user access
|
||||||
|
$user_id = get_current_user_id();
|
||||||
|
$has_access = ldsb_check_user_access($course_id, $user_id);
|
||||||
|
$access_text = esc_html($button_text);
|
||||||
|
|
||||||
|
if (!$has_access && !is_user_logged_in()) {
|
||||||
|
$access_text = __('Login to Start', 'learndash-start-button');
|
||||||
|
$course_url = wp_login_url($course_url);
|
||||||
|
} elseif (!$has_access) {
|
||||||
|
$access_text = __('Enroll to Start', 'learndash-start-button');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build wrapper classes
|
||||||
|
$wrapper_classes = array('ldsb-button-wrapper');
|
||||||
|
if ($alignment) {
|
||||||
|
$wrapper_classes[] = 'align-' . $alignment;
|
||||||
|
}
|
||||||
|
if ($custom_class) {
|
||||||
|
$wrapper_classes[] = $custom_class;
|
||||||
|
}
|
||||||
|
$wrapper_class_string = implode(' ', $wrapper_classes);
|
||||||
|
|
||||||
|
// Build link attributes
|
||||||
|
$link_attrs = array(
|
||||||
|
'href' => esc_url($course_url),
|
||||||
|
'class' => 'ldsb-start-button',
|
||||||
|
'role' => 'button',
|
||||||
|
'data-course-id' => $course_id
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($new_tab) {
|
||||||
|
$link_attrs['target'] = '_blank';
|
||||||
|
$link_attrs['rel'] = 'noopener noreferrer';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build attributes string
|
||||||
|
$attrs_string = '';
|
||||||
|
foreach ($link_attrs as $key => $value) {
|
||||||
|
$attrs_string .= sprintf(' %s="%s"', $key, esc_attr($value));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the button HTML
|
||||||
|
$button_html = sprintf(
|
||||||
|
'<div class="%s">
|
||||||
|
<a%s>
|
||||||
|
<span class="ldsb-button-text">%s</span>
|
||||||
|
<svg class="ldsb-button-arrow" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||||
|
<path d="M5 12H19M19 12L12 5M19 12L12 19" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>',
|
||||||
|
esc_attr($wrapper_class_string),
|
||||||
|
$attrs_string,
|
||||||
|
$access_text
|
||||||
|
);
|
||||||
|
|
||||||
|
// Allow filtering of output
|
||||||
|
return apply_filters('ldsb_button_html', $button_html, $atts);
|
||||||
|
}
|
||||||
|
add_shortcode('learndash_start_button', 'ldsb_start_button_shortcode');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register Gutenberg blocks
|
||||||
|
*
|
||||||
|
* @since 1.0.0
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
function ldsb_register_blocks() {
|
||||||
|
// Check if Gutenberg is available
|
||||||
|
if (!function_exists('register_block_type')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register block editor script
|
||||||
|
wp_register_script(
|
||||||
|
'ldsb-block-editor',
|
||||||
|
LDSB_PLUGIN_URL . 'assets/js/block.js',
|
||||||
|
array('wp-blocks', 'wp-element', 'wp-editor', 'wp-components', 'wp-data', 'wp-i18n'),
|
||||||
|
LDSB_VERSION,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add translation support
|
||||||
|
wp_set_script_translations('ldsb-block-editor', 'learndash-start-button');
|
||||||
|
|
||||||
|
// Register block editor styles
|
||||||
|
wp_register_style(
|
||||||
|
'ldsb-block-editor-style',
|
||||||
|
LDSB_PLUGIN_URL . 'assets/css/editor.css',
|
||||||
|
array('wp-edit-blocks'),
|
||||||
|
LDSB_VERSION
|
||||||
|
);
|
||||||
|
|
||||||
|
// Register the Course Start button block
|
||||||
|
register_block_type('ldsb/start-button', array(
|
||||||
|
'editor_script' => 'ldsb-block-editor',
|
||||||
|
'editor_style' => 'ldsb-block-editor-style',
|
||||||
|
'render_callback' => 'ldsb_render_block',
|
||||||
|
'attributes' => array(
|
||||||
|
'courseId' => array(
|
||||||
|
'type' => 'number',
|
||||||
|
'default' => 0
|
||||||
|
),
|
||||||
|
'buttonText' => array(
|
||||||
|
'type' => 'string',
|
||||||
|
'default' => __('Start Course', 'learndash-start-button')
|
||||||
|
),
|
||||||
|
'newTab' => array(
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => false
|
||||||
|
),
|
||||||
|
'alignment' => array(
|
||||||
|
'type' => 'string',
|
||||||
|
'default' => 'left',
|
||||||
|
'enum' => array('left', 'center', 'right')
|
||||||
|
)
|
||||||
|
),
|
||||||
|
'supports' => array(
|
||||||
|
'align' => false, // We handle alignment internally
|
||||||
|
'className' => true,
|
||||||
|
'html' => false
|
||||||
|
)
|
||||||
|
));
|
||||||
|
|
||||||
|
// Register the Group Start button block
|
||||||
|
register_block_type('ldsb/start-group', array(
|
||||||
|
'editor_script' => 'ldsb-block-editor',
|
||||||
|
'editor_style' => 'ldsb-block-editor-style',
|
||||||
|
'render_callback' => 'ldsb_render_group_block',
|
||||||
|
'attributes' => array(
|
||||||
|
'groupId' => array(
|
||||||
|
'type' => 'number',
|
||||||
|
'default' => 0
|
||||||
|
),
|
||||||
|
'buttonText' => array(
|
||||||
|
'type' => 'string',
|
||||||
|
'default' => __('Start First Course', 'learndash-start-button')
|
||||||
|
),
|
||||||
|
'newTab' => array(
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => false
|
||||||
|
),
|
||||||
|
'alignment' => array(
|
||||||
|
'type' => 'string',
|
||||||
|
'default' => 'left',
|
||||||
|
'enum' => array('left', 'center', 'right')
|
||||||
|
)
|
||||||
|
),
|
||||||
|
'supports' => array(
|
||||||
|
'align' => false,
|
||||||
|
'className' => true,
|
||||||
|
'html' => false
|
||||||
|
)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
add_action('init', 'ldsb_register_blocks');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render block callback
|
||||||
|
*
|
||||||
|
* @since 1.0.0
|
||||||
|
* @param array $attributes Block attributes
|
||||||
|
* @return string Block HTML output
|
||||||
|
*/
|
||||||
|
function ldsb_render_block($attributes) {
|
||||||
|
// Sanitize attributes
|
||||||
|
$course_id = isset($attributes['courseId']) ? absint($attributes['courseId']) : 0;
|
||||||
|
$button_text = isset($attributes['buttonText']) ? sanitize_text_field($attributes['buttonText']) : __('Start Course', 'learndash-start-button');
|
||||||
|
$new_tab = isset($attributes['newTab']) && $attributes['newTab'] ? 'yes' : 'no';
|
||||||
|
$alignment = isset($attributes['alignment']) ? sanitize_key($attributes['alignment']) : 'left';
|
||||||
|
|
||||||
|
// Pass alignment to shortcode
|
||||||
|
return ldsb_start_button_shortcode(array(
|
||||||
|
'course_id' => $course_id,
|
||||||
|
'text' => $button_text,
|
||||||
|
'new_tab' => $new_tab,
|
||||||
|
'alignment' => $alignment
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render group block callback
|
||||||
|
*
|
||||||
|
* @since 1.0.0
|
||||||
|
* @param array $attributes Block attributes
|
||||||
|
* @return string Block HTML output
|
||||||
|
*/
|
||||||
|
function ldsb_render_group_block($attributes) {
|
||||||
|
// Sanitize attributes
|
||||||
|
$group_id = isset($attributes['groupId']) ? absint($attributes['groupId']) : 0;
|
||||||
|
$button_text = isset($attributes['buttonText']) ? sanitize_text_field($attributes['buttonText']) : __('Start First Course', 'learndash-start-button');
|
||||||
|
$new_tab = isset($attributes['newTab']) && $attributes['newTab'] ? 'yes' : 'no';
|
||||||
|
$alignment = isset($attributes['alignment']) ? sanitize_key($attributes['alignment']) : 'left';
|
||||||
|
|
||||||
|
// Pass to group shortcode
|
||||||
|
return ldsb_start_group_shortcode(array(
|
||||||
|
'group_id' => $group_id,
|
||||||
|
'text' => $button_text,
|
||||||
|
'new_tab' => $new_tab,
|
||||||
|
'alignment' => $alignment
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin activation hook
|
||||||
|
*
|
||||||
|
* @since 1.0.0
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
function ldsb_activate() {
|
||||||
|
// Check minimum requirements
|
||||||
|
if (version_compare(PHP_VERSION, '7.2', '<')) {
|
||||||
|
deactivate_plugins(LDSB_PLUGIN_BASENAME);
|
||||||
|
wp_die(__('This plugin requires PHP 7.2 or higher.', 'learndash-start-button'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set default options
|
||||||
|
add_option('ldsb_version', LDSB_VERSION);
|
||||||
|
|
||||||
|
// Note: flush_rewrite_rules() removed - this plugin doesn't register custom post types or rewrite rules
|
||||||
|
}
|
||||||
|
register_activation_hook(__FILE__, 'ldsb_activate');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin deactivation hook
|
||||||
|
*
|
||||||
|
* @since 1.0.0
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
function ldsb_deactivate() {
|
||||||
|
// Note: No cleanup needed on deactivation
|
||||||
|
// flush_rewrite_rules() should NOT be called on deactivation per WordPress best practices
|
||||||
|
}
|
||||||
|
register_deactivation_hook(__FILE__, 'ldsb_deactivate');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load plugin textdomain
|
||||||
|
*
|
||||||
|
* @since 1.0.0
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
function ldsb_load_textdomain() {
|
||||||
|
load_plugin_textdomain(
|
||||||
|
'learndash-start-button',
|
||||||
|
false,
|
||||||
|
dirname(LDSB_PLUGIN_BASENAME) . '/languages'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
add_action('plugins_loaded', 'ldsb_load_textdomain');
|
||||||
|
|
||||||
|
// End of file - no closing PHP tag to prevent whitespace issues
|
||||||
32
native/wordpress/learndash-start-button/uninstall.php
Normal file
32
native/wordpress/learndash-start-button/uninstall.php
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* LearnDash Start Now Button - Uninstall
|
||||||
|
*
|
||||||
|
* Handles plugin cleanup when deleted through WordPress admin
|
||||||
|
*
|
||||||
|
* @package LearnDashStartButton
|
||||||
|
* @since 1.0.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
// If uninstall not called from WordPress, exit
|
||||||
|
if (!defined('WP_UNINSTALL_PLUGIN')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up options
|
||||||
|
delete_option('ldsb_version');
|
||||||
|
|
||||||
|
// Clean up any transients (if you add any in the future)
|
||||||
|
delete_transient('ldsb_courses_cache');
|
||||||
|
|
||||||
|
// Clean up user meta if any (currently none)
|
||||||
|
// delete_metadata('user', 0, 'ldsb_user_preference', '', true);
|
||||||
|
|
||||||
|
// Clean up post meta if any (currently none)
|
||||||
|
// delete_post_meta_by_key('_ldsb_button_override');
|
||||||
|
|
||||||
|
// Flush rewrite rules
|
||||||
|
flush_rewrite_rules();
|
||||||
|
|
||||||
|
// Note: We don't delete the block from posts as that could break content
|
||||||
|
// The blocks will gracefully degrade to showing nothing if plugin is removed
|
||||||
112
native/wordpress/maple-code-blocks/.htaccess
Normal file
112
native/wordpress/maple-code-blocks/.htaccess
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
# GitHub Code Viewer Plugin - Security Rules
|
||||||
|
|
||||||
|
# Prevent directory browsing
|
||||||
|
Options -Indexes
|
||||||
|
|
||||||
|
# Deny access to all files by default
|
||||||
|
<FilesMatch ".*">
|
||||||
|
Order Deny,Allow
|
||||||
|
Deny from all
|
||||||
|
</FilesMatch>
|
||||||
|
|
||||||
|
# Allow access to specific file types only
|
||||||
|
<FilesMatch "\.(css|js|png|jpg|jpeg|gif|svg|woff|woff2|ttf|eot)$">
|
||||||
|
Order Allow,Deny
|
||||||
|
Allow from all
|
||||||
|
</FilesMatch>
|
||||||
|
|
||||||
|
# Specifically allow access to the main plugin file
|
||||||
|
<Files "maple-code-blocks.php">
|
||||||
|
Order Allow,Deny
|
||||||
|
Allow from all
|
||||||
|
</Files>
|
||||||
|
|
||||||
|
# Protect sensitive files
|
||||||
|
<FilesMatch "(^\.|wp-config\.php|\.htaccess|\.htpasswd|error_log|readme\.html|license\.txt|install\.php|php\.ini|php5\.ini)">
|
||||||
|
Order Allow,Deny
|
||||||
|
Deny from all
|
||||||
|
</FilesMatch>
|
||||||
|
|
||||||
|
# Disable PHP execution in subdirectories (except the root plugin file)
|
||||||
|
<FilesMatch "\.php$">
|
||||||
|
<If "%{REQUEST_URI} !~ m#^.*/maple-code-blocks/maple-code-blocks\.php$#">
|
||||||
|
Order Deny,Allow
|
||||||
|
Deny from all
|
||||||
|
</If>
|
||||||
|
</FilesMatch>
|
||||||
|
|
||||||
|
# Prevent script injection
|
||||||
|
<IfModule mod_rewrite.c>
|
||||||
|
RewriteEngine On
|
||||||
|
RewriteBase /
|
||||||
|
RewriteCond %{QUERY_STRING} (<|%3C).*script.*(>|%3E) [NC,OR]
|
||||||
|
RewriteCond %{QUERY_STRING} GLOBALS(=|[|%[0-9A-Z]{0,2}) [OR]
|
||||||
|
RewriteCond %{QUERY_STRING} _REQUEST(=|[|%[0-9A-Z]{0,2})
|
||||||
|
RewriteRule ^(.*)$ - [F,L]
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
# Disable XML-RPC if not needed
|
||||||
|
<Files xmlrpc.php>
|
||||||
|
Order Deny,Allow
|
||||||
|
Deny from all
|
||||||
|
</Files>
|
||||||
|
|
||||||
|
# Add security headers
|
||||||
|
<IfModule mod_headers.c>
|
||||||
|
Header set X-Content-Type-Options "nosniff"
|
||||||
|
Header set X-Frame-Options "SAMEORIGIN"
|
||||||
|
Header set X-XSS-Protection "1; mode=block"
|
||||||
|
Header set Referrer-Policy "strict-origin-when-cross-origin"
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
# Hotlinking protection disabled - not needed for WordPress plugins
|
||||||
|
# WordPress plugins need their assets accessible to the host site
|
||||||
|
# <IfModule mod_rewrite.c>
|
||||||
|
# RewriteEngine on
|
||||||
|
# RewriteCond %{HTTP_REFERER} !^$
|
||||||
|
# RewriteCond %{HTTP_REFERER} !^https?://(www\.)?%{HTTP_HOST} [NC]
|
||||||
|
# RewriteRule \.(css|js|png|jpg|jpeg|gif|svg)$ - [F,NC,L]
|
||||||
|
# </IfModule>
|
||||||
|
|
||||||
|
# Compress text files
|
||||||
|
<IfModule mod_deflate.c>
|
||||||
|
AddOutputFilterByType DEFLATE text/plain
|
||||||
|
AddOutputFilterByType DEFLATE text/html
|
||||||
|
AddOutputFilterByType DEFLATE text/css
|
||||||
|
AddOutputFilterByType DEFLATE application/javascript
|
||||||
|
AddOutputFilterByType DEFLATE application/json
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
# Set proper MIME types
|
||||||
|
<IfModule mod_mime.c>
|
||||||
|
AddType text/css .css
|
||||||
|
AddType application/javascript .js
|
||||||
|
AddType application/json .json
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
# Cache control for static assets
|
||||||
|
<IfModule mod_expires.c>
|
||||||
|
ExpiresActive On
|
||||||
|
ExpiresByType text/css "access plus 1 month"
|
||||||
|
ExpiresByType application/javascript "access plus 1 month"
|
||||||
|
ExpiresByType image/png "access plus 1 month"
|
||||||
|
ExpiresByType image/jpg "access plus 1 month"
|
||||||
|
ExpiresByType image/jpeg "access plus 1 month"
|
||||||
|
ExpiresByType image/gif "access plus 1 month"
|
||||||
|
ExpiresByType image/svg+xml "access plus 1 month"
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
|
# Disable server signature
|
||||||
|
ServerSignature Off
|
||||||
|
|
||||||
|
# Prevent access to hidden files
|
||||||
|
<FilesMatch "^\.">
|
||||||
|
Order Allow,Deny
|
||||||
|
Deny from all
|
||||||
|
</FilesMatch>
|
||||||
|
|
||||||
|
# Block access to backup and source files
|
||||||
|
<FilesMatch "(\.(bak|backup|config|dist|fla|inc|ini|log|psd|sh|sql|sw[op])|~)$">
|
||||||
|
Order Allow,Deny
|
||||||
|
Deny from all
|
||||||
|
</FilesMatch>
|
||||||
117
native/wordpress/maple-code-blocks/LICENSE
Normal file
117
native/wordpress/maple-code-blocks/LICENSE
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 2, June 1991
|
||||||
|
|
||||||
|
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
|
||||||
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
|
||||||
|
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it.
|
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights.
|
||||||
|
|
||||||
|
We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software.
|
||||||
|
|
||||||
|
Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations.
|
||||||
|
|
||||||
|
Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||||
|
|
||||||
|
0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you".
|
||||||
|
|
||||||
|
Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does.
|
||||||
|
|
||||||
|
1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee.
|
||||||
|
|
||||||
|
2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change.
|
||||||
|
|
||||||
|
b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License.
|
||||||
|
|
||||||
|
c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.)
|
||||||
|
|
||||||
|
These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it.
|
||||||
|
|
||||||
|
Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program.
|
||||||
|
|
||||||
|
In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License.
|
||||||
|
|
||||||
|
3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following:
|
||||||
|
|
||||||
|
a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or,
|
||||||
|
|
||||||
|
b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or,
|
||||||
|
|
||||||
|
c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.)
|
||||||
|
|
||||||
|
The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable.
|
||||||
|
|
||||||
|
If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code.
|
||||||
|
|
||||||
|
4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance.
|
||||||
|
|
||||||
|
5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it.
|
||||||
|
|
||||||
|
6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License.
|
||||||
|
|
||||||
|
7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program.
|
||||||
|
|
||||||
|
If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances.
|
||||||
|
|
||||||
|
It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice.
|
||||||
|
|
||||||
|
This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License.
|
||||||
|
|
||||||
|
8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License.
|
||||||
|
|
||||||
|
9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation.
|
||||||
|
|
||||||
|
10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally.
|
||||||
|
|
||||||
|
NO WARRANTY
|
||||||
|
|
||||||
|
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
one line to give the program's name and an idea of what it does. Copyright (C) yyyy name of author
|
||||||
|
|
||||||
|
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If the program is interactive, make it output a short notice like this when it starts in an interactive mode:
|
||||||
|
|
||||||
|
Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
|
The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program.
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names:
|
||||||
|
|
||||||
|
Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||||
|
|
||||||
|
signature of Ty Coon, 1 April 1989 Ty Coon, President of Vice
|
||||||
336
native/wordpress/maple-code-blocks/README.md
Normal file
336
native/wordpress/maple-code-blocks/README.md
Normal file
|
|
@ -0,0 +1,336 @@
|
||||||
|
# Maple Code Blocks WordPress Plugin
|
||||||
|
|
||||||
|
A beautiful and secure WordPress plugin that displays code files from GitHub repositories in a terminal/IDE-style interface with syntax highlighting.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
✨ **Beautiful Interface**
|
||||||
|
- Terminal/IDE-style code viewer
|
||||||
|
- Dark and light themes
|
||||||
|
- Syntax highlighting for 30+ languages
|
||||||
|
- Line numbers
|
||||||
|
- Tabbed interface for multiple files
|
||||||
|
|
||||||
|
🔒 **Security First**
|
||||||
|
- All code is HTML-escaped (no XSS risk)
|
||||||
|
- Code is displayed as text only, never executed
|
||||||
|
- Multiple layers of content sanitization
|
||||||
|
- Binary files automatically filtered
|
||||||
|
- File size limits to prevent performance issues
|
||||||
|
|
||||||
|
🚀 **Functionality**
|
||||||
|
- File browser with search
|
||||||
|
- Copy code to clipboard
|
||||||
|
- Fullscreen mode
|
||||||
|
- Repository caching for performance
|
||||||
|
- Responsive design
|
||||||
|
- AJAX-powered for smooth experience
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Upload the `maple-code-blocks` folder to `/wp-content/plugins/`
|
||||||
|
2. Activate the plugin through the 'Plugins' menu in WordPress
|
||||||
|
3. Configure settings in Settings > Maple Code Blocks
|
||||||
|
|
||||||
|
# Maple Code Blocks - WordPress Plugin
|
||||||
|
|
||||||
|
A beautiful and secure WordPress plugin by **SSP Media** that displays code files from **GitHub, GitLab, Bitbucket, and Codeberg** repositories in a terminal/IDE-style interface with syntax highlighting. Now with **full Gutenberg block editor support**!
|
||||||
|
|
||||||
|
## About
|
||||||
|
|
||||||
|
**Maple Code Blocks** is developed and maintained by [SSP Media](https://sspmedia.ca/wordpress/), a Canadian web development agency specializing in WordPress solutions.
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
|
||||||
|
✅ **Public Access** - Visitors can view code without logging in (configurable)
|
||||||
|
✅ **Multi-Platform** - Supports GitHub, GitLab, Bitbucket, and Codeberg
|
||||||
|
✅ **Beautiful Themes** - Dark, Light, Monokai, Solarized
|
||||||
|
✅ **Gutenberg Ready** - Full block editor support
|
||||||
|
✅ **Secure** - Rate limiting, XSS protection, OWASP compliant
|
||||||
|
|
||||||
|
### Public Viewing Configuration
|
||||||
|
|
||||||
|
By default, all visitors can view public repository code. To restrict viewing to logged-in users only:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Add to your theme's functions.php
|
||||||
|
add_filter('mcb_require_login_for_viewing', '__return_true');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Supported Formats
|
||||||
|
|
||||||
|
### All Platforms Support These Formats:
|
||||||
|
|
||||||
|
#### 1. GitHub (default platform)
|
||||||
|
```
|
||||||
|
[maple_code_block repo="facebook/react"]
|
||||||
|
[maple_code_block repo="github:facebook/react"]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. GitLab
|
||||||
|
```
|
||||||
|
[maple_code_block repo="gitlab:gitlab-org/gitlab"]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Bitbucket
|
||||||
|
```
|
||||||
|
[maple_code_block repo="bitbucket:atlassian/python-bitbucket"]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. Codeberg
|
||||||
|
```
|
||||||
|
[maple_code_block repo="codeberg:forgejo/forgejo"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Format Guide:
|
||||||
|
- **Simple format:** `owner/repo` (defaults to GitHub)
|
||||||
|
- **Platform prefix:** `platform:owner/repo`
|
||||||
|
- **Platforms:** `github`, `gitlab`, `bitbucket`, `codeberg`
|
||||||
|
|
||||||
|
### Full Examples with Options:
|
||||||
|
|
||||||
|
✅ **GitHub** - github.com
|
||||||
|
✅ **GitLab** - gitlab.com
|
||||||
|
✅ **Bitbucket** - bitbucket.org
|
||||||
|
✅ **Codeberg** - codeberg.org
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
✨ **Beautiful Interface**
|
||||||
|
- Terminal/IDE-style code viewer
|
||||||
|
- Dark and light themes
|
||||||
|
- Syntax highlighting for 30+ languages
|
||||||
|
- Line numbers
|
||||||
|
- Tabbed interface for multiple files
|
||||||
|
- **NEW: Gutenberg block with live preview**
|
||||||
|
- **NEW: Block variations and patterns**
|
||||||
|
|
||||||
|
🔒 **Security First**
|
||||||
|
- All code is HTML-escaped (no XSS risk)
|
||||||
|
- Code is displayed as text only, never executed
|
||||||
|
- Multiple layers of content sanitization
|
||||||
|
- Binary files automatically filtered
|
||||||
|
- File size limits to prevent performance issues
|
||||||
|
|
||||||
|
🚀 **Functionality**
|
||||||
|
- File browser with search
|
||||||
|
- Copy code to clipboard
|
||||||
|
- Fullscreen mode
|
||||||
|
- Repository caching for performance
|
||||||
|
- Responsive design
|
||||||
|
- AJAX-powered for smooth experience
|
||||||
|
- **NEW: Visual block editor**
|
||||||
|
- **NEW: Pre-built block patterns**
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Upload the `maple-code-blocks` folder to `/wp-content/plugins/`
|
||||||
|
2. Activate the plugin through the 'Plugins' menu in WordPress
|
||||||
|
3. Configure settings in Settings > Maple Code Blocks
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Method 1: Gutenberg Block (Recommended)
|
||||||
|
|
||||||
|
#### Using the Block Editor
|
||||||
|
|
||||||
|
1. In the WordPress block editor, click the "+" button to add a new block
|
||||||
|
2. Search for "Maple Code Blocks"
|
||||||
|
3. Select the block from the "Maple Code Blocks" category
|
||||||
|
4. Configure the repository and settings in the block sidebar
|
||||||
|
5. Preview your code viewer in real-time
|
||||||
|
|
||||||
|
#### Block Variations
|
||||||
|
|
||||||
|
The plugin includes pre-configured block variations:
|
||||||
|
- **React Component**: Display React components
|
||||||
|
- **Documentation Viewer**: Perfect for README files
|
||||||
|
- **Full Repository Browser**: Browse entire repositories
|
||||||
|
- **Code Snippet**: Display specific code files
|
||||||
|
- **Tutorial Code**: Ideal for educational content
|
||||||
|
|
||||||
|
#### Block Patterns
|
||||||
|
|
||||||
|
Ready-to-use layouts available in the Pattern Library:
|
||||||
|
- **Code with Explanation**: Code blocks with explanatory text
|
||||||
|
- **Side-by-Side Comparison**: Compare two implementations
|
||||||
|
- **Code Gallery**: Showcase multiple examples
|
||||||
|
- **Featured Code**: Highlight important implementations
|
||||||
|
- **Tutorial Steps**: Step-by-step code tutorials
|
||||||
|
|
||||||
|
#### Block Settings
|
||||||
|
|
||||||
|
Configure directly in the block editor sidebar:
|
||||||
|
- **Repository**: Enter the GitHub repository (owner/repository)
|
||||||
|
- **Theme**: Choose from Dark, Light, Monokai, or Solarized
|
||||||
|
- **Height**: Set custom height (px, %, vh, em, rem)
|
||||||
|
- **Line Numbers**: Toggle line numbers on/off
|
||||||
|
- **Initial File**: Select a file to display on load
|
||||||
|
- **Title**: Add an optional title
|
||||||
|
- **Alignment**: Support for wide and full width
|
||||||
|
|
||||||
|
### Method 2: Classic Shortcode
|
||||||
|
|
||||||
|
#### Basic Shortcode
|
||||||
|
|
||||||
|
```
|
||||||
|
[maple_code_block repo="owner/repository"]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Platform-Specific Examples
|
||||||
|
|
||||||
|
**GitHub (default):**
|
||||||
|
```
|
||||||
|
[maple_code_block repo="facebook/react"]
|
||||||
|
```
|
||||||
|
|
||||||
|
**GitLab:**
|
||||||
|
```
|
||||||
|
[maple_code_block repo="gitlab:gitlab-org/gitlab"]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Bitbucket:**
|
||||||
|
```
|
||||||
|
[maple_code_block repo="bitbucket:atlassian/python-bitbucket"]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Codeberg:**
|
||||||
|
```
|
||||||
|
[maple_code_block repo="codeberg:forgejo/forgejo"]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Using Full URLs:**
|
||||||
|
```
|
||||||
|
[maple_code_block repo="https://gitlab.com/fdroid/fdroidclient"]
|
||||||
|
[maple_code_block repo="https://bitbucket.org/mailchimp/mandrill-api-php"]
|
||||||
|
[maple_code_block repo="https://codeberg.org/forgejo/forgejo"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Advanced Options
|
||||||
|
|
||||||
|
```
|
||||||
|
[github_code_viewer
|
||||||
|
repo="facebook/react"
|
||||||
|
theme="dark"
|
||||||
|
height="500px"
|
||||||
|
show_line_numbers="true"
|
||||||
|
initial_file="README.md"
|
||||||
|
title="React Source Code"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Parameter | Description | Default |
|
||||||
|
|-----------|-------------|---------|
|
||||||
|
| `repo` | GitHub repository (owner/name) | Required |
|
||||||
|
| `theme` | Color theme (dark/light/monokai/solarized) | dark |
|
||||||
|
| `height` | Viewer height | 600px |
|
||||||
|
| `show_line_numbers` | Show line numbers (true/false) | true |
|
||||||
|
| `initial_file` | File to load initially | none |
|
||||||
|
| `title` | Title above viewer | none |
|
||||||
|
|
||||||
|
## Security Features
|
||||||
|
|
||||||
|
### Multiple Layers of Protection
|
||||||
|
|
||||||
|
1. **Server-side Escaping**: All content is HTML-escaped using PHP's `htmlspecialchars()`
|
||||||
|
2. **JavaScript Context Escaping**: Additional escaping for JavaScript contexts
|
||||||
|
3. **Content Type Validation**: Binary files are automatically detected and rejected
|
||||||
|
4. **Size Limits**: Files over 1MB are not displayed
|
||||||
|
5. **Pattern Matching**: Dangerous patterns are replaced with safe alternatives
|
||||||
|
6. **Safe Rendering**: Code is inserted as text content, never as HTML
|
||||||
|
|
||||||
|
### What This Means
|
||||||
|
|
||||||
|
- ✅ JavaScript code is displayed but NEVER executed
|
||||||
|
- ✅ HTML tags are shown as text, not rendered
|
||||||
|
- ✅ No possibility of XSS attacks
|
||||||
|
- ✅ Safe to display any code from any public repository
|
||||||
|
- ✅ WordPress site and database remain completely secure
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### GitHub Personal Access Token (Optional)
|
||||||
|
|
||||||
|
For public repositories, no token is required. However, GitHub limits unauthenticated requests to 60 per hour. To increase this limit:
|
||||||
|
|
||||||
|
1. Go to Settings > Maple Code Blocks
|
||||||
|
2. [Generate a GitHub token](https://github.com/settings/tokens) (no special permissions needed)
|
||||||
|
3. Enter the token and save
|
||||||
|
|
||||||
|
### Cache Settings
|
||||||
|
|
||||||
|
Files are cached for 1 hour by default to improve performance. You can adjust this in the settings.
|
||||||
|
|
||||||
|
## Supported Languages
|
||||||
|
|
||||||
|
The plugin includes syntax highlighting for:
|
||||||
|
|
||||||
|
- JavaScript, TypeScript, JSX, TSX
|
||||||
|
- Python, Ruby, PHP
|
||||||
|
- Java, C, C++, C#
|
||||||
|
- Go, Rust, Swift, Kotlin
|
||||||
|
- HTML, CSS, SCSS, SASS, LESS
|
||||||
|
- JSON, XML, YAML
|
||||||
|
- SQL, Bash, Markdown
|
||||||
|
- And many more...
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
- **Caching**: Repository contents are cached to reduce API calls
|
||||||
|
- **Lazy Loading**: Files are loaded on-demand
|
||||||
|
- **Optimized Rendering**: Syntax highlighting is applied efficiently
|
||||||
|
- **Responsive**: Works smoothly on all devices
|
||||||
|
|
||||||
|
## Styling
|
||||||
|
|
||||||
|
The plugin includes four built-in themes:
|
||||||
|
|
||||||
|
1. **Dark** - VS Code inspired dark theme
|
||||||
|
2. **Light** - Clean light theme
|
||||||
|
3. **Monokai** - Popular Sublime Text theme
|
||||||
|
4. **Solarized** - Eye-friendly color scheme
|
||||||
|
|
||||||
|
## Browser Compatibility
|
||||||
|
|
||||||
|
- Chrome/Edge (latest)
|
||||||
|
- Firefox (latest)
|
||||||
|
- Safari (latest)
|
||||||
|
- Opera (latest)
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- WordPress 5.0 or higher
|
||||||
|
- PHP 7.2 or higher
|
||||||
|
- JavaScript enabled in browser
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues, feature requests, or questions, please contact the plugin author or submit a support ticket.
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For support, documentation, and updates, visit [SSP Media WordPress Plugins](https://sspmedia.ca/wordpress/).
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
GPL v2 or later
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Maple Code Blocks** is proudly developed by [SSP Media](https://sspmedia.ca/wordpress/) 🍁
|
||||||
|
|
||||||
|
## Changelog
|
||||||
|
|
||||||
|
### Version 1.0.0
|
||||||
|
- Initial release
|
||||||
|
- Core functionality
|
||||||
|
- Security features
|
||||||
|
- Four themes
|
||||||
|
- Syntax highlighting for 30+ languages
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
- Syntax highlighting powered by a custom lightweight Prism.js implementation
|
||||||
|
- Icons from various open-source icon sets
|
||||||
|
- Inspired by VS Code and other modern code editors
|
||||||
2
native/wordpress/maple-code-blocks/admin/index.php
Normal file
2
native/wordpress/maple-code-blocks/admin/index.php
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
<?php
|
||||||
|
// Silence is golden.
|
||||||
280
native/wordpress/maple-code-blocks/admin/settings-page.php
Normal file
280
native/wordpress/maple-code-blocks/admin/settings-page.php
Normal file
|
|
@ -0,0 +1,280 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Admin Settings Page
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Prevent direct access
|
||||||
|
if (!defined('WPINC')) {
|
||||||
|
die('Direct access not permitted.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional security check
|
||||||
|
if (!function_exists('current_user_can')) {
|
||||||
|
die('WordPress environment not loaded.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check user capabilities
|
||||||
|
if (!current_user_can('manage_options')) {
|
||||||
|
wp_die('You do not have sufficient permissions to access this page.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save settings
|
||||||
|
if (isset($_POST['mcb_save_settings'])) {
|
||||||
|
// Verify nonce
|
||||||
|
if (!isset($_POST['_wpnonce']) || !wp_verify_nonce($_POST['_wpnonce'], 'mcb_settings_nonce')) {
|
||||||
|
wp_die('Security check failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Double-check capabilities
|
||||||
|
if (!current_user_can('manage_options')) {
|
||||||
|
wp_die('Insufficient permissions');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save platform tokens
|
||||||
|
$platforms = array('github', 'gitlab', 'bitbucket', 'codeberg');
|
||||||
|
foreach ($platforms as $platform) {
|
||||||
|
$token_field = 'mcb_' . $platform . '_token';
|
||||||
|
$token = sanitize_text_field($_POST[$token_field] ?? '');
|
||||||
|
update_option($token_field, $token);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save cache duration
|
||||||
|
$cache_duration = absint($_POST['mcb_cache_duration']);
|
||||||
|
update_option('mcb_cache_duration', $cache_duration);
|
||||||
|
|
||||||
|
// Save default theme
|
||||||
|
$default_theme = sanitize_text_field($_POST['mcb_default_theme']);
|
||||||
|
update_option('mcb_default_theme', $default_theme);
|
||||||
|
|
||||||
|
echo '<div class="notice notice-success"><p>Settings saved successfully!</p></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current settings
|
||||||
|
$github_token = get_option('mcb_github_token', '');
|
||||||
|
$gitlab_token = get_option('mcb_gitlab_token', '');
|
||||||
|
$bitbucket_token = get_option('mcb_bitbucket_token', '');
|
||||||
|
$codeberg_token = get_option('mcb_codeberg_token', '');
|
||||||
|
$cache_duration = get_option('mcb_cache_duration', 3600);
|
||||||
|
$default_theme = get_option('mcb_default_theme', 'dark');
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="wrap">
|
||||||
|
<h1>Maple Code Blocks Settings</h1>
|
||||||
|
<p>Configure your Maple Code Blocks plugin settings below. Developed by <a href="https://sspmedia.ca/wordpress/" target="_blank">SSP Media</a>.</p>
|
||||||
|
|
||||||
|
<form method="post" action="">
|
||||||
|
<?php wp_nonce_field('mcb_settings_nonce'); ?>
|
||||||
|
|
||||||
|
<table class="form-table">
|
||||||
|
<tr>
|
||||||
|
<th colspan="2">
|
||||||
|
<h2>Platform API Tokens</h2>
|
||||||
|
<p>Optional: Add personal access tokens to increase API rate limits for each platform.</p>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="mcb_github_token">GitHub Token</label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="text" id="mcb_github_token" name="mcb_github_token"
|
||||||
|
value="<?php echo esc_attr($github_token); ?>" class="regular-text" />
|
||||||
|
<p class="description">
|
||||||
|
<a href="https://github.com/settings/tokens" target="_blank">Generate GitHub token</a>
|
||||||
|
(no special permissions needed for public repos)
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="mcb_gitlab_token">GitLab Token</label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="text" id="mcb_gitlab_token" name="mcb_gitlab_token"
|
||||||
|
value="<?php echo esc_attr($gitlab_token); ?>" class="regular-text" />
|
||||||
|
<p class="description">
|
||||||
|
<a href="https://gitlab.com/-/profile/personal_access_tokens" target="_blank">Generate GitLab token</a>
|
||||||
|
(read_api scope for public repos)
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="mcb_bitbucket_token">Bitbucket App Password</label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="text" id="mcb_bitbucket_token" name="mcb_bitbucket_token"
|
||||||
|
value="<?php echo esc_attr($bitbucket_token); ?>" class="regular-text" />
|
||||||
|
<p class="description">
|
||||||
|
<a href="https://bitbucket.org/account/settings/app-passwords/" target="_blank">Generate Bitbucket app password</a>
|
||||||
|
(repository read permission)
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="mcb_codeberg_token">Codeberg Token</label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="text" id="mcb_codeberg_token" name="mcb_codeberg_token"
|
||||||
|
value="<?php echo esc_attr($codeberg_token); ?>" class="regular-text" />
|
||||||
|
<p class="description">
|
||||||
|
<a href="https://codeberg.org/user/settings/applications" target="_blank">Generate Codeberg token</a>
|
||||||
|
(read:repository scope)
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<th colspan="2">
|
||||||
|
<h2>General Settings</h2>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="mcb_cache_duration">Cache Duration</label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<input type="number" id="mcb_cache_duration" name="mcb_cache_duration"
|
||||||
|
value="<?php echo esc_attr($cache_duration); ?>" min="0" /> seconds
|
||||||
|
<p class="description">
|
||||||
|
How long to cache repository files (default: 3600 seconds = 1 hour).<br>
|
||||||
|
Set to 0 to disable caching.
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<th scope="row">
|
||||||
|
<label for="mcb_default_theme">Default Theme</label>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
<select id="mcb_default_theme" name="mcb_default_theme">
|
||||||
|
<option value="dark" <?php selected($default_theme, 'dark'); ?>>Dark</option>
|
||||||
|
<option value="light" <?php selected($default_theme, 'light'); ?>>Light</option>
|
||||||
|
<option value="monokai" <?php selected($default_theme, 'monokai'); ?>>Monokai</option>
|
||||||
|
<option value="solarized" <?php selected($default_theme, 'solarized'); ?>>Solarized</option>
|
||||||
|
</select>
|
||||||
|
<p class="description">
|
||||||
|
Default theme for the code viewer. Can be overridden per shortcode.
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p class="submit">
|
||||||
|
<input type="submit" name="mcb_save_settings" class="button-primary" value="Save Settings" />
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<h2>Usage Instructions</h2>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>Supported Platforms</h3>
|
||||||
|
<p>This plugin supports repositories from:</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>GitHub</strong> - github.com</li>
|
||||||
|
<li><strong>GitLab</strong> - gitlab.com</li>
|
||||||
|
<li><strong>Bitbucket</strong> - bitbucket.org</li>
|
||||||
|
<li><strong>Codeberg</strong> - codeberg.org</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Repository Formats</h3>
|
||||||
|
<p>You can specify repositories in multiple ways:</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Default (GitHub):</strong> <code>owner/repository</code></li>
|
||||||
|
<li><strong>With platform prefix:</strong> <code>gitlab:owner/repository</code></li>
|
||||||
|
<li><strong>Full URL:</strong> <code>https://gitlab.com/owner/repository</code></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Basic Usage</h3>
|
||||||
|
<p>Use the following shortcode to display repository code:</p>
|
||||||
|
<code>[maple_code_block repo="owner/repository"]</code>
|
||||||
|
|
||||||
|
<h3>Platform Examples</h3>
|
||||||
|
<ul>
|
||||||
|
<li><strong>GitHub:</strong> <code>[maple_code_block repo="facebook/react"]</code></li>
|
||||||
|
<li><strong>GitLab:</strong> <code>[maple_code_block repo="gitlab:gitlab-org/gitlab"]</code></li>
|
||||||
|
<li><strong>Bitbucket:</strong> <code>[maple_code_block repo="bitbucket:atlassian/python-bitbucket"]</code></li>
|
||||||
|
<li><strong>Codeberg:</strong> <code>[maple_code_block repo="codeberg:forgejo/forgejo"]</code></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Shortcode Parameters</h3>
|
||||||
|
<ul>
|
||||||
|
<li><strong>repo</strong> - (required) The GitHub repository in format "owner/repository"</li>
|
||||||
|
<li><strong>theme</strong> - Theme: dark, light, monokai, or solarized (default: dark)</li>
|
||||||
|
<li><strong>height</strong> - Height of the viewer (default: 600px)</li>
|
||||||
|
<li><strong>show_line_numbers</strong> - Show line numbers: true or false (default: true)</li>
|
||||||
|
<li><strong>initial_file</strong> - Path to file to load initially</li>
|
||||||
|
<li><strong>title</strong> - Optional title to display above the viewer</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Example</h3>
|
||||||
|
<code>[github_code_viewer repo="facebook/react" theme="dark" height="500px" initial_file="README.md" title="React Source Code"]</code>
|
||||||
|
|
||||||
|
<h3>Security Features</h3>
|
||||||
|
<ul>
|
||||||
|
<li>✅ All code is HTML-escaped to prevent XSS attacks</li>
|
||||||
|
<li>✅ JavaScript code is displayed as text only, never executed</li>
|
||||||
|
<li>✅ Binary files are automatically filtered out</li>
|
||||||
|
<li>✅ File size limits prevent loading huge files</li>
|
||||||
|
<li>✅ Content is sanitized multiple times before display</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<h3>About</h3>
|
||||||
|
<p>
|
||||||
|
<strong>GitHub Code Viewer</strong> Version 1.0.0<br>
|
||||||
|
This plugin displays code from GitHub repositories in a beautiful, safe terminal/IDE-style interface.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Features:</strong>
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>Beautiful syntax highlighting with Prism.js</li>
|
||||||
|
<li>Terminal/IDE-style interface</li>
|
||||||
|
<li>File browser with search</li>
|
||||||
|
<li>Tabbed interface for multiple files</li>
|
||||||
|
<li>Copy code functionality</li>
|
||||||
|
<li>Line numbers</li>
|
||||||
|
<li>Multiple themes</li>
|
||||||
|
<li>Fullscreen mode</li>
|
||||||
|
<li>Responsive design</li>
|
||||||
|
<li>Safe display (no code execution)</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.card {
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #ccd0d4;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 20px 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.card h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
.card code {
|
||||||
|
display: block;
|
||||||
|
padding: 10px;
|
||||||
|
background: #f1f1f1;
|
||||||
|
border-radius: 3px;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
292
native/wordpress/maple-code-blocks/assets/css/block-editor.css
Normal file
292
native/wordpress/maple-code-blocks/assets/css/block-editor.css
Normal file
|
|
@ -0,0 +1,292 @@
|
||||||
|
/**
|
||||||
|
* GitHub Code Viewer Block Editor Styles
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Block Editor Wrapper */
|
||||||
|
.mcb-block-editor {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Block Preview */
|
||||||
|
.mcb-block-preview {
|
||||||
|
min-height: 200px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-block-preview:hover {
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Preview Header */
|
||||||
|
.mcb-preview-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-preview-header svg {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Preview Info Grid */
|
||||||
|
.mcb-preview-info {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 15px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-preview-info > div {
|
||||||
|
padding: 5px 10px;
|
||||||
|
background: rgba(0, 0, 0, 0.05);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-styles-wrapper .mcb-block-preview.theme-dark .mcb-preview-info > div {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Inspector Controls Customization */
|
||||||
|
.mcb-height-control {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-height-control label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Popular Repos Buttons */
|
||||||
|
.mcb-popular-repos {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 5px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Validation States */
|
||||||
|
.mcb-validation-success {
|
||||||
|
color: #00a32a;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-validation-error {
|
||||||
|
color: #cc1818;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading State */
|
||||||
|
.mcb-loading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-loading .components-spinner {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Placeholder Styles */
|
||||||
|
.wp-block-maple-code-blocks-code-viewer .components-placeholder {
|
||||||
|
min-height: 200px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-block-maple-code-blocks-code-viewer .components-placeholder .components-placeholder__label {
|
||||||
|
font-size: 18px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-block-maple-code-blocks-code-viewer .components-placeholder .components-placeholder__instructions {
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Quick Start Section */
|
||||||
|
.mcb-quick-start {
|
||||||
|
margin-top: 15px;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-quick-start strong {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-quick-start .components-button {
|
||||||
|
margin: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Block Alignment Support */
|
||||||
|
.wp-block-maple-code-blocks-code-viewer.alignwide {
|
||||||
|
max-width: 1280px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-block-maple-code-blocks-code-viewer.alignfull {
|
||||||
|
max-width: none;
|
||||||
|
margin-left: calc(50% - 50vw);
|
||||||
|
margin-right: calc(50% - 50vw);
|
||||||
|
width: 100vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Panel Body Customization */
|
||||||
|
.components-panel__body .mcb-repo-input {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.components-panel__body .mcb-theme-preview {
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-top: 10px;
|
||||||
|
font-family: 'Monaco', 'Menlo', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.components-panel__body .mcb-theme-preview.dark {
|
||||||
|
background: #1e1e1e;
|
||||||
|
color: #d4d4d4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.components-panel__body .mcb-theme-preview.light {
|
||||||
|
background: #ffffff;
|
||||||
|
color: #333333;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* External Link Style */
|
||||||
|
.components-external-link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toolbar Buttons */
|
||||||
|
.block-editor-block-toolbar .mcb-toolbar-group {
|
||||||
|
border-right: 1px solid #e0e0e0;
|
||||||
|
padding-right: 6px;
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Notice Improvements */
|
||||||
|
.components-notice.mcb-notice {
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.components-notice.mcb-notice .components-notice__content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Selected State */
|
||||||
|
.wp-block-maple-code-blocks-code-viewer.is-selected .mcb-block-preview {
|
||||||
|
box-shadow: 0 0 0 1px #007cba;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Help Text */
|
||||||
|
.components-base-control__help {
|
||||||
|
font-size: 12px;
|
||||||
|
font-style: italic;
|
||||||
|
color: #757575;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* File Selector */
|
||||||
|
.mcb-file-selector {
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 5px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-file-selector .mcb-file-option {
|
||||||
|
padding: 5px 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-file-selector .mcb-file-option:hover {
|
||||||
|
background: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-file-selector .mcb-file-option.selected {
|
||||||
|
background: #007cba;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design */
|
||||||
|
@media (max-width: 782px) {
|
||||||
|
.mcb-preview-info {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-quick-start .components-button {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark Theme Support for Editor */
|
||||||
|
.editor-styles-wrapper .mcb-block-preview[data-theme="dark"] {
|
||||||
|
background: #1e1e1e;
|
||||||
|
color: #d4d4d4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-styles-wrapper .mcb-block-preview[data-theme="light"] {
|
||||||
|
background: #ffffff;
|
||||||
|
color: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animation for validation */
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-validating {
|
||||||
|
animation: pulse 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom scrollbar for file list */
|
||||||
|
.mcb-file-selector::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-file-selector::-webkit-scrollbar-track {
|
||||||
|
background: #f1f1f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-file-selector::-webkit-scrollbar-thumb {
|
||||||
|
background: #888;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-file-selector::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #555;
|
||||||
|
}
|
||||||
202
native/wordpress/maple-code-blocks/assets/css/block-style.css
Normal file
202
native/wordpress/maple-code-blocks/assets/css/block-style.css
Normal file
|
|
@ -0,0 +1,202 @@
|
||||||
|
/**
|
||||||
|
* GitHub Code Viewer Block Frontend Styles
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Block Wrapper */
|
||||||
|
.mcb-block-wrapper {
|
||||||
|
margin: 30px auto;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Alignment Support */
|
||||||
|
.mcb-block-wrapper.alignleft {
|
||||||
|
float: left;
|
||||||
|
margin-right: 20px;
|
||||||
|
max-width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-block-wrapper.alignright {
|
||||||
|
float: right;
|
||||||
|
margin-left: 20px;
|
||||||
|
max-width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-block-wrapper.aligncenter {
|
||||||
|
display: block;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-block-wrapper.aligncenter .maple-code-blocks {
|
||||||
|
display: inline-block;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-block-wrapper.alignwide {
|
||||||
|
max-width: 1280px;
|
||||||
|
width: 100%;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-block-wrapper.alignfull {
|
||||||
|
max-width: none;
|
||||||
|
width: 100vw;
|
||||||
|
position: relative;
|
||||||
|
left: 50%;
|
||||||
|
right: 50%;
|
||||||
|
margin-left: -50vw;
|
||||||
|
margin-right: -50vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure the viewer respects container width */
|
||||||
|
.mcb-block-wrapper .maple-code-blocks {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Classes Support */
|
||||||
|
.mcb-block-wrapper.is-style-minimal {
|
||||||
|
box-shadow: none;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-block-wrapper.is-style-rounded .maple-code-blocks {
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-block-wrapper.is-style-shadowed .maple-code-blocks {
|
||||||
|
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Adjustments */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.mcb-block-wrapper.alignleft,
|
||||||
|
.mcb-block-wrapper.alignright {
|
||||||
|
float: none;
|
||||||
|
max-width: 100%;
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-block-wrapper.alignfull {
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading State for Frontend */
|
||||||
|
.mcb-block-wrapper.is-loading {
|
||||||
|
min-height: 400px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-block-wrapper.is-loading::after {
|
||||||
|
content: 'Loading repository...';
|
||||||
|
color: #666;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Error State */
|
||||||
|
.mcb-block-wrapper .mcb-error {
|
||||||
|
padding: 20px;
|
||||||
|
background: #fee;
|
||||||
|
border: 1px solid #fcc;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #c00;
|
||||||
|
text-align: center;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Integration with theme styles */
|
||||||
|
.entry-content .mcb-block-wrapper {
|
||||||
|
margin-top: 2em;
|
||||||
|
margin-bottom: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-content .mcb-block-wrapper:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.entry-content .mcb-block-wrapper:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Print Styles */
|
||||||
|
@media print {
|
||||||
|
.mcb-block-wrapper .mcb-controls,
|
||||||
|
.mcb-block-wrapper .mcb-status-bar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-block-wrapper .maple-code-blocks {
|
||||||
|
box-shadow: none;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Accessibility Improvements */
|
||||||
|
.mcb-block-wrapper .maple-code-blocks:focus-within {
|
||||||
|
outline: 2px solid #007cba;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* High Contrast Mode Support */
|
||||||
|
@media (prefers-contrast: high) {
|
||||||
|
.mcb-block-wrapper .maple-code-blocks {
|
||||||
|
border: 2px solid currentColor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reduced Motion Support */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.mcb-block-wrapper .maple-code-blocks * {
|
||||||
|
animation-duration: 0.01ms !important;
|
||||||
|
animation-iteration-count: 1 !important;
|
||||||
|
transition-duration: 0.01ms !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark Mode Support */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.mcb-block-wrapper.is-loading {
|
||||||
|
background: #2a2a2a;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-block-wrapper.is-loading::after {
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Nested Block Support */
|
||||||
|
.wp-block-group .mcb-block-wrapper,
|
||||||
|
.wp-block-column .mcb-block-wrapper {
|
||||||
|
margin-top: 1.5em;
|
||||||
|
margin-bottom: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pattern Library Support */
|
||||||
|
.mcb-block-wrapper[data-pattern="documentation"] .maple-code-blocks {
|
||||||
|
height: 400px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-block-wrapper[data-pattern="showcase"] .maple-code-blocks {
|
||||||
|
height: 600px !important;
|
||||||
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-block-wrapper[data-pattern="inline"] .maple-code-blocks {
|
||||||
|
height: 300px !important;
|
||||||
|
display: inline-block;
|
||||||
|
width: auto;
|
||||||
|
min-width: 500px;
|
||||||
|
}
|
||||||
2
native/wordpress/maple-code-blocks/assets/css/index.php
Normal file
2
native/wordpress/maple-code-blocks/assets/css/index.php
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
<?php
|
||||||
|
// Silence is golden.
|
||||||
529
native/wordpress/maple-code-blocks/assets/css/mcb-styles.css
Normal file
529
native/wordpress/maple-code-blocks/assets/css/mcb-styles.css
Normal file
|
|
@ -0,0 +1,529 @@
|
||||||
|
/* GitHub Code Viewer - Main Styles */
|
||||||
|
|
||||||
|
.maple-code-blocks {
|
||||||
|
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||||
|
background: #1e1e1e;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark Theme (Default) */
|
||||||
|
.maple-code-blocks[data-theme="dark"],
|
||||||
|
.maple-code-blocks.mcb-theme-dark {
|
||||||
|
background: #1e1e1e;
|
||||||
|
color: #d4d4d4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.maple-code-blocks[data-theme="dark"] .mcb-header,
|
||||||
|
.maple-code-blocks.mcb-theme-dark .mcb-header {
|
||||||
|
background: linear-gradient(180deg, #2d2d30 0%, #252526 100%);
|
||||||
|
border-bottom: 1px solid #3e3e42;
|
||||||
|
}
|
||||||
|
|
||||||
|
.maple-code-blocks[data-theme="dark"] .mcb-sidebar,
|
||||||
|
.maple-code-blocks.mcb-theme-dark .mcb-sidebar {
|
||||||
|
background: #252526;
|
||||||
|
border-right: 1px solid #3e3e42;
|
||||||
|
}
|
||||||
|
|
||||||
|
.maple-code-blocks[data-theme="dark"] .mcb-editor,
|
||||||
|
.maple-code-blocks.mcb-theme-dark .mcb-editor {
|
||||||
|
background: #1e1e1e;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light Theme */
|
||||||
|
.maple-code-blocks[data-theme="light"],
|
||||||
|
.maple-code-blocks.mcb-theme-light {
|
||||||
|
background: #ffffff;
|
||||||
|
color: #333333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.maple-code-blocks[data-theme="light"] .mcb-header,
|
||||||
|
.maple-code-blocks.mcb-theme-light .mcb-header {
|
||||||
|
background: linear-gradient(180deg, #f3f3f3 0%, #e8e8e8 100%);
|
||||||
|
border-bottom: 1px solid #d4d4d4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.maple-code-blocks[data-theme="light"] .mcb-sidebar,
|
||||||
|
.maple-code-blocks.mcb-theme-light .mcb-sidebar {
|
||||||
|
background: #f3f3f3;
|
||||||
|
border-right: 1px solid #e8e8e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.maple-code-blocks[data-theme="light"] .mcb-editor,
|
||||||
|
.maple-code-blocks.mcb-theme-light .mcb-editor {
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
.mcb-header {
|
||||||
|
padding: 12px 16px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-repo-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-github-icon,
|
||||||
|
.mcb-platform-icon {
|
||||||
|
fill: currentColor;
|
||||||
|
opacity: 0.7;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-repo-name {
|
||||||
|
font-size: 13px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-controls button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-controls button:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-controls button svg {
|
||||||
|
fill: currentColor;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Content Area */
|
||||||
|
.mcb-content {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar */
|
||||||
|
.mcb-sidebar {
|
||||||
|
width: 240px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-search-box {
|
||||||
|
padding: 8px;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-search-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 8px;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: inherit;
|
||||||
|
font-size: 12px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-search-input:focus {
|
||||||
|
border-color: #007acc;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* File List */
|
||||||
|
.mcb-file-list {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-file-item {
|
||||||
|
padding: 6px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
transition: background-color 0.1s;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-file-item:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-file-item.active {
|
||||||
|
background: rgba(0, 122, 204, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-file-icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-file-name {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-file-size {
|
||||||
|
font-size: 11px;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Folder styles */
|
||||||
|
.mcb-folder .mcb-file-icon,
|
||||||
|
.mcb-parent-folder .mcb-file-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-folder {
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-folder:hover,
|
||||||
|
.mcb-parent-folder:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-parent-folder {
|
||||||
|
cursor: pointer;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
padding-bottom: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-breadcrumb {
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
font-size: 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-path-label {
|
||||||
|
opacity: 0.6;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-current-path {
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Editor Area */
|
||||||
|
.mcb-editor {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tabs */
|
||||||
|
.mcb-tabs {
|
||||||
|
display: flex;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
overflow-x: auto;
|
||||||
|
min-height: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-tab {
|
||||||
|
padding: 8px 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-right: 1px solid rgba(255, 255, 255, 0.05);
|
||||||
|
font-size: 13px;
|
||||||
|
white-space: nowrap;
|
||||||
|
user-select: none;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-tab:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-tab.active {
|
||||||
|
background: var(--editor-bg, #1e1e1e);
|
||||||
|
border-bottom: 2px solid #007acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-tab-close {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 2px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-tab-close:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Code Area */
|
||||||
|
.mcb-code-area {
|
||||||
|
flex: 1;
|
||||||
|
overflow: auto;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-welcome {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100%;
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-welcome svg {
|
||||||
|
fill: currentColor;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-welcome h4 {
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-welcome p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Code Container */
|
||||||
|
.mcb-code-container {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-code-header {
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-filename {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-copy-btn {
|
||||||
|
padding: 4px 12px;
|
||||||
|
background: rgba(0, 122, 204, 0.2);
|
||||||
|
border: 1px solid #007acc;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #007acc;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-copy-btn:hover {
|
||||||
|
background: rgba(0, 122, 204, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-copy-btn.copied {
|
||||||
|
background: rgba(0, 255, 0, 0.2);
|
||||||
|
border-color: #00ff00;
|
||||||
|
color: #00ff00;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Code Wrapper */
|
||||||
|
.mcb-code-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-code-wrapper pre {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
tab-size: 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-code-wrapper code {
|
||||||
|
display: block;
|
||||||
|
font-family: inherit;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Line Numbers */
|
||||||
|
.line-numbers .line-number {
|
||||||
|
display: inline-block;
|
||||||
|
width: 40px;
|
||||||
|
text-align: right;
|
||||||
|
padding-right: 12px;
|
||||||
|
color: #858585;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-numbers .line-content {
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status Bar */
|
||||||
|
.mcb-status-bar {
|
||||||
|
padding: 4px 16px;
|
||||||
|
background: rgba(0, 122, 204, 0.15);
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-status-text {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-file-info {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading Spinner */
|
||||||
|
.mcb-loading {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 32px;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-spinner {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.2);
|
||||||
|
border-top-color: #007acc;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-loading span {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Error State */
|
||||||
|
.mcb-error {
|
||||||
|
padding: 16px;
|
||||||
|
background: #ff5252;
|
||||||
|
color: white;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin: 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbars */
|
||||||
|
.mcb-file-list::-webkit-scrollbar,
|
||||||
|
.mcb-code-wrapper::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-file-list::-webkit-scrollbar-track,
|
||||||
|
.mcb-code-wrapper::-webkit-scrollbar-track {
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-file-list::-webkit-scrollbar-thumb,
|
||||||
|
.mcb-code-wrapper::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-file-list::-webkit-scrollbar-thumb:hover,
|
||||||
|
.mcb-code-wrapper::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fullscreen Mode */
|
||||||
|
.maple-code-blocks.fullscreen {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 999999;
|
||||||
|
height: 100vh !important;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.mcb-sidebar {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-code-wrapper {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-numbers .line-number {
|
||||||
|
width: 30px;
|
||||||
|
padding-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.mcb-sidebar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-header {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mcb-repo-info {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
187
native/wordpress/maple-code-blocks/assets/css/prism.css
Normal file
187
native/wordpress/maple-code-blocks/assets/css/prism.css
Normal file
|
|
@ -0,0 +1,187 @@
|
||||||
|
/* PrismJS - Minimal syntax highlighting styles */
|
||||||
|
/* Optimized for GitHub Code Viewer Plugin */
|
||||||
|
|
||||||
|
/* Base styles */
|
||||||
|
code[class*="language-"],
|
||||||
|
pre[class*="language-"] {
|
||||||
|
color: #d4d4d4;
|
||||||
|
background: none;
|
||||||
|
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
text-align: left;
|
||||||
|
white-space: pre;
|
||||||
|
word-spacing: normal;
|
||||||
|
word-break: normal;
|
||||||
|
word-wrap: normal;
|
||||||
|
line-height: 1.6;
|
||||||
|
tab-size: 4;
|
||||||
|
hyphens: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Token colors - Dark theme default */
|
||||||
|
.token.comment,
|
||||||
|
.token.prolog,
|
||||||
|
.token.doctype,
|
||||||
|
.token.cdata {
|
||||||
|
color: #6a9955;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.punctuation {
|
||||||
|
color: #d4d4d4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.namespace {
|
||||||
|
opacity: .7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.property,
|
||||||
|
.token.tag,
|
||||||
|
.token.boolean,
|
||||||
|
.token.number,
|
||||||
|
.token.constant,
|
||||||
|
.token.symbol,
|
||||||
|
.token.deleted {
|
||||||
|
color: #b5cea8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.selector,
|
||||||
|
.token.attr-name,
|
||||||
|
.token.string,
|
||||||
|
.token.char,
|
||||||
|
.token.builtin,
|
||||||
|
.token.inserted {
|
||||||
|
color: #ce9178;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.operator,
|
||||||
|
.token.entity,
|
||||||
|
.token.url,
|
||||||
|
.language-css .token.string,
|
||||||
|
.style .token.string {
|
||||||
|
color: #d4d4d4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.atrule,
|
||||||
|
.token.attr-value,
|
||||||
|
.token.keyword {
|
||||||
|
color: #569cd6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.function,
|
||||||
|
.token.class-name {
|
||||||
|
color: #dcdcaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.regex,
|
||||||
|
.token.important,
|
||||||
|
.token.variable {
|
||||||
|
color: #d16969;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.important,
|
||||||
|
.token.bold {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.italic {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.entity {
|
||||||
|
cursor: help;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light theme overrides */
|
||||||
|
.maple-code-blocks[data-theme="light"] .token.comment,
|
||||||
|
.maple-code-blocks[data-theme="light"] .token.prolog,
|
||||||
|
.maple-code-blocks[data-theme="light"] .token.doctype,
|
||||||
|
.maple-code-blocks[data-theme="light"] .token.cdata {
|
||||||
|
color: #008000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.maple-code-blocks[data-theme="light"] .token.punctuation {
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.maple-code-blocks[data-theme="light"] .token.property,
|
||||||
|
.maple-code-blocks[data-theme="light"] .token.tag,
|
||||||
|
.maple-code-blocks[data-theme="light"] .token.boolean,
|
||||||
|
.maple-code-blocks[data-theme="light"] .token.number,
|
||||||
|
.maple-code-blocks[data-theme="light"] .token.constant,
|
||||||
|
.maple-code-blocks[data-theme="light"] .token.symbol {
|
||||||
|
color: #098658;
|
||||||
|
}
|
||||||
|
|
||||||
|
.maple-code-blocks[data-theme="light"] .token.selector,
|
||||||
|
.maple-code-blocks[data-theme="light"] .token.attr-name,
|
||||||
|
.maple-code-blocks[data-theme="light"] .token.string,
|
||||||
|
.maple-code-blocks[data-theme="light"] .token.char,
|
||||||
|
.maple-code-blocks[data-theme="light"] .token.builtin,
|
||||||
|
.maple-code-blocks[data-theme="light"] .token.inserted {
|
||||||
|
color: #a31515;
|
||||||
|
}
|
||||||
|
|
||||||
|
.maple-code-blocks[data-theme="light"] .token.operator,
|
||||||
|
.maple-code-blocks[data-theme="light"] .token.entity,
|
||||||
|
.maple-code-blocks[data-theme="light"] .token.url,
|
||||||
|
.maple-code-blocks[data-theme="light"] .language-css .token.string,
|
||||||
|
.maple-code-blocks[data-theme="light"] .style .token.string {
|
||||||
|
color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.maple-code-blocks[data-theme="light"] .token.atrule,
|
||||||
|
.maple-code-blocks[data-theme="light"] .token.attr-value,
|
||||||
|
.maple-code-blocks[data-theme="light"] .token.keyword {
|
||||||
|
color: #0000ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.maple-code-blocks[data-theme="light"] .token.function {
|
||||||
|
color: #795e26;
|
||||||
|
}
|
||||||
|
|
||||||
|
.maple-code-blocks[data-theme="light"] .token.class-name {
|
||||||
|
color: #267f99;
|
||||||
|
}
|
||||||
|
|
||||||
|
.maple-code-blocks[data-theme="light"] .token.regex,
|
||||||
|
.maple-code-blocks[data-theme="light"] .token.important,
|
||||||
|
.maple-code-blocks[data-theme="light"] .token.variable {
|
||||||
|
color: #e90;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Line highlighting */
|
||||||
|
.line-highlight {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
margin-top: 1em;
|
||||||
|
background: linear-gradient(to right, rgba(255, 255, 255, .1) 70%, rgba(255, 255, 255, 0));
|
||||||
|
pointer-events: none;
|
||||||
|
line-height: inherit;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Selection styling */
|
||||||
|
pre[class*="language-"]::-moz-selection,
|
||||||
|
pre[class*="language-"] ::-moz-selection,
|
||||||
|
code[class*="language-"]::-moz-selection,
|
||||||
|
code[class*="language-"] ::-moz-selection {
|
||||||
|
text-shadow: none;
|
||||||
|
background: #264f78;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre[class*="language-"]::selection,
|
||||||
|
pre[class*="language-"] ::selection,
|
||||||
|
code[class*="language-"]::selection,
|
||||||
|
code[class*="language-"] ::selection {
|
||||||
|
text-shadow: none;
|
||||||
|
background: #264f78;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light theme selection */
|
||||||
|
.maple-code-blocks[data-theme="light"] pre[class*="language-"]::selection,
|
||||||
|
.maple-code-blocks[data-theme="light"] pre[class*="language-"] ::selection,
|
||||||
|
.maple-code-blocks[data-theme="light"] code[class*="language-"]::selection,
|
||||||
|
.maple-code-blocks[data-theme="light"] code[class*="language-"] ::selection {
|
||||||
|
background: #add6ff;
|
||||||
|
}
|
||||||
2
native/wordpress/maple-code-blocks/assets/index.php
Normal file
2
native/wordpress/maple-code-blocks/assets/index.php
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
<?php
|
||||||
|
// Silence is golden.
|
||||||
442
native/wordpress/maple-code-blocks/assets/js/block-editor.js
Normal file
442
native/wordpress/maple-code-blocks/assets/js/block-editor.js
Normal file
|
|
@ -0,0 +1,442 @@
|
||||||
|
/**
|
||||||
|
* Maple Code Blocks - Gutenberg Block
|
||||||
|
*
|
||||||
|
* Block registration for the Maple Code Blocks plugin
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function(blocks, element, editor, components, i18n, data, apiFetch) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const el = element.createElement;
|
||||||
|
const { registerBlockType } = blocks;
|
||||||
|
const { Fragment } = element;
|
||||||
|
const { InspectorControls, BlockControls, AlignmentToolbar, useBlockProps } = editor || {};
|
||||||
|
|
||||||
|
// For older WordPress versions, fallback
|
||||||
|
const BlockProps = useBlockProps || function(props) { return props; };
|
||||||
|
const {
|
||||||
|
PanelBody,
|
||||||
|
PanelRow,
|
||||||
|
TextControl,
|
||||||
|
SelectControl,
|
||||||
|
ToggleControl,
|
||||||
|
Button,
|
||||||
|
Placeholder,
|
||||||
|
Spinner,
|
||||||
|
Notice,
|
||||||
|
ToolbarGroup,
|
||||||
|
ToolbarButton,
|
||||||
|
__experimentalUnitControl: UnitControl,
|
||||||
|
ExternalLink
|
||||||
|
} = components;
|
||||||
|
const { __ } = i18n;
|
||||||
|
const { useState, useEffect } = element;
|
||||||
|
const { useSelect } = data;
|
||||||
|
|
||||||
|
|
||||||
|
// Debug: Log that script is loading
|
||||||
|
console.log('Maple Code Blocks: Script loaded, attempting to register block');
|
||||||
|
|
||||||
|
// Register the block
|
||||||
|
registerBlockType('maple-code-blocks/code-block', {
|
||||||
|
title: __('Maple Code Block', 'maple-code-blocks'),
|
||||||
|
description: __('Display code from GitHub, GitLab, Bitbucket, or Codeberg repositories', 'maple-code-blocks'),
|
||||||
|
icon: {
|
||||||
|
src: 'editor-code',
|
||||||
|
background: '#0366d6',
|
||||||
|
foreground: '#ffffff'
|
||||||
|
},
|
||||||
|
category: 'maple-code-blocks',
|
||||||
|
keywords: ['github', 'code', 'repository', 'syntax', 'highlight', 'viewer'],
|
||||||
|
attributes: {
|
||||||
|
repository: {
|
||||||
|
type: 'string',
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
theme: {
|
||||||
|
type: 'string',
|
||||||
|
default: 'dark'
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
type: 'string',
|
||||||
|
default: '600px'
|
||||||
|
},
|
||||||
|
showLineNumbers: {
|
||||||
|
type: 'boolean',
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
initialFile: {
|
||||||
|
type: 'string',
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: 'string',
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
isValid: {
|
||||||
|
type: 'boolean',
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
repoFiles: {
|
||||||
|
type: 'array',
|
||||||
|
default: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
supports: {
|
||||||
|
align: ['wide', 'full'],
|
||||||
|
className: true,
|
||||||
|
customClassName: true,
|
||||||
|
html: false,
|
||||||
|
anchor: true
|
||||||
|
},
|
||||||
|
|
||||||
|
edit: function(props) {
|
||||||
|
const { attributes, setAttributes, className, isSelected } = props;
|
||||||
|
const {
|
||||||
|
repository,
|
||||||
|
theme,
|
||||||
|
height,
|
||||||
|
showLineNumbers,
|
||||||
|
initialFile,
|
||||||
|
title,
|
||||||
|
isValid,
|
||||||
|
repoFiles
|
||||||
|
} = attributes;
|
||||||
|
|
||||||
|
const [isValidating, setIsValidating] = useState(false);
|
||||||
|
const [validationError, setValidationError] = useState('');
|
||||||
|
const [isLoadingFiles, setIsLoadingFiles] = useState(false);
|
||||||
|
const [popularRepo, setPopularRepo] = useState('');
|
||||||
|
|
||||||
|
const blockProps = BlockProps({
|
||||||
|
className: className + ' mcb-block-editor'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validate repository when it changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (repository && repository.includes('/')) {
|
||||||
|
validateRepository();
|
||||||
|
}
|
||||||
|
}, [repository]);
|
||||||
|
|
||||||
|
// Validate repository format and existence
|
||||||
|
const validateRepository = () => {
|
||||||
|
setIsValidating(true);
|
||||||
|
setValidationError('');
|
||||||
|
|
||||||
|
// Basic format validation
|
||||||
|
const repoPattern = /^[a-zA-Z0-9\-_]+\/[a-zA-Z0-9\-_\.]+$/;
|
||||||
|
if (!repoPattern.test(repository)) {
|
||||||
|
setValidationError('Invalid format. Use: owner/repository');
|
||||||
|
setIsValidating(false);
|
||||||
|
setAttributes({ isValid: false });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate with API
|
||||||
|
apiFetch({
|
||||||
|
path: '/maple-code-blocks/v1/validate-repo',
|
||||||
|
method: 'POST',
|
||||||
|
data: { repository: repository }
|
||||||
|
}).then(response => {
|
||||||
|
setAttributes({ isValid: true });
|
||||||
|
setValidationError('');
|
||||||
|
loadRepositoryFiles();
|
||||||
|
}).catch(error => {
|
||||||
|
setValidationError(error.message || 'Repository not found or inaccessible');
|
||||||
|
setAttributes({ isValid: false });
|
||||||
|
}).finally(() => {
|
||||||
|
setIsValidating(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load repository files for initial file selection
|
||||||
|
const loadRepositoryFiles = () => {
|
||||||
|
setIsLoadingFiles(true);
|
||||||
|
|
||||||
|
apiFetch({
|
||||||
|
path: '/maple-code-blocks/v1/get-files',
|
||||||
|
method: 'POST',
|
||||||
|
data: { repository: repository }
|
||||||
|
}).then(response => {
|
||||||
|
setAttributes({ repoFiles: response.files || [] });
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('Failed to load files:', error);
|
||||||
|
}).finally(() => {
|
||||||
|
setIsLoadingFiles(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set a popular repository
|
||||||
|
const setPopularRepository = (repo) => {
|
||||||
|
setAttributes({ repository: repo });
|
||||||
|
setPopularRepo('');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Height units for the control
|
||||||
|
const units = [
|
||||||
|
{ value: 'px', label: 'px' },
|
||||||
|
{ value: '%', label: '%' },
|
||||||
|
{ value: 'vh', label: 'vh' },
|
||||||
|
{ value: 'em', label: 'em' },
|
||||||
|
{ value: 'rem', label: 'rem' }
|
||||||
|
];
|
||||||
|
|
||||||
|
return el(Fragment, {},
|
||||||
|
// Block Controls Toolbar
|
||||||
|
el(BlockControls, {},
|
||||||
|
el(ToolbarGroup, {},
|
||||||
|
el(ToolbarButton, {
|
||||||
|
icon: 'update',
|
||||||
|
label: __('Refresh Repository', 'maple-code-blocks'),
|
||||||
|
onClick: validateRepository,
|
||||||
|
disabled: !repository || isValidating
|
||||||
|
}),
|
||||||
|
el(ToolbarButton, {
|
||||||
|
icon: 'external',
|
||||||
|
label: __('View on GitHub', 'maple-code-blocks'),
|
||||||
|
onClick: () => window.open('https://github.com/' + repository, '_blank'),
|
||||||
|
disabled: !repository || !isValid
|
||||||
|
})
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
|
// Inspector Controls (Sidebar)
|
||||||
|
el(InspectorControls, {},
|
||||||
|
el(PanelBody, {
|
||||||
|
title: __('Repository Settings', 'maple-code-blocks'),
|
||||||
|
initialOpen: true
|
||||||
|
},
|
||||||
|
el(TextControl, {
|
||||||
|
label: __('GitHub Repository', 'maple-code-blocks'),
|
||||||
|
value: repository,
|
||||||
|
onChange: (value) => setAttributes({ repository: value }),
|
||||||
|
placeholder: 'owner/repository',
|
||||||
|
help: __('Format: owner/repository (e.g., facebook/react)', 'maple-code-blocks')
|
||||||
|
}),
|
||||||
|
|
||||||
|
isValidating && el(Spinner),
|
||||||
|
|
||||||
|
validationError && el(Notice, {
|
||||||
|
status: 'error',
|
||||||
|
isDismissible: false
|
||||||
|
}, validationError),
|
||||||
|
|
||||||
|
isValid && !isValidating && el(Notice, {
|
||||||
|
status: 'success',
|
||||||
|
isDismissible: false
|
||||||
|
}, __('✓ Repository validated', 'maple-code-blocks')),
|
||||||
|
|
||||||
|
el(PanelRow, {},
|
||||||
|
el('div', { style: { width: '100%' } },
|
||||||
|
el('label', {}, __('Popular Repositories', 'maple-code-blocks')),
|
||||||
|
el(SelectControl, {
|
||||||
|
value: popularRepo,
|
||||||
|
onChange: setPopularRepository,
|
||||||
|
options: [
|
||||||
|
{ label: __('Select a repository...', 'maple-code-blocks'), value: '' },
|
||||||
|
...mcbBlockData.popularRepos.map(repo => ({
|
||||||
|
label: repo,
|
||||||
|
value: repo
|
||||||
|
}))
|
||||||
|
]
|
||||||
|
})
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
|
repository && el(ExternalLink, {
|
||||||
|
href: 'https://github.com/' + repository
|
||||||
|
}, __('View on GitHub →', 'maple-code-blocks'))
|
||||||
|
),
|
||||||
|
|
||||||
|
el(PanelBody, {
|
||||||
|
title: __('Display Settings', 'maple-code-blocks'),
|
||||||
|
initialOpen: false
|
||||||
|
},
|
||||||
|
el(TextControl, {
|
||||||
|
label: __('Title (Optional)', 'maple-code-blocks'),
|
||||||
|
value: title,
|
||||||
|
onChange: (value) => setAttributes({ title: value }),
|
||||||
|
placeholder: __('e.g., React Source Code', 'maple-code-blocks')
|
||||||
|
}),
|
||||||
|
|
||||||
|
el(SelectControl, {
|
||||||
|
label: __('Theme', 'maple-code-blocks'),
|
||||||
|
value: theme,
|
||||||
|
onChange: (value) => setAttributes({ theme: value }),
|
||||||
|
options: mcbBlockData.themes
|
||||||
|
}),
|
||||||
|
|
||||||
|
el('div', {
|
||||||
|
className: 'mcb-height-control',
|
||||||
|
style: { marginBottom: '20px' }
|
||||||
|
},
|
||||||
|
el('label', {}, __('Height', 'maple-code-blocks')),
|
||||||
|
el(TextControl, {
|
||||||
|
value: height,
|
||||||
|
onChange: (value) => setAttributes({ height: value }),
|
||||||
|
placeholder: '600px',
|
||||||
|
help: __('Examples: 600px, 80vh, 100%', 'maple-code-blocks')
|
||||||
|
})
|
||||||
|
),
|
||||||
|
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: __('Show Line Numbers', 'maple-code-blocks'),
|
||||||
|
checked: showLineNumbers,
|
||||||
|
onChange: (value) => setAttributes({ showLineNumbers: value })
|
||||||
|
})
|
||||||
|
),
|
||||||
|
|
||||||
|
el(PanelBody, {
|
||||||
|
title: __('Advanced Settings', 'maple-code-blocks'),
|
||||||
|
initialOpen: false
|
||||||
|
},
|
||||||
|
isLoadingFiles && el(Spinner),
|
||||||
|
|
||||||
|
!isLoadingFiles && repoFiles.length > 0 && el(SelectControl, {
|
||||||
|
label: __('Initial File to Display', 'maple-code-blocks'),
|
||||||
|
value: initialFile,
|
||||||
|
onChange: (value) => setAttributes({ initialFile: value }),
|
||||||
|
options: [
|
||||||
|
{ label: __('None (Show file browser)', 'maple-code-blocks'), value: '' },
|
||||||
|
...repoFiles.map(file => ({
|
||||||
|
label: file.name,
|
||||||
|
value: file.path
|
||||||
|
}))
|
||||||
|
],
|
||||||
|
help: __('Select a file to display when the viewer loads', 'maple-code-blocks')
|
||||||
|
})
|
||||||
|
)
|
||||||
|
),
|
||||||
|
|
||||||
|
// Main Block Content
|
||||||
|
el('div', blockProps,
|
||||||
|
!repository ?
|
||||||
|
// Empty state placeholder
|
||||||
|
el(Placeholder, {
|
||||||
|
icon: 'editor-code',
|
||||||
|
label: __('GitHub Code Viewer', 'maple-code-blocks'),
|
||||||
|
instructions: __('Display code from any public GitHub repository', 'maple-code-blocks')
|
||||||
|
},
|
||||||
|
el(TextControl, {
|
||||||
|
value: repository,
|
||||||
|
onChange: (value) => setAttributes({ repository: value }),
|
||||||
|
placeholder: 'owner/repository',
|
||||||
|
label: __('Repository', 'maple-code-blocks')
|
||||||
|
}),
|
||||||
|
el('div', { style: { marginTop: '10px' } },
|
||||||
|
el('strong', {}, __('Quick Start:', 'maple-code-blocks')),
|
||||||
|
el('div', { style: { marginTop: '5px' } },
|
||||||
|
mcbBlockData.popularRepos.slice(0, 3).map(repo =>
|
||||||
|
el(Button, {
|
||||||
|
key: repo,
|
||||||
|
isSecondary: true,
|
||||||
|
onClick: () => setAttributes({ repository: repo }),
|
||||||
|
style: { margin: '2px' }
|
||||||
|
}, repo)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) :
|
||||||
|
// Preview state
|
||||||
|
el('div', {
|
||||||
|
className: 'mcb-block-preview',
|
||||||
|
style: {
|
||||||
|
background: theme === 'dark' ? '#1e1e1e' : '#ffffff',
|
||||||
|
border: '1px solid #e0e0e0',
|
||||||
|
borderRadius: '4px',
|
||||||
|
padding: '20px',
|
||||||
|
minHeight: '200px'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title && el('h3', {
|
||||||
|
style: {
|
||||||
|
margin: '0 0 10px 0',
|
||||||
|
color: theme === 'dark' ? '#ffffff' : '#000000'
|
||||||
|
}
|
||||||
|
}, title),
|
||||||
|
|
||||||
|
el('div', {
|
||||||
|
className: 'mcb-preview-header',
|
||||||
|
style: {
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: '15px',
|
||||||
|
paddingBottom: '10px',
|
||||||
|
borderBottom: '1px solid ' + (theme === 'dark' ? '#444' : '#e0e0e0')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
el('svg', {
|
||||||
|
width: '20',
|
||||||
|
height: '20',
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
style: { marginRight: '10px' }
|
||||||
|
},
|
||||||
|
el('path', {
|
||||||
|
fill: theme === 'dark' ? '#ffffff' : '#000000',
|
||||||
|
d: 'M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z'
|
||||||
|
})
|
||||||
|
),
|
||||||
|
el('span', {
|
||||||
|
style: {
|
||||||
|
fontSize: '14px',
|
||||||
|
color: theme === 'dark' ? '#d4d4d4' : '#666'
|
||||||
|
}
|
||||||
|
}, repository)
|
||||||
|
),
|
||||||
|
|
||||||
|
el('div', {
|
||||||
|
className: 'mcb-preview-info',
|
||||||
|
style: {
|
||||||
|
display: 'flex',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: '15px',
|
||||||
|
fontSize: '13px',
|
||||||
|
color: theme === 'dark' ? '#969696' : '#666'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
el('div', {}, '🎨 Theme: ' + theme),
|
||||||
|
el('div', {}, '📏 Height: ' + height),
|
||||||
|
el('div', {}, showLineNumbers ? '✅ Line Numbers' : '❌ No Line Numbers'),
|
||||||
|
initialFile && el('div', {}, '📄 Initial: ' + initialFile.split('/').pop())
|
||||||
|
),
|
||||||
|
|
||||||
|
isValidating && el('div', {
|
||||||
|
style: {
|
||||||
|
textAlign: 'center',
|
||||||
|
padding: '20px',
|
||||||
|
color: theme === 'dark' ? '#ffffff' : '#000000'
|
||||||
|
}
|
||||||
|
}, el(Spinner), ' Validating repository...'),
|
||||||
|
|
||||||
|
!isValidating && isValid && el('div', {
|
||||||
|
style: {
|
||||||
|
marginTop: '15px',
|
||||||
|
padding: '10px',
|
||||||
|
background: theme === 'dark' ? '#0e4429' : '#d4edda',
|
||||||
|
borderRadius: '4px',
|
||||||
|
color: theme === 'dark' ? '#52c41a' : '#155724',
|
||||||
|
fontSize: '13px'
|
||||||
|
}
|
||||||
|
}, '✓ Repository validated and ready to display')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
save: function(props) {
|
||||||
|
// For server-side rendered blocks, we need to return something
|
||||||
|
// This will be replaced by the PHP render callback
|
||||||
|
return el('div', { className: 'maple-code-block-placeholder' }, 'Loading...');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
})(
|
||||||
|
window.wp.blocks,
|
||||||
|
window.wp.element,
|
||||||
|
window.wp.blockEditor || window.wp.editor,
|
||||||
|
window.wp.components,
|
||||||
|
window.wp.i18n,
|
||||||
|
window.wp.data,
|
||||||
|
window.wp.apiFetch
|
||||||
|
);
|
||||||
225
native/wordpress/maple-code-blocks/assets/js/block-variations.js
Normal file
225
native/wordpress/maple-code-blocks/assets/js/block-variations.js
Normal file
|
|
@ -0,0 +1,225 @@
|
||||||
|
/**
|
||||||
|
* GitHub Code Viewer Block Variations
|
||||||
|
* Provides pre-configured block patterns for common use cases
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function(blocks, domReady) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
domReady(function() {
|
||||||
|
// Register block variations
|
||||||
|
blocks.registerBlockVariation('maple-code-blocks/code-block', {
|
||||||
|
name: 'react-component',
|
||||||
|
title: 'Maple: React Component',
|
||||||
|
description: 'Display a React component from GitHub',
|
||||||
|
icon: 'editor-code',
|
||||||
|
attributes: {
|
||||||
|
repository: 'facebook/react',
|
||||||
|
theme: 'dark',
|
||||||
|
height: '500px',
|
||||||
|
showLineNumbers: true,
|
||||||
|
initialFile: 'packages/react/src/React.js'
|
||||||
|
},
|
||||||
|
scope: ['inserter']
|
||||||
|
});
|
||||||
|
|
||||||
|
blocks.registerBlockVariation('maple-code-blocks/code-block', {
|
||||||
|
name: 'documentation-viewer',
|
||||||
|
title: 'Maple: Documentation Viewer',
|
||||||
|
description: 'Display README or documentation files',
|
||||||
|
icon: 'media-document',
|
||||||
|
attributes: {
|
||||||
|
repository: '',
|
||||||
|
theme: 'light',
|
||||||
|
height: '400px',
|
||||||
|
showLineNumbers: false,
|
||||||
|
initialFile: 'README.md'
|
||||||
|
},
|
||||||
|
scope: ['inserter']
|
||||||
|
});
|
||||||
|
|
||||||
|
blocks.registerBlockVariation('maple-code-blocks/code-block', {
|
||||||
|
name: 'full-repository',
|
||||||
|
title: 'Maple: Full Repository Browser',
|
||||||
|
description: 'Browse entire repository with file tree',
|
||||||
|
icon: 'category',
|
||||||
|
attributes: {
|
||||||
|
repository: '',
|
||||||
|
theme: 'dark',
|
||||||
|
height: '700px',
|
||||||
|
showLineNumbers: true,
|
||||||
|
initialFile: ''
|
||||||
|
},
|
||||||
|
scope: ['inserter']
|
||||||
|
});
|
||||||
|
|
||||||
|
blocks.registerBlockVariation('maple-code-blocks/code-block', {
|
||||||
|
name: 'code-snippet',
|
||||||
|
title: 'Maple: Code Snippet',
|
||||||
|
description: 'Display a specific code file',
|
||||||
|
icon: 'editor-code',
|
||||||
|
attributes: {
|
||||||
|
repository: '',
|
||||||
|
theme: 'monokai',
|
||||||
|
height: '300px',
|
||||||
|
showLineNumbers: true,
|
||||||
|
initialFile: ''
|
||||||
|
},
|
||||||
|
scope: ['inserter']
|
||||||
|
});
|
||||||
|
|
||||||
|
blocks.registerBlockVariation('maple-code-blocks/code-block', {
|
||||||
|
name: 'tutorial-code',
|
||||||
|
title: 'Maple: Tutorial Code',
|
||||||
|
description: 'Perfect for coding tutorials and education',
|
||||||
|
icon: 'welcome-learn-more',
|
||||||
|
attributes: {
|
||||||
|
repository: '',
|
||||||
|
theme: 'solarized',
|
||||||
|
height: '450px',
|
||||||
|
showLineNumbers: true,
|
||||||
|
title: 'Example Code'
|
||||||
|
},
|
||||||
|
scope: ['inserter']
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register block styles (additional styling options)
|
||||||
|
blocks.registerBlockStyle('maple-code-blocks/code-block', {
|
||||||
|
name: 'default',
|
||||||
|
label: 'Default',
|
||||||
|
isDefault: true
|
||||||
|
});
|
||||||
|
|
||||||
|
blocks.registerBlockStyle('maple-code-blocks/code-block', {
|
||||||
|
name: 'minimal',
|
||||||
|
label: 'Minimal',
|
||||||
|
className: 'is-style-minimal'
|
||||||
|
});
|
||||||
|
|
||||||
|
blocks.registerBlockStyle('maple-code-blocks/code-block', {
|
||||||
|
name: 'rounded',
|
||||||
|
label: 'Rounded',
|
||||||
|
className: 'is-style-rounded'
|
||||||
|
});
|
||||||
|
|
||||||
|
blocks.registerBlockStyle('maple-code-blocks/code-block', {
|
||||||
|
name: 'shadowed',
|
||||||
|
label: 'Shadowed',
|
||||||
|
className: 'is-style-shadowed'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Register block patterns for complete layouts
|
||||||
|
if (wp.blockEditor && wp.blockEditor.registerBlockPattern) {
|
||||||
|
// Code Comparison Pattern
|
||||||
|
wp.blockEditor.registerBlockPattern('maple-code-blocks/code-comparison', {
|
||||||
|
title: 'Maple: Code Comparison',
|
||||||
|
description: 'Compare code from two different repositories',
|
||||||
|
categories: ['maple-code-blocks'],
|
||||||
|
content: `
|
||||||
|
<!-- wp:columns -->
|
||||||
|
<div class="wp-block-columns">
|
||||||
|
<!-- wp:column -->
|
||||||
|
<div class="wp-block-column">
|
||||||
|
<!-- wp:heading {"level":3} -->
|
||||||
|
<h3>Original Implementation</h3>
|
||||||
|
<!-- /wp:heading -->
|
||||||
|
<!-- wp:maple-code-blocks/code-block {"repository":"facebook/react","theme":"dark","height":"400px"} /-->
|
||||||
|
</div>
|
||||||
|
<!-- /wp:column -->
|
||||||
|
<!-- wp:column -->
|
||||||
|
<div class="wp-block-column">
|
||||||
|
<!-- wp:heading {"level":3} -->
|
||||||
|
<h3>Alternative Implementation</h3>
|
||||||
|
<!-- /wp:heading -->
|
||||||
|
<!-- wp:maple-code-blocks/code-block {"repository":"vuejs/vue","theme":"dark","height":"400px"} /-->
|
||||||
|
</div>
|
||||||
|
<!-- /wp:column -->
|
||||||
|
</div>
|
||||||
|
<!-- /wp:columns -->`
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tutorial Pattern
|
||||||
|
wp.blockEditor.registerBlockPattern('maple-code-blocks/tutorial-layout', {
|
||||||
|
title: 'Maple: Tutorial Layout',
|
||||||
|
description: 'Code tutorial with explanation',
|
||||||
|
categories: ['maple-code-blocks'],
|
||||||
|
content: `
|
||||||
|
<!-- wp:group {"backgroundColor":"light-gray","padding":{"top":"40px","right":"40px","bottom":"40px","left":"40px"}} -->
|
||||||
|
<div class="wp-block-group has-light-gray-background-color has-background" style="padding-top:40px;padding-right:40px;padding-bottom:40px;padding-left:40px">
|
||||||
|
<!-- wp:heading -->
|
||||||
|
<h2>Code Tutorial</h2>
|
||||||
|
<!-- /wp:heading -->
|
||||||
|
<!-- wp:paragraph -->
|
||||||
|
<p>Here's an example of how to implement this feature:</p>
|
||||||
|
<!-- /wp:paragraph -->
|
||||||
|
<!-- wp:maple-code-blocks/code-block {"repository":"","theme":"light","height":"350px","showLineNumbers":true,"title":"Step 1: Basic Setup"} /-->
|
||||||
|
<!-- wp:paragraph -->
|
||||||
|
<p>Now let's add some advanced functionality:</p>
|
||||||
|
<!-- /wp:paragraph -->
|
||||||
|
<!-- wp:maple-code-blocks/code-block {"repository":"","theme":"light","height":"350px","showLineNumbers":true,"title":"Step 2: Advanced Features"} /-->
|
||||||
|
</div>
|
||||||
|
<!-- /wp:group -->`
|
||||||
|
});
|
||||||
|
|
||||||
|
// Documentation Pattern
|
||||||
|
wp.blockEditor.registerBlockPattern('maple-code-blocks/documentation', {
|
||||||
|
title: 'Maple: Documentation Section',
|
||||||
|
description: 'Documentation with embedded code',
|
||||||
|
categories: ['maple-code-blocks'],
|
||||||
|
content: `
|
||||||
|
<!-- wp:heading -->
|
||||||
|
<h2>API Documentation</h2>
|
||||||
|
<!-- /wp:heading -->
|
||||||
|
<!-- wp:paragraph -->
|
||||||
|
<p>This library provides a simple interface for working with the API.</p>
|
||||||
|
<!-- /wp:paragraph -->
|
||||||
|
<!-- wp:heading {"level":3} -->
|
||||||
|
<h3>Installation</h3>
|
||||||
|
<!-- /wp:heading -->
|
||||||
|
<!-- wp:code -->
|
||||||
|
<pre class="wp-block-code"><code>npm install example-library</code></pre>
|
||||||
|
<!-- /wp:code -->
|
||||||
|
<!-- wp:heading {"level":3} -->
|
||||||
|
<h3>Source Code</h3>
|
||||||
|
<!-- /wp:heading -->
|
||||||
|
<!-- wp:maple-code-blocks/code-block {"repository":"","theme":"dark","height":"500px","showLineNumbers":true} /-->
|
||||||
|
<!-- wp:heading {"level":3} -->
|
||||||
|
<h3>Examples</h3>
|
||||||
|
<!-- /wp:heading -->
|
||||||
|
<!-- wp:maple-code-blocks/code-block {"repository":"","theme":"dark","height":"300px","showLineNumbers":false,"initialFile":"examples/basic.js"} /-->`
|
||||||
|
});
|
||||||
|
|
||||||
|
// Showcase Pattern
|
||||||
|
wp.blockEditor.registerBlockPattern('maple-code-blocks/showcase', {
|
||||||
|
title: 'Maple: Project Showcase',
|
||||||
|
description: 'Showcase a GitHub project',
|
||||||
|
categories: ['maple-code-blocks'],
|
||||||
|
content: `
|
||||||
|
<!-- wp:cover {"url":"","dimRatio":50,"align":"full"} -->
|
||||||
|
<div class="wp-block-cover alignfull">
|
||||||
|
<div class="wp-block-cover__inner-container">
|
||||||
|
<!-- wp:heading {"textAlign":"center","level":1} -->
|
||||||
|
<h1 class="has-text-align-center">Project Name</h1>
|
||||||
|
<!-- /wp:heading -->
|
||||||
|
<!-- wp:paragraph {"align":"center"} -->
|
||||||
|
<p class="has-text-align-center">A brief description of your amazing project</p>
|
||||||
|
<!-- /wp:paragraph -->
|
||||||
|
<!-- wp:buttons {"layout":{"type":"flex","justifyContent":"center"}} -->
|
||||||
|
<div class="wp-block-buttons">
|
||||||
|
<!-- wp:button -->
|
||||||
|
<div class="wp-block-button"><a class="wp-block-button__link">View on GitHub</a></div>
|
||||||
|
<!-- /wp:button -->
|
||||||
|
</div>
|
||||||
|
<!-- /wp:buttons -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- /wp:cover -->
|
||||||
|
<!-- wp:maple-code-blocks/code-block {"repository":"","theme":"dark","height":"600px","showLineNumbers":true,"align":"full"} /-->`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
})(
|
||||||
|
window.wp.blocks,
|
||||||
|
window.wp.domReady
|
||||||
|
);
|
||||||
2
native/wordpress/maple-code-blocks/assets/js/index.php
Normal file
2
native/wordpress/maple-code-blocks/assets/js/index.php
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
<?php
|
||||||
|
// Silence is golden.
|
||||||
647
native/wordpress/maple-code-blocks/assets/js/mcb-script.js
Normal file
647
native/wordpress/maple-code-blocks/assets/js/mcb-script.js
Normal file
|
|
@ -0,0 +1,647 @@
|
||||||
|
/**
|
||||||
|
* GitHub Code Viewer - Main JavaScript
|
||||||
|
*/
|
||||||
|
|
||||||
|
(function($) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// GitHub Code Viewer Class
|
||||||
|
class GitHubCodeViewer {
|
||||||
|
constructor(element) {
|
||||||
|
this.$element = $(element);
|
||||||
|
this.repo = this.$element.data('repo');
|
||||||
|
this.theme = this.$element.data('theme');
|
||||||
|
this.showLineNumbers = this.$element.data('show-line-numbers');
|
||||||
|
this.initialFile = this.$element.data('initial-file');
|
||||||
|
|
||||||
|
this.files = [];
|
||||||
|
this.openTabs = [];
|
||||||
|
this.activeTab = null;
|
||||||
|
this.fileCache = {};
|
||||||
|
this.activeRequests = []; // Track active AJAX requests
|
||||||
|
this.currentPath = ''; // Track current folder path
|
||||||
|
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.bindEvents();
|
||||||
|
this.loadRepositoryFiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
bindEvents() {
|
||||||
|
// Search functionality with debouncing
|
||||||
|
let searchTimeout;
|
||||||
|
this.$element.on('input', '.mcb-search-input', (e) => {
|
||||||
|
clearTimeout(searchTimeout);
|
||||||
|
searchTimeout = setTimeout(() => {
|
||||||
|
this.filterFiles($(e.target).val());
|
||||||
|
}, 300); // Debounce for 300ms
|
||||||
|
});
|
||||||
|
|
||||||
|
// File/folder selection
|
||||||
|
this.$element.on('click', '.mcb-file-item', (e) => {
|
||||||
|
const $item = $(e.currentTarget);
|
||||||
|
const isFolder = $item.data('is-folder');
|
||||||
|
const path = $item.data('path');
|
||||||
|
|
||||||
|
if (isFolder) {
|
||||||
|
// Navigate to folder
|
||||||
|
this.loadRepositoryFiles(path);
|
||||||
|
} else {
|
||||||
|
// Load file
|
||||||
|
this.loadFile(path);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tab management
|
||||||
|
this.$element.on('click', '.mcb-tab', (e) => {
|
||||||
|
const $tab = $(e.currentTarget);
|
||||||
|
const filePath = $tab.data('path');
|
||||||
|
this.switchToTab(filePath);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.$element.on('click', '.mcb-tab-close', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const $tab = $(e.target).closest('.mcb-tab');
|
||||||
|
const filePath = $tab.data('path');
|
||||||
|
this.closeTab(filePath);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Copy button
|
||||||
|
this.$element.on('click', '.mcb-copy-btn', (e) => {
|
||||||
|
this.copyCode($(e.target));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Home button - go to root
|
||||||
|
this.$element.on('click', '.mcb-home-btn', () => {
|
||||||
|
this.loadRepositoryFiles(''); // Load root
|
||||||
|
});
|
||||||
|
|
||||||
|
// Refresh button
|
||||||
|
this.$element.on('click', '.mcb-refresh-btn', () => {
|
||||||
|
this.refreshFiles();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fullscreen toggle
|
||||||
|
this.$element.on('click', '.mcb-fullscreen-btn', () => {
|
||||||
|
this.toggleFullscreen();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadRepositoryFiles(path = '') {
|
||||||
|
console.log('MCB: Loading repository files for:', this.repo, 'path:', path);
|
||||||
|
const $fileList = this.$element.find('.mcb-file-list');
|
||||||
|
|
||||||
|
// Update current path
|
||||||
|
this.currentPath = path;
|
||||||
|
|
||||||
|
// Show loading indicator
|
||||||
|
$fileList.html('<div class="mcb-loading"><div class="mcb-spinner"></div><span>Loading files...</span></div>');
|
||||||
|
|
||||||
|
// Abort any pending requests
|
||||||
|
this.abortActiveRequests();
|
||||||
|
|
||||||
|
console.log('MCB: Making AJAX request to:', mcb_ajax.ajax_url);
|
||||||
|
const request = $.ajax({
|
||||||
|
url: mcb_ajax.ajax_url,
|
||||||
|
type: 'POST',
|
||||||
|
data: {
|
||||||
|
action: 'mcb_get_repo_files',
|
||||||
|
repo: this.repo,
|
||||||
|
path: path,
|
||||||
|
nonce: mcb_ajax.nonce
|
||||||
|
},
|
||||||
|
success: (response) => {
|
||||||
|
console.log('MCB: AJAX response:', response);
|
||||||
|
this.removeRequest(request);
|
||||||
|
if (response.success) {
|
||||||
|
console.log('MCB: Files loaded:', response.data);
|
||||||
|
this.files = response.data;
|
||||||
|
this.renderFileList();
|
||||||
|
|
||||||
|
// Load initial file if specified
|
||||||
|
if (this.initialFile) {
|
||||||
|
const file = this.files.find(f => f.path === this.initialFile);
|
||||||
|
if (file) {
|
||||||
|
this.loadFile(this.initialFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('MCB: Error:', response.data);
|
||||||
|
$fileList.html('<div class="mcb-error">' + response.data + '</div>');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (xhr, status, error) => {
|
||||||
|
console.error('MCB: AJAX failed:', status, error);
|
||||||
|
this.removeRequest(request);
|
||||||
|
$fileList.html('<div class="mcb-error">Failed to load repository files: ' + error + '</div>');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.activeRequests.push(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderFileList() {
|
||||||
|
const $fileList = this.$element.find('.mcb-file-list');
|
||||||
|
$fileList.empty();
|
||||||
|
|
||||||
|
// Add current path breadcrumb if not at root
|
||||||
|
if (this.currentPath) {
|
||||||
|
const $breadcrumb = $('<div class="mcb-breadcrumb">');
|
||||||
|
$breadcrumb.append('<span class="mcb-path-label">Path: </span>');
|
||||||
|
$breadcrumb.append('<span class="mcb-current-path">/' + this.escapeHtml(this.currentPath) + '</span>');
|
||||||
|
$fileList.append($breadcrumb);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render files and folders
|
||||||
|
this.files.forEach(file => {
|
||||||
|
const $item = $('<div class="mcb-file-item">')
|
||||||
|
.attr('data-path', file.path)
|
||||||
|
.attr('data-name', file.name.toLowerCase())
|
||||||
|
.attr('data-is-folder', file.is_folder || false);
|
||||||
|
|
||||||
|
// Add appropriate icon
|
||||||
|
let icon;
|
||||||
|
if (file.type === 'parent') {
|
||||||
|
icon = '⬆'; // Up arrow for parent
|
||||||
|
$item.addClass('mcb-parent-folder');
|
||||||
|
} else if (file.is_folder) {
|
||||||
|
icon = '📁'; // Folder icon
|
||||||
|
$item.addClass('mcb-folder');
|
||||||
|
} else {
|
||||||
|
icon = this.getFileIcon(file.type);
|
||||||
|
$item.addClass('mcb-file');
|
||||||
|
}
|
||||||
|
|
||||||
|
$item.append('<span class="mcb-file-icon">' + icon + '</span>');
|
||||||
|
$item.append('<span class="mcb-file-name">' + this.escapeHtml(file.name) + '</span>');
|
||||||
|
|
||||||
|
// Add file size for files only
|
||||||
|
if (!file.is_folder) {
|
||||||
|
const sizeStr = this.formatFileSize(file.size);
|
||||||
|
$item.append('<span class="mcb-file-size">' + sizeStr + '</span>');
|
||||||
|
}
|
||||||
|
|
||||||
|
$fileList.append($item);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
organizeFileTree(files) {
|
||||||
|
const tree = {};
|
||||||
|
files.forEach(file => {
|
||||||
|
const parts = file.path.split('/');
|
||||||
|
let current = tree;
|
||||||
|
|
||||||
|
parts.forEach((part, index) => {
|
||||||
|
if (index === parts.length - 1) {
|
||||||
|
// It's a file
|
||||||
|
current[part] = file;
|
||||||
|
} else {
|
||||||
|
// It's a directory
|
||||||
|
if (!current[part]) {
|
||||||
|
current[part] = {};
|
||||||
|
}
|
||||||
|
current = current[part];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return tree;
|
||||||
|
}
|
||||||
|
|
||||||
|
filterFiles(searchTerm) {
|
||||||
|
const $items = this.$element.find('.mcb-file-item');
|
||||||
|
const term = searchTerm.toLowerCase();
|
||||||
|
|
||||||
|
$items.each((index, item) => {
|
||||||
|
const $item = $(item);
|
||||||
|
const fileName = $item.data('name');
|
||||||
|
|
||||||
|
if (!term || fileName.includes(term)) {
|
||||||
|
$item.show();
|
||||||
|
} else {
|
||||||
|
$item.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadFile(filePath) {
|
||||||
|
// Limit cache size to prevent memory issues
|
||||||
|
this.limitCacheSize();
|
||||||
|
|
||||||
|
// Check if file is already in cache
|
||||||
|
if (this.fileCache[filePath]) {
|
||||||
|
this.displayFile(filePath, this.fileCache[filePath]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update status
|
||||||
|
this.updateStatus('Loading file...');
|
||||||
|
|
||||||
|
const request = $.ajax({
|
||||||
|
url: mcb_ajax.ajax_url,
|
||||||
|
type: 'POST',
|
||||||
|
data: {
|
||||||
|
action: 'mcb_load_file',
|
||||||
|
repo: this.repo,
|
||||||
|
file_path: filePath,
|
||||||
|
nonce: mcb_ajax.nonce
|
||||||
|
},
|
||||||
|
success: (response) => {
|
||||||
|
this.removeRequest(request);
|
||||||
|
if (response.success) {
|
||||||
|
this.fileCache[filePath] = response.data;
|
||||||
|
this.displayFile(filePath, response.data);
|
||||||
|
this.updateStatus('Ready');
|
||||||
|
} else {
|
||||||
|
this.showError('Failed to load file: ' + response.data);
|
||||||
|
this.updateStatus('Error loading file');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
this.removeRequest(request);
|
||||||
|
this.showError('Network error while loading file');
|
||||||
|
this.updateStatus('Network error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.activeRequests.push(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
displayFile(filePath, fileData) {
|
||||||
|
// Store in cache
|
||||||
|
this.fileCache[filePath] = fileData;
|
||||||
|
|
||||||
|
// Limit number of open tabs to prevent memory issues
|
||||||
|
const maxTabs = 10;
|
||||||
|
|
||||||
|
// Add to tabs if not already open
|
||||||
|
if (!this.openTabs.includes(filePath)) {
|
||||||
|
// Check tab limit
|
||||||
|
if (this.openTabs.length >= maxTabs) {
|
||||||
|
// Close the oldest tab
|
||||||
|
const oldestTab = this.openTabs[0];
|
||||||
|
this.closeTab(oldestTab);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.openTabs.push(filePath);
|
||||||
|
this.addTab(filePath, fileData.filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only switch to tab if not already active
|
||||||
|
if (this.activeTab !== filePath) {
|
||||||
|
this.switchToTab(filePath);
|
||||||
|
} else {
|
||||||
|
// Just update the content if already active
|
||||||
|
const $codeArea = this.$element.find('.mcb-code-area');
|
||||||
|
const safeContent = this.createSafeCodeDisplay(fileData.content, fileData.filename);
|
||||||
|
$codeArea.html(safeContent);
|
||||||
|
|
||||||
|
// Apply syntax highlighting
|
||||||
|
if (typeof Prism !== 'undefined') {
|
||||||
|
Prism.highlightAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update status
|
||||||
|
const fileSize = this.formatFileSize(fileData.content.length);
|
||||||
|
const lineCount = fileData.content.split('\n').length;
|
||||||
|
this.$element.find('.mcb-file-info').text(fileData.filename + ' • ' + lineCount + ' lines • ' + fileSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display content
|
||||||
|
const $codeArea = this.$element.find('.mcb-code-area');
|
||||||
|
|
||||||
|
// Create safe HTML content
|
||||||
|
const safeContent = this.createSafeCodeDisplay(fileData.content, fileData.filename);
|
||||||
|
$codeArea.html(safeContent);
|
||||||
|
|
||||||
|
// Apply syntax highlighting if Prism is loaded
|
||||||
|
if (typeof Prism !== 'undefined') {
|
||||||
|
Prism.highlightAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark file as active in sidebar
|
||||||
|
this.$element.find('.mcb-file-item').removeClass('active');
|
||||||
|
this.$element.find('.mcb-file-item[data-path="' + filePath + '"]').addClass('active');
|
||||||
|
|
||||||
|
// Update file info
|
||||||
|
const file = this.files.find(f => f.path === filePath);
|
||||||
|
if (file) {
|
||||||
|
this.updateFileInfo(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createSafeCodeDisplay(content, filename) {
|
||||||
|
// The content is already escaped by PHP, but we'll double-check
|
||||||
|
const $container = $('<div class="mcb-code-container">');
|
||||||
|
|
||||||
|
// Header
|
||||||
|
const $header = $('<div class="mcb-code-header">');
|
||||||
|
$header.append($('<span class="mcb-filename">').text(filename));
|
||||||
|
|
||||||
|
// Copy button - store content in data, not attribute
|
||||||
|
const $copyBtn = $('<button class="mcb-copy-btn">Copy</button>');
|
||||||
|
$copyBtn.data('content', content); // Use data() instead of attr()
|
||||||
|
$header.append($copyBtn);
|
||||||
|
|
||||||
|
$container.append($header);
|
||||||
|
|
||||||
|
// Code wrapper
|
||||||
|
const $wrapper = $('<div class="mcb-code-wrapper">');
|
||||||
|
const language = this.detectLanguage(filename);
|
||||||
|
|
||||||
|
// Create pre/code structure
|
||||||
|
const $pre = $('<pre>').addClass('line-numbers');
|
||||||
|
const $code = $('<code>').addClass('language-' + language);
|
||||||
|
|
||||||
|
// CRITICAL: Ensure content is text, not HTML
|
||||||
|
$code.text(content);
|
||||||
|
|
||||||
|
$pre.append($code);
|
||||||
|
$wrapper.append($pre);
|
||||||
|
$container.append($wrapper);
|
||||||
|
|
||||||
|
return $container;
|
||||||
|
}
|
||||||
|
|
||||||
|
addTab(filePath, filename) {
|
||||||
|
const $tabs = this.$element.find('.mcb-tabs');
|
||||||
|
|
||||||
|
const $tab = $('<div class="mcb-tab">')
|
||||||
|
.attr('data-path', filePath);
|
||||||
|
|
||||||
|
$tab.append('<span class="mcb-tab-title">' + this.escapeHtml(filename) + '</span>');
|
||||||
|
$tab.append('<span class="mcb-tab-close">×</span>');
|
||||||
|
|
||||||
|
$tabs.append($tab);
|
||||||
|
}
|
||||||
|
|
||||||
|
switchToTab(filePath) {
|
||||||
|
// Prevent switching if already active
|
||||||
|
if (this.activeTab === filePath) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.activeTab = filePath;
|
||||||
|
|
||||||
|
// Update tab states
|
||||||
|
this.$element.find('.mcb-tab').removeClass('active');
|
||||||
|
this.$element.find('.mcb-tab[data-path="' + filePath + '"]').addClass('active');
|
||||||
|
|
||||||
|
// Load file content if cached, otherwise just display the tab
|
||||||
|
if (this.fileCache[filePath]) {
|
||||||
|
// Display cached content without calling displayFile
|
||||||
|
const $codeArea = this.$element.find('.mcb-code-area');
|
||||||
|
const fileData = this.fileCache[filePath];
|
||||||
|
const safeContent = this.createSafeCodeDisplay(fileData.content, fileData.filename);
|
||||||
|
$codeArea.html(safeContent);
|
||||||
|
|
||||||
|
// Apply syntax highlighting if Prism is loaded
|
||||||
|
if (typeof Prism !== 'undefined') {
|
||||||
|
Prism.highlightAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update status
|
||||||
|
const fileSize = this.formatFileSize(fileData.content.length);
|
||||||
|
const lineCount = fileData.content.split('\n').length;
|
||||||
|
this.$element.find('.mcb-file-info').text(fileData.filename + ' • ' + lineCount + ' lines • ' + fileSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closeTab(filePath) {
|
||||||
|
// Remove from open tabs
|
||||||
|
const index = this.openTabs.indexOf(filePath);
|
||||||
|
if (index > -1) {
|
||||||
|
this.openTabs.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove tab element
|
||||||
|
this.$element.find('.mcb-tab[data-path="' + filePath + '"]').remove();
|
||||||
|
|
||||||
|
// If this was the active tab, switch to another
|
||||||
|
if (this.activeTab === filePath) {
|
||||||
|
if (this.openTabs.length > 0) {
|
||||||
|
this.switchToTab(this.openTabs[this.openTabs.length - 1]);
|
||||||
|
} else {
|
||||||
|
// Show welcome screen
|
||||||
|
this.showWelcome();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
copyCode($button) {
|
||||||
|
const content = $button.data('content'); // Use data() instead of attr()
|
||||||
|
|
||||||
|
if (!content) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create temporary textarea
|
||||||
|
const $temp = $('<textarea>');
|
||||||
|
$temp.val(content);
|
||||||
|
$('body').append($temp);
|
||||||
|
$temp.select();
|
||||||
|
|
||||||
|
try {
|
||||||
|
document.execCommand('copy');
|
||||||
|
$button.text('Copied!').addClass('copied');
|
||||||
|
setTimeout(() => {
|
||||||
|
$button.text('Copy').removeClass('copied');
|
||||||
|
}, 2000);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to copy:', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
$temp.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshFiles() {
|
||||||
|
// Abort any pending requests first
|
||||||
|
this.abortActiveRequests();
|
||||||
|
|
||||||
|
// Clear cache
|
||||||
|
this.fileCache = {};
|
||||||
|
|
||||||
|
// Reload files at current path
|
||||||
|
this.loadRepositoryFiles(this.currentPath);
|
||||||
|
this.updateStatus('Repository refreshed');
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleFullscreen() {
|
||||||
|
this.$element.toggleClass('fullscreen');
|
||||||
|
}
|
||||||
|
|
||||||
|
showWelcome() {
|
||||||
|
const $codeArea = this.$element.find('.mcb-code-area');
|
||||||
|
const welcome = `
|
||||||
|
<div class="mcb-welcome">
|
||||||
|
<svg viewBox="0 0 24 24" width="64" height="64">
|
||||||
|
<path d="M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z"/>
|
||||||
|
</svg>
|
||||||
|
<h4>GitHub Code Viewer</h4>
|
||||||
|
<p>Select a file from the sidebar to view its content</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
$codeArea.html(welcome);
|
||||||
|
}
|
||||||
|
|
||||||
|
showError(message) {
|
||||||
|
const $codeArea = this.$element.find('.mcb-code-area');
|
||||||
|
$codeArea.html('<div class="mcb-error">' + this.escapeHtml(message) + '</div>');
|
||||||
|
}
|
||||||
|
|
||||||
|
updateStatus(text) {
|
||||||
|
this.$element.find('.mcb-status-text').text(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFileInfo(file) {
|
||||||
|
const info = file.name + ' • ' + this.formatFileSize(file.size) + ' • ' + file.type;
|
||||||
|
this.$element.find('.mcb-file-info').text(info);
|
||||||
|
}
|
||||||
|
|
||||||
|
formatFileSize(bytes) {
|
||||||
|
if (bytes === 0) return '0 B';
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['B', 'KB', 'MB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
detectLanguage(filename) {
|
||||||
|
const ext = filename.split('.').pop().toLowerCase();
|
||||||
|
const languageMap = {
|
||||||
|
'js': 'javascript',
|
||||||
|
'jsx': 'jsx',
|
||||||
|
'ts': 'typescript',
|
||||||
|
'tsx': 'tsx',
|
||||||
|
'py': 'python',
|
||||||
|
'rb': 'ruby',
|
||||||
|
'php': 'php',
|
||||||
|
'java': 'java',
|
||||||
|
'c': 'c',
|
||||||
|
'cpp': 'cpp',
|
||||||
|
'cs': 'csharp',
|
||||||
|
'go': 'go',
|
||||||
|
'rs': 'rust',
|
||||||
|
'swift': 'swift',
|
||||||
|
'kt': 'kotlin',
|
||||||
|
'scala': 'scala',
|
||||||
|
'r': 'r',
|
||||||
|
'sql': 'sql',
|
||||||
|
'sh': 'bash',
|
||||||
|
'yml': 'yaml',
|
||||||
|
'json': 'json',
|
||||||
|
'xml': 'xml',
|
||||||
|
'html': 'html',
|
||||||
|
'css': 'css',
|
||||||
|
'scss': 'scss',
|
||||||
|
'md': 'markdown'
|
||||||
|
};
|
||||||
|
|
||||||
|
return languageMap[ext] || 'plain';
|
||||||
|
}
|
||||||
|
|
||||||
|
getFileIcon(type) {
|
||||||
|
const icons = {
|
||||||
|
'javascript': '📜',
|
||||||
|
'python': '🐍',
|
||||||
|
'php': '🐘',
|
||||||
|
'java': '☕',
|
||||||
|
'cpp': '⚙️',
|
||||||
|
'go': '🐹',
|
||||||
|
'rust': '🦀',
|
||||||
|
'ruby': '💎',
|
||||||
|
'html': '🌐',
|
||||||
|
'css': '🎨',
|
||||||
|
'json': '📋',
|
||||||
|
'markdown': '📝',
|
||||||
|
'default': '📄'
|
||||||
|
};
|
||||||
|
|
||||||
|
return icons[type] || icons.default;
|
||||||
|
}
|
||||||
|
|
||||||
|
escapeHtml(text) {
|
||||||
|
const map = {
|
||||||
|
'&': '&',
|
||||||
|
'<': '<',
|
||||||
|
'>': '>',
|
||||||
|
'"': '"',
|
||||||
|
"'": '''
|
||||||
|
};
|
||||||
|
|
||||||
|
return text.replace(/[&<>"']/g, m => map[m]);
|
||||||
|
}
|
||||||
|
|
||||||
|
limitCacheSize() {
|
||||||
|
// Limit cache to 20 files to prevent memory issues
|
||||||
|
const maxCacheSize = 20;
|
||||||
|
const cacheKeys = Object.keys(this.fileCache);
|
||||||
|
|
||||||
|
if (cacheKeys.length >= maxCacheSize) {
|
||||||
|
// Remove oldest cached files (FIFO)
|
||||||
|
const toRemove = cacheKeys.slice(0, cacheKeys.length - maxCacheSize + 1);
|
||||||
|
toRemove.forEach(key => {
|
||||||
|
delete this.fileCache[key];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanupBeforeUnload() {
|
||||||
|
// Clean up event listeners and large objects
|
||||||
|
this.$element.off();
|
||||||
|
this.abortActiveRequests();
|
||||||
|
this.fileCache = null;
|
||||||
|
this.files = null;
|
||||||
|
this.openTabs = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
abortActiveRequests() {
|
||||||
|
// Abort all pending AJAX requests
|
||||||
|
if (this.activeRequests && this.activeRequests.length > 0) {
|
||||||
|
this.activeRequests.forEach(request => {
|
||||||
|
if (request && request.abort) {
|
||||||
|
request.abort();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.activeRequests = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeRequest(request) {
|
||||||
|
const index = this.activeRequests.indexOf(request);
|
||||||
|
if (index > -1) {
|
||||||
|
this.activeRequests.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize all viewers on page
|
||||||
|
$(document).ready(() => {
|
||||||
|
console.log('MCB: Document ready, looking for .maple-code-blocks elements');
|
||||||
|
const viewers = [];
|
||||||
|
|
||||||
|
$('.maple-code-blocks').each(function() {
|
||||||
|
console.log('MCB: Initializing viewer for element:', this);
|
||||||
|
try {
|
||||||
|
const viewer = new GitHubCodeViewer(this);
|
||||||
|
viewers.push(viewer);
|
||||||
|
console.log('MCB: Viewer initialized successfully');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('MCB: Error initializing viewer:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('MCB: Total viewers initialized:', viewers.length);
|
||||||
|
|
||||||
|
// Cleanup on page unload to prevent memory leaks
|
||||||
|
$(window).on('beforeunload', () => {
|
||||||
|
viewers.forEach(viewer => {
|
||||||
|
if (viewer && viewer.cleanupBeforeUnload) {
|
||||||
|
viewer.cleanupBeforeUnload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
})(jQuery);
|
||||||
182
native/wordpress/maple-code-blocks/assets/js/prism.js
Normal file
182
native/wordpress/maple-code-blocks/assets/js/prism.js
Normal file
|
|
@ -0,0 +1,182 @@
|
||||||
|
/* PrismJS - Minimal Core for GitHub Code Viewer */
|
||||||
|
/* This is a simplified version focused on safety and basic highlighting */
|
||||||
|
|
||||||
|
(function(){
|
||||||
|
|
||||||
|
if (typeof self === 'undefined' || !self.Prism || !self.document) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var Prism = window.Prism = {
|
||||||
|
languages: {},
|
||||||
|
|
||||||
|
// Simplified tokenization with infinite loop prevention
|
||||||
|
tokenize: function(text, grammar) {
|
||||||
|
// Prevent tokenizing huge files
|
||||||
|
if (text.length > 100000) {
|
||||||
|
return [text]; // Return as plain text for very large files
|
||||||
|
}
|
||||||
|
|
||||||
|
var tokens = [];
|
||||||
|
var rest = text;
|
||||||
|
var maxIterations = 10000; // Prevent infinite loops
|
||||||
|
var iterations = 0;
|
||||||
|
|
||||||
|
while (rest.length > 0 && iterations < maxIterations) {
|
||||||
|
iterations++;
|
||||||
|
var matchFound = false;
|
||||||
|
|
||||||
|
for (var token in grammar) {
|
||||||
|
if (!grammar.hasOwnProperty(token) || !grammar[token]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var pattern = grammar[token];
|
||||||
|
if (pattern instanceof RegExp) {
|
||||||
|
var matches = rest.match(pattern);
|
||||||
|
|
||||||
|
if (matches && matches.index === 0) {
|
||||||
|
matchFound = true;
|
||||||
|
tokens.push({
|
||||||
|
type: token,
|
||||||
|
content: matches[0]
|
||||||
|
});
|
||||||
|
rest = rest.substring(matches[0].length);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no match found, consume one character as plain text
|
||||||
|
if (!matchFound) {
|
||||||
|
if (tokens.length > 0 && typeof tokens[tokens.length - 1] === 'string') {
|
||||||
|
tokens[tokens.length - 1] += rest.charAt(0);
|
||||||
|
} else {
|
||||||
|
tokens.push(rest.charAt(0));
|
||||||
|
}
|
||||||
|
rest = rest.substring(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add any remaining text
|
||||||
|
if (rest.length > 0) {
|
||||||
|
tokens.push(rest);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokens;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Highlight element
|
||||||
|
highlightElement: function(element) {
|
||||||
|
var language = element.className.match(/language-(\w+)/);
|
||||||
|
if (!language) return;
|
||||||
|
|
||||||
|
language = language[1];
|
||||||
|
var grammar = Prism.languages[language];
|
||||||
|
|
||||||
|
if (!grammar) return;
|
||||||
|
|
||||||
|
// Get text content (already HTML escaped by PHP)
|
||||||
|
var code = element.textContent;
|
||||||
|
|
||||||
|
// Simple tokenization
|
||||||
|
var tokens = Prism.tokenize(code, grammar);
|
||||||
|
|
||||||
|
// Build highlighted HTML (safe because content is already escaped)
|
||||||
|
var highlighted = tokens.map(function(token) {
|
||||||
|
if (typeof token === 'string') {
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
return '<span class="token ' + token.type + '">' + token.content + '</span>';
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
element.innerHTML = highlighted;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Highlight all code blocks
|
||||||
|
highlightAll: function() {
|
||||||
|
var elements = document.querySelectorAll('code[class*="language-"], [class*="language-"] code');
|
||||||
|
for (var i = 0, element; element = elements[i++];) {
|
||||||
|
Prism.highlightElement(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Define basic grammars for common languages
|
||||||
|
Prism.languages.javascript = {
|
||||||
|
'comment': /\/\/.*|\/\*[\s\S]*?\*\//,
|
||||||
|
'string': /(["'])(?:\\.|(?!\1)[^\\\r\n])*\1/,
|
||||||
|
'keyword': /\b(?:var|let|const|function|return|if|else|for|while|do|switch|case|break|continue|typeof|instanceof|new|this|throw|try|catch|finally|async|await|class|extends|super|import|export|default|yield)\b/,
|
||||||
|
'boolean': /\b(?:true|false)\b/,
|
||||||
|
'number': /\b0x[\da-f]+\b|(?:\b\d+\.?\d*|\B\.\d+)(?:e[+-]?\d+)?/i,
|
||||||
|
'function': /[_$a-zA-Z\xA0-\uFFFF][_$a-zA-Z0-9\xA0-\uFFFF]*(?=\s*\()/,
|
||||||
|
'operator': /[+\-*/%=!<>&|^~?:]+/,
|
||||||
|
'punctuation': /[{}[\];(),.:]/
|
||||||
|
};
|
||||||
|
|
||||||
|
Prism.languages.python = {
|
||||||
|
'comment': /#.*/,
|
||||||
|
'string': /(["'])(?:\\.|(?!\1)[^\\\r\n])*\1/,
|
||||||
|
'keyword': /\b(?:and|as|assert|async|await|break|class|continue|def|del|elif|else|except|exec|finally|for|from|global|if|import|in|is|lambda|not|or|pass|print|raise|return|try|while|with|yield)\b/,
|
||||||
|
'boolean': /\b(?:True|False|None)\b/,
|
||||||
|
'number': /\b0x[\da-f]+\b|(?:\b\d+\.?\d*|\B\.\d+)(?:e[+-]?\d+)?j?/i,
|
||||||
|
'operator': /[+\-*/%=!<>&|^~]+/,
|
||||||
|
'punctuation': /[{}[\];(),.:]/
|
||||||
|
};
|
||||||
|
|
||||||
|
Prism.languages.php = {
|
||||||
|
'comment': /\/\/.*|\/\*[\s\S]*?\*\/|#.*/,
|
||||||
|
'string': /(["'])(?:\\.|(?!\1)[^\\\r\n])*\1/,
|
||||||
|
'keyword': /\b(?:abstract|and|array|as|break|callable|case|catch|class|clone|const|continue|declare|default|die|do|echo|else|elseif|empty|enddeclare|endfor|endforeach|endif|endswitch|endwhile|eval|exit|extends|final|finally|for|foreach|function|global|goto|if|implements|include|include_once|instanceof|insteadof|interface|isset|list|namespace|new|or|print|private|protected|public|require|require_once|return|static|switch|throw|trait|try|unset|use|var|while|xor|yield)\b/i,
|
||||||
|
'boolean': /\b(?:false|true)\b/i,
|
||||||
|
'variable': /\$\w+/,
|
||||||
|
'number': /\b0x[\da-f]+\b|(?:\b\d+\.?\d*|\B\.\d+)(?:e[+-]?\d+)?/i,
|
||||||
|
'operator': /[+\-*/%=!<>&|^~?:]+/,
|
||||||
|
'punctuation': /[{}[\];(),.:]/
|
||||||
|
};
|
||||||
|
|
||||||
|
Prism.languages.html = Prism.languages.xml = {
|
||||||
|
'comment': /<!--[\s\S]*?-->/,
|
||||||
|
'tag': {
|
||||||
|
pattern: /<\/?[^\s>\/]+(?:\s+[^\s>\/=]+(?:=(?:("|')(?:\\.|(?!\1)[^\\])*\1|[^\s'">=]+))?)*\s*\/?>/i,
|
||||||
|
inside: {
|
||||||
|
'punctuation': /^<\/?|\/?>$/,
|
||||||
|
'attr-name': /[^\s>\/]+/,
|
||||||
|
'attr-value': /=(?:("|')(?:\\.|(?!\1)[^\\])*\1|[^\s'">=]+)/i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Prism.languages.css = {
|
||||||
|
'comment': /\/\*[\s\S]*?\*\//,
|
||||||
|
'atrule': /@[\w-]+?.*?(?:;|(?=\s*\{))/i,
|
||||||
|
'url': /url\((?:(["'])(?:\\.|(?!\1)[^\\\r\n])*\1|.*?)\)/i,
|
||||||
|
'selector': /[^{}\s][^{}]*(?=\s*\{)/,
|
||||||
|
'string': /(["'])(?:\\.|(?!\1)[^\\\r\n])*\1/,
|
||||||
|
'property': /[-_a-z\xA0-\uFFFF][-\w\xA0-\uFFFF]*(?=\s*:)/i,
|
||||||
|
'important': /!important\b/i,
|
||||||
|
'punctuation': /[(){};:]/
|
||||||
|
};
|
||||||
|
|
||||||
|
Prism.languages.json = {
|
||||||
|
'property': /"(?:\\.|[^\\"\r\n])*"(?=\s*:)/i,
|
||||||
|
'string': /"(?:\\.|[^\\"\r\n])*"(?!\s*:)/,
|
||||||
|
'number': /\b0x[\dA-Fa-f]+\b|(?:\b\d+\.?\d*|\B\.\d+)(?:[Ee][+-]?\d+)?/,
|
||||||
|
'punctuation': /[{}[\]);,]/,
|
||||||
|
'operator': /:/g,
|
||||||
|
'boolean': /\b(?:true|false)\b/i,
|
||||||
|
'null': /\bnull\b/i
|
||||||
|
};
|
||||||
|
|
||||||
|
// Language aliases
|
||||||
|
Prism.languages.js = Prism.languages.javascript;
|
||||||
|
Prism.languages.py = Prism.languages.python;
|
||||||
|
|
||||||
|
// Auto-initialize
|
||||||
|
if (document.readyState !== 'loading') {
|
||||||
|
setTimeout(Prism.highlightAll, 0);
|
||||||
|
} else {
|
||||||
|
document.addEventListener('DOMContentLoaded', Prism.highlightAll);
|
||||||
|
}
|
||||||
|
|
||||||
|
})();
|
||||||
118
native/wordpress/maple-code-blocks/assets/js/simple-block.js
Normal file
118
native/wordpress/maple-code-blocks/assets/js/simple-block.js
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
// Maple Code Blocks - Simple Block Registration
|
||||||
|
console.log('Maple Code Blocks: Starting block registration');
|
||||||
|
|
||||||
|
(function(wp) {
|
||||||
|
// Check if wp.blocks exists
|
||||||
|
if (!wp || !wp.blocks) {
|
||||||
|
console.error('Maple Code Blocks: wp.blocks not available');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { registerBlockType } = wp.blocks;
|
||||||
|
const { TextControl, PanelBody, SelectControl } = wp.components;
|
||||||
|
const { InspectorControls } = wp.blockEditor || wp.editor || {};
|
||||||
|
const { Fragment, createElement: el } = wp.element;
|
||||||
|
|
||||||
|
console.log('Maple Code Blocks: Dependencies loaded, registering block...');
|
||||||
|
|
||||||
|
// Register the main Maple Code Block
|
||||||
|
const blockRegistered = registerBlockType('maple-code-blocks/code-block', {
|
||||||
|
title: 'Maple Code Block',
|
||||||
|
description: 'Display code from GitHub, GitLab, Bitbucket or Codeberg',
|
||||||
|
category: 'widgets',
|
||||||
|
icon: 'editor-code',
|
||||||
|
keywords: ['maple', 'code', 'github', 'gitlab', 'bitbucket'],
|
||||||
|
attributes: {
|
||||||
|
repository: {
|
||||||
|
type: 'string',
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
theme: {
|
||||||
|
type: 'string',
|
||||||
|
default: 'dark'
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
type: 'string',
|
||||||
|
default: '600px'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
edit: function(props) {
|
||||||
|
const { attributes, setAttributes } = props;
|
||||||
|
const { repository, theme, height } = attributes;
|
||||||
|
|
||||||
|
return el(Fragment, {},
|
||||||
|
el(InspectorControls, {},
|
||||||
|
el(PanelBody, { title: 'Repository Settings', initialOpen: true },
|
||||||
|
el(TextControl, {
|
||||||
|
label: 'Repository',
|
||||||
|
value: repository,
|
||||||
|
onChange: function(value) { setAttributes({ repository: value }) },
|
||||||
|
placeholder: 'e.g., facebook/react or gitlab:gnome/gimp',
|
||||||
|
help: 'Format: [platform:]owner/repo. Platforms: github (default), gitlab, bitbucket, codeberg. Examples: facebook/react, gitlab:gitlab-org/gitlab, bitbucket:atlassian/python-bitbucket, codeberg:forgejo/forgejo'
|
||||||
|
}),
|
||||||
|
el(SelectControl, {
|
||||||
|
label: 'Theme',
|
||||||
|
value: theme,
|
||||||
|
options: [
|
||||||
|
{ label: 'Dark', value: 'dark' },
|
||||||
|
{ label: 'Light', value: 'light' },
|
||||||
|
{ label: 'Monokai', value: 'monokai' },
|
||||||
|
{ label: 'Solarized', value: 'solarized' }
|
||||||
|
],
|
||||||
|
onChange: function(value) { setAttributes({ theme: value }) }
|
||||||
|
}),
|
||||||
|
el(TextControl, {
|
||||||
|
label: 'Height',
|
||||||
|
value: height,
|
||||||
|
onChange: function(value) { setAttributes({ height: value }) },
|
||||||
|
placeholder: '600px'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
),
|
||||||
|
el('div', {
|
||||||
|
className: 'maple-code-block-editor',
|
||||||
|
style: {
|
||||||
|
padding: '20px',
|
||||||
|
backgroundColor: theme === 'dark' ? '#1e1e1e' : '#fff',
|
||||||
|
color: theme === 'dark' ? '#fff' : '#000',
|
||||||
|
border: '1px solid #ddd',
|
||||||
|
borderRadius: '4px'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
el('h3', { style: { marginTop: 0 } }, 'Maple Code Block'),
|
||||||
|
repository ?
|
||||||
|
el('p', {}, 'Repository: ', el('strong', {}, repository)) :
|
||||||
|
el('p', { style: { color: '#999' } }, 'Enter a repository in the block settings'),
|
||||||
|
el('p', {}, 'Theme: ', el('strong', {}, theme)),
|
||||||
|
el('p', {}, 'Height: ', el('strong', {}, height))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
save: function() {
|
||||||
|
// Rendered server-side
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (blockRegistered) {
|
||||||
|
console.log('Maple Code Blocks: Block registered successfully!', blockRegistered);
|
||||||
|
} else {
|
||||||
|
console.error('Maple Code Blocks: Block registration failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
})(window.wp);
|
||||||
|
|
||||||
|
// Also try registering with global wp if window.wp fails
|
||||||
|
if (typeof wp !== 'undefined' && wp.blocks && !wp.blocks.getBlockType('maple-code-blocks/code-block')) {
|
||||||
|
console.log('Maple Code Blocks: Attempting registration with global wp');
|
||||||
|
wp.blocks.registerBlockType('maple-code-blocks/code-block', {
|
||||||
|
title: 'Maple Code Block',
|
||||||
|
description: 'Display code from repositories',
|
||||||
|
category: 'widgets',
|
||||||
|
icon: 'editor-code',
|
||||||
|
edit: function() { return wp.element.createElement('div', {}, 'Maple Code Block'); },
|
||||||
|
save: function() { return null; }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
(function(wp) {
|
||||||
|
const { registerBlockVariation } = wp.blocks;
|
||||||
|
|
||||||
|
wp.domReady(function() {
|
||||||
|
// Register variations for the Maple Code Block
|
||||||
|
registerBlockVariation('maple-code-blocks/code-block', {
|
||||||
|
name: 'maple-github',
|
||||||
|
title: 'Maple: GitHub Code',
|
||||||
|
description: 'Display code from GitHub',
|
||||||
|
icon: 'editor-code',
|
||||||
|
attributes: {
|
||||||
|
repository: 'facebook/react',
|
||||||
|
theme: 'dark'
|
||||||
|
},
|
||||||
|
scope: ['inserter']
|
||||||
|
});
|
||||||
|
|
||||||
|
registerBlockVariation('maple-code-blocks/code-block', {
|
||||||
|
name: 'maple-gitlab',
|
||||||
|
title: 'Maple: GitLab Code',
|
||||||
|
description: 'Display code from GitLab',
|
||||||
|
icon: 'editor-code',
|
||||||
|
attributes: {
|
||||||
|
repository: 'gitlab:gitlab-org/gitlab',
|
||||||
|
theme: 'dark'
|
||||||
|
},
|
||||||
|
scope: ['inserter']
|
||||||
|
});
|
||||||
|
|
||||||
|
registerBlockVariation('maple-code-blocks/code-block', {
|
||||||
|
name: 'maple-documentation',
|
||||||
|
title: 'Maple: Documentation',
|
||||||
|
description: 'Display README or docs',
|
||||||
|
icon: 'media-document',
|
||||||
|
attributes: {
|
||||||
|
repository: '',
|
||||||
|
theme: 'light',
|
||||||
|
height: '400px'
|
||||||
|
},
|
||||||
|
scope: ['inserter']
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Maple Code Blocks: Variations registered');
|
||||||
|
});
|
||||||
|
})(window.wp);
|
||||||
33
native/wordpress/maple-code-blocks/includes/basic-block.php
Normal file
33
native/wordpress/maple-code-blocks/includes/basic-block.php
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Ultra Simple Block Registration - PHP Only
|
||||||
|
* This ensures the block is registered even if JavaScript fails
|
||||||
|
*/
|
||||||
|
|
||||||
|
add_action('init', function() {
|
||||||
|
if (!function_exists('register_block_type')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register a basic block with PHP only
|
||||||
|
register_block_type('maple-code-blocks/basic', array(
|
||||||
|
'title' => 'Maple Code Block (Basic)',
|
||||||
|
'description' => 'Display code from repositories',
|
||||||
|
'category' => 'widgets',
|
||||||
|
'icon' => 'editor-code',
|
||||||
|
'keywords' => array('maple', 'code', 'github'),
|
||||||
|
'attributes' => array(
|
||||||
|
'repository' => array(
|
||||||
|
'type' => 'string',
|
||||||
|
'default' => ''
|
||||||
|
)
|
||||||
|
),
|
||||||
|
'render_callback' => function($attributes) {
|
||||||
|
$repo = isset($attributes['repository']) ? $attributes['repository'] : '';
|
||||||
|
if (empty($repo)) {
|
||||||
|
return '<p>Enter a repository in block settings</p>';
|
||||||
|
}
|
||||||
|
return do_shortcode('[maple_code_block repo="' . esc_attr($repo) . '"]');
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}, 100); // Very late priority to ensure everything is loaded
|
||||||
|
|
@ -0,0 +1,351 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Gutenberg Block Registration
|
||||||
|
* Registers the GitHub Code Viewer block for the block editor
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Prevent direct access
|
||||||
|
if (!defined('WPINC')) {
|
||||||
|
die('Direct access not permitted.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
class MCB_Block_Editor {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize block editor support
|
||||||
|
*/
|
||||||
|
public static function init() {
|
||||||
|
// Check if Gutenberg is available
|
||||||
|
if (!function_exists('register_block_type')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
add_action('init', array(__CLASS__, 'register_block'));
|
||||||
|
add_action('enqueue_block_editor_assets', array(__CLASS__, 'enqueue_block_editor_assets'));
|
||||||
|
add_action('enqueue_block_assets', array(__CLASS__, 'enqueue_block_assets'));
|
||||||
|
add_filter('block_categories_all', array(__CLASS__, 'add_block_category'), 10, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register the block
|
||||||
|
*/
|
||||||
|
public static function register_block() {
|
||||||
|
// Debug: Log that we're attempting to register
|
||||||
|
error_log('MCB: Attempting to register block');
|
||||||
|
|
||||||
|
// Register block editor script
|
||||||
|
wp_register_script(
|
||||||
|
'mcb-block-editor',
|
||||||
|
MCB_PLUGIN_URL . 'assets/js/block-editor.js',
|
||||||
|
array('wp-blocks', 'wp-element', 'wp-editor', 'wp-components', 'wp-i18n', 'wp-data'),
|
||||||
|
MCB_PLUGIN_VERSION,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
// Register block editor styles
|
||||||
|
wp_register_style(
|
||||||
|
'mcb-block-editor-style',
|
||||||
|
MCB_PLUGIN_URL . 'assets/css/block-editor.css',
|
||||||
|
array('wp-edit-blocks'),
|
||||||
|
MCB_PLUGIN_VERSION
|
||||||
|
);
|
||||||
|
|
||||||
|
// Register the block with simplified attributes first
|
||||||
|
$result = register_block_type('maple-code-blocks/code-block', array(
|
||||||
|
'editor_script' => 'mcb-block-editor',
|
||||||
|
'editor_style' => 'mcb-block-editor-style',
|
||||||
|
'render_callback' => array(__CLASS__, 'render_block'),
|
||||||
|
'attributes' => array(
|
||||||
|
'repository' => array(
|
||||||
|
'type' => 'string',
|
||||||
|
'default' => ''
|
||||||
|
),
|
||||||
|
'theme' => array(
|
||||||
|
'type' => 'string',
|
||||||
|
'default' => 'dark'
|
||||||
|
),
|
||||||
|
'height' => array(
|
||||||
|
'type' => 'string',
|
||||||
|
'default' => '600px'
|
||||||
|
),
|
||||||
|
'showLineNumbers' => array(
|
||||||
|
'type' => 'boolean',
|
||||||
|
'default' => true
|
||||||
|
),
|
||||||
|
'initialFile' => array(
|
||||||
|
'type' => 'string',
|
||||||
|
'default' => ''
|
||||||
|
),
|
||||||
|
'title' => array(
|
||||||
|
'type' => 'string',
|
||||||
|
'default' => ''
|
||||||
|
),
|
||||||
|
'align' => array(
|
||||||
|
'type' => 'string',
|
||||||
|
'default' => 'none'
|
||||||
|
),
|
||||||
|
'className' => array(
|
||||||
|
'type' => 'string',
|
||||||
|
'default' => ''
|
||||||
|
)
|
||||||
|
),
|
||||||
|
'supports' => array(
|
||||||
|
'align' => array('wide', 'full'),
|
||||||
|
'className' => true,
|
||||||
|
'customClassName' => true,
|
||||||
|
'html' => false,
|
||||||
|
'anchor' => true
|
||||||
|
),
|
||||||
|
'example' => array(
|
||||||
|
'attributes' => array(
|
||||||
|
'repository' => 'facebook/react',
|
||||||
|
'theme' => 'dark',
|
||||||
|
'height' => '400px',
|
||||||
|
'title' => 'React Source Code Example'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
));
|
||||||
|
|
||||||
|
// Add block variations
|
||||||
|
wp_register_script(
|
||||||
|
'mcb-block-variations',
|
||||||
|
MCB_PLUGIN_URL . 'assets/js/block-variations.js',
|
||||||
|
array('wp-blocks', 'wp-dom-ready'),
|
||||||
|
MCB_PLUGIN_VERSION,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
wp_enqueue_script('mcb-block-variations');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the block on the frontend
|
||||||
|
*/
|
||||||
|
public static function render_block($attributes, $content) {
|
||||||
|
// Validate required attributes
|
||||||
|
if (empty($attributes['repository'])) {
|
||||||
|
return '<div class="mcb-error">Please specify a repository (e.g., owner/repository)</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanitize attributes
|
||||||
|
$repository = sanitize_text_field($attributes['repository']);
|
||||||
|
|
||||||
|
// Validate repository format
|
||||||
|
if (!MCB_Security::validate_repo_format($repository)) {
|
||||||
|
return '<div class="mcb-error">Invalid repository format. Use: owner/repository</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build shortcode attributes
|
||||||
|
$shortcode_atts = array(
|
||||||
|
'repo' => $repository,
|
||||||
|
'theme' => sanitize_text_field($attributes['theme'] ?? 'dark'),
|
||||||
|
'height' => sanitize_text_field($attributes['height'] ?? '600px'),
|
||||||
|
'show_line_numbers' => $attributes['showLineNumbers'] ? 'true' : 'false',
|
||||||
|
'initial_file' => sanitize_text_field($attributes['initialFile'] ?? ''),
|
||||||
|
'title' => sanitize_text_field($attributes['title'] ?? '')
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add alignment class if needed
|
||||||
|
$wrapper_class = 'mcb-block-wrapper';
|
||||||
|
if (!empty($attributes['align'])) {
|
||||||
|
$wrapper_class .= ' align' . esc_attr($attributes['align']);
|
||||||
|
}
|
||||||
|
if (!empty($attributes['className'])) {
|
||||||
|
$wrapper_class .= ' ' . esc_attr($attributes['className']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate shortcode
|
||||||
|
$shortcode = '[maple_code_block';
|
||||||
|
foreach ($shortcode_atts as $key => $value) {
|
||||||
|
if (!empty($value)) {
|
||||||
|
$shortcode .= ' ' . $key . '="' . esc_attr($value) . '"';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$shortcode .= ']';
|
||||||
|
|
||||||
|
// Render with wrapper
|
||||||
|
return sprintf(
|
||||||
|
'<div class="%s">%s</div>',
|
||||||
|
esc_attr($wrapper_class),
|
||||||
|
do_shortcode($shortcode)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enqueue block editor assets
|
||||||
|
*/
|
||||||
|
public static function enqueue_block_editor_assets() {
|
||||||
|
// Debug log
|
||||||
|
error_log('MCB: Enqueueing block editor assets');
|
||||||
|
|
||||||
|
// Make sure our script is registered
|
||||||
|
if (!wp_script_is('mcb-block-editor', 'registered')) {
|
||||||
|
error_log('MCB: Script not registered, registering now');
|
||||||
|
wp_register_script(
|
||||||
|
'mcb-block-editor',
|
||||||
|
MCB_PLUGIN_URL . 'assets/js/block-editor.js',
|
||||||
|
array('wp-blocks', 'wp-element', 'wp-editor', 'wp-components', 'wp-i18n', 'wp-data'),
|
||||||
|
MCB_PLUGIN_VERSION,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Localize script with data for the editor
|
||||||
|
wp_localize_script('mcb-block-editor', 'mcbBlockData', array(
|
||||||
|
'pluginUrl' => MCB_PLUGIN_URL,
|
||||||
|
'themes' => array(
|
||||||
|
array('label' => 'Dark', 'value' => 'dark'),
|
||||||
|
array('label' => 'Light', 'value' => 'light'),
|
||||||
|
array('label' => 'Monokai', 'value' => 'monokai'),
|
||||||
|
array('label' => 'Solarized', 'value' => 'solarized')
|
||||||
|
),
|
||||||
|
'defaultHeight' => '600px',
|
||||||
|
'popularRepos' => array(
|
||||||
|
'facebook/react',
|
||||||
|
'vuejs/vue',
|
||||||
|
'angular/angular',
|
||||||
|
'microsoft/vscode',
|
||||||
|
'torvalds/linux',
|
||||||
|
'tensorflow/tensorflow',
|
||||||
|
'kubernetes/kubernetes',
|
||||||
|
'nodejs/node',
|
||||||
|
'rust-lang/rust',
|
||||||
|
'golang/go'
|
||||||
|
),
|
||||||
|
'nonce' => wp_create_nonce('mcb_block_nonce')
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enqueue frontend block assets
|
||||||
|
*/
|
||||||
|
public static function enqueue_block_assets() {
|
||||||
|
// Only enqueue on frontend
|
||||||
|
if (!is_admin()) {
|
||||||
|
// These will be enqueued by the shortcode handler
|
||||||
|
// We just need to ensure the block wrapper styles are loaded
|
||||||
|
wp_enqueue_style(
|
||||||
|
'mcb-block-style',
|
||||||
|
MCB_PLUGIN_URL . 'assets/css/block-style.css',
|
||||||
|
array(),
|
||||||
|
MCB_PLUGIN_VERSION
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add custom block category
|
||||||
|
*/
|
||||||
|
public static function add_block_category($categories, $post) {
|
||||||
|
return array_merge(
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'slug' => 'maple-code-blocks',
|
||||||
|
'title' => __('Maple Code Blocks', 'maple-code-blocks'),
|
||||||
|
'icon' => 'editor-code'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
$categories
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register REST API endpoint for repository validation
|
||||||
|
*/
|
||||||
|
public static function register_rest_routes() {
|
||||||
|
register_rest_route('maple-code-blocks/v1', '/validate-repo', array(
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => array(__CLASS__, 'validate_repository'),
|
||||||
|
'permission_callback' => function() {
|
||||||
|
return current_user_can('edit_posts');
|
||||||
|
},
|
||||||
|
'args' => array(
|
||||||
|
'repository' => array(
|
||||||
|
'required' => true,
|
||||||
|
'type' => 'string',
|
||||||
|
'sanitize_callback' => 'sanitize_text_field'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
));
|
||||||
|
|
||||||
|
register_rest_route('maple-code-blocks/v1', '/get-files', array(
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => array(__CLASS__, 'get_repository_files'),
|
||||||
|
'permission_callback' => function() {
|
||||||
|
return current_user_can('edit_posts');
|
||||||
|
},
|
||||||
|
'args' => array(
|
||||||
|
'repository' => array(
|
||||||
|
'required' => true,
|
||||||
|
'type' => 'string',
|
||||||
|
'sanitize_callback' => 'sanitize_text_field'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate repository via REST API
|
||||||
|
*/
|
||||||
|
public static function validate_repository($request) {
|
||||||
|
$repository = $request->get_param('repository');
|
||||||
|
|
||||||
|
// Validate format
|
||||||
|
if (!MCB_Security::validate_repo_format($repository)) {
|
||||||
|
return new WP_Error('invalid_format', 'Invalid repository format. Use: owner/repository');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quick check with GitHub API
|
||||||
|
$github_api = new MCB_GitHub_API();
|
||||||
|
$result = $github_api->validate_repository_exists($repository);
|
||||||
|
|
||||||
|
if (is_wp_error($result)) {
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'valid' => true,
|
||||||
|
'repository' => $repository
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get repository files for block editor preview
|
||||||
|
*/
|
||||||
|
public static function get_repository_files($request) {
|
||||||
|
$repository = $request->get_param('repository');
|
||||||
|
|
||||||
|
// Validate format
|
||||||
|
if (!MCB_Security::validate_repo_format($repository)) {
|
||||||
|
return new WP_Error('invalid_format', 'Invalid repository format');
|
||||||
|
}
|
||||||
|
|
||||||
|
$github_api = new MCB_GitHub_API();
|
||||||
|
$files = $github_api->get_repository_files($repository);
|
||||||
|
|
||||||
|
if (is_wp_error($files)) {
|
||||||
|
return $files;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return only file names for the editor
|
||||||
|
$file_list = array_map(function($file) {
|
||||||
|
return array(
|
||||||
|
'path' => $file['path'],
|
||||||
|
'name' => $file['name']
|
||||||
|
);
|
||||||
|
}, array_slice($files, 0, 10)); // Limit to 10 files for preview
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'files' => $file_list,
|
||||||
|
'total' => count($files)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize block editor support
|
||||||
|
add_action('init', array('MCB_Block_Editor', 'init'));
|
||||||
|
add_action('rest_api_init', array('MCB_Block_Editor', 'register_rest_routes'));
|
||||||
|
|
@ -0,0 +1,273 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Block Patterns Registration
|
||||||
|
* Registers reusable block patterns for the GitHub Code Viewer
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Prevent direct access
|
||||||
|
if (!defined('WPINC')) {
|
||||||
|
die('Direct access not permitted.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
class MCB_Block_Patterns {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize block patterns
|
||||||
|
*/
|
||||||
|
public static function init() {
|
||||||
|
add_action('init', array(__CLASS__, 'register_pattern_category'));
|
||||||
|
add_action('init', array(__CLASS__, 'register_patterns'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register pattern category
|
||||||
|
*/
|
||||||
|
public static function register_pattern_category() {
|
||||||
|
if (function_exists('register_block_pattern_category')) {
|
||||||
|
register_block_pattern_category(
|
||||||
|
'maple-code-blocks',
|
||||||
|
array(
|
||||||
|
'label' => __('GitHub Code Viewer', 'maple-code-blocks')
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register block patterns
|
||||||
|
*/
|
||||||
|
public static function register_patterns() {
|
||||||
|
if (!function_exists('register_block_pattern')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Code with Explanation Pattern
|
||||||
|
register_block_pattern(
|
||||||
|
'maple-code-blocks/code-with-explanation',
|
||||||
|
array(
|
||||||
|
'title' => __('Maple: Code with Explanation', 'maple-code-blocks'),
|
||||||
|
'description' => __('Display code with explanatory text', 'maple-code-blocks'),
|
||||||
|
'categories' => array('maple-code-blocks', 'text'),
|
||||||
|
'keywords' => array('code', 'github', 'explanation', 'tutorial'),
|
||||||
|
'content' => '<!-- wp:group {"backgroundColor":"white","style":{"spacing":{"padding":{"top":"30px","right":"30px","bottom":"30px","left":"30px"}}}} -->
|
||||||
|
<div class="wp-block-group has-white-background-color has-background" style="padding-top:30px;padding-right:30px;padding-bottom:30px;padding-left:30px">
|
||||||
|
<!-- wp:heading {"level":3} -->
|
||||||
|
<h3>' . __('Understanding the Code', 'maple-code-blocks') . '</h3>
|
||||||
|
<!-- /wp:heading -->
|
||||||
|
|
||||||
|
<!-- wp:paragraph -->
|
||||||
|
<p>' . __('This example demonstrates a key concept in modern development:', 'maple-code-blocks') . '</p>
|
||||||
|
<!-- /wp:paragraph -->
|
||||||
|
|
||||||
|
<!-- wp:maple-code-blocks/code-block {"repository":"","theme":"dark","height":"400px","showLineNumbers":true} /-->
|
||||||
|
|
||||||
|
<!-- wp:list -->
|
||||||
|
<ul>
|
||||||
|
<li>' . __('Line 1-5: Initial setup and configuration', 'maple-code-blocks') . '</li>
|
||||||
|
<li>' . __('Line 6-15: Core functionality implementation', 'maple-code-blocks') . '</li>
|
||||||
|
<li>' . __('Line 16-20: Error handling and cleanup', 'maple-code-blocks') . '</li>
|
||||||
|
</ul>
|
||||||
|
<!-- /wp:list -->
|
||||||
|
</div>
|
||||||
|
<!-- /wp:group -->'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Side-by-side Code Comparison
|
||||||
|
register_block_pattern(
|
||||||
|
'maple-code-blocks/side-by-side',
|
||||||
|
array(
|
||||||
|
'title' => __('Maple: Side-by-Side Comparison', 'maple-code-blocks'),
|
||||||
|
'description' => __('Compare two code implementations', 'maple-code-blocks'),
|
||||||
|
'categories' => array('maple-code-blocks', 'columns'),
|
||||||
|
'keywords' => array('compare', 'code', 'github', 'columns'),
|
||||||
|
'content' => '<!-- wp:columns {"align":"wide"} -->
|
||||||
|
<div class="wp-block-columns alignwide">
|
||||||
|
<!-- wp:column -->
|
||||||
|
<div class="wp-block-column">
|
||||||
|
<!-- wp:heading {"level":4,"style":{"color":{"text":"#0073aa"}}} -->
|
||||||
|
<h4 class="has-text-color" style="color:#0073aa">' . __('Method A', 'maple-code-blocks') . '</h4>
|
||||||
|
<!-- /wp:heading -->
|
||||||
|
|
||||||
|
<!-- wp:maple-code-blocks/code-block {"repository":"","theme":"light","height":"350px","showLineNumbers":true} /-->
|
||||||
|
</div>
|
||||||
|
<!-- /wp:column -->
|
||||||
|
|
||||||
|
<!-- wp:column -->
|
||||||
|
<div class="wp-block-column">
|
||||||
|
<!-- wp:heading {"level":4,"style":{"color":{"text":"#00a32a"}}} -->
|
||||||
|
<h4 class="has-text-color" style="color:#00a32a">' . __('Method B', 'maple-code-blocks') . '</h4>
|
||||||
|
<!-- /wp:heading -->
|
||||||
|
|
||||||
|
<!-- wp:maple-code-blocks/code-block {"repository":"","theme":"light","height":"350px","showLineNumbers":true} /-->
|
||||||
|
</div>
|
||||||
|
<!-- /wp:column -->
|
||||||
|
</div>
|
||||||
|
<!-- /wp:columns -->'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Code Gallery Pattern
|
||||||
|
register_block_pattern(
|
||||||
|
'maple-code-blocks/code-gallery',
|
||||||
|
array(
|
||||||
|
'title' => __('Maple: Code Gallery', 'maple-code-blocks'),
|
||||||
|
'description' => __('Showcase multiple code examples', 'maple-code-blocks'),
|
||||||
|
'categories' => array('maple-code-blocks', 'gallery'),
|
||||||
|
'keywords' => array('gallery', 'showcase', 'multiple', 'code'),
|
||||||
|
'content' => '<!-- wp:heading {"textAlign":"center"} -->
|
||||||
|
<h2 class="has-text-align-center">' . __('Code Examples', 'maple-code-blocks') . '</h2>
|
||||||
|
<!-- /wp:heading -->
|
||||||
|
|
||||||
|
<!-- wp:spacer {"height":"30px"} -->
|
||||||
|
<div style="height:30px" aria-hidden="true" class="wp-block-spacer"></div>
|
||||||
|
<!-- /wp:spacer -->
|
||||||
|
|
||||||
|
<!-- wp:columns -->
|
||||||
|
<div class="wp-block-columns">
|
||||||
|
<!-- wp:column -->
|
||||||
|
<div class="wp-block-column">
|
||||||
|
<!-- wp:heading {"level":5,"textAlign":"center"} -->
|
||||||
|
<h5 class="has-text-align-center">' . __('Example 1', 'maple-code-blocks') . '</h5>
|
||||||
|
<!-- /wp:heading -->
|
||||||
|
|
||||||
|
<!-- wp:maple-code-blocks/code-block {"repository":"","theme":"monokai","height":"250px","showLineNumbers":false} /-->
|
||||||
|
</div>
|
||||||
|
<!-- /wp:column -->
|
||||||
|
|
||||||
|
<!-- wp:column -->
|
||||||
|
<div class="wp-block-column">
|
||||||
|
<!-- wp:heading {"level":5,"textAlign":"center"} -->
|
||||||
|
<h5 class="has-text-align-center">' . __('Example 2', 'maple-code-blocks') . '</h5>
|
||||||
|
<!-- /wp:heading -->
|
||||||
|
|
||||||
|
<!-- wp:maple-code-blocks/code-block {"repository":"","theme":"monokai","height":"250px","showLineNumbers":false} /-->
|
||||||
|
</div>
|
||||||
|
<!-- /wp:column -->
|
||||||
|
|
||||||
|
<!-- wp:column -->
|
||||||
|
<div class="wp-block-column">
|
||||||
|
<!-- wp:heading {"level":5,"textAlign":"center"} -->
|
||||||
|
<h5 class="has-text-align-center">' . __('Example 3', 'maple-code-blocks') . '</h5>
|
||||||
|
<!-- /wp:heading -->
|
||||||
|
|
||||||
|
<!-- wp:maple-code-blocks/code-block {"repository":"","theme":"monokai","height":"250px","showLineNumbers":false} /-->
|
||||||
|
</div>
|
||||||
|
<!-- /wp:column -->
|
||||||
|
</div>
|
||||||
|
<!-- /wp:columns -->'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Featured Code Pattern
|
||||||
|
register_block_pattern(
|
||||||
|
'maple-code-blocks/featured-code',
|
||||||
|
array(
|
||||||
|
'title' => __('Maple: Featured Code', 'maple-code-blocks'),
|
||||||
|
'description' => __('Highlight important code with context', 'maple-code-blocks'),
|
||||||
|
'categories' => array('maple-code-blocks', 'featured'),
|
||||||
|
'keywords' => array('featured', 'highlight', 'code', 'important'),
|
||||||
|
'content' => '<!-- wp:group {"align":"wide","style":{"spacing":{"padding":{"top":"40px","right":"40px","bottom":"40px","left":"40px"}},"border":{"radius":"8px"}},"backgroundColor":"light-gray"} -->
|
||||||
|
<div class="wp-block-group alignwide has-light-gray-background-color has-background" style="border-radius:8px;padding-top:40px;padding-right:40px;padding-bottom:40px;padding-left:40px">
|
||||||
|
<!-- wp:columns -->
|
||||||
|
<div class="wp-block-columns">
|
||||||
|
<!-- wp:column {"width":"40%"} -->
|
||||||
|
<div class="wp-block-column" style="flex-basis:40%">
|
||||||
|
<!-- wp:heading {"level":3} -->
|
||||||
|
<h3>' . __('Featured Implementation', 'maple-code-blocks') . '</h3>
|
||||||
|
<!-- /wp:heading -->
|
||||||
|
|
||||||
|
<!-- wp:paragraph -->
|
||||||
|
<p>' . __('This code demonstrates best practices for this common pattern.', 'maple-code-blocks') . '</p>
|
||||||
|
<!-- /wp:paragraph -->
|
||||||
|
|
||||||
|
<!-- wp:buttons -->
|
||||||
|
<div class="wp-block-buttons">
|
||||||
|
<!-- wp:button -->
|
||||||
|
<div class="wp-block-button"><a class="wp-block-button__link">' . __('View on GitHub', 'maple-code-blocks') . '</a></div>
|
||||||
|
<!-- /wp:button -->
|
||||||
|
</div>
|
||||||
|
<!-- /wp:buttons -->
|
||||||
|
</div>
|
||||||
|
<!-- /wp:column -->
|
||||||
|
|
||||||
|
<!-- wp:column {"width":"60%"} -->
|
||||||
|
<div class="wp-block-column" style="flex-basis:60%">
|
||||||
|
<!-- wp:maple-code-blocks/code-block {"repository":"","theme":"dark","height":"350px","showLineNumbers":true} /-->
|
||||||
|
</div>
|
||||||
|
<!-- /wp:column -->
|
||||||
|
</div>
|
||||||
|
<!-- /wp:columns -->
|
||||||
|
</div>
|
||||||
|
<!-- /wp:group -->'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Tutorial Step Pattern
|
||||||
|
register_block_pattern(
|
||||||
|
'maple-code-blocks/tutorial-steps',
|
||||||
|
array(
|
||||||
|
'title' => __('Maple: Tutorial Steps', 'maple-code-blocks'),
|
||||||
|
'description' => __('Step-by-step code tutorial', 'maple-code-blocks'),
|
||||||
|
'categories' => array('maple-code-blocks', 'text'),
|
||||||
|
'keywords' => array('tutorial', 'steps', 'education', 'learn'),
|
||||||
|
'content' => '<!-- wp:group -->
|
||||||
|
<div class="wp-block-group">
|
||||||
|
<!-- wp:heading -->
|
||||||
|
<h2>' . __('Building Your First Application', 'maple-code-blocks') . '</h2>
|
||||||
|
<!-- /wp:heading -->
|
||||||
|
|
||||||
|
<!-- wp:group {"style":{"spacing":{"margin":{"top":"30px","bottom":"30px"}}}} -->
|
||||||
|
<div class="wp-block-group" style="margin-top:30px;margin-bottom:30px">
|
||||||
|
<!-- wp:heading {"level":3,"style":{"typography":{"fontSize":"20px"}}} -->
|
||||||
|
<h3 style="font-size:20px">📝 ' . __('Step 1: Setup', 'maple-code-blocks') . '</h3>
|
||||||
|
<!-- /wp:heading -->
|
||||||
|
|
||||||
|
<!-- wp:paragraph -->
|
||||||
|
<p>' . __('First, we need to set up our development environment:', 'maple-code-blocks') . '</p>
|
||||||
|
<!-- /wp:paragraph -->
|
||||||
|
|
||||||
|
<!-- wp:maple-code-blocks/code-block {"repository":"","theme":"light","height":"200px","showLineNumbers":true} /-->
|
||||||
|
</div>
|
||||||
|
<!-- /wp:group -->
|
||||||
|
|
||||||
|
<!-- wp:group {"style":{"spacing":{"margin":{"top":"30px","bottom":"30px"}}}} -->
|
||||||
|
<div class="wp-block-group" style="margin-top:30px;margin-bottom:30px">
|
||||||
|
<!-- wp:heading {"level":3,"style":{"typography":{"fontSize":"20px"}}} -->
|
||||||
|
<h3 style="font-size:20px">🔧 ' . __('Step 2: Configuration', 'maple-code-blocks') . '</h3>
|
||||||
|
<!-- /wp:heading -->
|
||||||
|
|
||||||
|
<!-- wp:paragraph -->
|
||||||
|
<p>' . __('Next, configure the application settings:', 'maple-code-blocks') . '</p>
|
||||||
|
<!-- /wp:paragraph -->
|
||||||
|
|
||||||
|
<!-- wp:maple-code-blocks/code-block {"repository":"","theme":"light","height":"250px","showLineNumbers":true} /-->
|
||||||
|
</div>
|
||||||
|
<!-- /wp:group -->
|
||||||
|
|
||||||
|
<!-- wp:group {"style":{"spacing":{"margin":{"top":"30px","bottom":"30px"}}}} -->
|
||||||
|
<div class="wp-block-group" style="margin-top:30px;margin-bottom:30px">
|
||||||
|
<!-- wp:heading {"level":3,"style":{"typography":{"fontSize":"20px"}}} -->
|
||||||
|
<h3 style="font-size:20px">🚀 ' . __('Step 3: Deploy', 'maple-code-blocks') . '</h3>
|
||||||
|
<!-- /wp:heading -->
|
||||||
|
|
||||||
|
<!-- wp:paragraph -->
|
||||||
|
<p>' . __('Finally, deploy your application:', 'maple-code-blocks') . '</p>
|
||||||
|
<!-- /wp:paragraph -->
|
||||||
|
|
||||||
|
<!-- wp:maple-code-blocks/code-block {"repository":"","theme":"light","height":"200px","showLineNumbers":true} /-->
|
||||||
|
</div>
|
||||||
|
<!-- /wp:group -->
|
||||||
|
</div>
|
||||||
|
<!-- /wp:group -->'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize patterns
|
||||||
|
MCB_Block_Patterns::init();
|
||||||
|
|
@ -0,0 +1,222 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Code Renderer Class
|
||||||
|
* Safely renders code content with proper escaping and syntax highlighting
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Prevent direct access
|
||||||
|
if (!defined('WPINC')) {
|
||||||
|
die('Direct access not permitted.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
class MCB_Code_Renderer {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render code content safely
|
||||||
|
*/
|
||||||
|
public function render_code($content, $filename) {
|
||||||
|
// Validate content before processing
|
||||||
|
$validation = $this->validate_content($content);
|
||||||
|
if (is_wp_error($validation)) {
|
||||||
|
return '<div class="gcv-error">' . esc_html($validation->get_error_message()) . '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// First, ensure the content is treated as plain text
|
||||||
|
// Multiple layers of safety to prevent any code execution
|
||||||
|
|
||||||
|
// 1. Convert to UTF-8 if needed
|
||||||
|
if (!mb_check_encoding($content, 'UTF-8')) {
|
||||||
|
$content = mb_convert_encoding($content, 'UTF-8', mb_detect_encoding($content));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Remove any null bytes
|
||||||
|
$content = str_replace("\0", '', $content);
|
||||||
|
|
||||||
|
// 3. HTML encode everything - this is crucial for safety
|
||||||
|
$safe_content = htmlspecialchars($content, ENT_QUOTES | ENT_HTML5 | ENT_SUBSTITUTE, 'UTF-8', false);
|
||||||
|
|
||||||
|
// 4. Additional escaping for JavaScript context
|
||||||
|
$safe_content = $this->escape_for_javascript($safe_content);
|
||||||
|
|
||||||
|
// 5. Get language for syntax highlighting
|
||||||
|
$language = $this->detect_language($filename);
|
||||||
|
|
||||||
|
// 6. Prepare the code block with line numbers
|
||||||
|
$lines = explode("\n", $safe_content);
|
||||||
|
$formatted_code = $this->format_with_line_numbers($lines);
|
||||||
|
|
||||||
|
// 7. Wrap in proper HTML structure
|
||||||
|
$output = '<div class="gcv-code-container" data-language="' . esc_attr($language) . '">';
|
||||||
|
$output .= '<div class="gcv-code-header">';
|
||||||
|
$output .= '<span class="gcv-filename">' . esc_html(basename($filename)) . '</span>';
|
||||||
|
$output .= '<button class="gcv-copy-btn" data-content="' . esc_attr($safe_content) . '">Copy</button>';
|
||||||
|
$output .= '</div>';
|
||||||
|
$output .= '<div class="gcv-code-wrapper">';
|
||||||
|
$output .= '<pre class="line-numbers"><code class="language-' . esc_attr($language) . '">';
|
||||||
|
$output .= $formatted_code;
|
||||||
|
$output .= '</code></pre>';
|
||||||
|
$output .= '</div>';
|
||||||
|
$output .= '</div>';
|
||||||
|
|
||||||
|
return $output;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format code with line numbers
|
||||||
|
*/
|
||||||
|
private function format_with_line_numbers($lines) {
|
||||||
|
$output = '';
|
||||||
|
$line_count = count($lines);
|
||||||
|
$digit_count = strlen((string)$line_count);
|
||||||
|
|
||||||
|
foreach ($lines as $index => $line) {
|
||||||
|
$line_num = $index + 1;
|
||||||
|
$padded_num = str_pad($line_num, $digit_count, ' ', STR_PAD_LEFT);
|
||||||
|
$output .= '<span class="line-number" data-line="' . $line_num . '">' . $padded_num . '</span>';
|
||||||
|
$output .= '<span class="line-content">' . $line . '</span>' . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
return rtrim($output);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Additional escaping for JavaScript context
|
||||||
|
*/
|
||||||
|
private function escape_for_javascript($content) {
|
||||||
|
// Escape any remaining potentially dangerous patterns
|
||||||
|
$patterns = array(
|
||||||
|
'/<script/i' => '<script',
|
||||||
|
'/<\/script/i' => '</script',
|
||||||
|
'/javascript:/i' => 'javascript:',
|
||||||
|
'/on\w+\s*=/i' => 'on_event=',
|
||||||
|
'/<iframe/i' => '<iframe',
|
||||||
|
'/<object/i' => '<object',
|
||||||
|
'/<embed/i' => '<embed',
|
||||||
|
'/<applet/i' => '<applet'
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($patterns as $pattern => $replacement) {
|
||||||
|
$content = preg_replace($pattern, $replacement, $content);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect programming language from filename
|
||||||
|
*/
|
||||||
|
private function detect_language($filename) {
|
||||||
|
$extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
|
||||||
|
$name_lower = strtolower($filename);
|
||||||
|
|
||||||
|
// Map extensions to Prism.js language identifiers
|
||||||
|
$language_map = array(
|
||||||
|
'php' => 'php',
|
||||||
|
'js' => 'javascript',
|
||||||
|
'jsx' => 'jsx',
|
||||||
|
'ts' => 'typescript',
|
||||||
|
'tsx' => 'tsx',
|
||||||
|
'py' => 'python',
|
||||||
|
'rb' => 'ruby',
|
||||||
|
'java' => 'java',
|
||||||
|
'c' => 'c',
|
||||||
|
'cpp' => 'cpp',
|
||||||
|
'cc' => 'cpp',
|
||||||
|
'cxx' => 'cpp',
|
||||||
|
'h' => 'c',
|
||||||
|
'hpp' => 'cpp',
|
||||||
|
'cs' => 'csharp',
|
||||||
|
'swift' => 'swift',
|
||||||
|
'kt' => 'kotlin',
|
||||||
|
'go' => 'go',
|
||||||
|
'rs' => 'rust',
|
||||||
|
'scala' => 'scala',
|
||||||
|
'r' => 'r',
|
||||||
|
'sql' => 'sql',
|
||||||
|
'sh' => 'bash',
|
||||||
|
'bash' => 'bash',
|
||||||
|
'yml' => 'yaml',
|
||||||
|
'yaml' => 'yaml',
|
||||||
|
'json' => 'json',
|
||||||
|
'xml' => 'xml',
|
||||||
|
'html' => 'html',
|
||||||
|
'htm' => 'html',
|
||||||
|
'css' => 'css',
|
||||||
|
'scss' => 'scss',
|
||||||
|
'sass' => 'sass',
|
||||||
|
'less' => 'less',
|
||||||
|
'md' => 'markdown',
|
||||||
|
'markdown' => 'markdown',
|
||||||
|
'txt' => 'plain',
|
||||||
|
'ini' => 'ini',
|
||||||
|
'conf' => 'ini',
|
||||||
|
'cfg' => 'ini'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Special file names
|
||||||
|
if ($name_lower === 'dockerfile') {
|
||||||
|
return 'docker';
|
||||||
|
}
|
||||||
|
if ($name_lower === 'makefile' || $name_lower === 'gnumakefile') {
|
||||||
|
return 'makefile';
|
||||||
|
}
|
||||||
|
if ($name_lower === '.gitignore') {
|
||||||
|
return 'git';
|
||||||
|
}
|
||||||
|
if ($name_lower === '.htaccess') {
|
||||||
|
return 'apacheconf';
|
||||||
|
}
|
||||||
|
if ($name_lower === '.env') {
|
||||||
|
return 'bash';
|
||||||
|
}
|
||||||
|
|
||||||
|
return isset($language_map[$extension]) ? $language_map[$extension] : 'plain';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitize and validate file content before rendering
|
||||||
|
*/
|
||||||
|
public function validate_content($content) {
|
||||||
|
// Check for binary content
|
||||||
|
if ($this->is_binary($content)) {
|
||||||
|
return new WP_Error('binary_file', 'Binary files cannot be displayed');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check file size (limit to 1MB for performance)
|
||||||
|
if (strlen($content) > 1048576) {
|
||||||
|
return new WP_Error('file_too_large', 'File is too large to display (max 1MB)');
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if content appears to be binary
|
||||||
|
*/
|
||||||
|
private function is_binary($content) {
|
||||||
|
// Check for null bytes or excessive non-printable characters
|
||||||
|
$null_count = substr_count($content, "\0");
|
||||||
|
if ($null_count > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sample first 8192 bytes
|
||||||
|
$sample = substr($content, 0, 8192);
|
||||||
|
$non_printable = 0;
|
||||||
|
|
||||||
|
for ($i = 0; $i < strlen($sample); $i++) {
|
||||||
|
$char = ord($sample[$i]);
|
||||||
|
// Allow common whitespace and printable ASCII
|
||||||
|
if ($char < 32 && $char !== 9 && $char !== 10 && $char !== 13) {
|
||||||
|
$non_printable++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If more than 30% non-printable, consider it binary
|
||||||
|
return ($non_printable / strlen($sample)) > 0.3;
|
||||||
|
}
|
||||||
|
}
|
||||||
749
native/wordpress/maple-code-blocks/includes/class-github-api.php
Normal file
749
native/wordpress/maple-code-blocks/includes/class-github-api.php
Normal file
|
|
@ -0,0 +1,749 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Git Platform API Handler Class
|
||||||
|
* Handles API interactions for GitHub, GitLab, Bitbucket, and Codeberg
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Prevent direct access
|
||||||
|
if (!defined('WPINC')) {
|
||||||
|
die('Direct access not permitted.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
class MCB_GitHub_API {
|
||||||
|
|
||||||
|
private $cache_duration = 3600; // 1 hour cache
|
||||||
|
|
||||||
|
// Platform API configurations
|
||||||
|
private $platforms = array(
|
||||||
|
'github' => array(
|
||||||
|
'name' => 'GitHub',
|
||||||
|
'api_base' => 'https://api.github.com',
|
||||||
|
'repo_pattern' => '/^[a-zA-Z0-9\-_]+\/[a-zA-Z0-9\-_\.]+$/',
|
||||||
|
'web_base' => 'https://github.com'
|
||||||
|
),
|
||||||
|
'gitlab' => array(
|
||||||
|
'name' => 'GitLab',
|
||||||
|
'api_base' => 'https://gitlab.com/api/v4',
|
||||||
|
'repo_pattern' => '/^[a-zA-Z0-9\-_]+\/[a-zA-Z0-9\-_\.]+$/',
|
||||||
|
'web_base' => 'https://gitlab.com'
|
||||||
|
),
|
||||||
|
'bitbucket' => array(
|
||||||
|
'name' => 'Bitbucket',
|
||||||
|
'api_base' => 'https://api.bitbucket.org/2.0',
|
||||||
|
'repo_pattern' => '/^[a-zA-Z0-9\-_]+\/[a-zA-Z0-9\-_\.]+$/',
|
||||||
|
'web_base' => 'https://bitbucket.org'
|
||||||
|
),
|
||||||
|
'codeberg' => array(
|
||||||
|
'name' => 'Codeberg',
|
||||||
|
'api_base' => 'https://codeberg.org/api/v1',
|
||||||
|
'repo_pattern' => '/^[a-zA-Z0-9\-_]+\/[a-zA-Z0-9\-_\.]+$/',
|
||||||
|
'web_base' => 'https://codeberg.org'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect platform from repository string
|
||||||
|
*/
|
||||||
|
private function detect_platform($repo_string) {
|
||||||
|
// Check for platform prefix (e.g., "gitlab:owner/repo")
|
||||||
|
if (strpos($repo_string, ':') !== false) {
|
||||||
|
list($platform, $repo) = explode(':', $repo_string, 2);
|
||||||
|
if (isset($this->platforms[$platform])) {
|
||||||
|
return array('platform' => $platform, 'repo' => $repo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for full URLs
|
||||||
|
if (strpos($repo_string, 'https://') === 0 || strpos($repo_string, 'http://') === 0) {
|
||||||
|
foreach ($this->platforms as $key => $config) {
|
||||||
|
if (strpos($repo_string, $config['web_base']) !== false) {
|
||||||
|
// Extract owner/repo from URL
|
||||||
|
$parsed = parse_url($repo_string);
|
||||||
|
$path = trim($parsed['path'], '/');
|
||||||
|
$parts = explode('/', $path);
|
||||||
|
if (count($parts) >= 2) {
|
||||||
|
return array(
|
||||||
|
'platform' => $key,
|
||||||
|
'repo' => $parts[0] . '/' . $parts[1]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to GitHub for backward compatibility
|
||||||
|
return array('platform' => 'github', 'repo' => $repo_string);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get repository files list
|
||||||
|
*/
|
||||||
|
public function get_repository_files($repo_string, $path = '') {
|
||||||
|
$platform_info = $this->detect_platform($repo_string);
|
||||||
|
$platform = $platform_info['platform'];
|
||||||
|
$repo = $platform_info['repo'];
|
||||||
|
|
||||||
|
// Validate repo format
|
||||||
|
if (!preg_match($this->platforms[$platform]['repo_pattern'], $repo)) {
|
||||||
|
return new WP_Error('invalid_repo', 'Invalid repository format. Use: owner/repository');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check cache first
|
||||||
|
$cache_key = 'mcb_repo_' . $platform . '_' . md5($repo . '_' . $path);
|
||||||
|
$cached_data = get_transient($cache_key);
|
||||||
|
|
||||||
|
if ($cached_data !== false) {
|
||||||
|
return $cached_data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get files based on platform
|
||||||
|
switch ($platform) {
|
||||||
|
case 'github':
|
||||||
|
$files = $this->get_github_files($repo, $path);
|
||||||
|
break;
|
||||||
|
case 'gitlab':
|
||||||
|
$files = $this->get_gitlab_files($repo, $path);
|
||||||
|
break;
|
||||||
|
case 'bitbucket':
|
||||||
|
$files = $this->get_bitbucket_files($repo, $path);
|
||||||
|
break;
|
||||||
|
case 'codeberg':
|
||||||
|
$files = $this->get_codeberg_files($repo, $path);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return new WP_Error('unsupported_platform', 'Unsupported platform');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_wp_error($files)) {
|
||||||
|
// Cache the results
|
||||||
|
set_transient($cache_key, $files, $this->cache_duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $files;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get file content from repository
|
||||||
|
*/
|
||||||
|
public function get_file_content($repo_string, $file_path) {
|
||||||
|
$platform_info = $this->detect_platform($repo_string);
|
||||||
|
$platform = $platform_info['platform'];
|
||||||
|
$repo = $platform_info['repo'];
|
||||||
|
|
||||||
|
// Validate inputs
|
||||||
|
if (!preg_match($this->platforms[$platform]['repo_pattern'], $repo)) {
|
||||||
|
return new WP_Error('invalid_repo', 'Invalid repository format');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanitize and validate file path
|
||||||
|
$file_path = trim($file_path, '/');
|
||||||
|
|
||||||
|
// Block path traversal attempts
|
||||||
|
if (strpos($file_path, '..') !== false ||
|
||||||
|
strpos($file_path, '//') !== false ||
|
||||||
|
strpos($file_path, '\\') !== false ||
|
||||||
|
preg_match('/[<>"|*?]/', $file_path)) {
|
||||||
|
return new WP_Error('invalid_path', 'Invalid file path');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check cache
|
||||||
|
$cache_key = 'mcb_file_' . $platform . '_' . md5($repo . $file_path);
|
||||||
|
$cached_content = get_transient($cache_key);
|
||||||
|
|
||||||
|
if ($cached_content !== false) {
|
||||||
|
return $cached_content;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get content based on platform
|
||||||
|
switch ($platform) {
|
||||||
|
case 'github':
|
||||||
|
$content = $this->get_github_file_content($repo, $file_path);
|
||||||
|
break;
|
||||||
|
case 'gitlab':
|
||||||
|
$content = $this->get_gitlab_file_content($repo, $file_path);
|
||||||
|
break;
|
||||||
|
case 'bitbucket':
|
||||||
|
$content = $this->get_bitbucket_file_content($repo, $file_path);
|
||||||
|
break;
|
||||||
|
case 'codeberg':
|
||||||
|
$content = $this->get_codeberg_file_content($repo, $file_path);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return new WP_Error('unsupported_platform', 'Unsupported platform');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_wp_error($content)) {
|
||||||
|
// Cache the content
|
||||||
|
set_transient($cache_key, $content, $this->cache_duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GitHub-specific file listing
|
||||||
|
*/
|
||||||
|
private function get_github_files($repo, $path = '') {
|
||||||
|
$url = $this->platforms['github']['api_base'] . '/repos/' . $repo . '/contents';
|
||||||
|
if (!empty($path)) {
|
||||||
|
$url .= '/' . ltrim($path, '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = $this->make_api_request($url, 'github');
|
||||||
|
|
||||||
|
if (is_wp_error($response)) {
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->parse_github_contents($response, $repo, $path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GitLab-specific file listing
|
||||||
|
*/
|
||||||
|
private function get_gitlab_files($repo, $path = '') {
|
||||||
|
// GitLab uses project ID or URL-encoded path
|
||||||
|
$project_id = urlencode($repo);
|
||||||
|
$url = $this->platforms['gitlab']['api_base'] . '/projects/' . $project_id . '/repository/tree?recursive=true&per_page=100';
|
||||||
|
$response = $this->make_api_request($url, 'gitlab');
|
||||||
|
|
||||||
|
if (is_wp_error($response)) {
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->parse_gitlab_contents($response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bitbucket-specific file listing
|
||||||
|
*/
|
||||||
|
private function get_bitbucket_files($repo, $path = '') {
|
||||||
|
$url = $this->platforms['bitbucket']['api_base'] . '/repositories/' . $repo . '/src';
|
||||||
|
$response = $this->make_api_request($url, 'bitbucket');
|
||||||
|
|
||||||
|
if (is_wp_error($response)) {
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->parse_bitbucket_contents($response, $repo);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Codeberg-specific file listing (uses Gitea API)
|
||||||
|
*/
|
||||||
|
private function get_codeberg_files($repo, $path = '') {
|
||||||
|
$url = $this->platforms['codeberg']['api_base'] . '/repos/' . $repo . '/contents';
|
||||||
|
if (!empty($path)) {
|
||||||
|
$url .= '/' . ltrim($path, '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = $this->make_api_request($url, 'codeberg');
|
||||||
|
|
||||||
|
if (is_wp_error($response)) {
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Codeberg uses Gitea, similar to GitHub API
|
||||||
|
return $this->parse_github_contents($response, $repo, $path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GitHub file content retrieval
|
||||||
|
*/
|
||||||
|
private function get_github_file_content($repo, $file_path) {
|
||||||
|
$url = $this->platforms['github']['api_base'] . '/repos/' . $repo . '/contents/' . $file_path;
|
||||||
|
$response = $this->make_api_request($url, 'github');
|
||||||
|
|
||||||
|
if (is_wp_error($response)) {
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode($response, true);
|
||||||
|
|
||||||
|
if (!isset($data['content'])) {
|
||||||
|
return new WP_Error('no_content', 'File content not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return base64_decode($data['content']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GitLab file content retrieval
|
||||||
|
*/
|
||||||
|
private function get_gitlab_file_content($repo, $file_path) {
|
||||||
|
$project_id = urlencode($repo);
|
||||||
|
$file_path_encoded = urlencode($file_path);
|
||||||
|
$url = $this->platforms['gitlab']['api_base'] . '/projects/' . $project_id . '/repository/files/' . $file_path_encoded . '/raw?ref=main';
|
||||||
|
|
||||||
|
// Try main branch first, then master
|
||||||
|
$response = $this->make_api_request($url, 'gitlab');
|
||||||
|
|
||||||
|
if (is_wp_error($response)) {
|
||||||
|
// Try master branch
|
||||||
|
$url = $this->platforms['gitlab']['api_base'] . '/projects/' . $project_id . '/repository/files/' . $file_path_encoded . '/raw?ref=master';
|
||||||
|
$response = $this->make_api_request($url, 'gitlab');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bitbucket file content retrieval
|
||||||
|
*/
|
||||||
|
private function get_bitbucket_file_content($repo, $file_path) {
|
||||||
|
// Get default branch first
|
||||||
|
$repo_url = $this->platforms['bitbucket']['api_base'] . '/repositories/' . $repo;
|
||||||
|
$repo_response = $this->make_api_request($repo_url, 'bitbucket');
|
||||||
|
|
||||||
|
if (is_wp_error($repo_response)) {
|
||||||
|
return $repo_response;
|
||||||
|
}
|
||||||
|
|
||||||
|
$repo_data = json_decode($repo_response, true);
|
||||||
|
$branch = isset($repo_data['mainbranch']['name']) ? $repo_data['mainbranch']['name'] : 'master';
|
||||||
|
|
||||||
|
$url = $this->platforms['bitbucket']['api_base'] . '/repositories/' . $repo . '/src/' . $branch . '/' . $file_path;
|
||||||
|
return $this->make_api_request($url, 'bitbucket');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Codeberg file content retrieval
|
||||||
|
*/
|
||||||
|
private function get_codeberg_file_content($repo, $file_path) {
|
||||||
|
// Codeberg uses Gitea API, similar to GitHub
|
||||||
|
$url = $this->platforms['codeberg']['api_base'] . '/repos/' . $repo . '/contents/' . $file_path;
|
||||||
|
$response = $this->make_api_request($url, 'codeberg');
|
||||||
|
|
||||||
|
if (is_wp_error($response)) {
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode($response, true);
|
||||||
|
|
||||||
|
if (!isset($data['content'])) {
|
||||||
|
return new WP_Error('no_content', 'File content not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return base64_decode($data['content']);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make HTTP request to Git platform API
|
||||||
|
*/
|
||||||
|
private function make_api_request($url, $platform) {
|
||||||
|
// SSRF Protection - validate URL
|
||||||
|
$parsed_url = parse_url($url);
|
||||||
|
|
||||||
|
// Only allow HTTPS protocol
|
||||||
|
if ($parsed_url['scheme'] !== 'https') {
|
||||||
|
return new WP_Error('invalid_protocol', 'Only HTTPS is allowed');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only allow known platform hosts
|
||||||
|
$allowed_hosts = array(
|
||||||
|
'api.github.com',
|
||||||
|
'gitlab.com',
|
||||||
|
'api.bitbucket.org',
|
||||||
|
'codeberg.org'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!in_array($parsed_url['host'], $allowed_hosts)) {
|
||||||
|
return new WP_Error('invalid_host', 'Invalid API host');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add request throttling to prevent rate limit issues
|
||||||
|
static $last_request_time = 0;
|
||||||
|
$min_interval = 0.1; // Minimum 100ms between requests
|
||||||
|
|
||||||
|
$current_time = microtime(true);
|
||||||
|
$time_since_last = $current_time - $last_request_time;
|
||||||
|
|
||||||
|
if ($time_since_last < $min_interval) {
|
||||||
|
usleep(($min_interval - $time_since_last) * 1000000);
|
||||||
|
}
|
||||||
|
|
||||||
|
$args = array(
|
||||||
|
'timeout' => 10, // Reduced timeout to prevent hanging
|
||||||
|
'redirection' => 3, // Limit redirects
|
||||||
|
'headers' => $this->get_platform_headers($platform)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add authentication if available
|
||||||
|
$token = $this->get_platform_token($platform);
|
||||||
|
if (!empty($token)) {
|
||||||
|
$args['headers']['Authorization'] = $this->get_auth_header($platform, $token);
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = wp_remote_get($url, $args);
|
||||||
|
|
||||||
|
if (is_wp_error($response)) {
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
$status_code = wp_remote_retrieve_response_code($response);
|
||||||
|
|
||||||
|
if ($status_code !== 200) {
|
||||||
|
return new WP_Error('api_error', 'API returned status: ' . $status_code);
|
||||||
|
}
|
||||||
|
|
||||||
|
$last_request_time = microtime(true);
|
||||||
|
|
||||||
|
return wp_remote_retrieve_body($response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get platform-specific headers
|
||||||
|
*/
|
||||||
|
private function get_platform_headers($platform) {
|
||||||
|
switch ($platform) {
|
||||||
|
case 'github':
|
||||||
|
return array(
|
||||||
|
'Accept' => 'application/vnd.github.v3+json',
|
||||||
|
'User-Agent' => 'WordPress/GitHub-Code-Viewer'
|
||||||
|
);
|
||||||
|
case 'gitlab':
|
||||||
|
return array(
|
||||||
|
'Accept' => 'application/json',
|
||||||
|
'User-Agent' => 'WordPress/GitHub-Code-Viewer'
|
||||||
|
);
|
||||||
|
case 'bitbucket':
|
||||||
|
return array(
|
||||||
|
'Accept' => 'application/json',
|
||||||
|
'User-Agent' => 'WordPress/GitHub-Code-Viewer'
|
||||||
|
);
|
||||||
|
case 'codeberg':
|
||||||
|
return array(
|
||||||
|
'Accept' => 'application/json',
|
||||||
|
'User-Agent' => 'WordPress/GitHub-Code-Viewer'
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return array(
|
||||||
|
'User-Agent' => 'WordPress/GitHub-Code-Viewer'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get platform token if configured
|
||||||
|
*/
|
||||||
|
private function get_platform_token($platform) {
|
||||||
|
// Use secure token manager if available
|
||||||
|
if (class_exists('MCB_Token_Manager')) {
|
||||||
|
return MCB_Token_Manager::get_token($platform);
|
||||||
|
}
|
||||||
|
// Fallback to direct option (for backwards compatibility)
|
||||||
|
return get_option('mcb_' . $platform . '_token', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get authorization header format for platform
|
||||||
|
*/
|
||||||
|
private function get_auth_header($platform, $token) {
|
||||||
|
switch ($platform) {
|
||||||
|
case 'github':
|
||||||
|
return 'token ' . $token;
|
||||||
|
case 'gitlab':
|
||||||
|
return 'Bearer ' . $token;
|
||||||
|
case 'bitbucket':
|
||||||
|
return 'Bearer ' . $token;
|
||||||
|
case 'codeberg':
|
||||||
|
return 'token ' . $token;
|
||||||
|
default:
|
||||||
|
return 'Bearer ' . $token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse GitHub/Codeberg repository contents
|
||||||
|
*/
|
||||||
|
private function parse_github_contents($response, $repo, $current_path = '') {
|
||||||
|
$items = json_decode($response, true);
|
||||||
|
$files = array();
|
||||||
|
|
||||||
|
if (!is_array($items)) {
|
||||||
|
return $files;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add parent directory navigation if not at root
|
||||||
|
if (!empty($current_path)) {
|
||||||
|
$parent_path = dirname($current_path);
|
||||||
|
if ($parent_path === '.') {
|
||||||
|
$parent_path = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$files[] = array(
|
||||||
|
'name' => '..',
|
||||||
|
'path' => $parent_path,
|
||||||
|
'size' => 0,
|
||||||
|
'type' => 'parent',
|
||||||
|
'is_folder' => true,
|
||||||
|
'url' => ''
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$code_extensions = $this->get_code_extensions();
|
||||||
|
|
||||||
|
// First add all directories
|
||||||
|
foreach ($items as $item) {
|
||||||
|
if ($item['type'] === 'dir') {
|
||||||
|
$files[] = array(
|
||||||
|
'name' => $item['name'],
|
||||||
|
'path' => $item['path'],
|
||||||
|
'size' => 0,
|
||||||
|
'type' => 'folder',
|
||||||
|
'is_folder' => true,
|
||||||
|
'url' => $item['html_url']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then add all files
|
||||||
|
foreach ($items as $item) {
|
||||||
|
if ($item['type'] === 'file') {
|
||||||
|
$extension = strtolower(pathinfo($item['name'], PATHINFO_EXTENSION));
|
||||||
|
$name_lower = strtolower($item['name']);
|
||||||
|
|
||||||
|
// Include all files, not just code files, for better browsing
|
||||||
|
$files[] = array(
|
||||||
|
'name' => $item['name'],
|
||||||
|
'path' => $item['path'],
|
||||||
|
'size' => $item['size'],
|
||||||
|
'type' => $this->get_file_type($item['name']),
|
||||||
|
'is_folder' => false,
|
||||||
|
'url' => $item['html_url']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $files;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse GitLab repository contents
|
||||||
|
*/
|
||||||
|
private function parse_gitlab_contents($response) {
|
||||||
|
$items = json_decode($response, true);
|
||||||
|
$files = array();
|
||||||
|
|
||||||
|
if (!is_array($items)) {
|
||||||
|
return $files;
|
||||||
|
}
|
||||||
|
|
||||||
|
$code_extensions = $this->get_code_extensions();
|
||||||
|
|
||||||
|
foreach ($items as $item) {
|
||||||
|
if ($item['type'] === 'blob') { // GitLab uses 'blob' for files
|
||||||
|
$extension = strtolower(pathinfo($item['name'], PATHINFO_EXTENSION));
|
||||||
|
$name_lower = strtolower($item['name']);
|
||||||
|
|
||||||
|
if (in_array($extension, $code_extensions) || $this->is_code_file($name_lower)) {
|
||||||
|
$files[] = array(
|
||||||
|
'name' => $item['name'],
|
||||||
|
'path' => $item['path'],
|
||||||
|
'size' => 0, // GitLab doesn't provide size in tree API
|
||||||
|
'type' => $this->get_file_type($item['name']),
|
||||||
|
'url' => '' // Will need to construct if needed
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $files;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse Bitbucket repository contents
|
||||||
|
*/
|
||||||
|
private function parse_bitbucket_contents($response, $repo) {
|
||||||
|
$data = json_decode($response, true);
|
||||||
|
$files = array();
|
||||||
|
|
||||||
|
if (!isset($data['values']) || !is_array($data['values'])) {
|
||||||
|
return $files;
|
||||||
|
}
|
||||||
|
|
||||||
|
$code_extensions = $this->get_code_extensions();
|
||||||
|
|
||||||
|
foreach ($data['values'] as $item) {
|
||||||
|
if ($item['type'] === 'commit_file') {
|
||||||
|
$extension = strtolower(pathinfo($item['path'], PATHINFO_EXTENSION));
|
||||||
|
$name_lower = strtolower(basename($item['path']));
|
||||||
|
|
||||||
|
if (in_array($extension, $code_extensions) || $this->is_code_file($name_lower)) {
|
||||||
|
$files[] = array(
|
||||||
|
'name' => basename($item['path']),
|
||||||
|
'path' => $item['path'],
|
||||||
|
'size' => isset($item['size']) ? $item['size'] : 0,
|
||||||
|
'type' => $this->get_file_type(basename($item['path'])),
|
||||||
|
'url' => 'https://bitbucket.org/' . $repo . '/src/master/' . $item['path']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $files;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get list of code file extensions
|
||||||
|
*/
|
||||||
|
private function get_code_extensions() {
|
||||||
|
return array(
|
||||||
|
'php', 'js', 'jsx', 'ts', 'tsx', 'py', 'rb', 'java', 'c', 'cpp', 'cc', 'cxx',
|
||||||
|
'h', 'hpp', 'cs', 'swift', 'kt', 'go', 'rs', 'scala', 'r', 'sql', 'sh', 'bash',
|
||||||
|
'yml', 'yaml', 'json', 'xml', 'html', 'css', 'scss', 'sass', 'less',
|
||||||
|
'md', 'markdown', 'txt', 'ini', 'conf', 'dockerfile', 'makefile',
|
||||||
|
'vue', 'svelte', 'elm', 'clj', 'ex', 'exs', 'erl', 'hrl', 'lua', 'pl', 'pm'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if filename indicates a code file
|
||||||
|
*/
|
||||||
|
private function is_code_file($filename) {
|
||||||
|
$code_files = array(
|
||||||
|
'dockerfile', 'makefile', '.gitignore', '.env', '.htaccess',
|
||||||
|
'gemfile', 'rakefile', 'gulpfile', 'gruntfile', 'webpack.config.js',
|
||||||
|
'package.json', 'composer.json', 'cargo.toml', 'go.mod'
|
||||||
|
);
|
||||||
|
|
||||||
|
return in_array($filename, $code_files);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get file type based on extension
|
||||||
|
*/
|
||||||
|
private function get_file_type($filename) {
|
||||||
|
$extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
|
||||||
|
$name_lower = strtolower($filename);
|
||||||
|
|
||||||
|
$type_map = array(
|
||||||
|
'php' => 'php',
|
||||||
|
'js' => 'javascript',
|
||||||
|
'jsx' => 'jsx',
|
||||||
|
'ts' => 'typescript',
|
||||||
|
'tsx' => 'tsx',
|
||||||
|
'py' => 'python',
|
||||||
|
'rb' => 'ruby',
|
||||||
|
'java' => 'java',
|
||||||
|
'c' => 'c',
|
||||||
|
'cpp' => 'cpp',
|
||||||
|
'cc' => 'cpp',
|
||||||
|
'cxx' => 'cpp',
|
||||||
|
'h' => 'c',
|
||||||
|
'hpp' => 'cpp',
|
||||||
|
'cs' => 'csharp',
|
||||||
|
'swift' => 'swift',
|
||||||
|
'kt' => 'kotlin',
|
||||||
|
'go' => 'go',
|
||||||
|
'rs' => 'rust',
|
||||||
|
'scala' => 'scala',
|
||||||
|
'r' => 'r',
|
||||||
|
'sql' => 'sql',
|
||||||
|
'sh' => 'bash',
|
||||||
|
'bash' => 'bash',
|
||||||
|
'yml' => 'yaml',
|
||||||
|
'yaml' => 'yaml',
|
||||||
|
'json' => 'json',
|
||||||
|
'xml' => 'xml',
|
||||||
|
'html' => 'html',
|
||||||
|
'css' => 'css',
|
||||||
|
'scss' => 'scss',
|
||||||
|
'sass' => 'sass',
|
||||||
|
'less' => 'less',
|
||||||
|
'md' => 'markdown',
|
||||||
|
'markdown' => 'markdown'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check special file names
|
||||||
|
if ($name_lower === 'dockerfile') {
|
||||||
|
return 'docker';
|
||||||
|
}
|
||||||
|
if ($name_lower === 'makefile') {
|
||||||
|
return 'makefile';
|
||||||
|
}
|
||||||
|
|
||||||
|
return isset($type_map[$extension]) ? $type_map[$extension] : 'plain';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate if repository exists
|
||||||
|
*/
|
||||||
|
public function validate_repository_exists($repo_string) {
|
||||||
|
$platform_info = $this->detect_platform($repo_string);
|
||||||
|
$platform = $platform_info['platform'];
|
||||||
|
$repo = $platform_info['repo'];
|
||||||
|
|
||||||
|
// Validate format first
|
||||||
|
if (!preg_match($this->platforms[$platform]['repo_pattern'], $repo)) {
|
||||||
|
return new WP_Error('invalid_repo', 'Invalid repository format');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check cache first
|
||||||
|
$cache_key = 'mcb_repo_valid_' . $platform . '_' . md5($repo);
|
||||||
|
$cached_result = get_transient($cache_key);
|
||||||
|
|
||||||
|
if ($cached_result !== false) {
|
||||||
|
return $cached_result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check with platform API
|
||||||
|
$exists = false;
|
||||||
|
switch ($platform) {
|
||||||
|
case 'github':
|
||||||
|
$url = $this->platforms['github']['api_base'] . '/repos/' . $repo;
|
||||||
|
break;
|
||||||
|
case 'gitlab':
|
||||||
|
$project_id = urlencode($repo);
|
||||||
|
$url = $this->platforms['gitlab']['api_base'] . '/projects/' . $project_id;
|
||||||
|
break;
|
||||||
|
case 'bitbucket':
|
||||||
|
$url = $this->platforms['bitbucket']['api_base'] . '/repositories/' . $repo;
|
||||||
|
break;
|
||||||
|
case 'codeberg':
|
||||||
|
$url = $this->platforms['codeberg']['api_base'] . '/repos/' . $repo;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return new WP_Error('unsupported_platform', 'Unsupported platform');
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = $this->make_api_request($url, $platform);
|
||||||
|
|
||||||
|
if (!is_wp_error($response)) {
|
||||||
|
$data = json_decode($response, true);
|
||||||
|
if (isset($data['id']) || isset($data['uuid']) || isset($data['name'])) {
|
||||||
|
$exists = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($exists) {
|
||||||
|
set_transient($cache_key, true, 3600);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new WP_Error('repo_not_found', 'Repository not found or is private');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get platform display name
|
||||||
|
*/
|
||||||
|
public function get_platform_name($repo_string) {
|
||||||
|
$platform_info = $this->detect_platform($repo_string);
|
||||||
|
return $this->platforms[$platform_info['platform']]['name'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get repository web URL
|
||||||
|
*/
|
||||||
|
public function get_repository_url($repo_string) {
|
||||||
|
$platform_info = $this->detect_platform($repo_string);
|
||||||
|
$platform = $platform_info['platform'];
|
||||||
|
$repo = $platform_info['repo'];
|
||||||
|
|
||||||
|
return $this->platforms[$platform]['web_base'] . '/' . $repo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,324 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Token Manager Class
|
||||||
|
* Handles secure token storage with encryption
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Prevent direct access
|
||||||
|
if (!defined('WPINC')) {
|
||||||
|
die('Direct access not permitted.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
class MCB_Token_Manager {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if encryption is available
|
||||||
|
*/
|
||||||
|
public static function encryption_available() {
|
||||||
|
return function_exists('openssl_encrypt') && function_exists('openssl_decrypt');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypt token for secure storage
|
||||||
|
*/
|
||||||
|
public static function encrypt_token($token) {
|
||||||
|
if (empty($token)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use WordPress salts for encryption
|
||||||
|
if (self::encryption_available()) {
|
||||||
|
$key = substr(hash('sha256', wp_salt('auth')), 0, 32);
|
||||||
|
$iv = substr(hash('sha256', wp_salt('secure')), 0, 16);
|
||||||
|
|
||||||
|
$encrypted = openssl_encrypt($token, 'AES-256-CBC', $key, 0, $iv);
|
||||||
|
if ($encrypted !== false) {
|
||||||
|
return base64_encode($encrypted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to base64 if encryption not available
|
||||||
|
return base64_encode($token);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypt token for use
|
||||||
|
*/
|
||||||
|
public static function decrypt_token($encrypted_token) {
|
||||||
|
if (empty($encrypted_token)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use WordPress salts for decryption
|
||||||
|
if (self::encryption_available()) {
|
||||||
|
$key = substr(hash('sha256', wp_salt('auth')), 0, 32);
|
||||||
|
$iv = substr(hash('sha256', wp_salt('secure')), 0, 16);
|
||||||
|
|
||||||
|
$decoded = base64_decode($encrypted_token);
|
||||||
|
if ($decoded !== false) {
|
||||||
|
$decrypted = openssl_decrypt($decoded, 'AES-256-CBC', $key, 0, $iv);
|
||||||
|
if ($decrypted !== false) {
|
||||||
|
return $decrypted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to base64 if encryption not available
|
||||||
|
return base64_decode($encrypted_token);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store encrypted token
|
||||||
|
*/
|
||||||
|
public static function store_token($platform, $token) {
|
||||||
|
if (empty($token)) {
|
||||||
|
delete_option('mcb_' . $platform . '_token_encrypted');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$encrypted = self::encrypt_token($token);
|
||||||
|
return update_option('mcb_' . $platform . '_token_encrypted', $encrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve and decrypt token
|
||||||
|
*/
|
||||||
|
public static function get_token($platform) {
|
||||||
|
$encrypted = get_option('mcb_' . $platform . '_token_encrypted', '');
|
||||||
|
if (empty($encrypted)) {
|
||||||
|
// Check for legacy unencrypted token
|
||||||
|
$legacy = get_option('mcb_' . $platform . '_token', '');
|
||||||
|
if (!empty($legacy)) {
|
||||||
|
// Migrate to encrypted storage
|
||||||
|
self::store_token($platform, $legacy);
|
||||||
|
delete_option('mcb_' . $platform . '_token');
|
||||||
|
return $legacy;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::decrypt_token($encrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove all tokens (for privacy/GDPR)
|
||||||
|
*/
|
||||||
|
public static function remove_all_tokens() {
|
||||||
|
$platforms = array('github', 'gitlab', 'bitbucket', 'codeberg');
|
||||||
|
foreach ($platforms as $platform) {
|
||||||
|
delete_option('mcb_' . $platform . '_token');
|
||||||
|
delete_option('mcb_' . $platform . '_token_encrypted');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Privacy Manager Class
|
||||||
|
* Handles GDPR compliance and privacy features
|
||||||
|
*/
|
||||||
|
class MCB_Privacy_Manager {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize privacy features
|
||||||
|
*/
|
||||||
|
public static function init() {
|
||||||
|
// WordPress privacy policy content
|
||||||
|
add_action('admin_init', array(__CLASS__, 'privacy_policy_content'));
|
||||||
|
|
||||||
|
// Data exporter for GDPR
|
||||||
|
add_filter('wp_privacy_personal_data_exporters', array(__CLASS__, 'register_data_exporter'));
|
||||||
|
|
||||||
|
// Data eraser for GDPR
|
||||||
|
add_filter('wp_privacy_personal_data_erasers', array(__CLASS__, 'register_data_eraser'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if privacy mode is enabled
|
||||||
|
*/
|
||||||
|
public static function is_privacy_mode() {
|
||||||
|
return get_option('mcb_privacy_mode', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add suggested privacy policy content
|
||||||
|
*/
|
||||||
|
public static function privacy_policy_content() {
|
||||||
|
if (!function_exists('wp_add_privacy_policy_content')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = '
|
||||||
|
<h3>Git Code Viewer Plugin</h3>
|
||||||
|
<p>When you visit pages that display code repositories using the Git Code Viewer plugin:</p>
|
||||||
|
<ul>
|
||||||
|
<li>We temporarily store an anonymized version of your IP address (for 60 seconds) to prevent abuse through rate limiting.</li>
|
||||||
|
<li>If security events occur, we may log your browser user agent (for 7 days) for security monitoring.</li>
|
||||||
|
<li>We do not use cookies or tracking technologies.</li>
|
||||||
|
<li>We only access publicly available repository data from GitHub, GitLab, Bitbucket, and Codeberg.</li>
|
||||||
|
<li>No personal information is shared with third parties.</li>
|
||||||
|
<li>All data is automatically deleted after the retention period.</li>
|
||||||
|
</ul>
|
||||||
|
<p>If Privacy Mode is enabled, no IP addresses or user agents are collected.</p>
|
||||||
|
';
|
||||||
|
|
||||||
|
wp_add_privacy_policy_content(
|
||||||
|
'Git Code Viewer',
|
||||||
|
wp_kses_post($content)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register data exporter for GDPR requests
|
||||||
|
*/
|
||||||
|
public static function register_data_exporter($exporters) {
|
||||||
|
$exporters['git-code-viewer'] = array(
|
||||||
|
'exporter_friendly_name' => __('Git Code Viewer Plugin'),
|
||||||
|
'callback' => array(__CLASS__, 'data_exporter')
|
||||||
|
);
|
||||||
|
return $exporters;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export user data for GDPR requests
|
||||||
|
*/
|
||||||
|
public static function data_exporter($email_address, $page = 1) {
|
||||||
|
$export_items = array();
|
||||||
|
|
||||||
|
// Get security logs that might contain this user's data
|
||||||
|
$logs = get_transient('mcb_security_logs');
|
||||||
|
|
||||||
|
if (is_array($logs)) {
|
||||||
|
$user_data = array();
|
||||||
|
|
||||||
|
// Note: We don't store email addresses, so we can't directly match
|
||||||
|
// This is actually good for privacy!
|
||||||
|
$user_data[] = array(
|
||||||
|
'name' => 'Security Logs Notice',
|
||||||
|
'value' => 'The Git Code Viewer plugin does not store email addresses. Any security logs contain only anonymized IP addresses and user agents.'
|
||||||
|
);
|
||||||
|
|
||||||
|
$export_items[] = array(
|
||||||
|
'group_id' => 'git-code-viewer',
|
||||||
|
'group_label' => 'Git Code Viewer Data',
|
||||||
|
'item_id' => 'gcv-notice',
|
||||||
|
'data' => $user_data
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'data' => $export_items,
|
||||||
|
'done' => true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register data eraser for GDPR requests
|
||||||
|
*/
|
||||||
|
public static function register_data_eraser($erasers) {
|
||||||
|
$erasers['git-code-viewer'] = array(
|
||||||
|
'eraser_friendly_name' => __('Git Code Viewer Plugin'),
|
||||||
|
'callback' => array(__CLASS__, 'data_eraser')
|
||||||
|
);
|
||||||
|
return $erasers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Erase user data for GDPR requests
|
||||||
|
*/
|
||||||
|
public static function data_eraser($email_address, $page = 1) {
|
||||||
|
// Since we don't store email addresses or persistent user data,
|
||||||
|
// we can only clear all transient data if requested
|
||||||
|
|
||||||
|
if ($page === 1) {
|
||||||
|
// Clear all security logs
|
||||||
|
delete_transient('mcb_security_logs');
|
||||||
|
|
||||||
|
// Clear all rate limit transients (they expire in 60 seconds anyway)
|
||||||
|
global $wpdb;
|
||||||
|
$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_mcb_rate_%'");
|
||||||
|
$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_mcb_rate_%'");
|
||||||
|
}
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'items_removed' => true,
|
||||||
|
'items_retained' => false,
|
||||||
|
'messages' => array('All temporary Git Code Viewer data has been removed.'),
|
||||||
|
'done' => true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get anonymized IP for privacy compliance
|
||||||
|
*/
|
||||||
|
public static function get_anonymized_ip($ip) {
|
||||||
|
if (self::is_privacy_mode()) {
|
||||||
|
return 'privacy-mode';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Anonymize IP by removing last octet for IPv4 or last 80 bits for IPv6
|
||||||
|
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||||
|
$parts = explode('.', $ip);
|
||||||
|
$parts[3] = '0';
|
||||||
|
return implode('.', $parts);
|
||||||
|
} elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
||||||
|
// Zero out last 80 bits
|
||||||
|
return substr($ip, 0, strrpos($ip, ':')) . ':0:0:0:0:0';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up old data automatically
|
||||||
|
*/
|
||||||
|
public static function cleanup_old_data() {
|
||||||
|
// This is called on a daily schedule
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
// Remove expired rate limit transients
|
||||||
|
$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_mcb_%' AND option_value < UNIX_TIMESTAMP()");
|
||||||
|
|
||||||
|
// Remove orphaned transients (fixed MySQL compatibility issue)
|
||||||
|
// First get all valid transient names
|
||||||
|
$valid_transients = $wpdb->get_col(
|
||||||
|
"SELECT CONCAT('_transient_', SUBSTRING(option_name, 20))
|
||||||
|
FROM {$wpdb->options}
|
||||||
|
WHERE option_name LIKE '_transient_timeout_mcb_%'"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!empty($valid_transients)) {
|
||||||
|
// Build a safe IN clause
|
||||||
|
$placeholders = array_fill(0, count($valid_transients), '%s');
|
||||||
|
$in_clause = implode(',', $placeholders);
|
||||||
|
|
||||||
|
// Delete orphaned transients
|
||||||
|
$wpdb->query(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"DELETE FROM {$wpdb->options}
|
||||||
|
WHERE option_name LIKE '_transient_mcb_%'
|
||||||
|
AND option_name NOT IN ($in_clause)",
|
||||||
|
$valid_transients
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// No valid transients with timeouts, remove all MCB transients
|
||||||
|
$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_mcb_%'");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also clean up expired timeout options
|
||||||
|
$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_mcb_%' AND option_value < UNIX_TIMESTAMP()");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize privacy features
|
||||||
|
MCB_Privacy_Manager::init();
|
||||||
|
|
||||||
|
// Schedule cleanup (weekly is sufficient for transient cleanup)
|
||||||
|
if (!wp_next_scheduled('mcb_privacy_cleanup')) {
|
||||||
|
wp_schedule_event(time(), 'weekly', 'mcb_privacy_cleanup');
|
||||||
|
}
|
||||||
|
add_action('mcb_privacy_cleanup', array('MCB_Privacy_Manager', 'cleanup_old_data'));
|
||||||
|
|
@ -0,0 +1,385 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Critical Security Fixes and Plugin Compatibility
|
||||||
|
* Implementation of audit recommendations
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Prevent direct access
|
||||||
|
if (!defined('WPINC')) {
|
||||||
|
die('Direct access not permitted.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
class MCB_Security_Fixes {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize security fixes and compatibility patches
|
||||||
|
*/
|
||||||
|
public static function init() {
|
||||||
|
// Fix 1: SSRF DNS Rebinding Protection
|
||||||
|
add_filter('mcb_validate_api_url', array(__CLASS__, 'validate_api_url_dns'), 10, 2);
|
||||||
|
|
||||||
|
// Fix 2: Wordfence Compatibility
|
||||||
|
add_action('init', array(__CLASS__, 'wordfence_compatibility'));
|
||||||
|
|
||||||
|
// Fix 3: Error Suppression in Production
|
||||||
|
add_action('init', array(__CLASS__, 'suppress_errors_production'));
|
||||||
|
|
||||||
|
// Fix 4: Token Autoloading Fix
|
||||||
|
add_filter('mcb_update_option', array(__CLASS__, 'disable_autoload'), 10, 3);
|
||||||
|
|
||||||
|
// Fix 5: User Agent Hashing
|
||||||
|
add_filter('mcb_log_user_agent', array(__CLASS__, 'hash_user_agent'));
|
||||||
|
|
||||||
|
// Fix 6: Plugin Conflict Detection
|
||||||
|
add_action('admin_notices', array(__CLASS__, 'compatibility_notices'));
|
||||||
|
|
||||||
|
// Fix 7: Performance Optimization
|
||||||
|
add_action('init', array(__CLASS__, 'optimize_loading'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CRITICAL FIX: SSRF DNS Rebinding Protection
|
||||||
|
* Prevents accessing internal network resources
|
||||||
|
*/
|
||||||
|
public static function validate_api_url_dns($valid, $url) {
|
||||||
|
$parsed = parse_url($url);
|
||||||
|
if (!$parsed || !isset($parsed['host'])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$host = $parsed['host'];
|
||||||
|
|
||||||
|
// Whitelist of allowed API hosts
|
||||||
|
$allowed_hosts = array(
|
||||||
|
'api.github.com',
|
||||||
|
'gitlab.com',
|
||||||
|
'api.bitbucket.org',
|
||||||
|
'codeberg.org'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!in_array($host, $allowed_hosts, true)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get IP address of the host
|
||||||
|
$ip = gethostbyname($host);
|
||||||
|
|
||||||
|
// If resolution failed, block
|
||||||
|
if ($ip === $host) {
|
||||||
|
error_log('GCV Security: DNS resolution failed for ' . $host);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for private and reserved IP ranges
|
||||||
|
if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
|
||||||
|
error_log('GCV Security: Blocked private/reserved IP ' . $ip . ' for host ' . $host);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional check for IPv6
|
||||||
|
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
||||||
|
// Check for IPv6 private ranges
|
||||||
|
$private_ranges = array(
|
||||||
|
'fc00::/7', // Unique local addresses
|
||||||
|
'fe80::/10', // Link-local addresses
|
||||||
|
'::1/128', // Loopback
|
||||||
|
'::/128', // Unspecified
|
||||||
|
'::ffff:0:0/96' // IPv4-mapped addresses
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($private_ranges as $range) {
|
||||||
|
if (self::ip_in_range($ip, $range)) {
|
||||||
|
error_log('GCV Security: Blocked private IPv6 ' . $ip);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wordfence Compatibility Layer
|
||||||
|
*/
|
||||||
|
public static function wordfence_compatibility() {
|
||||||
|
if (!defined('WORDFENCE_VERSION')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add API URLs to Wordfence whitelist
|
||||||
|
if (class_exists('wfConfig')) {
|
||||||
|
$whitelisted = wfConfig::get('whitelisted');
|
||||||
|
$whitelist_urls = array(
|
||||||
|
'api.github.com',
|
||||||
|
'gitlab.com',
|
||||||
|
'api.bitbucket.org',
|
||||||
|
'codeberg.org'
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($whitelist_urls as $url) {
|
||||||
|
if (strpos($whitelisted, $url) === false) {
|
||||||
|
$whitelisted .= "\n" . $url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wfConfig::set('whitelisted', $whitelisted);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable plugin rate limiting if Wordfence is active
|
||||||
|
if (has_filter('mcb_enable_rate_limiting')) {
|
||||||
|
add_filter('mcb_enable_rate_limiting', '__return_false');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add Wordfence compatibility headers
|
||||||
|
add_action('mcb_before_api_request', function() {
|
||||||
|
if (function_exists('wfUtils::doNotCache')) {
|
||||||
|
wfUtils::doNotCache();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Suppress errors in production environments
|
||||||
|
*/
|
||||||
|
public static function suppress_errors_production() {
|
||||||
|
// Only in production (non-debug mode)
|
||||||
|
if (defined('WP_DEBUG') && WP_DEBUG) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Suppress PHP errors in AJAX handlers
|
||||||
|
if (wp_doing_ajax()) {
|
||||||
|
@error_reporting(0);
|
||||||
|
@ini_set('display_errors', '0');
|
||||||
|
@ini_set('display_startup_errors', '0');
|
||||||
|
|
||||||
|
// Set custom error handler
|
||||||
|
set_error_handler(function($errno, $errstr, $errfile, $errline) {
|
||||||
|
// Log to error log but don't display
|
||||||
|
if (WP_DEBUG_LOG) {
|
||||||
|
error_log(sprintf(
|
||||||
|
'GCV Error: %s in %s on line %d',
|
||||||
|
$errstr,
|
||||||
|
$errfile,
|
||||||
|
$errline
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return true; // Prevent default error handler
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fix token autoloading issue
|
||||||
|
*/
|
||||||
|
public static function disable_autoload($value, $option, $autoload) {
|
||||||
|
// Disable autoload for token options
|
||||||
|
if (strpos($option, 'mcb_') === 0 && strpos($option, '_token') !== false) {
|
||||||
|
return 'no';
|
||||||
|
}
|
||||||
|
return $autoload;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hash user agents for privacy
|
||||||
|
*/
|
||||||
|
public static function hash_user_agent($user_agent) {
|
||||||
|
if (empty($user_agent)) {
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use a consistent salt for hashing
|
||||||
|
$salt = wp_salt('auth');
|
||||||
|
|
||||||
|
// Create a hash that's consistent but not reversible
|
||||||
|
return substr(hash('sha256', $salt . $user_agent), 0, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show compatibility notices
|
||||||
|
*/
|
||||||
|
public static function compatibility_notices() {
|
||||||
|
$notices = array();
|
||||||
|
|
||||||
|
// Wordfence compatibility notice
|
||||||
|
if (defined('WORDFENCE_VERSION')) {
|
||||||
|
$notices[] = array(
|
||||||
|
'type' => 'warning',
|
||||||
|
'message' => __('Git Code Viewer: Wordfence detected. Please ensure the following URLs are whitelisted in Wordfence settings: api.github.com, gitlab.com, api.bitbucket.org, codeberg.org', 'maple-code-blocks'),
|
||||||
|
'dismissible' => true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// WooCommerce compatibility (informational)
|
||||||
|
if (class_exists('WooCommerce')) {
|
||||||
|
// No issues, just log for debugging
|
||||||
|
error_log('GCV: WooCommerce detected - compatibility mode active');
|
||||||
|
}
|
||||||
|
|
||||||
|
// LearnDash compatibility (informational)
|
||||||
|
if (defined('LEARNDASH_VERSION')) {
|
||||||
|
// No issues, just log for debugging
|
||||||
|
error_log('GCV: LearnDash detected - compatibility mode active');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Memory limit warning
|
||||||
|
$memory_limit = ini_get('memory_limit');
|
||||||
|
if ($memory_limit && self::convert_to_bytes($memory_limit) < 67108864) { // 64MB
|
||||||
|
$notices[] = array(
|
||||||
|
'type' => 'warning',
|
||||||
|
'message' => __('Git Code Viewer: Low memory limit detected. Consider increasing memory_limit to at least 64MB for optimal performance.', 'maple-code-blocks'),
|
||||||
|
'dismissible' => true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Display notices
|
||||||
|
foreach ($notices as $notice) {
|
||||||
|
$dismissible = $notice['dismissible'] ? 'is-dismissible' : '';
|
||||||
|
printf(
|
||||||
|
'<div class="notice notice-%s %s"><p>%s</p></div>',
|
||||||
|
esc_attr($notice['type']),
|
||||||
|
esc_attr($dismissible),
|
||||||
|
esc_html($notice['message'])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optimize plugin loading
|
||||||
|
*/
|
||||||
|
public static function optimize_loading() {
|
||||||
|
// Hook into admin_init for screen-specific optimizations
|
||||||
|
if (is_admin()) {
|
||||||
|
add_action('admin_init', function() {
|
||||||
|
// Check screen only after it's available
|
||||||
|
add_action('current_screen', function($screen) {
|
||||||
|
if ($screen && $screen->id !== 'settings_page_maple-code-blocks') {
|
||||||
|
remove_action('admin_enqueue_scripts', array('Maple_Code_Blocks', 'enqueue_scripts'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optimize transient cleanup
|
||||||
|
if (!wp_next_scheduled('mcb_cleanup_transients')) {
|
||||||
|
wp_schedule_event(time(), 'daily', 'mcb_cleanup_transients');
|
||||||
|
}
|
||||||
|
|
||||||
|
add_action('mcb_cleanup_transients', function() {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
// Use WordPress function instead of direct query
|
||||||
|
if (function_exists('delete_expired_transients')) {
|
||||||
|
delete_expired_transients();
|
||||||
|
} else {
|
||||||
|
// Fallback for older WordPress versions
|
||||||
|
$wpdb->query(
|
||||||
|
"DELETE FROM {$wpdb->options}
|
||||||
|
WHERE option_name LIKE '_transient_timeout_mcb_%'
|
||||||
|
AND option_value < UNIX_TIMESTAMP()"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper: Check if IP is in range
|
||||||
|
*/
|
||||||
|
private static function ip_in_range($ip, $range) {
|
||||||
|
if (strpos($range, '/') === false) {
|
||||||
|
$range .= '/32';
|
||||||
|
}
|
||||||
|
|
||||||
|
list($subnet, $bits) = explode('/', $range);
|
||||||
|
|
||||||
|
// IPv4
|
||||||
|
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) &&
|
||||||
|
filter_var($subnet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||||
|
$ip = ip2long($ip);
|
||||||
|
$subnet = ip2long($subnet);
|
||||||
|
$mask = -1 << (32 - $bits);
|
||||||
|
$subnet &= $mask;
|
||||||
|
return ($ip & $mask) == $subnet;
|
||||||
|
}
|
||||||
|
|
||||||
|
// IPv6
|
||||||
|
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) &&
|
||||||
|
filter_var($subnet, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
||||||
|
$ip_bin = inet_pton($ip);
|
||||||
|
$subnet_bin = inet_pton($subnet);
|
||||||
|
$bytes = $bits / 8;
|
||||||
|
$remainder = $bits % 8;
|
||||||
|
|
||||||
|
if ($bytes > 0) {
|
||||||
|
if (substr($ip_bin, 0, $bytes) !== substr($subnet_bin, 0, $bytes)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($remainder > 0 && $bytes < 16) {
|
||||||
|
$mask = 0xFF << (8 - $remainder);
|
||||||
|
$ip_byte = ord($ip_bin[$bytes]);
|
||||||
|
$subnet_byte = ord($subnet_bin[$bytes]);
|
||||||
|
if (($ip_byte & $mask) !== ($subnet_byte & $mask)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper: Convert memory string to bytes
|
||||||
|
*/
|
||||||
|
private static function convert_to_bytes($val) {
|
||||||
|
$val = trim($val);
|
||||||
|
$last = strtolower($val[strlen($val)-1]);
|
||||||
|
$val = (int)$val;
|
||||||
|
|
||||||
|
switch($last) {
|
||||||
|
case 'g':
|
||||||
|
$val *= 1024;
|
||||||
|
case 'm':
|
||||||
|
$val *= 1024;
|
||||||
|
case 'k':
|
||||||
|
$val *= 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize security fixes
|
||||||
|
add_action('plugins_loaded', array('MCB_Security_Fixes', 'init'), 5);
|
||||||
|
add_action('rest_api_init', function() {
|
||||||
|
register_rest_route('maple-code-blocks/v1', '/health', array(
|
||||||
|
'methods' => 'GET',
|
||||||
|
'callback' => function() {
|
||||||
|
$health = array(
|
||||||
|
'status' => 'healthy',
|
||||||
|
'version' => '2.0.0',
|
||||||
|
'php_version' => PHP_VERSION,
|
||||||
|
'wordpress_version' => get_bloginfo('version'),
|
||||||
|
'memory_usage' => memory_get_usage(true),
|
||||||
|
'memory_limit' => ini_get('memory_limit'),
|
||||||
|
'wordfence_active' => defined('WORDFENCE_VERSION'),
|
||||||
|
'woocommerce_active' => class_exists('WooCommerce'),
|
||||||
|
'learndash_active' => defined('LEARNDASH_VERSION'),
|
||||||
|
'cache_size' => count(get_transient('mcb_security_logs') ?: array()),
|
||||||
|
'rate_limit_active' => !defined('WORDFENCE_VERSION')
|
||||||
|
);
|
||||||
|
|
||||||
|
return new WP_REST_Response($health, 200);
|
||||||
|
},
|
||||||
|
'permission_callback' => function() {
|
||||||
|
return current_user_can('manage_options');
|
||||||
|
}
|
||||||
|
));
|
||||||
|
});
|
||||||
322
native/wordpress/maple-code-blocks/includes/class-security.php
Normal file
322
native/wordpress/maple-code-blocks/includes/class-security.php
Normal file
|
|
@ -0,0 +1,322 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Security Handler Class
|
||||||
|
* Implements OWASP security best practices
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Prevent direct access
|
||||||
|
if (!defined('WPINC')) {
|
||||||
|
die('Direct access not permitted.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
class MCB_Security {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize security features
|
||||||
|
*/
|
||||||
|
public static function init() {
|
||||||
|
// Add security headers
|
||||||
|
add_action('send_headers', array(__CLASS__, 'add_security_headers'));
|
||||||
|
|
||||||
|
// Add Content Security Policy
|
||||||
|
add_action('wp_head', array(__CLASS__, 'add_csp_meta'));
|
||||||
|
|
||||||
|
// Note: Global input sanitization removed - sanitize data at point of use instead
|
||||||
|
// This prevents conflicts with other plugins (e.g., WPForms)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add security headers
|
||||||
|
*/
|
||||||
|
public static function add_security_headers() {
|
||||||
|
// Only add headers on pages with our plugin
|
||||||
|
if (!self::is_plugin_active_on_page()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// X-Content-Type-Options
|
||||||
|
header('X-Content-Type-Options: nosniff');
|
||||||
|
|
||||||
|
// X-Frame-Options
|
||||||
|
header('X-Frame-Options: SAMEORIGIN');
|
||||||
|
|
||||||
|
// X-XSS-Protection (legacy but still useful)
|
||||||
|
header('X-XSS-Protection: 1; mode=block');
|
||||||
|
|
||||||
|
// Referrer Policy
|
||||||
|
header('Referrer-Policy: strict-origin-when-cross-origin');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add Content Security Policy meta tag
|
||||||
|
*/
|
||||||
|
public static function add_csp_meta() {
|
||||||
|
if (!self::is_plugin_active_on_page()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strict CSP for code viewer areas
|
||||||
|
$csp = "default-src 'self'; " .
|
||||||
|
"script-src 'self' 'unsafe-inline' 'unsafe-eval'; " . // Needed for Prism.js
|
||||||
|
"style-src 'self' 'unsafe-inline'; " . // Needed for inline styles
|
||||||
|
"img-src 'self' data: https:; " .
|
||||||
|
"connect-src 'self'; " .
|
||||||
|
"font-src 'self' data:; " .
|
||||||
|
"object-src 'none'; " .
|
||||||
|
"base-uri 'self'; " .
|
||||||
|
"form-action 'self'; " .
|
||||||
|
"frame-ancestors 'self';";
|
||||||
|
|
||||||
|
echo '<meta http-equiv="Content-Security-Policy" content="' . esc_attr($csp) . '">' . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if plugin is active on current page
|
||||||
|
*/
|
||||||
|
private static function is_plugin_active_on_page() {
|
||||||
|
global $post;
|
||||||
|
|
||||||
|
if (!is_a($post, 'WP_Post')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return has_shortcode($post->post_content, 'maple_code_block');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitize global input arrays
|
||||||
|
*/
|
||||||
|
public static function sanitize_global_input() {
|
||||||
|
// Sanitize $_GET
|
||||||
|
if (!empty($_GET)) {
|
||||||
|
$_GET = self::sanitize_array($_GET);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanitize $_POST
|
||||||
|
if (!empty($_POST)) {
|
||||||
|
$_POST = self::sanitize_array($_POST);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanitize $_REQUEST
|
||||||
|
if (!empty($_REQUEST)) {
|
||||||
|
$_REQUEST = self::sanitize_array($_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanitize $_COOKIE
|
||||||
|
if (!empty($_COOKIE)) {
|
||||||
|
$_COOKIE = self::sanitize_array($_COOKIE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively sanitize array
|
||||||
|
*/
|
||||||
|
private static function sanitize_array($array) {
|
||||||
|
foreach ($array as $key => $value) {
|
||||||
|
if (is_array($value)) {
|
||||||
|
$array[$key] = self::sanitize_array($value);
|
||||||
|
} else {
|
||||||
|
// Remove null bytes
|
||||||
|
$value = str_replace(chr(0), '', $value);
|
||||||
|
|
||||||
|
// Strip tags and encode special chars
|
||||||
|
$array[$key] = htmlspecialchars(strip_tags($value), ENT_QUOTES, 'UTF-8');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $array;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate repository format
|
||||||
|
* Supports: owner/repo, platform:owner/repo, or full URLs
|
||||||
|
*/
|
||||||
|
public static function validate_repo_format($repo) {
|
||||||
|
// Remove any whitespace
|
||||||
|
$repo = trim($repo);
|
||||||
|
|
||||||
|
// If it's a full URL, validate it
|
||||||
|
if (strpos($repo, 'https://') === 0 || strpos($repo, 'http://') === 0) {
|
||||||
|
// Check if it's from a supported platform
|
||||||
|
$supported_domains = array(
|
||||||
|
'github.com',
|
||||||
|
'gitlab.com',
|
||||||
|
'bitbucket.org',
|
||||||
|
'codeberg.org'
|
||||||
|
);
|
||||||
|
|
||||||
|
$parsed = parse_url($repo);
|
||||||
|
if (!$parsed || !isset($parsed['host'])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$domain_valid = false;
|
||||||
|
foreach ($supported_domains as $domain) {
|
||||||
|
if (strpos($parsed['host'], $domain) !== false) {
|
||||||
|
$domain_valid = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$domain_valid) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract path and validate format
|
||||||
|
if (!isset($parsed['path'])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$path = trim($parsed['path'], '/');
|
||||||
|
$parts = explode('/', $path);
|
||||||
|
|
||||||
|
// Need at least owner/repo
|
||||||
|
if (count($parts) < 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate owner and repo names
|
||||||
|
$owner = $parts[0];
|
||||||
|
$repo_name = $parts[1];
|
||||||
|
|
||||||
|
if (!preg_match('/^[a-zA-Z0-9\-_]+$/', $owner) ||
|
||||||
|
!preg_match('/^[a-zA-Z0-9\-_\.]+$/', $repo_name)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for platform prefix (e.g., gitlab:owner/repo)
|
||||||
|
if (strpos($repo, ':') !== false) {
|
||||||
|
list($platform, $repo_path) = explode(':', $repo, 2);
|
||||||
|
|
||||||
|
// Validate platform
|
||||||
|
$valid_platforms = array('github', 'gitlab', 'bitbucket', 'codeberg');
|
||||||
|
if (!in_array($platform, $valid_platforms)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate repo path
|
||||||
|
if (!preg_match('/^[a-zA-Z0-9\-_]+\/[a-zA-Z0-9\-_\.]+$/', $repo_path)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Standard format: owner/repo
|
||||||
|
if (!preg_match('/^[a-zA-Z0-9\-_]+\/[a-zA-Z0-9\-_\.]+$/', $repo)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check length limits
|
||||||
|
$parts = explode('/', $repo);
|
||||||
|
if (count($parts) !== 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Owner: 1-39 characters (GitHub limit)
|
||||||
|
if (strlen($parts[0]) < 1 || strlen($parts[0]) > 39) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Repo name: 1-100 characters (GitHub limit)
|
||||||
|
if (strlen($parts[1]) < 1 || strlen($parts[1]) > 100) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate file path
|
||||||
|
*/
|
||||||
|
public static function validate_file_path($path) {
|
||||||
|
// Remove leading/trailing slashes
|
||||||
|
$path = trim($path, '/');
|
||||||
|
|
||||||
|
// Check for path traversal attempts
|
||||||
|
$dangerous_patterns = array(
|
||||||
|
'..',
|
||||||
|
'//',
|
||||||
|
'\\',
|
||||||
|
'.git',
|
||||||
|
'.env',
|
||||||
|
'wp-config',
|
||||||
|
'.htaccess',
|
||||||
|
'.htpasswd'
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($dangerous_patterns as $pattern) {
|
||||||
|
if (stripos($path, $pattern) !== false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only allow safe characters
|
||||||
|
if (!preg_match('/^[a-zA-Z0-9\-_\.\/]+$/', $path)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check path depth (max 10 levels)
|
||||||
|
if (substr_count($path, '/') > 10) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate secure random token
|
||||||
|
*/
|
||||||
|
public static function generate_token($length = 32) {
|
||||||
|
if (function_exists('random_bytes')) {
|
||||||
|
return bin2hex(random_bytes($length));
|
||||||
|
} elseif (function_exists('openssl_random_pseudo_bytes')) {
|
||||||
|
return bin2hex(openssl_random_pseudo_bytes($length));
|
||||||
|
} else {
|
||||||
|
// Fallback to less secure method
|
||||||
|
return wp_generate_password($length * 2, false, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log security events
|
||||||
|
*/
|
||||||
|
public static function log_security_event($event_type, $details = array()) {
|
||||||
|
$log_entry = array(
|
||||||
|
'timestamp' => current_time('mysql'),
|
||||||
|
'event_type' => $event_type,
|
||||||
|
'ip_address' => isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '',
|
||||||
|
'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '',
|
||||||
|
'details' => $details
|
||||||
|
);
|
||||||
|
|
||||||
|
// Store in WordPress transient for review (expires in 7 days)
|
||||||
|
$logs = get_transient('mcb_security_logs');
|
||||||
|
if (!is_array($logs)) {
|
||||||
|
$logs = array();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep only last 100 entries
|
||||||
|
if (count($logs) >= 100) {
|
||||||
|
array_shift($logs);
|
||||||
|
}
|
||||||
|
|
||||||
|
$logs[] = $log_entry;
|
||||||
|
set_transient('mcb_security_logs', $logs, 7 * DAY_IN_SECONDS);
|
||||||
|
|
||||||
|
// For critical events, also log to error log
|
||||||
|
if (in_array($event_type, array('invalid_nonce', 'rate_limit_exceeded', 'path_traversal_attempt'))) {
|
||||||
|
error_log('GCV Security Event: ' . json_encode($log_entry));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize security features
|
||||||
|
MCB_Security::init();
|
||||||
190
native/wordpress/maple-code-blocks/includes/class-shortcode.php
Normal file
190
native/wordpress/maple-code-blocks/includes/class-shortcode.php
Normal file
|
|
@ -0,0 +1,190 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Shortcode Handler Class
|
||||||
|
* Manages the [github_code_viewer] shortcode
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Prevent direct access
|
||||||
|
if (!defined('WPINC')) {
|
||||||
|
die('Direct access not permitted.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
class MCB_Shortcode {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize shortcode
|
||||||
|
*/
|
||||||
|
public static function init() {
|
||||||
|
add_shortcode('maple_code_block', array(__CLASS__, 'render_shortcode'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the shortcode
|
||||||
|
*/
|
||||||
|
public static function render_shortcode($atts) {
|
||||||
|
// Parse shortcode attributes
|
||||||
|
$atts = shortcode_atts(array(
|
||||||
|
'repo' => '',
|
||||||
|
'theme' => 'dark',
|
||||||
|
'height' => '600px',
|
||||||
|
'show_line_numbers' => 'true',
|
||||||
|
'initial_file' => '',
|
||||||
|
'title' => ''
|
||||||
|
), $atts, 'github_code_viewer');
|
||||||
|
|
||||||
|
// Validate repository
|
||||||
|
if (empty($atts['repo'])) {
|
||||||
|
return '<div class="mcb-error">Error: No repository specified. Use repo="owner/repository"</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanitize attributes
|
||||||
|
$repo = sanitize_text_field($atts['repo']);
|
||||||
|
$theme = in_array($atts['theme'], array('dark', 'light', 'monokai', 'solarized')) ? $atts['theme'] : 'dark';
|
||||||
|
|
||||||
|
// Validate height - only allow specific units
|
||||||
|
$height = sanitize_text_field($atts['height']);
|
||||||
|
if (!preg_match('/^\d+(px|%|em|rem|vh)$/', $height)) {
|
||||||
|
$height = '600px'; // Default to safe value if invalid
|
||||||
|
}
|
||||||
|
|
||||||
|
$show_line_numbers = filter_var($atts['show_line_numbers'], FILTER_VALIDATE_BOOLEAN);
|
||||||
|
$initial_file = sanitize_text_field($atts['initial_file']);
|
||||||
|
$title = sanitize_text_field($atts['title']);
|
||||||
|
|
||||||
|
// Detect platform from repo string
|
||||||
|
$platform = 'github'; // default
|
||||||
|
$display_repo = $repo;
|
||||||
|
|
||||||
|
if (strpos($repo, ':') !== false) {
|
||||||
|
list($platform, $display_repo) = explode(':', $repo, 2);
|
||||||
|
} elseif (strpos($repo, 'https://') === 0 || strpos($repo, 'http://') === 0) {
|
||||||
|
// Parse URL to detect platform
|
||||||
|
if (strpos($repo, 'gitlab.com') !== false) {
|
||||||
|
$platform = 'gitlab';
|
||||||
|
} elseif (strpos($repo, 'bitbucket.org') !== false) {
|
||||||
|
$platform = 'bitbucket';
|
||||||
|
} elseif (strpos($repo, 'codeberg.org') !== false) {
|
||||||
|
$platform = 'codeberg';
|
||||||
|
}
|
||||||
|
// Extract repo name from URL
|
||||||
|
$parsed = parse_url($repo);
|
||||||
|
if ($parsed && isset($parsed['path'])) {
|
||||||
|
$display_repo = trim($parsed['path'], '/');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate unique ID for this instance
|
||||||
|
$viewer_id = 'mcb-' . uniqid();
|
||||||
|
|
||||||
|
// Build the viewer HTML
|
||||||
|
ob_start();
|
||||||
|
?>
|
||||||
|
<div id="<?php echo esc_attr($viewer_id); ?>" class="maple-code-blocks mcb-theme-<?php echo esc_attr($theme); ?>"
|
||||||
|
data-repo="<?php echo esc_attr($repo); ?>"
|
||||||
|
data-theme="<?php echo esc_attr($theme); ?>"
|
||||||
|
data-show-line-numbers="<?php echo $show_line_numbers ? 'true' : 'false'; ?>"
|
||||||
|
data-initial-file="<?php echo esc_attr($initial_file); ?>"
|
||||||
|
style="height: <?php echo esc_attr($height); ?>;">
|
||||||
|
|
||||||
|
<div class="mcb-header">
|
||||||
|
<?php if (!empty($title)) : ?>
|
||||||
|
<h3 class="mcb-title"><?php echo esc_html($title); ?></h3>
|
||||||
|
<?php endif; ?>
|
||||||
|
<div class="mcb-repo-info">
|
||||||
|
<?php
|
||||||
|
// Display platform-specific icon
|
||||||
|
switch($platform) {
|
||||||
|
case 'gitlab':
|
||||||
|
?>
|
||||||
|
<svg class="mcb-platform-icon" viewBox="0 0 24 24" width="20" height="20">
|
||||||
|
<path fill="#FC6D26" d="M22.65 14.39L12 22.13 1.35 14.39a.84.84 0 0 1-.3-.94l1.22-3.78 2.44-7.51A.42.42 0 0 1 4.82 2a.43.43 0 0 1 .58 0 .42.42 0 0 1 .11.18l2.44 7.49h8.1l2.44-7.51A.42.42 0 0 1 18.6 2a.43.43 0 0 1 .58 0 .42.42 0 0 1 .11.18l2.44 7.51L23 13.45a.84.84 0 0 1-.35.94z"/>
|
||||||
|
</svg>
|
||||||
|
<?php
|
||||||
|
break;
|
||||||
|
case 'bitbucket':
|
||||||
|
?>
|
||||||
|
<svg class="mcb-platform-icon" viewBox="0 0 24 24" width="20" height="20">
|
||||||
|
<path fill="#0052CC" d="M3.28 2.42a1 1 0 00-.97.8l-2.3 14.3a1.35 1.35 0 00.78 1.51l9.84 4.13c.36.15.77.15 1.13 0l9.86-4.13a1.35 1.35 0 00.78-1.5l-2.3-14.31a1 1 0 00-.97-.8H3.28zm8.3 12.66h-3.8l-1-6.4h5.56l.75 6.4z"/>
|
||||||
|
</svg>
|
||||||
|
<?php
|
||||||
|
break;
|
||||||
|
case 'codeberg':
|
||||||
|
?>
|
||||||
|
<svg class="mcb-platform-icon" viewBox="0 0 24 24" width="20" height="20">
|
||||||
|
<path fill="#2185D0" d="M12 2C6.48 2 2 6.48 2 12c0 4.42 2.87 8.17 6.84 9.49.5.09.68-.22.68-.48 0-.24-.01-1.02-.01-1.85-2.78.6-3.37-1.18-3.37-1.18-.45-1.15-1.11-1.46-1.11-1.46-.91-.62.07-.61.07-.61 1 .07 1.53 1.03 1.53 1.03.89 1.53 2.34 1.09 2.91.83.09-.64.35-1.09.63-1.34-2.22-.25-4.55-1.11-4.55-4.93 0-1.09.39-1.98 1.03-2.68-.1-.25-.45-1.27.1-2.64 0 0 .84-.27 2.75 1.03A9.58 9.58 0 0112 6.8c.85.004 1.71.115 2.51.34 1.91-1.3 2.75-1.03 2.75-1.03.55 1.37.2 2.39.1 2.64.64.7 1.03 1.59 1.03 2.68 0 3.83-2.34 4.68-4.56 4.92.36.31.68.92.68 1.85 0 1.34-.01 2.42-.01 2.75 0 .27.18.58.69.48A10.01 10.01 0 0022 12c0-5.52-4.48-10-10-10z"/>
|
||||||
|
</svg>
|
||||||
|
<?php
|
||||||
|
break;
|
||||||
|
case 'github':
|
||||||
|
default:
|
||||||
|
?>
|
||||||
|
<svg class="mcb-platform-icon" viewBox="0 0 24 24" width="20" height="20">
|
||||||
|
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
|
||||||
|
</svg>
|
||||||
|
<?php
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<span class="mcb-repo-name"><?php echo esc_html($display_repo); ?></span>
|
||||||
|
</div>
|
||||||
|
<div class="mcb-controls">
|
||||||
|
<button class="mcb-home-btn" title="Go to root">
|
||||||
|
<svg viewBox="0 0 24 24" width="18" height="18">
|
||||||
|
<path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button class="mcb-refresh-btn" title="Refresh files">
|
||||||
|
<svg viewBox="0 0 24 24" width="18" height="18">
|
||||||
|
<path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button class="mcb-fullscreen-btn" title="Toggle fullscreen">
|
||||||
|
<svg viewBox="0 0 24 24" width="18" height="18">
|
||||||
|
<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mcb-content">
|
||||||
|
<div class="mcb-sidebar">
|
||||||
|
<div class="mcb-search-box">
|
||||||
|
<input type="text" class="mcb-search-input" placeholder="Search files...">
|
||||||
|
</div>
|
||||||
|
<div class="mcb-file-list">
|
||||||
|
<div class="mcb-loading">
|
||||||
|
<div class="mcb-spinner"></div>
|
||||||
|
<span>Loading repository files...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mcb-editor">
|
||||||
|
<div class="mcb-tabs">
|
||||||
|
<!-- Tabs will be added dynamically -->
|
||||||
|
</div>
|
||||||
|
<div class="mcb-code-area">
|
||||||
|
<div class="mcb-welcome">
|
||||||
|
<svg viewBox="0 0 24 24" width="64" height="64">
|
||||||
|
<path d="M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z"/>
|
||||||
|
</svg>
|
||||||
|
<h4>GitHub Code Viewer</h4>
|
||||||
|
<p>Select a file from the sidebar to view its content</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mcb-status-bar">
|
||||||
|
<span class="mcb-status-text">Ready</span>
|
||||||
|
<span class="mcb-file-info"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
return ob_get_clean();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Simplified Gutenberg Block Registration for Maple Code Blocks
|
||||||
|
*/
|
||||||
|
|
||||||
|
class MCB_Simple_Block {
|
||||||
|
|
||||||
|
public static function init() {
|
||||||
|
// Register block on init
|
||||||
|
add_action('init', array(__CLASS__, 'register_block'));
|
||||||
|
|
||||||
|
// Make sure script is enqueued in editor
|
||||||
|
add_action('enqueue_block_editor_assets', array(__CLASS__, 'enqueue_editor_assets'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function register_block() {
|
||||||
|
// Only register if Gutenberg is available
|
||||||
|
if (!function_exists('register_block_type')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register the block
|
||||||
|
register_block_type('maple-code-blocks/code-block', array(
|
||||||
|
'render_callback' => array(__CLASS__, 'render_block'),
|
||||||
|
'attributes' => array(
|
||||||
|
'repository' => array(
|
||||||
|
'type' => 'string',
|
||||||
|
'default' => ''
|
||||||
|
),
|
||||||
|
'theme' => array(
|
||||||
|
'type' => 'string',
|
||||||
|
'default' => 'dark'
|
||||||
|
),
|
||||||
|
'height' => array(
|
||||||
|
'type' => 'string',
|
||||||
|
'default' => '600px'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function enqueue_editor_assets() {
|
||||||
|
// Enqueue the block editor script
|
||||||
|
wp_enqueue_script(
|
||||||
|
'maple-code-blocks-editor',
|
||||||
|
MCB_PLUGIN_URL . 'assets/js/simple-block.js',
|
||||||
|
array('wp-blocks', 'wp-element', 'wp-editor', 'wp-components', 'wp-i18n'),
|
||||||
|
MCB_PLUGIN_VERSION,
|
||||||
|
false // Load in header, not footer
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add inline script to ensure registration happens
|
||||||
|
wp_add_inline_script('maple-code-blocks-editor', '
|
||||||
|
console.log("Maple Code Blocks: Script loaded");
|
||||||
|
if (typeof wp !== "undefined" && wp.blocks) {
|
||||||
|
console.log("Maple Code Blocks: wp.blocks is available");
|
||||||
|
} else {
|
||||||
|
console.error("Maple Code Blocks: wp.blocks not available");
|
||||||
|
}
|
||||||
|
', 'after');
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function render_block($attributes) {
|
||||||
|
$repository = isset($attributes['repository']) ? $attributes['repository'] : '';
|
||||||
|
$theme = isset($attributes['theme']) ? $attributes['theme'] : 'dark';
|
||||||
|
$height = isset($attributes['height']) ? $attributes['height'] : '600px';
|
||||||
|
|
||||||
|
if (empty($repository)) {
|
||||||
|
return '<div class="notice notice-warning">Please enter a repository (e.g., facebook/react)</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the shortcode for rendering
|
||||||
|
return do_shortcode(sprintf(
|
||||||
|
'[maple_code_block repo="%s" theme="%s" height="%s"]',
|
||||||
|
esc_attr($repository),
|
||||||
|
esc_attr($theme),
|
||||||
|
esc_attr($height)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MCB_Simple_Block::init();
|
||||||
2
native/wordpress/maple-code-blocks/includes/index.php
Normal file
2
native/wordpress/maple-code-blocks/includes/index.php
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
<?php
|
||||||
|
// Silence is golden.
|
||||||
398
native/wordpress/maple-code-blocks/includes/security-audit.php
Normal file
398
native/wordpress/maple-code-blocks/includes/security-audit.php
Normal file
|
|
@ -0,0 +1,398 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Security Audit Script for GitHub Code Viewer Plugin
|
||||||
|
* Run this to verify all security measures are in place
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Prevent direct access
|
||||||
|
if (!defined('WPINC')) {
|
||||||
|
die('Direct access not permitted.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
class MCB_Security_Audit {
|
||||||
|
|
||||||
|
private $issues = array();
|
||||||
|
private $passed = array();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run complete security audit
|
||||||
|
*/
|
||||||
|
public function run_audit() {
|
||||||
|
$this->check_file_permissions();
|
||||||
|
$this->check_index_files();
|
||||||
|
$this->check_htaccess_files();
|
||||||
|
$this->check_php_files_protection();
|
||||||
|
$this->check_input_validation();
|
||||||
|
$this->check_output_escaping();
|
||||||
|
$this->check_nonce_usage();
|
||||||
|
$this->check_capability_checks();
|
||||||
|
$this->check_ssl_usage();
|
||||||
|
$this->check_rate_limiting();
|
||||||
|
|
||||||
|
return $this->generate_report();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check file permissions
|
||||||
|
*/
|
||||||
|
private function check_file_permissions() {
|
||||||
|
$plugin_dir = plugin_dir_path(__FILE__);
|
||||||
|
|
||||||
|
// Check directory permissions (should be 755 or stricter)
|
||||||
|
if (is_readable($plugin_dir)) {
|
||||||
|
$perms = fileperms($plugin_dir);
|
||||||
|
$octal = substr(sprintf('%o', $perms), -3);
|
||||||
|
|
||||||
|
if ($octal > '755') {
|
||||||
|
$this->issues[] = 'Directory permissions too permissive: ' . $octal;
|
||||||
|
} else {
|
||||||
|
$this->passed[] = 'Directory permissions OK: ' . $octal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check file permissions (should be 644 or stricter)
|
||||||
|
$files = glob($plugin_dir . '*.php');
|
||||||
|
foreach ($files as $file) {
|
||||||
|
$perms = fileperms($file);
|
||||||
|
$octal = substr(sprintf('%o', $perms), -3);
|
||||||
|
|
||||||
|
if ($octal > '644') {
|
||||||
|
$this->issues[] = 'File permissions too permissive for ' . basename($file) . ': ' . $octal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($this->issues)) {
|
||||||
|
$this->passed[] = 'All file permissions are secure';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check for index.php files in all directories
|
||||||
|
*/
|
||||||
|
private function check_index_files() {
|
||||||
|
$plugin_dir = plugin_dir_path(__FILE__);
|
||||||
|
$directories = array(
|
||||||
|
$plugin_dir,
|
||||||
|
$plugin_dir . 'admin/',
|
||||||
|
$plugin_dir . 'includes/',
|
||||||
|
$plugin_dir . 'assets/',
|
||||||
|
$plugin_dir . 'assets/css/',
|
||||||
|
$plugin_dir . 'assets/js/'
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($directories as $dir) {
|
||||||
|
if (is_dir($dir) && !file_exists($dir . 'index.php')) {
|
||||||
|
$this->issues[] = 'Missing index.php in ' . str_replace($plugin_dir, '', $dir);
|
||||||
|
} else {
|
||||||
|
$this->passed[] = 'index.php present in ' . str_replace($plugin_dir, '', $dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check for .htaccess files
|
||||||
|
*/
|
||||||
|
private function check_htaccess_files() {
|
||||||
|
$plugin_dir = plugin_dir_path(__FILE__);
|
||||||
|
$required_htaccess = array(
|
||||||
|
$plugin_dir => 'root',
|
||||||
|
$plugin_dir . 'admin/' => 'admin',
|
||||||
|
$plugin_dir . 'includes/' => 'includes',
|
||||||
|
$plugin_dir . 'assets/' => 'assets'
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($required_htaccess as $dir => $name) {
|
||||||
|
if (file_exists($dir . '.htaccess')) {
|
||||||
|
$this->passed[] = '.htaccess present in ' . $name . ' directory';
|
||||||
|
} else {
|
||||||
|
$this->issues[] = 'Missing .htaccess in ' . $name . ' directory';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check PHP files have direct access protection
|
||||||
|
*/
|
||||||
|
private function check_php_files_protection() {
|
||||||
|
$plugin_dir = plugin_dir_path(__FILE__);
|
||||||
|
$php_files = $this->get_all_php_files($plugin_dir);
|
||||||
|
|
||||||
|
foreach ($php_files as $file) {
|
||||||
|
$content = file_get_contents($file);
|
||||||
|
|
||||||
|
// Skip index.php files
|
||||||
|
if (basename($file) === 'index.php') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for ABSPATH or WPINC checks
|
||||||
|
if (!strpos($content, 'ABSPATH') && !strpos($content, 'WPINC')) {
|
||||||
|
$this->issues[] = 'No direct access protection in ' . str_replace($plugin_dir, '', $file);
|
||||||
|
} else {
|
||||||
|
$this->passed[] = 'Direct access protected: ' . basename($file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check input validation
|
||||||
|
*/
|
||||||
|
private function check_input_validation() {
|
||||||
|
$checks = array(
|
||||||
|
'sanitize_text_field' => 'Text sanitization',
|
||||||
|
'wp_verify_nonce' => 'Nonce verification',
|
||||||
|
'esc_attr' => 'Attribute escaping',
|
||||||
|
'esc_html' => 'HTML escaping',
|
||||||
|
'esc_js' => 'JavaScript escaping',
|
||||||
|
'absint' => 'Integer validation',
|
||||||
|
'filter_var' => 'Input filtering'
|
||||||
|
);
|
||||||
|
|
||||||
|
$plugin_dir = plugin_dir_path(__FILE__);
|
||||||
|
$php_files = $this->get_all_php_files($plugin_dir);
|
||||||
|
|
||||||
|
foreach ($checks as $function => $description) {
|
||||||
|
$found = false;
|
||||||
|
foreach ($php_files as $file) {
|
||||||
|
$content = file_get_contents($file);
|
||||||
|
if (strpos($content, $function) !== false) {
|
||||||
|
$found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($found) {
|
||||||
|
$this->passed[] = $description . ' implemented (' . $function . ')';
|
||||||
|
} else {
|
||||||
|
$this->issues[] = $description . ' not found (' . $function . ')';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check output escaping
|
||||||
|
*/
|
||||||
|
private function check_output_escaping() {
|
||||||
|
$escaping_functions = array(
|
||||||
|
'esc_html',
|
||||||
|
'esc_attr',
|
||||||
|
'esc_url',
|
||||||
|
'esc_js',
|
||||||
|
'htmlspecialchars'
|
||||||
|
);
|
||||||
|
|
||||||
|
$plugin_dir = plugin_dir_path(__FILE__);
|
||||||
|
$php_files = $this->get_all_php_files($plugin_dir);
|
||||||
|
|
||||||
|
$escaping_found = false;
|
||||||
|
foreach ($php_files as $file) {
|
||||||
|
$content = file_get_contents($file);
|
||||||
|
foreach ($escaping_functions as $func) {
|
||||||
|
if (strpos($content, $func) !== false) {
|
||||||
|
$escaping_found = true;
|
||||||
|
break 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($escaping_found) {
|
||||||
|
$this->passed[] = 'Output escaping implemented';
|
||||||
|
} else {
|
||||||
|
$this->issues[] = 'No output escaping functions found';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check nonce usage
|
||||||
|
*/
|
||||||
|
private function check_nonce_usage() {
|
||||||
|
$nonce_functions = array(
|
||||||
|
'wp_create_nonce',
|
||||||
|
'wp_verify_nonce',
|
||||||
|
'wp_nonce_field',
|
||||||
|
'check_admin_referer'
|
||||||
|
);
|
||||||
|
|
||||||
|
$plugin_dir = plugin_dir_path(__FILE__);
|
||||||
|
$php_files = $this->get_all_php_files($plugin_dir);
|
||||||
|
|
||||||
|
$nonce_found = 0;
|
||||||
|
foreach ($php_files as $file) {
|
||||||
|
$content = file_get_contents($file);
|
||||||
|
foreach ($nonce_functions as $func) {
|
||||||
|
if (strpos($content, $func) !== false) {
|
||||||
|
$nonce_found++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($nonce_found >= 2) { // Should have both create and verify
|
||||||
|
$this->passed[] = 'Nonce protection implemented';
|
||||||
|
} else {
|
||||||
|
$this->issues[] = 'Insufficient nonce protection';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check capability checks
|
||||||
|
*/
|
||||||
|
private function check_capability_checks() {
|
||||||
|
$capability_functions = array(
|
||||||
|
'current_user_can',
|
||||||
|
'user_can',
|
||||||
|
'is_admin'
|
||||||
|
);
|
||||||
|
|
||||||
|
$plugin_dir = plugin_dir_path(__FILE__);
|
||||||
|
$php_files = $this->get_all_php_files($plugin_dir);
|
||||||
|
|
||||||
|
$capability_found = false;
|
||||||
|
foreach ($php_files as $file) {
|
||||||
|
$content = file_get_contents($file);
|
||||||
|
foreach ($capability_functions as $func) {
|
||||||
|
if (strpos($content, $func) !== false) {
|
||||||
|
$capability_found = true;
|
||||||
|
break 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($capability_found) {
|
||||||
|
$this->passed[] = 'Capability checks implemented';
|
||||||
|
} else {
|
||||||
|
$this->issues[] = 'No capability checks found';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check SSL usage
|
||||||
|
*/
|
||||||
|
private function check_ssl_usage() {
|
||||||
|
$plugin_dir = plugin_dir_path(__FILE__);
|
||||||
|
$php_files = $this->get_all_php_files($plugin_dir);
|
||||||
|
|
||||||
|
$https_enforced = false;
|
||||||
|
foreach ($php_files as $file) {
|
||||||
|
$content = file_get_contents($file);
|
||||||
|
if (strpos($content, 'https://api.github.com') !== false) {
|
||||||
|
$https_enforced = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($https_enforced) {
|
||||||
|
$this->passed[] = 'HTTPS enforced for API calls';
|
||||||
|
} else {
|
||||||
|
$this->issues[] = 'HTTPS not enforced for API calls';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check rate limiting
|
||||||
|
*/
|
||||||
|
private function check_rate_limiting() {
|
||||||
|
$plugin_dir = plugin_dir_path(__FILE__);
|
||||||
|
$php_files = $this->get_all_php_files($plugin_dir);
|
||||||
|
|
||||||
|
$rate_limiting = false;
|
||||||
|
foreach ($php_files as $file) {
|
||||||
|
$content = file_get_contents($file);
|
||||||
|
if (strpos($content, 'rate_limit') !== false || strpos($content, 'throttl') !== false) {
|
||||||
|
$rate_limiting = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($rate_limiting) {
|
||||||
|
$this->passed[] = 'Rate limiting implemented';
|
||||||
|
} else {
|
||||||
|
$this->issues[] = 'No rate limiting found';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all PHP files recursively
|
||||||
|
*/
|
||||||
|
private function get_all_php_files($dir) {
|
||||||
|
$files = array();
|
||||||
|
$iterator = new RecursiveIteratorIterator(
|
||||||
|
new RecursiveDirectoryIterator($dir)
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($iterator as $file) {
|
||||||
|
if ($file->isFile() && $file->getExtension() === 'php') {
|
||||||
|
$files[] = $file->getPathname();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $files;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate audit report
|
||||||
|
*/
|
||||||
|
private function generate_report() {
|
||||||
|
$report = array(
|
||||||
|
'timestamp' => current_time('mysql'),
|
||||||
|
'passed_count' => count($this->passed),
|
||||||
|
'issues_count' => count($this->issues),
|
||||||
|
'passed' => $this->passed,
|
||||||
|
'issues' => $this->issues,
|
||||||
|
'score' => $this->calculate_score(),
|
||||||
|
'status' => empty($this->issues) ? 'SECURE' : 'NEEDS ATTENTION'
|
||||||
|
);
|
||||||
|
|
||||||
|
return $report;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate security score
|
||||||
|
*/
|
||||||
|
private function calculate_score() {
|
||||||
|
$total = count($this->passed) + count($this->issues);
|
||||||
|
if ($total === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return round((count($this->passed) / $total) * 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run audit if requested
|
||||||
|
if (isset($_GET['mcb_security_audit']) && current_user_can('manage_options')) {
|
||||||
|
$audit = new MCB_Security_Audit();
|
||||||
|
$report = $audit->run_audit();
|
||||||
|
|
||||||
|
echo '<div class="wrap">';
|
||||||
|
echo '<h1>GitHub Code Viewer Security Audit</h1>';
|
||||||
|
echo '<div class="notice notice-' . (empty($report['issues']) ? 'success' : 'warning') . '">';
|
||||||
|
echo '<p><strong>Security Score: ' . $report['score'] . '%</strong></p>';
|
||||||
|
echo '<p>Status: ' . $report['status'] . '</p>';
|
||||||
|
echo '</div>';
|
||||||
|
|
||||||
|
if (!empty($report['passed'])) {
|
||||||
|
echo '<h2>✅ Passed Checks (' . $report['passed_count'] . ')</h2>';
|
||||||
|
echo '<ul>';
|
||||||
|
foreach ($report['passed'] as $pass) {
|
||||||
|
echo '<li style="color: green;">✓ ' . esc_html($pass) . '</li>';
|
||||||
|
}
|
||||||
|
echo '</ul>';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($report['issues'])) {
|
||||||
|
echo '<h2>⚠️ Issues Found (' . $report['issues_count'] . ')</h2>';
|
||||||
|
echo '<ul>';
|
||||||
|
foreach ($report['issues'] as $issue) {
|
||||||
|
echo '<li style="color: red;">✗ ' . esc_html($issue) . '</li>';
|
||||||
|
}
|
||||||
|
echo '</ul>';
|
||||||
|
}
|
||||||
|
|
||||||
|
echo '<p><em>Audit completed at ' . $report['timestamp'] . '</em></p>';
|
||||||
|
echo '</div>';
|
||||||
|
}
|
||||||
2
native/wordpress/maple-code-blocks/index.php
Normal file
2
native/wordpress/maple-code-blocks/index.php
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
<?php
|
||||||
|
// Silence is golden.
|
||||||
286
native/wordpress/maple-code-blocks/maple-code-blocks.php
Normal file
286
native/wordpress/maple-code-blocks/maple-code-blocks.php
Normal file
|
|
@ -0,0 +1,286 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Maple Code Blocks
|
||||||
|
* Plugin URI: https://sspmedia.ca/wordpress/
|
||||||
|
* Description: Display code files from GitHub, GitLab, Bitbucket, and Codeberg repositories in a beautiful, safe terminal/IDE-style view
|
||||||
|
* Version: 2.0.0
|
||||||
|
* Author: SSP Media
|
||||||
|
* Author URI: https://sspmedia.ca/wordpress/
|
||||||
|
* License: GPL v2 or later
|
||||||
|
* Text Domain: maple-code-blocks
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Prevent direct access
|
||||||
|
if (!defined('WPINC')) {
|
||||||
|
die('Direct access not permitted.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define plugin constants
|
||||||
|
define('MCB_PLUGIN_URL', plugin_dir_url(__FILE__));
|
||||||
|
define('MCB_PLUGIN_PATH', plugin_dir_path(__FILE__));
|
||||||
|
define('MCB_PLUGIN_VERSION', '2.0.0');
|
||||||
|
define('MCB_PLUGIN_BASENAME', plugin_basename(__FILE__));
|
||||||
|
|
||||||
|
// Include required files
|
||||||
|
require_once MCB_PLUGIN_PATH . 'includes/class-security.php';
|
||||||
|
require_once MCB_PLUGIN_PATH . 'includes/class-security-fixes.php';
|
||||||
|
require_once MCB_PLUGIN_PATH . 'includes/class-privacy-manager.php';
|
||||||
|
require_once MCB_PLUGIN_PATH . 'includes/class-github-api.php';
|
||||||
|
require_once MCB_PLUGIN_PATH . 'includes/class-code-renderer.php';
|
||||||
|
require_once MCB_PLUGIN_PATH . 'includes/class-shortcode.php';
|
||||||
|
require_once MCB_PLUGIN_PATH . 'includes/class-simple-block.php';
|
||||||
|
require_once MCB_PLUGIN_PATH . 'includes/basic-block.php';
|
||||||
|
// require_once MCB_PLUGIN_PATH . 'includes/class-block-editor.php';
|
||||||
|
// require_once MCB_PLUGIN_PATH . 'includes/class-block-patterns.php';
|
||||||
|
|
||||||
|
// Initialize plugin
|
||||||
|
class Maple_Code_Blocks {
|
||||||
|
|
||||||
|
private static $instance = null;
|
||||||
|
|
||||||
|
public static function get_instance() {
|
||||||
|
if (null === self::$instance) {
|
||||||
|
self::$instance = new self();
|
||||||
|
}
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function __construct() {
|
||||||
|
add_action('init', array($this, 'init'));
|
||||||
|
add_action('wp_enqueue_scripts', array($this, 'enqueue_scripts'));
|
||||||
|
add_action('wp_ajax_mcb_load_file', array($this, 'ajax_load_file'));
|
||||||
|
add_action('wp_ajax_nopriv_mcb_load_file', array($this, 'ajax_load_file'));
|
||||||
|
add_action('wp_ajax_mcb_get_repo_files', array($this, 'ajax_get_repo_files'));
|
||||||
|
add_action('wp_ajax_nopriv_mcb_get_repo_files', array($this, 'ajax_get_repo_files'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function init() {
|
||||||
|
// Initialize shortcode
|
||||||
|
MCB_Shortcode::init();
|
||||||
|
|
||||||
|
// Simple block is self-initializing via class-simple-block.php
|
||||||
|
|
||||||
|
// Add admin menu if needed
|
||||||
|
if (is_admin()) {
|
||||||
|
add_action('admin_menu', array($this, 'add_admin_menu'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function enqueue_scripts() {
|
||||||
|
// Load styles
|
||||||
|
wp_enqueue_style('prism-css', MCB_PLUGIN_URL . 'assets/css/prism.css', array(), MCB_PLUGIN_VERSION);
|
||||||
|
wp_enqueue_style('mcb-styles', MCB_PLUGIN_URL . 'assets/css/mcb-styles.css', array(), MCB_PLUGIN_VERSION);
|
||||||
|
|
||||||
|
// Load scripts
|
||||||
|
wp_enqueue_script('prism-core', MCB_PLUGIN_URL . 'assets/js/prism.js', array(), MCB_PLUGIN_VERSION, true);
|
||||||
|
wp_enqueue_script('mcb-script', MCB_PLUGIN_URL . 'assets/js/mcb-script.js', array('jquery'), MCB_PLUGIN_VERSION, true);
|
||||||
|
|
||||||
|
// Localize script for AJAX
|
||||||
|
wp_localize_script('mcb-script', 'mcb_ajax', array(
|
||||||
|
'ajax_url' => admin_url('admin-ajax.php'),
|
||||||
|
'nonce' => wp_create_nonce('mcb_ajax_nonce')
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function ajax_load_file() {
|
||||||
|
// Check request method
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
wp_die('Invalid request method', 405);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rate limiting check
|
||||||
|
if (class_exists('MCB_Privacy_Manager') && MCB_Privacy_Manager::is_privacy_mode()) {
|
||||||
|
// Use nonce-based rate limiting in privacy mode
|
||||||
|
$rate_limit_key = 'mcb_rate_' . wp_get_current_user()->ID . '_' . wp_create_nonce('mcb_rate');
|
||||||
|
} else {
|
||||||
|
// IP-based rate limiting
|
||||||
|
$user_ip = $this->get_client_ip();
|
||||||
|
$rate_limit_key = 'mcb_rate_' . md5($user_ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
$requests = get_transient($rate_limit_key);
|
||||||
|
|
||||||
|
if ($requests !== false && $requests > 30) { // 30 requests per minute
|
||||||
|
wp_die('Rate limit exceeded. Please try again later.', 429);
|
||||||
|
}
|
||||||
|
|
||||||
|
set_transient($rate_limit_key, ($requests ? $requests + 1 : 1), 60);
|
||||||
|
|
||||||
|
// Verify nonce
|
||||||
|
if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mcb_ajax_nonce')) {
|
||||||
|
wp_die('Security check failed', 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: We allow public viewing of public repositories by default
|
||||||
|
// Site admins can restrict access if needed via filter
|
||||||
|
$require_login = apply_filters('mcb_require_login_for_viewing', false);
|
||||||
|
|
||||||
|
if ($require_login && !is_user_logged_in()) {
|
||||||
|
wp_die('Please log in to view code repositories', 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$repo = isset($_POST['repo']) ? sanitize_text_field($_POST['repo']) : '';
|
||||||
|
$file_path = isset($_POST['file_path']) ? sanitize_text_field($_POST['file_path']) : '';
|
||||||
|
|
||||||
|
// Additional validation using security class
|
||||||
|
if (!MCB_Security::validate_repo_format($repo)) {
|
||||||
|
MCB_Security::log_security_event('invalid_repo_format', array('repo' => $repo));
|
||||||
|
wp_send_json_error('Invalid repository format');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!MCB_Security::validate_file_path($file_path)) {
|
||||||
|
MCB_Security::log_security_event('invalid_file_path', array('path' => $file_path));
|
||||||
|
wp_send_json_error('Invalid file path');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$github_api = new MCB_GitHub_API();
|
||||||
|
$content = $github_api->get_file_content($repo, $file_path);
|
||||||
|
|
||||||
|
if (is_wp_error($content)) {
|
||||||
|
wp_send_json_error($content->get_error_message());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate content before sending
|
||||||
|
$renderer = new MCB_Code_Renderer();
|
||||||
|
$validation = $renderer->validate_content($content);
|
||||||
|
|
||||||
|
if (is_wp_error($validation)) {
|
||||||
|
wp_send_json_error($validation->get_error_message());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return raw content - JavaScript will handle escaping and rendering
|
||||||
|
// This is more secure as it avoids double-escaping issues
|
||||||
|
wp_send_json_success(array(
|
||||||
|
'content' => $content, // Send raw content, not rendered HTML
|
||||||
|
'filename' => basename($file_path)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function ajax_get_repo_files() {
|
||||||
|
// Check request method
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
wp_die('Invalid request method', 405);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rate limiting check
|
||||||
|
$user_ip = $this->get_client_ip();
|
||||||
|
$rate_limit_key = 'mcb_rate_' . md5($user_ip);
|
||||||
|
$requests = get_transient($rate_limit_key);
|
||||||
|
|
||||||
|
if ($requests !== false && $requests > 30) { // 30 requests per minute
|
||||||
|
wp_die('Rate limit exceeded. Please try again later.', 429);
|
||||||
|
}
|
||||||
|
|
||||||
|
set_transient($rate_limit_key, ($requests ? $requests + 1 : 1), 60);
|
||||||
|
|
||||||
|
// Verify nonce
|
||||||
|
if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'mcb_ajax_nonce')) {
|
||||||
|
wp_die('Security check failed', 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: We allow public viewing of public repositories by default
|
||||||
|
// Site admins can restrict access if needed via filter
|
||||||
|
$require_login = apply_filters('mcb_require_login_for_viewing', false);
|
||||||
|
|
||||||
|
if ($require_login && !is_user_logged_in()) {
|
||||||
|
wp_die('Please log in to view code repositories', 403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$repo = isset($_POST['repo']) ? sanitize_text_field($_POST['repo']) : '';
|
||||||
|
$path = isset($_POST['path']) ? sanitize_text_field($_POST['path']) : '';
|
||||||
|
|
||||||
|
// Validate repository format
|
||||||
|
if (!MCB_Security::validate_repo_format($repo)) {
|
||||||
|
wp_send_json_error('Invalid repository format');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$github_api = new MCB_GitHub_API();
|
||||||
|
$files = $github_api->get_repository_files($repo, $path);
|
||||||
|
|
||||||
|
if (is_wp_error($files)) {
|
||||||
|
wp_send_json_error($files->get_error_message());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_send_json_success($files);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function add_admin_menu() {
|
||||||
|
add_options_page(
|
||||||
|
'GitHub Code Viewer Settings',
|
||||||
|
'GitHub Code Viewer',
|
||||||
|
'manage_options',
|
||||||
|
'maple-code-blocks',
|
||||||
|
array($this, 'admin_page')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function admin_page() {
|
||||||
|
include MCB_PLUGIN_PATH . 'admin/settings-page.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get client IP address for rate limiting
|
||||||
|
*/
|
||||||
|
private function get_client_ip() {
|
||||||
|
$ip_keys = array('HTTP_CF_CONNECTING_IP', 'HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR');
|
||||||
|
foreach ($ip_keys as $key) {
|
||||||
|
if (array_key_exists($key, $_SERVER) === true) {
|
||||||
|
$ips = explode(',', $_SERVER[$key]);
|
||||||
|
foreach ($ips as $ip) {
|
||||||
|
$ip = trim($ip);
|
||||||
|
if (filter_var($ip, FILTER_VALIDATE_IP,
|
||||||
|
FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false) {
|
||||||
|
return $ip;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '0.0.0.0';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the plugin
|
||||||
|
Maple_Code_Blocks::get_instance();
|
||||||
|
|
||||||
|
// Register deactivation hook to clean up scheduled events
|
||||||
|
register_deactivation_hook(__FILE__, 'mcb_deactivate');
|
||||||
|
function mcb_deactivate() {
|
||||||
|
// Remove scheduled cleanup event
|
||||||
|
$timestamp = wp_next_scheduled('mcb_privacy_cleanup');
|
||||||
|
if ($timestamp) {
|
||||||
|
wp_unschedule_event($timestamp, 'mcb_privacy_cleanup');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear all MCB transients
|
||||||
|
global $wpdb;
|
||||||
|
$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_mcb_%'");
|
||||||
|
$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_mcb_%'");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register uninstall hook for complete cleanup
|
||||||
|
register_uninstall_hook(__FILE__, 'mcb_uninstall');
|
||||||
|
function mcb_uninstall() {
|
||||||
|
// Remove all plugin options
|
||||||
|
delete_option('mcb_settings');
|
||||||
|
delete_option('mcb_github_token_encrypted');
|
||||||
|
delete_option('mcb_gitlab_token_encrypted');
|
||||||
|
delete_option('mcb_bitbucket_token_encrypted');
|
||||||
|
delete_option('mcb_codeberg_token_encrypted');
|
||||||
|
|
||||||
|
// Clear all transients
|
||||||
|
global $wpdb;
|
||||||
|
$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_mcb_%'");
|
||||||
|
$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_mcb_%'");
|
||||||
|
$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_site_transient_mcb_%'");
|
||||||
|
$wpdb->query("DELETE FROM {$wpdb->options} WHERE option_name LIKE '_site_transient_timeout_mcb_%'");
|
||||||
|
}
|
||||||
117
native/wordpress/maple-gdpr-cookies/LICENSE
Normal file
117
native/wordpress/maple-gdpr-cookies/LICENSE
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 2, June 1991
|
||||||
|
|
||||||
|
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
|
||||||
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
|
||||||
|
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it.
|
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights.
|
||||||
|
|
||||||
|
We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software.
|
||||||
|
|
||||||
|
Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations.
|
||||||
|
|
||||||
|
Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||||
|
|
||||||
|
0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you".
|
||||||
|
|
||||||
|
Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does.
|
||||||
|
|
||||||
|
1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee.
|
||||||
|
|
||||||
|
2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change.
|
||||||
|
|
||||||
|
b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License.
|
||||||
|
|
||||||
|
c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.)
|
||||||
|
|
||||||
|
These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it.
|
||||||
|
|
||||||
|
Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program.
|
||||||
|
|
||||||
|
In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License.
|
||||||
|
|
||||||
|
3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following:
|
||||||
|
|
||||||
|
a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or,
|
||||||
|
|
||||||
|
b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or,
|
||||||
|
|
||||||
|
c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.)
|
||||||
|
|
||||||
|
The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable.
|
||||||
|
|
||||||
|
If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code.
|
||||||
|
|
||||||
|
4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance.
|
||||||
|
|
||||||
|
5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it.
|
||||||
|
|
||||||
|
6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License.
|
||||||
|
|
||||||
|
7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program.
|
||||||
|
|
||||||
|
If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances.
|
||||||
|
|
||||||
|
It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice.
|
||||||
|
|
||||||
|
This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License.
|
||||||
|
|
||||||
|
8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License.
|
||||||
|
|
||||||
|
9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation.
|
||||||
|
|
||||||
|
10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally.
|
||||||
|
|
||||||
|
NO WARRANTY
|
||||||
|
|
||||||
|
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
one line to give the program's name and an idea of what it does. Copyright (C) yyyy name of author
|
||||||
|
|
||||||
|
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If the program is interactive, make it output a short notice like this when it starts in an interactive mode:
|
||||||
|
|
||||||
|
Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
|
The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program.
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names:
|
||||||
|
|
||||||
|
Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||||
|
|
||||||
|
signature of Ty Coon, 1 April 1989 Ty Coon, President of Vice
|
||||||
107
native/wordpress/maple-gdpr-cookies/README.md
Normal file
107
native/wordpress/maple-gdpr-cookies/README.md
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
# Maple GDPR Cookies - FIXED VERSION v4.0.1
|
||||||
|
|
||||||
|
## 🔧 Critical Fix Applied
|
||||||
|
|
||||||
|
### Issue Fixed:
|
||||||
|
**Fatal Error:** Call to undefined function `wp_cache_delete_group()`
|
||||||
|
|
||||||
|
### Root Cause:
|
||||||
|
The function `wp_cache_delete_group()` does not exist in WordPress core. This was causing a fatal error during plugin activation at line 273.
|
||||||
|
|
||||||
|
### Solution Implemented:
|
||||||
|
Completely rewrote the `mgc_clear_all_caches()` function (lines 145-190) to use proper WordPress cache functions:
|
||||||
|
|
||||||
|
1. **wp_cache_flush()** - Clears all WordPress object cache
|
||||||
|
2. **delete_transient()** - Removes plugin-specific transients
|
||||||
|
3. **wp_cache_delete()** - Deletes specific cache keys
|
||||||
|
4. **Database query** - Cleans up orphaned transient entries
|
||||||
|
5. **Action hooks** - Allows other plugins to integrate
|
||||||
|
|
||||||
|
### What Changed:
|
||||||
|
|
||||||
|
#### BEFORE (Lines 145-150 - BROKEN):
|
||||||
|
```php
|
||||||
|
function mgc_clear_all_caches() {
|
||||||
|
wp_cache_delete_group('maple-gdpr-cookies'); // ❌ Doesn't exist
|
||||||
|
delete_transient('mgc_settings');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### AFTER (Lines 145-190 - FIXED):
|
||||||
|
```php
|
||||||
|
function mgc_clear_all_caches() {
|
||||||
|
// Method 1: Clear WordPress object cache
|
||||||
|
if (function_exists('wp_cache_flush')) {
|
||||||
|
wp_cache_flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method 2: Clear plugin-specific transients
|
||||||
|
global $wpdb;
|
||||||
|
$transients = $wpdb->get_col(
|
||||||
|
"SELECT option_name FROM $wpdb->options
|
||||||
|
WHERE option_name LIKE '_transient_mgc_%'"
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($transients as $transient) {
|
||||||
|
$transient_key = str_replace('_transient_', '', $transient);
|
||||||
|
delete_transient($transient_key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method 3: Clear specific plugin caches
|
||||||
|
$cache_keys = array('mgc_settings', 'mgc_stats', 'mgc_consent_logs');
|
||||||
|
foreach ($cache_keys as $key) {
|
||||||
|
wp_cache_delete($key, 'maple-gdpr-cookies');
|
||||||
|
delete_transient($key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method 4: WooCommerce compatibility
|
||||||
|
if (function_exists('wc_delete_shop_order_transients')) {
|
||||||
|
wc_delete_shop_order_transients();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method 5: Extensibility hook
|
||||||
|
do_action('mgc_clear_caches');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## ✅ Benefits of the Fix:
|
||||||
|
|
||||||
|
1. **Works Everywhere** - Compatible with all WordPress installations
|
||||||
|
2. **No Fatal Errors** - Uses only existing WordPress functions
|
||||||
|
3. **Comprehensive** - Clears all types of caches (object, transient, database)
|
||||||
|
4. **Plugin Compatibility** - Includes WooCommerce integration
|
||||||
|
5. **Extensible** - Developers can hook into cache clearing
|
||||||
|
|
||||||
|
## 🚀 Installation:
|
||||||
|
|
||||||
|
1. **Deactivate** the old version (if active)
|
||||||
|
2. **Delete** the old plugin folder
|
||||||
|
3. **Upload** this fixed version
|
||||||
|
4. **Activate** the plugin
|
||||||
|
|
||||||
|
The plugin will now activate successfully without fatal errors!
|
||||||
|
|
||||||
|
## 📋 Version History:
|
||||||
|
|
||||||
|
- **v4.0.1** (Oct 24, 2025) - CRITICAL FIX: Replaced wp_cache_delete_group() with proper cache functions
|
||||||
|
- **v4.0.0** - Original version with cache bug
|
||||||
|
|
||||||
|
## 🔒 No Other Changes:
|
||||||
|
|
||||||
|
All other functionality remains identical:
|
||||||
|
- GDPR compliance features ✅
|
||||||
|
- Security measures ✅
|
||||||
|
- WooCommerce/LearnDash compatibility ✅
|
||||||
|
- Consent logging ✅
|
||||||
|
- Cookie management ✅
|
||||||
|
|
||||||
|
## 📞 Support:
|
||||||
|
|
||||||
|
If you encounter any issues with this fix, please check:
|
||||||
|
1. WordPress version 5.0+ ✅
|
||||||
|
2. PHP version 7.2+ ✅
|
||||||
|
3. MySQL version 5.5.3+ ✅
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**This is a critical hotfix that resolves the activation fatal error while maintaining all plugin functionality.**
|
||||||
75
native/wordpress/maple-gdpr-cookies/admin/css/admin.css
Normal file
75
native/wordpress/maple-gdpr-cookies/admin/css/admin.css
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
/* Maple GDPR Cookies - Admin Styles */
|
||||||
|
.mgc-admin-wrap {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mgc-panel {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #ccd0d4;
|
||||||
|
box-shadow: 0 1px 1px rgba(0,0,0,.04);
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mgc-panel-header {
|
||||||
|
background: #f6f7f7;
|
||||||
|
border-bottom: 1px solid #ccd0d4;
|
||||||
|
padding: 15px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mgc-panel-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mgc-panel-body {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mgc-form-row {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mgc-form-row label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mgc-form-row input[type="text"],
|
||||||
|
.mgc-form-row input[type="url"],
|
||||||
|
.mgc-form-row input[type="number"],
|
||||||
|
.mgc-form-row textarea,
|
||||||
|
.mgc-form-row select {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mgc-form-row input[type="text"]#mgc_custom_button_color,
|
||||||
|
.mgc-form-row input[type="text"]#mgc_custom_button_hover_color {
|
||||||
|
max-width: 200px;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mgc-form-row .description {
|
||||||
|
margin-top: 5px;
|
||||||
|
font-style: italic;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mgc-form-row textarea {
|
||||||
|
min-height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mgc-preview {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 20px;
|
||||||
|
background: #f9f9f9;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mgc-save-button {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
12
native/wordpress/maple-gdpr-cookies/admin/css/index.php
Normal file
12
native/wordpress/maple-gdpr-cookies/admin/css/index.php
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Silence is golden.
|
||||||
|
*
|
||||||
|
* Prevent direct access to this file.
|
||||||
|
*
|
||||||
|
* @package WordPress
|
||||||
|
*/
|
||||||
|
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit; // Exit if accessed directly.
|
||||||
|
}
|
||||||
12
native/wordpress/maple-gdpr-cookies/admin/index.php
Normal file
12
native/wordpress/maple-gdpr-cookies/admin/index.php
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Silence is golden.
|
||||||
|
*
|
||||||
|
* Prevent direct access to this file.
|
||||||
|
*
|
||||||
|
* @package WordPress
|
||||||
|
*/
|
||||||
|
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit; // Exit if accessed directly.
|
||||||
|
}
|
||||||
120
native/wordpress/maple-gdpr-cookies/admin/js/admin.js
Normal file
120
native/wordpress/maple-gdpr-cookies/admin/js/admin.js
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
/* Maple GDPR Cookies - Admin JavaScript */
|
||||||
|
(function ($) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
$(document).ready(function () {
|
||||||
|
// Initialize color picker
|
||||||
|
if ($.fn.wpColorPicker) {
|
||||||
|
$(".mgc-color-picker").wpColorPicker();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom color input preview
|
||||||
|
function updateColorPreview(input) {
|
||||||
|
var color = $(input).val();
|
||||||
|
var hexPattern = /^#[0-9A-F]{6}$/i;
|
||||||
|
|
||||||
|
if (hexPattern.test(color)) {
|
||||||
|
$(input).css({
|
||||||
|
"border-left": "5px solid " + color,
|
||||||
|
"border-color": color,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
$(input).css({
|
||||||
|
"border-left": "",
|
||||||
|
"border-color": "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add event listeners for custom color inputs
|
||||||
|
$("#mgc_custom_button_color, #mgc_custom_button_hover_color").on(
|
||||||
|
"input change",
|
||||||
|
function () {
|
||||||
|
updateColorPreview(this);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initialize preview on page load
|
||||||
|
$("#mgc_custom_button_color, #mgc_custom_button_hover_color").each(
|
||||||
|
function () {
|
||||||
|
updateColorPreview(this);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Preview functionality
|
||||||
|
$("#mgc-preview-button").on("click", function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
// Trigger preview update
|
||||||
|
updatePreview();
|
||||||
|
});
|
||||||
|
|
||||||
|
function updatePreview() {
|
||||||
|
// Simple preview update logic
|
||||||
|
console.log("Preview updated");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle preference display type radio buttons
|
||||||
|
var $preferenceRadios = $('input[name="mgc_preference_display_type"]');
|
||||||
|
var $gdprWarning = $("#mgc-gdpr-warning");
|
||||||
|
|
||||||
|
// Function to toggle GDPR warning visibility
|
||||||
|
function toggleGdprWarning() {
|
||||||
|
var selectedType = $(
|
||||||
|
'input[name="mgc_preference_display_type"]:checked',
|
||||||
|
).val();
|
||||||
|
|
||||||
|
if (selectedType === "neither") {
|
||||||
|
// Create warning if it doesn't exist
|
||||||
|
if ($gdprWarning.length === 0) {
|
||||||
|
var warningHtml =
|
||||||
|
'<div id="mgc-gdpr-warning" style="background: #fff3cd; padding: 12px; border-left: 4px solid #ffc107; margin-top: 10px; margin-bottom: 15px;">' +
|
||||||
|
'<strong style="color: #856404;">⚠️ GDPR Compliance Reminder:</strong>' +
|
||||||
|
'<p style="margin: 5px 0; color: #856404;">To remain GDPR compliant, you must add this shortcode to your footer or make it easily accessible on all pages:</p>' +
|
||||||
|
'<code style="background: #fff; padding: 6px 10px; border-radius: 3px; display: inline-block; font-size: 14px; border: 1px solid #ffc107;">[mgc_cookie_preferences]</code>' +
|
||||||
|
'<p style="margin: 10px 0 0 0; font-size: 12px; color: #856404;">GDPR requires users to have easy access to withdraw their consent at any time.</p>' +
|
||||||
|
"</div>";
|
||||||
|
|
||||||
|
$(warningHtml).insertAfter($preferenceRadios.last().parent());
|
||||||
|
$gdprWarning = $("#mgc-gdpr-warning");
|
||||||
|
}
|
||||||
|
$gdprWarning.slideDown(200);
|
||||||
|
} else {
|
||||||
|
if ($gdprWarning.length > 0) {
|
||||||
|
$gdprWarning.slideUp(200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize warning on page load
|
||||||
|
toggleGdprWarning();
|
||||||
|
|
||||||
|
// Handle radio button changes
|
||||||
|
$preferenceRadios.on("change", function () {
|
||||||
|
toggleGdprWarning();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Form validation
|
||||||
|
$("form").on("submit", function (e) {
|
||||||
|
// Validate hex colors
|
||||||
|
var customColor = $("#mgc_custom_button_color").val();
|
||||||
|
var customHoverColor = $("#mgc_custom_button_hover_color").val();
|
||||||
|
var hexPattern = /^#[0-9A-F]{6}$/i;
|
||||||
|
|
||||||
|
if (customColor && !hexPattern.test(customColor)) {
|
||||||
|
alert("Custom Button Color must be a valid hex color (e.g., #3498db)");
|
||||||
|
$("#mgc_custom_button_color").focus();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (customHoverColor && !hexPattern.test(customHoverColor)) {
|
||||||
|
alert(
|
||||||
|
"Custom Button Hover Color must be a valid hex color (e.g., #2980b9)",
|
||||||
|
);
|
||||||
|
$("#mgc_custom_button_hover_color").focus();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})(jQuery);
|
||||||
12
native/wordpress/maple-gdpr-cookies/admin/js/index.php
Normal file
12
native/wordpress/maple-gdpr-cookies/admin/js/index.php
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Silence is golden.
|
||||||
|
*
|
||||||
|
* Prevent direct access to this file.
|
||||||
|
*
|
||||||
|
* @package WordPress
|
||||||
|
*/
|
||||||
|
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit; // Exit if accessed directly.
|
||||||
|
}
|
||||||
12
native/wordpress/maple-gdpr-cookies/admin/views/index.php
Normal file
12
native/wordpress/maple-gdpr-cookies/admin/views/index.php
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Silence is golden.
|
||||||
|
*
|
||||||
|
* Prevent direct access to this file.
|
||||||
|
*
|
||||||
|
* @package WordPress
|
||||||
|
*/
|
||||||
|
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit; // Exit if accessed directly.
|
||||||
|
}
|
||||||
106
native/wordpress/maple-gdpr-cookies/admin/views/logs.php
Normal file
106
native/wordpress/maple-gdpr-cookies/admin/views/logs.php
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Consent Logs View
|
||||||
|
*/
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
$table_name = $wpdb->prefix . 'mgc_consent_logs';
|
||||||
|
|
||||||
|
// Get logs with pagination
|
||||||
|
$per_page = 50;
|
||||||
|
$page = isset($_GET['paged']) ? absint($_GET['paged']) : 1;
|
||||||
|
$offset = ($page - 1) * $per_page;
|
||||||
|
|
||||||
|
$logs = $wpdb->get_results(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT * FROM $table_name ORDER BY consent_date DESC LIMIT %d OFFSET %d",
|
||||||
|
$per_page,
|
||||||
|
$offset
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$total_logs = $wpdb->get_var("SELECT COUNT(*) FROM $table_name");
|
||||||
|
$total_pages = ceil($total_logs / $per_page);
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="wrap mgc-admin-wrap">
|
||||||
|
<h1><?php echo esc_html(get_admin_page_title()); ?></h1>
|
||||||
|
|
||||||
|
<div class="mgc-panel">
|
||||||
|
<div class="mgc-panel-header">
|
||||||
|
<h3><?php _e('Consent Logs', 'maple-gdpr-cookies'); ?></h3>
|
||||||
|
</div>
|
||||||
|
<div class="mgc-panel-body">
|
||||||
|
<p><?php printf(__('Total logs: %d', 'maple-gdpr-cookies'), $total_logs); ?></p>
|
||||||
|
|
||||||
|
<?php if (!empty($logs)) : ?>
|
||||||
|
<table class="wp-list-table widefat fixed striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th><?php _e('Date', 'maple-gdpr-cookies'); ?></th>
|
||||||
|
<th><?php _e('User', 'maple-gdpr-cookies'); ?></th>
|
||||||
|
<th><?php _e('IP Address', 'maple-gdpr-cookies'); ?></th>
|
||||||
|
<th><?php _e('Consent Type', 'maple-gdpr-cookies'); ?></th>
|
||||||
|
<th><?php _e('Categories', 'maple-gdpr-cookies'); ?></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php foreach ($logs as $log) : ?>
|
||||||
|
<tr>
|
||||||
|
<td><?php echo esc_html($log->consent_date); ?></td>
|
||||||
|
<td>
|
||||||
|
<?php
|
||||||
|
if ($log->user_id) {
|
||||||
|
$user = get_userdata($log->user_id);
|
||||||
|
echo $user ? esc_html($user->display_name) : __('Unknown', 'maple-gdpr-cookies');
|
||||||
|
} else {
|
||||||
|
echo __('Guest', 'maple-gdpr-cookies');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</td>
|
||||||
|
<td><?php echo esc_html($log->ip_address); ?></td>
|
||||||
|
<td>
|
||||||
|
<span class="mgc-consent-<?php echo esc_attr($log->consent_type); ?>">
|
||||||
|
<?php echo esc_html(ucfirst($log->consent_type)); ?>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<?php
|
||||||
|
$categories = json_decode($log->categories, true);
|
||||||
|
if (!empty($categories) && is_array($categories)) {
|
||||||
|
echo esc_html(implode(', ', $categories));
|
||||||
|
} else {
|
||||||
|
echo __('N/A', 'maple-gdpr-cookies');
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<?php if ($total_pages > 1) : ?>
|
||||||
|
<div class="tablenav">
|
||||||
|
<div class="tablenav-pages">
|
||||||
|
<?php
|
||||||
|
echo paginate_links(array(
|
||||||
|
'base' => add_query_arg('paged', '%#%'),
|
||||||
|
'format' => '',
|
||||||
|
'prev_text' => __('«'),
|
||||||
|
'next_text' => __('»'),
|
||||||
|
'total' => $total_pages,
|
||||||
|
'current' => $page
|
||||||
|
));
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php else : ?>
|
||||||
|
<p><?php _e('No consent logs found.', 'maple-gdpr-cookies'); ?></p>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
172
native/wordpress/maple-gdpr-cookies/admin/views/settings.php
Normal file
172
native/wordpress/maple-gdpr-cookies/admin/views/settings.php
Normal file
|
|
@ -0,0 +1,172 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Admin Settings View
|
||||||
|
*/
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="wrap mgc-admin-wrap">
|
||||||
|
<h1><?php echo esc_html(get_admin_page_title()); ?></h1>
|
||||||
|
|
||||||
|
<?php settings_errors('mgc_messages'); ?>
|
||||||
|
|
||||||
|
<form method="post" action="">
|
||||||
|
<?php wp_nonce_field('mgc_settings_nonce'); ?>
|
||||||
|
|
||||||
|
<!-- General Settings Panel -->
|
||||||
|
<div class="mgc-panel">
|
||||||
|
<div class="mgc-panel-header">
|
||||||
|
<h3><?php _e('General Settings', 'maple-gdpr-cookies'); ?></h3>
|
||||||
|
</div>
|
||||||
|
<div class="mgc-panel-body">
|
||||||
|
<div class="mgc-form-row">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="mgc_enabled" value="1" <?php checked(get_option('mgc_enabled', true), true); ?>>
|
||||||
|
<?php _e('Enable Cookie Notice', 'maple-gdpr-cookies'); ?>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mgc-form-row">
|
||||||
|
<label for="mgc_notice_text"><?php _e('Notice Text', 'maple-gdpr-cookies'); ?></label>
|
||||||
|
<textarea id="mgc_notice_text" name="mgc_notice_text" rows="4"><?php echo esc_textarea(get_option('mgc_notice_text')); ?></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mgc-form-row">
|
||||||
|
<label for="mgc_accept_button_text"><?php _e('Accept Button Text', 'maple-gdpr-cookies'); ?></label>
|
||||||
|
<input type="text" id="mgc_accept_button_text" name="mgc_accept_button_text" value="<?php echo esc_attr(get_option('mgc_accept_button_text')); ?>">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mgc-form-row">
|
||||||
|
<label for="mgc_reject_button_text"><?php _e('Reject Button Text', 'maple-gdpr-cookies'); ?></label>
|
||||||
|
<input type="text" id="mgc_reject_button_text" name="mgc_reject_button_text" value="<?php echo esc_attr(get_option('mgc_reject_button_text')); ?>">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mgc-form-row">
|
||||||
|
<label for="mgc_privacy_policy_url"><?php _e('Privacy Policy URL', 'maple-gdpr-cookies'); ?></label>
|
||||||
|
<input type="url" id="mgc_privacy_policy_url" name="mgc_privacy_policy_url" value="<?php echo esc_url(get_option('mgc_privacy_policy_url')); ?>">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mgc-form-row">
|
||||||
|
<label for="mgc_cookie_expiry"><?php _e('Cookie Expiry (days)', 'maple-gdpr-cookies'); ?></label>
|
||||||
|
<input type="number" id="mgc_cookie_expiry" name="mgc_cookie_expiry" value="<?php echo esc_attr(get_option('mgc_cookie_expiry', 365)); ?>" min="1" max="365">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Appearance Panel -->
|
||||||
|
<div class="mgc-panel">
|
||||||
|
<div class="mgc-panel-header">
|
||||||
|
<h3><?php _e('Appearance', 'maple-gdpr-cookies'); ?></h3>
|
||||||
|
</div>
|
||||||
|
<div class="mgc-panel-body">
|
||||||
|
<div class="mgc-form-row">
|
||||||
|
<label for="mgc_position"><?php _e('Position', 'maple-gdpr-cookies'); ?></label>
|
||||||
|
<select id="mgc_position" name="mgc_position">
|
||||||
|
<option value="bottom" <?php selected(get_option('mgc_position', 'bottom'), 'bottom'); ?>><?php _e('Bottom', 'maple-gdpr-cookies'); ?></option>
|
||||||
|
<option value="top" <?php selected(get_option('mgc_position'), 'top'); ?>><?php _e('Top', 'maple-gdpr-cookies'); ?></option>
|
||||||
|
<option value="center" <?php selected(get_option('mgc_position'), 'center'); ?>><?php _e('Center Modal', 'maple-gdpr-cookies'); ?></option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mgc-form-row">
|
||||||
|
<label for="mgc_theme"><?php _e('Theme', 'maple-gdpr-cookies'); ?></label>
|
||||||
|
<select id="mgc_theme" name="mgc_theme">
|
||||||
|
<option value="light" <?php selected(get_option('mgc_theme', 'light'), 'light'); ?>><?php _e('Light', 'maple-gdpr-cookies'); ?></option>
|
||||||
|
<option value="dark" <?php selected(get_option('mgc_theme'), 'dark'); ?>><?php _e('Dark', 'maple-gdpr-cookies'); ?></option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mgc-form-row">
|
||||||
|
<label for="mgc_button_color"><?php _e('Button Color', 'maple-gdpr-cookies'); ?></label>
|
||||||
|
<select id="mgc_button_color" name="mgc_button_color">
|
||||||
|
<option value="black" <?php selected(get_option('mgc_button_color', 'blue'), 'black'); ?>><?php _e('Black', 'maple-gdpr-cookies'); ?></option>
|
||||||
|
<option value="dark-grey" <?php selected(get_option('mgc_button_color', 'blue'), 'dark-grey'); ?>><?php _e('Dark Grey', 'maple-gdpr-cookies'); ?></option>
|
||||||
|
<option value="blue" <?php selected(get_option('mgc_button_color', 'blue'), 'blue'); ?>><?php _e('Blue', 'maple-gdpr-cookies'); ?></option>
|
||||||
|
<option value="red" <?php selected(get_option('mgc_button_color', 'blue'), 'red'); ?>><?php _e('Red', 'maple-gdpr-cookies'); ?></option>
|
||||||
|
<option value="green" <?php selected(get_option('mgc_button_color', 'blue'), 'green'); ?>><?php _e('Green', 'maple-gdpr-cookies'); ?></option>
|
||||||
|
</select>
|
||||||
|
<p class="description"><?php _e('Select the color for all cookie notice buttons.', 'maple-gdpr-cookies'); ?></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mgc-form-row">
|
||||||
|
<label for="mgc_custom_button_color"><?php _e('Custom Button Color', 'maple-gdpr-cookies'); ?></label>
|
||||||
|
<input type="text" id="mgc_custom_button_color" name="mgc_custom_button_color" value="<?php echo esc_attr(get_option('mgc_custom_button_color', '')); ?>" placeholder="#3498db" pattern="^#[a-fA-F0-9]{6}$" maxlength="7">
|
||||||
|
<p class="description"><?php _e('Enter a hex color code (e.g., #3498db). If set, this overrides the dropdown selection above.', 'maple-gdpr-cookies'); ?></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mgc-form-row">
|
||||||
|
<label for="mgc_custom_button_hover_color"><?php _e('Custom Button Hover Color', 'maple-gdpr-cookies'); ?></label>
|
||||||
|
<input type="text" id="mgc_custom_button_hover_color" name="mgc_custom_button_hover_color" value="<?php echo esc_attr(get_option('mgc_custom_button_hover_color', '')); ?>" placeholder="#2980b9" pattern="^#[a-fA-F0-9]{6}$" maxlength="7">
|
||||||
|
<p class="description"><?php _e('Enter a hex color code for button hover state (e.g., #2980b9). If set, this overrides the default hover color.', 'maple-gdpr-cookies'); ?></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mgc-form-row">
|
||||||
|
<label for="mgc_custom_css"><?php _e('Custom CSS', 'maple-gdpr-cookies'); ?></label>
|
||||||
|
<textarea id="mgc_custom_css" name="mgc_custom_css" rows="10" placeholder=".mgc-notice { /* your styles */ }"><?php echo esc_textarea(get_option('mgc_custom_css')); ?></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mgc-form-row">
|
||||||
|
<label><?php _e('Preference Display Type', 'maple-gdpr-cookies'); ?></label>
|
||||||
|
<p class="description" style="margin-bottom: 10px;"><?php _e('Choose how users can access cookie preferences after giving consent (GDPR requirement for easy withdrawal).', 'maple-gdpr-cookies'); ?></p>
|
||||||
|
<label style="display: block; margin-bottom: 8px;">
|
||||||
|
<input type="radio" name="mgc_preference_display_type" value="icon" <?php checked(get_option('mgc_preference_display_type', 'icon'), 'icon'); ?>>
|
||||||
|
<?php _e('Floating Cookie Icon - A circular button with cookie icon in the bottom-left corner', 'maple-gdpr-cookies'); ?>
|
||||||
|
</label>
|
||||||
|
<label style="display: block; margin-bottom: 8px;">
|
||||||
|
<input type="radio" name="mgc_preference_display_type" value="footer" <?php checked(get_option('mgc_preference_display_type', 'icon'), 'footer'); ?>>
|
||||||
|
<?php _e('Footer Text Link - A small "Cookie Preferences" link in the bottom-left corner', 'maple-gdpr-cookies'); ?>
|
||||||
|
</label>
|
||||||
|
<label style="display: block; margin-bottom: 10px;">
|
||||||
|
<input type="radio" name="mgc_preference_display_type" value="neither" <?php checked(get_option('mgc_preference_display_type', 'icon'), 'neither'); ?>>
|
||||||
|
<?php _e('Neither - I will use the shortcode instead', 'maple-gdpr-cookies'); ?>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div style="background: #f0f0f1; padding: 12px; border-left: 4px solid #2271b1; margin-top: 15px;">
|
||||||
|
<strong><?php _e('Shortcode Option:', 'maple-gdpr-cookies'); ?></strong>
|
||||||
|
<p style="margin: 5px 0;"><?php _e('You can also place a cookie preferences link anywhere using the shortcode:', 'maple-gdpr-cookies'); ?></p>
|
||||||
|
<code style="background: #fff; padding: 4px 8px; border-radius: 3px; display: inline-block; font-size: 13px;">[mgc_cookie_preferences]</code>
|
||||||
|
<p style="margin: 10px 0 5px 0; font-size: 13px;"><?php _e('Customize the text:', 'maple-gdpr-cookies'); ?></p>
|
||||||
|
<code style="background: #fff; padding: 4px 8px; border-radius: 3px; display: inline-block; font-size: 13px;">[mgc_cookie_preferences text="Manage Cookies"]</code>
|
||||||
|
<p style="margin: 10px 0 0 0; font-size: 12px; color: #666;"><?php _e('Perfect for adding to your footer widget, pages, or posts.', 'maple-gdpr-cookies'); ?></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Advanced Settings Panel -->
|
||||||
|
<div class="mgc-panel">
|
||||||
|
<div class="mgc-panel-header">
|
||||||
|
<h3><?php _e('Advanced Settings', 'maple-gdpr-cookies'); ?></h3>
|
||||||
|
</div>
|
||||||
|
<div class="mgc-panel-body">
|
||||||
|
<div class="mgc-form-row">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="mgc_enable_logging" value="1" <?php checked(get_option('mgc_enable_logging', true), true); ?>>
|
||||||
|
<?php _e('Enable Consent Logging', 'maple-gdpr-cookies'); ?>
|
||||||
|
</label>
|
||||||
|
<p class="description"><?php _e('Log all consent decisions for GDPR compliance.', 'maple-gdpr-cookies'); ?></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mgc-form-row">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="mgc_show_reject_button" value="1" <?php checked(get_option('mgc_show_reject_button', true), true); ?>>
|
||||||
|
<?php _e('Show Reject Button', 'maple-gdpr-cookies'); ?>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mgc-form-row">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="mgc_show_settings_button" value="1" <?php checked(get_option('mgc_show_settings_button', true), true); ?>>
|
||||||
|
<?php _e('Show Settings Button', 'maple-gdpr-cookies'); ?>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mgc-save-button">
|
||||||
|
<?php submit_button(__('Save Settings', 'maple-gdpr-cookies'), 'primary', 'mgc_save_settings'); ?>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
12
native/wordpress/maple-gdpr-cookies/index.php
Normal file
12
native/wordpress/maple-gdpr-cookies/index.php
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Silence is golden.
|
||||||
|
*
|
||||||
|
* Prevent direct access to this file.
|
||||||
|
*
|
||||||
|
* @package WordPress
|
||||||
|
*/
|
||||||
|
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit; // Exit if accessed directly.
|
||||||
|
}
|
||||||
12
native/wordpress/maple-gdpr-cookies/languages/index.php
Normal file
12
native/wordpress/maple-gdpr-cookies/languages/index.php
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Silence is golden.
|
||||||
|
*
|
||||||
|
* Prevent direct access to this file.
|
||||||
|
*
|
||||||
|
* @package WordPress
|
||||||
|
*/
|
||||||
|
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit; // Exit if accessed directly.
|
||||||
|
}
|
||||||
721
native/wordpress/maple-gdpr-cookies/maple-gdpr-cookies.php
Normal file
721
native/wordpress/maple-gdpr-cookies/maple-gdpr-cookies.php
Normal file
|
|
@ -0,0 +1,721 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Maple GDPR Cookies
|
||||||
|
* Plugin URI: https://mapleopentech.ca/maple-gdpr-cookies
|
||||||
|
* Description: A lightweight, secure, and fully GDPR-compliant cookie consent plugin with script blocking
|
||||||
|
* Version: 4.1.3
|
||||||
|
* Requires at least: 5.0
|
||||||
|
* Requires PHP: 7.2
|
||||||
|
* Author: Your Name
|
||||||
|
* License: GPL v2 or later
|
||||||
|
* Text Domain: maple-gdpr-cookies
|
||||||
|
* WC requires at least: 3.0
|
||||||
|
* WC tested up to: 8.5
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Prevent direct access
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define plugin constants
|
||||||
|
define('MGC_PLUGIN_FILE', __FILE__);
|
||||||
|
define('MGC_PLUGIN_PATH', plugin_dir_path(__FILE__));
|
||||||
|
define('MGC_PLUGIN_URL', plugin_dir_url(__FILE__));
|
||||||
|
define('MGC_PLUGIN_VERSION', '4.1.3');
|
||||||
|
define('MGC_DB_VERSION', '2.0');
|
||||||
|
define('MGC_MIN_PHP_VERSION', '7.2');
|
||||||
|
define('MGC_MIN_WP_VERSION', '5.0');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin activation checks
|
||||||
|
*/
|
||||||
|
function mgc_check_requirements() {
|
||||||
|
$errors = array();
|
||||||
|
|
||||||
|
// Check PHP version
|
||||||
|
if (version_compare(PHP_VERSION, MGC_MIN_PHP_VERSION, '<')) {
|
||||||
|
$errors[] = sprintf(
|
||||||
|
__('Maple GDPR Cookies requires PHP %s or higher. Your server is running PHP %s.', 'maple-gdpr-cookies'),
|
||||||
|
MGC_MIN_PHP_VERSION,
|
||||||
|
PHP_VERSION
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check WordPress version
|
||||||
|
if (version_compare(get_bloginfo('version'), MGC_MIN_WP_VERSION, '<')) {
|
||||||
|
$errors[] = sprintf(
|
||||||
|
__('Maple GDPR Cookies requires WordPress %s or higher. You are running WordPress %s.', 'maple-gdpr-cookies'),
|
||||||
|
MGC_MIN_WP_VERSION,
|
||||||
|
get_bloginfo('version')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for MySQL version if using utf8mb4
|
||||||
|
global $wpdb;
|
||||||
|
if ($wpdb->db_version() && version_compare($wpdb->db_version(), '5.5.3', '<')) {
|
||||||
|
$errors[] = __('Maple GDPR Cookies requires MySQL 5.5.3 or higher for utf8mb4 support.', 'maple-gdpr-cookies');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create database tables on activation
|
||||||
|
*/
|
||||||
|
function mgc_create_tables() {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$charset_collate = $wpdb->get_charset_collate();
|
||||||
|
$table_name = $wpdb->prefix . 'mgc_consent_logs';
|
||||||
|
|
||||||
|
$sql = "CREATE TABLE IF NOT EXISTS $table_name (
|
||||||
|
id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||||
|
user_id bigint(20) UNSIGNED NULL,
|
||||||
|
ip_address varchar(45) NOT NULL,
|
||||||
|
user_agent varchar(255) NOT NULL,
|
||||||
|
consent_type varchar(20) NOT NULL,
|
||||||
|
categories text NULL,
|
||||||
|
consent_given tinyint(1) NOT NULL,
|
||||||
|
consent_date datetime NOT NULL,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY user_id (user_id),
|
||||||
|
KEY ip_address (ip_address),
|
||||||
|
KEY consent_date (consent_date)
|
||||||
|
) $charset_collate;";
|
||||||
|
|
||||||
|
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
|
||||||
|
dbDelta($sql);
|
||||||
|
|
||||||
|
// Store database version
|
||||||
|
update_option('mgc_db_version', MGC_DB_VERSION);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set default options on activation
|
||||||
|
*/
|
||||||
|
function mgc_set_defaults() {
|
||||||
|
$default_options = array(
|
||||||
|
'enabled' => true,
|
||||||
|
'notice_text' => __('We use cookies to make this site work properly and to understand how you use it. This includes <strong>essential cookies</strong> (required), <strong>analytics cookies</strong> (to understand site usage), and <strong>marketing cookies</strong> (for personalized content). You can accept all, reject optional cookies, or customize your preferences.', 'maple-gdpr-cookies'),
|
||||||
|
'accept_button_text' => __('Accept All', 'maple-gdpr-cookies'),
|
||||||
|
'reject_button_text' => __('Reject Optional', 'maple-gdpr-cookies'),
|
||||||
|
'settings_button_text' => __('Cookie Settings', 'maple-gdpr-cookies'),
|
||||||
|
'privacy_policy_url' => get_privacy_policy_url(),
|
||||||
|
'privacy_policy_text' => __('Privacy Policy', 'maple-gdpr-cookies'),
|
||||||
|
'cookie_expiry' => 365,
|
||||||
|
'position' => 'bottom',
|
||||||
|
'theme' => 'light',
|
||||||
|
'animation' => 'slide',
|
||||||
|
'button_color' => 'blue',
|
||||||
|
'custom_button_color' => '',
|
||||||
|
'custom_button_hover_color' => '',
|
||||||
|
'show_reject_button' => true,
|
||||||
|
'show_settings_button' => true,
|
||||||
|
'preference_display_type' => 'icon',
|
||||||
|
'custom_css' => '',
|
||||||
|
'enable_analytics' => true,
|
||||||
|
'enable_marketing' => true,
|
||||||
|
'enable_functional' => true,
|
||||||
|
'enable_logging' => true
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($default_options as $key => $value) {
|
||||||
|
if (get_option('mgc_' . $key) === false) {
|
||||||
|
add_option('mgc_' . $key, $value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all plugin caches - FIXED VERSION
|
||||||
|
*/
|
||||||
|
function mgc_clear_all_caches() {
|
||||||
|
// Method 1: Clear WordPress object cache (if available)
|
||||||
|
if (function_exists('wp_cache_flush')) {
|
||||||
|
wp_cache_flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method 2: Clear plugin-specific transients
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
// Get all mgc transients
|
||||||
|
$transients = $wpdb->get_col(
|
||||||
|
"SELECT option_name FROM $wpdb->options
|
||||||
|
WHERE option_name LIKE '_transient_mgc_%'
|
||||||
|
OR option_name LIKE '_transient_timeout_mgc_%'"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Delete each transient
|
||||||
|
foreach ($transients as $transient) {
|
||||||
|
if (strpos($transient, '_transient_timeout_') === 0) {
|
||||||
|
continue; // Skip timeout entries, they'll be deleted with the transient
|
||||||
|
}
|
||||||
|
|
||||||
|
$transient_key = str_replace('_transient_', '', $transient);
|
||||||
|
delete_transient($transient_key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method 3: Clear specific plugin caches
|
||||||
|
$cache_keys = array(
|
||||||
|
'mgc_settings',
|
||||||
|
'mgc_stats',
|
||||||
|
'mgc_consent_logs'
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($cache_keys as $key) {
|
||||||
|
wp_cache_delete($key, 'maple-gdpr-cookies');
|
||||||
|
delete_transient($key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method 4: Clear WooCommerce cache if present
|
||||||
|
if (function_exists('wc_delete_shop_order_transients')) {
|
||||||
|
wc_delete_shop_order_transients();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method 5: Trigger cache clear hooks for other plugins
|
||||||
|
do_action('mgc_clear_caches');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin activation
|
||||||
|
*/
|
||||||
|
function mgc_activate() {
|
||||||
|
// Check requirements
|
||||||
|
$errors = mgc_check_requirements();
|
||||||
|
if (!empty($errors)) {
|
||||||
|
deactivate_plugins(plugin_basename(__FILE__));
|
||||||
|
wp_die(
|
||||||
|
implode('<br>', $errors),
|
||||||
|
__('Plugin Activation Error', 'maple-gdpr-cookies'),
|
||||||
|
array('back_link' => true)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create database tables
|
||||||
|
mgc_create_tables();
|
||||||
|
|
||||||
|
// Set default options
|
||||||
|
mgc_set_defaults();
|
||||||
|
|
||||||
|
// Clear all caches - now using fixed function
|
||||||
|
mgc_clear_all_caches();
|
||||||
|
|
||||||
|
// Set activation flag
|
||||||
|
set_transient('mgc_activation_redirect', true, 30);
|
||||||
|
}
|
||||||
|
register_activation_hook(__FILE__, 'mgc_activate');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin deactivation
|
||||||
|
*/
|
||||||
|
function mgc_deactivate() {
|
||||||
|
// Clear all caches
|
||||||
|
mgc_clear_all_caches();
|
||||||
|
|
||||||
|
// Clear scheduled events
|
||||||
|
wp_clear_scheduled_hook('mgc_cleanup_logs');
|
||||||
|
wp_clear_scheduled_hook('mgc_optimize_database');
|
||||||
|
}
|
||||||
|
register_deactivation_hook(__FILE__, 'mgc_deactivate');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin uninstall - only if user chooses to delete data
|
||||||
|
*/
|
||||||
|
function mgc_uninstall() {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
// Always clear scheduled tasks even if keeping data
|
||||||
|
wp_clear_scheduled_hook('mgc_cleanup_logs');
|
||||||
|
wp_clear_scheduled_hook('mgc_optimize_database');
|
||||||
|
|
||||||
|
// Check if user wants to keep data
|
||||||
|
if (get_option('mgc_keep_data_on_uninstall', false)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop database tables
|
||||||
|
$table_name = $wpdb->prefix . 'mgc_consent_logs';
|
||||||
|
$wpdb->query("DROP TABLE IF EXISTS $table_name");
|
||||||
|
|
||||||
|
// Delete all plugin options
|
||||||
|
$wpdb->query("DELETE FROM $wpdb->options WHERE option_name LIKE 'mgc_%'");
|
||||||
|
|
||||||
|
// Clear all caches
|
||||||
|
mgc_clear_all_caches();
|
||||||
|
}
|
||||||
|
register_uninstall_hook(__FILE__, 'mgc_uninstall');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load plugin textdomain for translations
|
||||||
|
*/
|
||||||
|
function mgc_load_textdomain() {
|
||||||
|
load_plugin_textdomain(
|
||||||
|
'maple-gdpr-cookies',
|
||||||
|
false,
|
||||||
|
dirname(plugin_basename(__FILE__)) . '/languages'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
add_action('plugins_loaded', 'mgc_load_textdomain');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enqueue frontend scripts and styles
|
||||||
|
*/
|
||||||
|
function mgc_enqueue_frontend_assets() {
|
||||||
|
// Only load if enabled
|
||||||
|
if (!get_option('mgc_enabled', true)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user already has consent cookie
|
||||||
|
$has_consent = isset($_COOKIE['mgc_consent']);
|
||||||
|
|
||||||
|
// Enqueue CSS
|
||||||
|
wp_enqueue_style(
|
||||||
|
'mgc-frontend',
|
||||||
|
MGC_PLUGIN_URL . 'public/css/frontend.css',
|
||||||
|
array(),
|
||||||
|
MGC_PLUGIN_VERSION
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add custom CSS if provided
|
||||||
|
$custom_css = get_option('mgc_custom_css');
|
||||||
|
if (!empty($custom_css)) {
|
||||||
|
wp_add_inline_style('mgc-frontend', $custom_css);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add custom button colors if provided
|
||||||
|
$custom_button_color = get_option('mgc_custom_button_color');
|
||||||
|
$custom_button_hover_color = get_option('mgc_custom_button_hover_color');
|
||||||
|
|
||||||
|
if (!empty($custom_button_color) || !empty($custom_button_hover_color)) {
|
||||||
|
$color_css = '';
|
||||||
|
|
||||||
|
if (!empty($custom_button_color)) {
|
||||||
|
$color_css .= '.mgc-button { background: ' . esc_attr($custom_button_color) . ' !important; }';
|
||||||
|
$color_css .= '.mgc-floating-button { background: ' . esc_attr($custom_button_color) . ' !important; }';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($custom_button_hover_color)) {
|
||||||
|
$color_css .= '.mgc-button:hover { background: ' . esc_attr($custom_button_hover_color) . ' !important; }';
|
||||||
|
$color_css .= '.mgc-floating-button:hover { background: ' . esc_attr($custom_button_hover_color) . ' !important; }';
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_add_inline_style('mgc-frontend', $color_css);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enqueue the compliant JS (blocks scripts before consent)
|
||||||
|
wp_enqueue_script(
|
||||||
|
'mgc-frontend-compliant',
|
||||||
|
MGC_PLUGIN_URL . 'public/js/frontend-compliant.js',
|
||||||
|
array(),
|
||||||
|
MGC_PLUGIN_VERSION,
|
||||||
|
false // Load in head for early script blocking
|
||||||
|
);
|
||||||
|
|
||||||
|
// Localize script with settings
|
||||||
|
wp_localize_script('mgc-frontend-compliant', 'mgcSettings', array(
|
||||||
|
'ajaxUrl' => admin_url('admin-ajax.php'),
|
||||||
|
'nonce' => wp_create_nonce('mgc_consent_nonce'),
|
||||||
|
'noticeText' => get_option('mgc_notice_text'),
|
||||||
|
'acceptButtonText' => get_option('mgc_accept_button_text'),
|
||||||
|
'rejectButtonText' => get_option('mgc_reject_button_text'),
|
||||||
|
'settingsButtonText' => get_option('mgc_settings_button_text'),
|
||||||
|
'privacyPolicyUrl' => get_option('mgc_privacy_policy_url'),
|
||||||
|
'privacyPolicyText' => get_option('mgc_privacy_policy_text'),
|
||||||
|
'cookieExpiry' => intval(get_option('mgc_cookie_expiry', 365)),
|
||||||
|
'position' => get_option('mgc_position', 'bottom'),
|
||||||
|
'theme' => get_option('mgc_theme', 'light'),
|
||||||
|
'animation' => get_option('mgc_animation', 'slide'),
|
||||||
|
'buttonColor' => get_option('mgc_button_color', 'blue'),
|
||||||
|
'showRejectButton' => (bool) get_option('mgc_show_reject_button', true),
|
||||||
|
'showSettingsButton' => (bool) get_option('mgc_show_settings_button', true),
|
||||||
|
'preferenceDisplayType' => get_option('mgc_preference_display_type', 'icon')
|
||||||
|
));
|
||||||
|
}
|
||||||
|
add_action('wp_enqueue_scripts', 'mgc_enqueue_frontend_assets');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enqueue admin scripts and styles
|
||||||
|
*/
|
||||||
|
function mgc_enqueue_admin_assets($hook) {
|
||||||
|
// Only load on plugin settings pages
|
||||||
|
if (strpos($hook, 'maple-gdpr-cookies') === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enqueue WordPress color picker
|
||||||
|
wp_enqueue_style('wp-color-picker');
|
||||||
|
|
||||||
|
// Enqueue admin CSS
|
||||||
|
wp_enqueue_style(
|
||||||
|
'mgc-admin',
|
||||||
|
MGC_PLUGIN_URL . 'admin/css/admin.css',
|
||||||
|
array(),
|
||||||
|
MGC_PLUGIN_VERSION
|
||||||
|
);
|
||||||
|
|
||||||
|
// Enqueue admin JS
|
||||||
|
wp_enqueue_script(
|
||||||
|
'mgc-admin',
|
||||||
|
MGC_PLUGIN_URL . 'admin/js/admin.js',
|
||||||
|
array('jquery', 'wp-color-picker'),
|
||||||
|
MGC_PLUGIN_VERSION,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
add_action('admin_enqueue_scripts', 'mgc_enqueue_admin_assets');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add admin menu
|
||||||
|
*/
|
||||||
|
function mgc_add_admin_menu() {
|
||||||
|
add_menu_page(
|
||||||
|
__('Maple GDPR Cookies', 'maple-gdpr-cookies'),
|
||||||
|
__('Cookie Settings', 'maple-gdpr-cookies'),
|
||||||
|
'manage_options',
|
||||||
|
'maple-gdpr-cookies',
|
||||||
|
'mgc_admin_page',
|
||||||
|
'dashicons-shield',
|
||||||
|
100
|
||||||
|
);
|
||||||
|
|
||||||
|
add_submenu_page(
|
||||||
|
'maple-gdpr-cookies',
|
||||||
|
__('Settings', 'maple-gdpr-cookies'),
|
||||||
|
__('Settings', 'maple-gdpr-cookies'),
|
||||||
|
'manage_options',
|
||||||
|
'maple-gdpr-cookies',
|
||||||
|
'mgc_admin_page'
|
||||||
|
);
|
||||||
|
|
||||||
|
add_submenu_page(
|
||||||
|
'maple-gdpr-cookies',
|
||||||
|
__('Consent Logs', 'maple-gdpr-cookies'),
|
||||||
|
__('Consent Logs', 'maple-gdpr-cookies'),
|
||||||
|
'manage_options',
|
||||||
|
'maple-gdpr-cookies-logs',
|
||||||
|
'mgc_logs_page'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
add_action('admin_menu', 'mgc_add_admin_menu');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Admin page callback
|
||||||
|
*/
|
||||||
|
function mgc_admin_page() {
|
||||||
|
if (!current_user_can('manage_options')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle form submission
|
||||||
|
if (isset($_POST['mgc_save_settings']) && check_admin_referer('mgc_settings_nonce')) {
|
||||||
|
mgc_save_settings();
|
||||||
|
}
|
||||||
|
|
||||||
|
include MGC_PLUGIN_PATH . 'admin/views/settings.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs page callback
|
||||||
|
*/
|
||||||
|
function mgc_logs_page() {
|
||||||
|
if (!current_user_can('manage_options')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
include MGC_PLUGIN_PATH . 'admin/views/logs.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save settings
|
||||||
|
*/
|
||||||
|
function mgc_save_settings() {
|
||||||
|
$settings = array(
|
||||||
|
'enabled',
|
||||||
|
'notice_text',
|
||||||
|
'accept_button_text',
|
||||||
|
'reject_button_text',
|
||||||
|
'settings_button_text',
|
||||||
|
'privacy_policy_url',
|
||||||
|
'privacy_policy_text',
|
||||||
|
'cookie_expiry',
|
||||||
|
'position',
|
||||||
|
'theme',
|
||||||
|
'animation',
|
||||||
|
'button_color',
|
||||||
|
'custom_button_color',
|
||||||
|
'custom_button_hover_color',
|
||||||
|
'show_reject_button',
|
||||||
|
'show_settings_button',
|
||||||
|
'preference_display_type',
|
||||||
|
'custom_css',
|
||||||
|
'enable_analytics',
|
||||||
|
'enable_marketing',
|
||||||
|
'enable_functional',
|
||||||
|
'enable_logging'
|
||||||
|
);
|
||||||
|
|
||||||
|
// List of checkbox fields (need special handling since unchecked = no POST data)
|
||||||
|
$checkbox_fields = array(
|
||||||
|
'enabled',
|
||||||
|
'show_reject_button',
|
||||||
|
'show_settings_button',
|
||||||
|
'enable_analytics',
|
||||||
|
'enable_marketing',
|
||||||
|
'enable_functional',
|
||||||
|
'enable_logging'
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($settings as $setting) {
|
||||||
|
// Special handling for checkboxes
|
||||||
|
if (in_array($setting, $checkbox_fields)) {
|
||||||
|
// Checkbox: if it's in POST and = '1', it's checked. Otherwise it's unchecked.
|
||||||
|
$value = (isset($_POST['mgc_' . $setting]) && $_POST['mgc_' . $setting] == '1') ? true : false;
|
||||||
|
} else {
|
||||||
|
$value = isset($_POST['mgc_' . $setting]) ? $_POST['mgc_' . $setting] : '';
|
||||||
|
|
||||||
|
// Sanitize based on type
|
||||||
|
if ($setting === 'cookie_expiry') {
|
||||||
|
$value = absint($value);
|
||||||
|
// Enforce minimum and maximum values for GDPR compliance
|
||||||
|
if ($value < 1) $value = 1;
|
||||||
|
if ($value > 365) $value = 365;
|
||||||
|
} elseif ($setting === 'custom_css') {
|
||||||
|
$value = wp_strip_all_tags($value);
|
||||||
|
// Remove any javascript: or data: URLs to prevent CSS injection attacks
|
||||||
|
$value = preg_replace('/url\s*\(\s*[\'"]?\s*(?:javascript|data):/i', 'url(blocked:', $value);
|
||||||
|
// Limit length to prevent abuse (10KB should be more than enough for custom CSS)
|
||||||
|
$value = substr($value, 0, 10000);
|
||||||
|
} elseif (in_array($setting, array('custom_button_color', 'custom_button_hover_color'))) {
|
||||||
|
// Sanitize hex color
|
||||||
|
$value = sanitize_text_field($value);
|
||||||
|
// Validate hex color format
|
||||||
|
if (!empty($value) && !preg_match('/^#[a-fA-F0-9]{6}$/', $value)) {
|
||||||
|
$value = ''; // Clear invalid hex colors
|
||||||
|
}
|
||||||
|
} elseif ($setting === 'preference_display_type') {
|
||||||
|
// Validate preference display type
|
||||||
|
$value = sanitize_text_field($value);
|
||||||
|
if (!in_array($value, array('icon', 'footer', 'neither'))) {
|
||||||
|
$value = 'icon'; // Default to icon if invalid
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$value = sanitize_text_field($value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update_option('mgc_' . $setting, $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear caches after saving
|
||||||
|
mgc_clear_all_caches();
|
||||||
|
|
||||||
|
add_settings_error(
|
||||||
|
'mgc_messages',
|
||||||
|
'mgc_message',
|
||||||
|
__('Settings saved successfully.', 'maple-gdpr-cookies'),
|
||||||
|
'updated'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX handler for saving consent
|
||||||
|
*/
|
||||||
|
function mgc_save_consent() {
|
||||||
|
check_ajax_referer('mgc_consent_nonce', 'nonce');
|
||||||
|
|
||||||
|
// Rate limiting - max 10 consent saves per hour per IP to prevent abuse
|
||||||
|
$ip = mgc_get_ip_address();
|
||||||
|
$rate_key = 'mgc_consent_rate_' . md5($ip);
|
||||||
|
$attempts = get_transient($rate_key);
|
||||||
|
|
||||||
|
if ($attempts && $attempts >= 10) {
|
||||||
|
wp_send_json_error(array(
|
||||||
|
'message' => __('Too many requests. Please try again later.', 'maple-gdpr-cookies')
|
||||||
|
), 429);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment rate limit counter
|
||||||
|
set_transient($rate_key, ($attempts ? $attempts + 1 : 1), HOUR_IN_SECONDS);
|
||||||
|
|
||||||
|
$consent_type = sanitize_text_field($_POST['consent_type']);
|
||||||
|
$categories = isset($_POST['categories']) ? array_map('sanitize_text_field', $_POST['categories']) : array();
|
||||||
|
|
||||||
|
// Log consent if enabled
|
||||||
|
if (get_option('mgc_enable_logging', true)) {
|
||||||
|
mgc_log_consent($consent_type, $categories);
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_send_json_success(array(
|
||||||
|
'message' => __('Consent saved successfully', 'maple-gdpr-cookies')
|
||||||
|
));
|
||||||
|
}
|
||||||
|
add_action('wp_ajax_mgc_save_consent', 'mgc_save_consent');
|
||||||
|
add_action('wp_ajax_nopriv_mgc_save_consent', 'mgc_save_consent');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log user consent
|
||||||
|
*/
|
||||||
|
function mgc_log_consent($consent_type, $categories = array()) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$table_name = $wpdb->prefix . 'mgc_consent_logs';
|
||||||
|
|
||||||
|
// Get and anonymize IP address (GDPR compliance)
|
||||||
|
$ip_address = mgc_anonymize_ip(mgc_get_ip_address());
|
||||||
|
|
||||||
|
$wpdb->insert(
|
||||||
|
$table_name,
|
||||||
|
array(
|
||||||
|
'user_id' => get_current_user_id(),
|
||||||
|
'ip_address' => $ip_address,
|
||||||
|
'user_agent' => sanitize_text_field(substr($_SERVER['HTTP_USER_AGENT'], 0, 255)),
|
||||||
|
'consent_type' => $consent_type,
|
||||||
|
'categories' => json_encode($categories),
|
||||||
|
'consent_given' => ($consent_type === 'accept'),
|
||||||
|
'consent_date' => current_time('mysql')
|
||||||
|
),
|
||||||
|
array('%d', '%s', '%s', '%s', '%s', '%d', '%s')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user IP address
|
||||||
|
*/
|
||||||
|
function mgc_get_ip_address() {
|
||||||
|
$ip = '';
|
||||||
|
|
||||||
|
// Check if behind a proxy and validate
|
||||||
|
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
||||||
|
// X-Forwarded-For can contain multiple IPs, get the first one (original client)
|
||||||
|
$ip_list = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
|
||||||
|
$ip = trim($ip_list[0]);
|
||||||
|
} elseif (!empty($_SERVER['HTTP_CLIENT_IP'])) {
|
||||||
|
$ip = $_SERVER['HTTP_CLIENT_IP'];
|
||||||
|
} else {
|
||||||
|
$ip = $_SERVER['REMOTE_ADDR'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate it's a real IP address
|
||||||
|
if (!filter_var($ip, FILTER_VALIDATE_IP)) {
|
||||||
|
// If invalid, fall back to direct connection IP
|
||||||
|
$ip = !empty($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '0.0.0.0';
|
||||||
|
}
|
||||||
|
|
||||||
|
return sanitize_text_field($ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Anonymize IP address (GDPR compliance)
|
||||||
|
* Removes last octet for IPv4, last segment for IPv6
|
||||||
|
*/
|
||||||
|
function mgc_anonymize_ip($ip) {
|
||||||
|
// Validate and anonymize IPv4
|
||||||
|
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||||
|
return preg_replace('/\.\d+$/', '.0', $ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate and anonymize IPv6
|
||||||
|
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
||||||
|
return preg_replace('/:[^:]+$/', ':0', $ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If invalid IP, return masked version
|
||||||
|
return '0.0.0.0';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedule cleanup tasks
|
||||||
|
*/
|
||||||
|
function mgc_schedule_cleanup() {
|
||||||
|
if (!wp_next_scheduled('mgc_cleanup_logs')) {
|
||||||
|
wp_schedule_event(time(), 'daily', 'mgc_cleanup_logs');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!wp_next_scheduled('mgc_optimize_database')) {
|
||||||
|
wp_schedule_event(time(), 'weekly', 'mgc_optimize_database');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
add_action('wp', 'mgc_schedule_cleanup');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleanup old consent logs
|
||||||
|
*/
|
||||||
|
function mgc_cleanup_old_logs() {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$table_name = $wpdb->prefix . 'mgc_consent_logs';
|
||||||
|
$retention_days = apply_filters('mgc_log_retention_days', 365);
|
||||||
|
|
||||||
|
$wpdb->query(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"DELETE FROM $table_name WHERE consent_date < DATE_SUB(NOW(), INTERVAL %d DAY)",
|
||||||
|
$retention_days
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
add_action('mgc_cleanup_logs', 'mgc_cleanup_old_logs');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optimize database tables
|
||||||
|
*/
|
||||||
|
function mgc_optimize_database_tables() {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$table_name = $wpdb->prefix . 'mgc_consent_logs';
|
||||||
|
$wpdb->query("OPTIMIZE TABLE $table_name");
|
||||||
|
}
|
||||||
|
add_action('mgc_optimize_database', 'mgc_optimize_database_tables');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add settings link on plugins page
|
||||||
|
*/
|
||||||
|
function mgc_add_settings_link($links) {
|
||||||
|
$settings_link = '<a href="' . admin_url('admin.php?page=maple-gdpr-cookies') . '">' . __('Settings', 'maple-gdpr-cookies') . '</a>';
|
||||||
|
array_unshift($links, $settings_link);
|
||||||
|
return $links;
|
||||||
|
}
|
||||||
|
add_filter('plugin_action_links_' . plugin_basename(__FILE__), 'mgc_add_settings_link');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirect to settings page on activation
|
||||||
|
*/
|
||||||
|
function mgc_activation_redirect() {
|
||||||
|
if (get_transient('mgc_activation_redirect')) {
|
||||||
|
delete_transient('mgc_activation_redirect');
|
||||||
|
|
||||||
|
if (!isset($_GET['activate-multi'])) {
|
||||||
|
wp_safe_redirect(admin_url('admin.php?page=maple-gdpr-cookies'));
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
add_action('admin_init', 'mgc_activation_redirect');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cookie Preferences Shortcode
|
||||||
|
* Usage: [mgc_cookie_preferences] or [mgc_cookie_preferences text="Manage Cookies"]
|
||||||
|
*/
|
||||||
|
function mgc_cookie_preferences_shortcode($atts) {
|
||||||
|
// Only show if plugin is enabled
|
||||||
|
if (!get_option('mgc_enabled', true)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse attributes
|
||||||
|
$atts = shortcode_atts(array(
|
||||||
|
'text' => __('Cookie Preferences', 'maple-gdpr-cookies'),
|
||||||
|
'class' => 'mgc-preferences-link'
|
||||||
|
), $atts);
|
||||||
|
|
||||||
|
// Return link with data attribute that JavaScript will handle
|
||||||
|
return sprintf(
|
||||||
|
'<a href="#" class="%s" data-mgc-preferences-trigger aria-label="%s">%s</a>',
|
||||||
|
esc_attr($atts['class']),
|
||||||
|
esc_attr($atts['text']),
|
||||||
|
esc_html($atts['text'])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
add_shortcode('mgc_cookie_preferences', 'mgc_cookie_preferences_shortcode');
|
||||||
303
native/wordpress/maple-gdpr-cookies/public/css/frontend.css
Normal file
303
native/wordpress/maple-gdpr-cookies/public/css/frontend.css
Normal file
|
|
@ -0,0 +1,303 @@
|
||||||
|
/* Maple GDPR Cookies - Frontend Styles */
|
||||||
|
|
||||||
|
.mgc-notice {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 999999;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mgc-notice.mgc-position-bottom {
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mgc-notice.mgc-position-top {
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mgc-notice.mgc-position-center {
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
max-width: 600px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mgc-notice.mgc-theme-dark {
|
||||||
|
background: #2c3e50;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mgc-notice-content {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mgc-notice-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mgc-button {
|
||||||
|
padding: 10px 20px;
|
||||||
|
border: 2px solid #222222;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
background: #3498db;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mgc-button:hover {
|
||||||
|
background: #2980b9;
|
||||||
|
border-color: #222222;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button Color Variants */
|
||||||
|
.mgc-button-color-blue {
|
||||||
|
background: #3498db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mgc-button-color-blue:hover {
|
||||||
|
background: #2980b9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mgc-button-color-black {
|
||||||
|
background: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mgc-button-color-black:hover {
|
||||||
|
background: #1a1a1a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mgc-button-color-dark-grey {
|
||||||
|
background: #4a4a4a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mgc-button-color-dark-grey:hover {
|
||||||
|
background: #5a5a5a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mgc-button-color-red {
|
||||||
|
background: #e74c3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mgc-button-color-red:hover {
|
||||||
|
background: #c0392b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mgc-button-color-green {
|
||||||
|
background: #27ae60;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mgc-button-color-green:hover {
|
||||||
|
background: #229954;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mgc-button-accept {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mgc-button-reject {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mgc-button-settings {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mgc-privacy-link {
|
||||||
|
color: #3498db;
|
||||||
|
text-decoration: none;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mgc-privacy-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animation: Slide */
|
||||||
|
.mgc-animation-slide.mgc-position-bottom {
|
||||||
|
animation: mgc-slide-up 0.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mgc-animation-slide.mgc-position-top {
|
||||||
|
animation: mgc-slide-down 0.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mgc-slide-up {
|
||||||
|
from {
|
||||||
|
transform: translateY(100%);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mgc-slide-down {
|
||||||
|
from {
|
||||||
|
transform: translateY(-100%);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animation: Fade */
|
||||||
|
.mgc-animation-fade {
|
||||||
|
animation: mgc-fade-in 0.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mgc-fade-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.mgc-notice {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mgc-notice-buttons {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mgc-button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hidden state */
|
||||||
|
.mgc-hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Persistent Floating Cookie Settings Button */
|
||||||
|
.mgc-floating-button {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
left: 20px;
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #3498db;
|
||||||
|
color: #fff;
|
||||||
|
border: 2px solid #222222;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 999998;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mgc-floating-button:hover {
|
||||||
|
background: #2980b9;
|
||||||
|
transform: scale(1.1);
|
||||||
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mgc-floating-button svg {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer Preference Link */
|
||||||
|
.mgc-footer-preference-link {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 10px;
|
||||||
|
left: 10px;
|
||||||
|
font-size: 8pt;
|
||||||
|
color: #666;
|
||||||
|
text-decoration: none;
|
||||||
|
z-index: 999998;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mgc-footer-preference-link:hover {
|
||||||
|
color: #3498db;
|
||||||
|
text-decoration: underline;
|
||||||
|
background: rgba(255, 255, 255, 1);
|
||||||
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Shortcode Preference Link */
|
||||||
|
.mgc-preferences-link {
|
||||||
|
color: #666;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mgc-preferences-link:hover {
|
||||||
|
color: #3498db;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Settings Modal */
|
||||||
|
.mgc-settings-modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 9999999;
|
||||||
|
animation: mgc-fade-in 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mgc-settings-content {
|
||||||
|
background: #fff;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 8px;
|
||||||
|
max-width: 600px;
|
||||||
|
width: 90%;
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||||
|
animation: mgc-scale-in 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes mgc-scale-in {
|
||||||
|
from {
|
||||||
|
transform: scale(0.9);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mgc-category {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mgc-category:hover {
|
||||||
|
background: #f9f9f9;
|
||||||
|
}
|
||||||
12
native/wordpress/maple-gdpr-cookies/public/css/index.php
Normal file
12
native/wordpress/maple-gdpr-cookies/public/css/index.php
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Silence is golden.
|
||||||
|
*
|
||||||
|
* Prevent direct access to this file.
|
||||||
|
*
|
||||||
|
* @package WordPress
|
||||||
|
*/
|
||||||
|
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit; // Exit if accessed directly.
|
||||||
|
}
|
||||||
12
native/wordpress/maple-gdpr-cookies/public/index.php
Normal file
12
native/wordpress/maple-gdpr-cookies/public/index.php
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Silence is golden.
|
||||||
|
*
|
||||||
|
* Prevent direct access to this file.
|
||||||
|
*
|
||||||
|
* @package WordPress
|
||||||
|
*/
|
||||||
|
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit; // Exit if accessed directly.
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,500 @@
|
||||||
|
/* Maple GDPR Cookies - Script Blocker (GDPR Compliant) */
|
||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
// Check consent status immediately
|
||||||
|
const consentCookie = getCookie("mgc_consent");
|
||||||
|
const consentCategories = getCookie("mgc_consent_categories");
|
||||||
|
|
||||||
|
// If user has given consent, enable scripts
|
||||||
|
if (consentCookie === "accept" && consentCategories) {
|
||||||
|
const categories = JSON.parse(consentCategories || "[]");
|
||||||
|
enableScripts(categories);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block scripts on initial load (GDPR compliance - prior consent)
|
||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
blockScripts();
|
||||||
|
|
||||||
|
// Show notice if no consent given
|
||||||
|
if (!consentCookie) {
|
||||||
|
createNotice();
|
||||||
|
} else {
|
||||||
|
// Add persistent cookie settings button or footer text
|
||||||
|
addPersistentPreferenceLink();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle clicks on shortcode preference links
|
||||||
|
document.addEventListener("click", function (e) {
|
||||||
|
if (
|
||||||
|
e.target.matches("[data-mgc-preferences-trigger]") ||
|
||||||
|
e.target.closest("[data-mgc-preferences-trigger]")
|
||||||
|
) {
|
||||||
|
e.preventDefault();
|
||||||
|
showSettingsModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Block all non-essential scripts until consent is given
|
||||||
|
* CRITICAL for GDPR compliance
|
||||||
|
*/
|
||||||
|
function blockScripts() {
|
||||||
|
// Find all scripts with data-category attribute
|
||||||
|
const scripts = document.querySelectorAll("script[data-cookie-category]");
|
||||||
|
|
||||||
|
scripts.forEach(function (script) {
|
||||||
|
const category = script.getAttribute("data-cookie-category");
|
||||||
|
|
||||||
|
// Check if this category is allowed
|
||||||
|
if (!isCategoryAllowed(category)) {
|
||||||
|
// Block the script by changing type
|
||||||
|
if (script.type !== "text/plain") {
|
||||||
|
script.type = "text/plain";
|
||||||
|
script.setAttribute("data-original-type", "text/javascript");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable scripts for allowed categories
|
||||||
|
*/
|
||||||
|
function enableScripts(allowedCategories) {
|
||||||
|
// Always allow functional
|
||||||
|
if (!allowedCategories.includes("functional")) {
|
||||||
|
allowedCategories.push("functional");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find all blocked scripts
|
||||||
|
const scripts = document.querySelectorAll(
|
||||||
|
'script[data-cookie-category][type="text/plain"]',
|
||||||
|
);
|
||||||
|
|
||||||
|
scripts.forEach(function (script) {
|
||||||
|
const category = script.getAttribute("data-cookie-category");
|
||||||
|
|
||||||
|
// If category is allowed, re-enable script
|
||||||
|
if (allowedCategories.includes(category)) {
|
||||||
|
const originalType =
|
||||||
|
script.getAttribute("data-original-type") || "text/javascript";
|
||||||
|
|
||||||
|
// Create new script element (changing type on existing doesn't execute)
|
||||||
|
const newScript = document.createElement("script");
|
||||||
|
newScript.type = originalType;
|
||||||
|
|
||||||
|
// Copy attributes
|
||||||
|
Array.from(script.attributes).forEach(function (attr) {
|
||||||
|
if (attr.name !== "type" && attr.name !== "data-original-type") {
|
||||||
|
newScript.setAttribute(attr.name, attr.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Copy content
|
||||||
|
if (script.src) {
|
||||||
|
newScript.src = script.src;
|
||||||
|
} else {
|
||||||
|
newScript.textContent = script.textContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace old script with new one
|
||||||
|
script.parentNode.replaceChild(newScript, script);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Trigger event for other integrations
|
||||||
|
document.dispatchEvent(
|
||||||
|
new CustomEvent("mgc_scripts_loaded", {
|
||||||
|
detail: { categories: allowedCategories },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a category is currently allowed
|
||||||
|
*/
|
||||||
|
function isCategoryAllowed(category) {
|
||||||
|
const consentCookie = getCookie("mgc_consent");
|
||||||
|
if (consentCookie !== "accept") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const categories = JSON.parse(getCookie("mgc_consent_categories") || "[]");
|
||||||
|
return categories.includes(category) || category === "functional";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add persistent cookie preference link (GDPR requirement - easy withdrawal)
|
||||||
|
* Can be either a floating icon button or footer text link based on settings
|
||||||
|
*/
|
||||||
|
function addPersistentPreferenceLink() {
|
||||||
|
// Check what type of display is preferred
|
||||||
|
const displayType = window.mgcSettings?.preferenceDisplayType || "icon";
|
||||||
|
|
||||||
|
// If 'neither', don't add anything - user will use shortcode
|
||||||
|
if (displayType === "neither") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if element already exists
|
||||||
|
if (
|
||||||
|
document.querySelector(".mgc-floating-button") ||
|
||||||
|
document.querySelector(".mgc-footer-preference-link")
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (displayType === "footer") {
|
||||||
|
// Create footer text link
|
||||||
|
const footerLink = document.createElement("a");
|
||||||
|
footerLink.className = "mgc-footer-preference-link";
|
||||||
|
footerLink.href = "#";
|
||||||
|
footerLink.textContent = "Cookie Preferences";
|
||||||
|
footerLink.setAttribute("aria-label", "Cookie Preferences");
|
||||||
|
footerLink.onclick = function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
showSettingsModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
document.body.appendChild(footerLink);
|
||||||
|
} else {
|
||||||
|
// Create floating icon button (default)
|
||||||
|
const button = document.createElement("button");
|
||||||
|
button.className = "mgc-floating-button";
|
||||||
|
button.setAttribute("aria-label", "Cookie Settings");
|
||||||
|
button.innerHTML =
|
||||||
|
'<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8zm-1-13h2v6h-2zm0 8h2v2h-2z"/></svg>';
|
||||||
|
button.title = "Cookie Settings";
|
||||||
|
button.onclick = function () {
|
||||||
|
showSettingsModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
document.body.appendChild(button);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createNotice() {
|
||||||
|
const settings = window.mgcSettings || {};
|
||||||
|
|
||||||
|
// Create notice container
|
||||||
|
const notice = document.createElement("div");
|
||||||
|
notice.className =
|
||||||
|
"mgc-notice mgc-position-" +
|
||||||
|
(settings.position || "bottom") +
|
||||||
|
" mgc-theme-" +
|
||||||
|
(settings.theme || "light") +
|
||||||
|
" mgc-animation-" +
|
||||||
|
(settings.animation || "slide");
|
||||||
|
|
||||||
|
// Create content
|
||||||
|
const content = document.createElement("div");
|
||||||
|
content.className = "mgc-notice-content";
|
||||||
|
const noticeText = (
|
||||||
|
settings.noticeText ||
|
||||||
|
"We use cookies to enhance your browsing experience."
|
||||||
|
)
|
||||||
|
.replace(/\\"/g, '"')
|
||||||
|
.replace(/\\\\/g, "\\")
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/"Accept All"/g, "<strong>Accept All</strong>")
|
||||||
|
.replace(/"Reject All"/g, "<strong>Reject All</strong>");
|
||||||
|
content.innerHTML = noticeText;
|
||||||
|
|
||||||
|
// Add privacy policy link if provided
|
||||||
|
if (settings.privacyPolicyUrl) {
|
||||||
|
content.innerHTML +=
|
||||||
|
' <a href="' +
|
||||||
|
settings.privacyPolicyUrl +
|
||||||
|
'" class="mgc-privacy-link" target="_blank">' +
|
||||||
|
(settings.privacyPolicyText || "Privacy Policy") +
|
||||||
|
"</a>";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create buttons container
|
||||||
|
const buttons = document.createElement("div");
|
||||||
|
buttons.className = "mgc-notice-buttons";
|
||||||
|
|
||||||
|
// Accept button
|
||||||
|
const acceptBtn = document.createElement("button");
|
||||||
|
acceptBtn.className =
|
||||||
|
"mgc-button mgc-button-accept mgc-button-color-" +
|
||||||
|
(settings.buttonColor || "blue");
|
||||||
|
acceptBtn.textContent = settings.acceptButtonText || "Accept All";
|
||||||
|
acceptBtn.onclick = function () {
|
||||||
|
handleConsent("accept", ["analytics", "marketing", "functional"]);
|
||||||
|
};
|
||||||
|
buttons.appendChild(acceptBtn);
|
||||||
|
|
||||||
|
// Reject button (optional)
|
||||||
|
if (settings.showRejectButton) {
|
||||||
|
const rejectBtn = document.createElement("button");
|
||||||
|
rejectBtn.className =
|
||||||
|
"mgc-button mgc-button-reject mgc-button-color-" +
|
||||||
|
(settings.buttonColor || "blue");
|
||||||
|
rejectBtn.textContent = settings.rejectButtonText || "Reject All";
|
||||||
|
rejectBtn.onclick = function () {
|
||||||
|
handleConsent("reject", ["functional"]); // Only functional cookies
|
||||||
|
};
|
||||||
|
buttons.appendChild(rejectBtn);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Settings button (optional)
|
||||||
|
if (settings.showSettingsButton) {
|
||||||
|
const settingsBtn = document.createElement("button");
|
||||||
|
settingsBtn.className =
|
||||||
|
"mgc-button mgc-button-settings mgc-button-color-" +
|
||||||
|
(settings.buttonColor || "blue");
|
||||||
|
settingsBtn.textContent =
|
||||||
|
settings.settingsButtonText || "Cookie Settings";
|
||||||
|
settingsBtn.onclick = function () {
|
||||||
|
showSettingsModal();
|
||||||
|
};
|
||||||
|
buttons.appendChild(settingsBtn);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append elements
|
||||||
|
notice.appendChild(content);
|
||||||
|
notice.appendChild(buttons);
|
||||||
|
document.body.appendChild(notice);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSettingsModal() {
|
||||||
|
// Remove initial notice if present
|
||||||
|
const initialNotice = document.querySelector(".mgc-notice");
|
||||||
|
if (initialNotice) {
|
||||||
|
initialNotice.classList.add("mgc-hidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create settings modal
|
||||||
|
const modal = document.createElement("div");
|
||||||
|
modal.className = "mgc-settings-modal";
|
||||||
|
|
||||||
|
const modalContent = document.createElement("div");
|
||||||
|
modalContent.className = "mgc-settings-content";
|
||||||
|
|
||||||
|
// Modal title
|
||||||
|
const title = document.createElement("h3");
|
||||||
|
title.textContent = "Cookie Settings";
|
||||||
|
title.style.marginTop = "0";
|
||||||
|
modalContent.appendChild(title);
|
||||||
|
|
||||||
|
// Description
|
||||||
|
const desc = document.createElement("p");
|
||||||
|
desc.textContent = "Choose which types of cookies you want to allow:";
|
||||||
|
modalContent.appendChild(desc);
|
||||||
|
|
||||||
|
// Get current consent
|
||||||
|
const currentCategories = JSON.parse(
|
||||||
|
getCookie("mgc_consent_categories") || '["functional"]',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Cookie categories
|
||||||
|
const categories = [
|
||||||
|
{
|
||||||
|
id: "functional",
|
||||||
|
label: "Functional Cookies",
|
||||||
|
description:
|
||||||
|
"These cookies are essential for the website to function properly. They enable basic features like page navigation, security, and access to secure areas. The website cannot function properly without these cookies.",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "analytics",
|
||||||
|
label: "Analytics Cookies",
|
||||||
|
description:
|
||||||
|
"These cookies help us understand how visitors interact with our website by collecting and reporting information anonymously. They help us improve our website performance.",
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "marketing",
|
||||||
|
label: "Marketing Cookies",
|
||||||
|
description:
|
||||||
|
"These cookies are used to track visitors across websites and display personalized advertisements. They may be set by third-party advertising networks.",
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const checkboxes = [];
|
||||||
|
categories.forEach(function (cat) {
|
||||||
|
const categoryDiv = document.createElement("div");
|
||||||
|
categoryDiv.className = "mgc-category";
|
||||||
|
categoryDiv.style.marginBottom = "15px";
|
||||||
|
categoryDiv.style.padding = "10px";
|
||||||
|
categoryDiv.style.border = "1px solid #ddd";
|
||||||
|
categoryDiv.style.borderRadius = "4px";
|
||||||
|
|
||||||
|
const label = document.createElement("label");
|
||||||
|
label.style.display = "flex";
|
||||||
|
label.style.alignItems = "flex-start";
|
||||||
|
label.style.cursor = cat.required ? "not-allowed" : "pointer";
|
||||||
|
|
||||||
|
const checkbox = document.createElement("input");
|
||||||
|
checkbox.type = "checkbox";
|
||||||
|
checkbox.id = "mgc-cat-" + cat.id;
|
||||||
|
checkbox.value = cat.id;
|
||||||
|
checkbox.checked = currentCategories.includes(cat.id);
|
||||||
|
checkbox.disabled = cat.required;
|
||||||
|
checkbox.style.marginRight = "10px";
|
||||||
|
checkbox.style.marginTop = "3px";
|
||||||
|
|
||||||
|
if (!cat.required) {
|
||||||
|
checkboxes.push(checkbox);
|
||||||
|
}
|
||||||
|
|
||||||
|
const textDiv = document.createElement("div");
|
||||||
|
|
||||||
|
const labelText = document.createElement("strong");
|
||||||
|
labelText.textContent = cat.label;
|
||||||
|
if (cat.required) {
|
||||||
|
labelText.textContent += " (Always Active)";
|
||||||
|
}
|
||||||
|
|
||||||
|
const descText = document.createElement("div");
|
||||||
|
descText.textContent = cat.description;
|
||||||
|
descText.style.fontSize = "13px";
|
||||||
|
descText.style.color = "#666";
|
||||||
|
descText.style.marginTop = "3px";
|
||||||
|
|
||||||
|
textDiv.appendChild(labelText);
|
||||||
|
textDiv.appendChild(descText);
|
||||||
|
|
||||||
|
label.appendChild(checkbox);
|
||||||
|
label.appendChild(textDiv);
|
||||||
|
categoryDiv.appendChild(label);
|
||||||
|
modalContent.appendChild(categoryDiv);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Buttons
|
||||||
|
const buttonRow = document.createElement("div");
|
||||||
|
buttonRow.className = "mgc-notice-buttons";
|
||||||
|
buttonRow.style.marginTop = "20px";
|
||||||
|
|
||||||
|
// Save Preferences button
|
||||||
|
const saveBtn = document.createElement("button");
|
||||||
|
saveBtn.className =
|
||||||
|
"mgc-button mgc-button-accept mgc-button-color-" +
|
||||||
|
(window.mgcSettings?.buttonColor || "blue");
|
||||||
|
saveBtn.textContent = "Save Preferences";
|
||||||
|
saveBtn.onclick = function () {
|
||||||
|
const selectedCategories = ["functional"];
|
||||||
|
checkboxes.forEach(function (cb) {
|
||||||
|
if (cb.checked) {
|
||||||
|
selectedCategories.push(cb.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
handleConsent("accept", selectedCategories);
|
||||||
|
modal.remove();
|
||||||
|
};
|
||||||
|
buttonRow.appendChild(saveBtn);
|
||||||
|
|
||||||
|
// Accept All button
|
||||||
|
const acceptAll = document.createElement("button");
|
||||||
|
acceptAll.className =
|
||||||
|
"mgc-button mgc-button-accept mgc-button-color-" +
|
||||||
|
(window.mgcSettings?.buttonColor || "blue");
|
||||||
|
acceptAll.textContent = "Accept All";
|
||||||
|
acceptAll.onclick = function () {
|
||||||
|
handleConsent("accept", ["functional", "analytics", "marketing"]);
|
||||||
|
modal.remove();
|
||||||
|
};
|
||||||
|
buttonRow.appendChild(acceptAll);
|
||||||
|
|
||||||
|
// Reject Optional button
|
||||||
|
const rejectOptional = document.createElement("button");
|
||||||
|
rejectOptional.className =
|
||||||
|
"mgc-button mgc-button-settings mgc-button-color-" +
|
||||||
|
(window.mgcSettings?.buttonColor || "blue");
|
||||||
|
rejectOptional.textContent = "Reject Optional";
|
||||||
|
rejectOptional.onclick = function () {
|
||||||
|
handleConsent("accept", ["functional"]);
|
||||||
|
modal.remove();
|
||||||
|
};
|
||||||
|
buttonRow.appendChild(rejectOptional);
|
||||||
|
|
||||||
|
modalContent.appendChild(buttonRow);
|
||||||
|
modal.appendChild(modalContent);
|
||||||
|
document.body.appendChild(modal);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleConsent(type, categories) {
|
||||||
|
// Set cookies
|
||||||
|
const expiry = window.mgcSettings?.cookieExpiry || 365;
|
||||||
|
setCookie("mgc_consent", type, expiry);
|
||||||
|
setCookie("mgc_consent_categories", JSON.stringify(categories), expiry);
|
||||||
|
|
||||||
|
// Enable scripts for allowed categories
|
||||||
|
enableScripts(categories);
|
||||||
|
|
||||||
|
// Send to server
|
||||||
|
if (window.mgcSettings?.ajaxUrl) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("action", "mgc_save_consent");
|
||||||
|
formData.append("nonce", window.mgcSettings.nonce);
|
||||||
|
formData.append("consent_type", type);
|
||||||
|
|
||||||
|
categories.forEach(function (cat) {
|
||||||
|
formData.append("categories[]", cat);
|
||||||
|
});
|
||||||
|
|
||||||
|
fetch(window.mgcSettings.ajaxUrl, {
|
||||||
|
method: "POST",
|
||||||
|
body: formData,
|
||||||
|
}).catch(function () {
|
||||||
|
// Silently fail
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove notice
|
||||||
|
const notice = document.querySelector(".mgc-notice");
|
||||||
|
if (notice) {
|
||||||
|
notice.classList.add("mgc-hidden");
|
||||||
|
setTimeout(function () {
|
||||||
|
notice.remove();
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add persistent preference link
|
||||||
|
addPersistentPreferenceLink();
|
||||||
|
|
||||||
|
// Trigger event
|
||||||
|
document.dispatchEvent(
|
||||||
|
new CustomEvent("mgc_consent_saved", {
|
||||||
|
detail: { type: type, categories: categories },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Reload page to apply changes
|
||||||
|
if (type === "accept") {
|
||||||
|
setTimeout(function () {
|
||||||
|
window.location.reload();
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCookie(name, value, days) {
|
||||||
|
const d = new Date();
|
||||||
|
d.setTime(d.getTime() + days * 24 * 60 * 60 * 1000);
|
||||||
|
const expires = "expires=" + d.toUTCString();
|
||||||
|
const secure = window.location.protocol === "https:" ? ";Secure" : "";
|
||||||
|
document.cookie =
|
||||||
|
name +
|
||||||
|
"=" +
|
||||||
|
encodeURIComponent(value) +
|
||||||
|
";" +
|
||||||
|
expires +
|
||||||
|
";path=/;SameSite=Lax" +
|
||||||
|
secure;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCookie(name) {
|
||||||
|
const nameEQ = name + "=";
|
||||||
|
const ca = document.cookie.split(";");
|
||||||
|
for (let i = 0; i < ca.length; i++) {
|
||||||
|
let c = ca[i];
|
||||||
|
while (c.charAt(0) === " ") c = c.substring(1, c.length);
|
||||||
|
if (c.indexOf(nameEQ) === 0)
|
||||||
|
return decodeURIComponent(c.substring(nameEQ.length, c.length));
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})();
|
||||||
288
native/wordpress/maple-gdpr-cookies/public/js/frontend.js
Normal file
288
native/wordpress/maple-gdpr-cookies/public/js/frontend.js
Normal file
|
|
@ -0,0 +1,288 @@
|
||||||
|
/* Maple GDPR Cookies - Frontend JavaScript */
|
||||||
|
(function() {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Check if consent already given
|
||||||
|
if (getCookie('mgc_consent')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and show cookie notice
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
createNotice();
|
||||||
|
});
|
||||||
|
|
||||||
|
function createNotice() {
|
||||||
|
const settings = window.mgcSettings || {};
|
||||||
|
|
||||||
|
// Create notice container
|
||||||
|
const notice = document.createElement('div');
|
||||||
|
notice.className = 'mgc-notice mgc-position-' + (settings.position || 'bottom') +
|
||||||
|
' mgc-theme-' + (settings.theme || 'light') +
|
||||||
|
' mgc-animation-' + (settings.animation || 'slide');
|
||||||
|
|
||||||
|
// Create content
|
||||||
|
const content = document.createElement('div');
|
||||||
|
content.className = 'mgc-notice-content';
|
||||||
|
// Decode HTML entities and strip slashes
|
||||||
|
const noticeText = (settings.noticeText || 'We use cookies to enhance your browsing experience.')
|
||||||
|
.replace(/\\"/g, '"')
|
||||||
|
.replace(/\\\\/g, '\\')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/"Accept All"/g, '<strong>Accept All</strong>')
|
||||||
|
.replace(/"Reject All"/g, '<strong>Reject All</strong>');
|
||||||
|
content.innerHTML = noticeText;
|
||||||
|
|
||||||
|
// Add privacy policy link if provided
|
||||||
|
if (settings.privacyPolicyUrl) {
|
||||||
|
content.innerHTML += ' <a href="' + settings.privacyPolicyUrl + '" class="mgc-privacy-link" target="_blank">' +
|
||||||
|
(settings.privacyPolicyText || 'Privacy Policy') + '</a>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create buttons container
|
||||||
|
const buttons = document.createElement('div');
|
||||||
|
buttons.className = 'mgc-notice-buttons';
|
||||||
|
|
||||||
|
// Accept button
|
||||||
|
const acceptBtn = document.createElement('button');
|
||||||
|
acceptBtn.className = 'mgc-button mgc-button-accept mgc-button-color-' + (settings.buttonColor || 'blue');
|
||||||
|
acceptBtn.textContent = settings.acceptButtonText || 'Accept All';
|
||||||
|
acceptBtn.onclick = function() {
|
||||||
|
handleConsent('accept', ['analytics', 'marketing', 'functional']);
|
||||||
|
};
|
||||||
|
buttons.appendChild(acceptBtn);
|
||||||
|
|
||||||
|
// Reject button (optional)
|
||||||
|
if (settings.showRejectButton) {
|
||||||
|
const rejectBtn = document.createElement('button');
|
||||||
|
rejectBtn.className = 'mgc-button mgc-button-reject mgc-button-color-' + (settings.buttonColor || 'blue');
|
||||||
|
rejectBtn.textContent = settings.rejectButtonText || 'Reject All';
|
||||||
|
rejectBtn.onclick = function() {
|
||||||
|
handleConsent('reject', []);
|
||||||
|
};
|
||||||
|
buttons.appendChild(rejectBtn);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Settings button (optional)
|
||||||
|
if (settings.showSettingsButton) {
|
||||||
|
const settingsBtn = document.createElement('button');
|
||||||
|
settingsBtn.className = 'mgc-button mgc-button-settings mgc-button-color-' + (settings.buttonColor || 'blue');
|
||||||
|
settingsBtn.textContent = settings.settingsButtonText || 'Cookie Settings';
|
||||||
|
settingsBtn.onclick = function() {
|
||||||
|
showSettingsModal();
|
||||||
|
};
|
||||||
|
buttons.appendChild(settingsBtn);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append elements
|
||||||
|
notice.appendChild(content);
|
||||||
|
notice.appendChild(buttons);
|
||||||
|
document.body.appendChild(notice);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSettingsModal() {
|
||||||
|
// Remove initial notice
|
||||||
|
const initialNotice = document.querySelector('.mgc-notice');
|
||||||
|
if (initialNotice) {
|
||||||
|
initialNotice.classList.add('mgc-hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create settings modal
|
||||||
|
const modal = document.createElement('div');
|
||||||
|
modal.className = 'mgc-settings-modal';
|
||||||
|
|
||||||
|
const modalContent = document.createElement('div');
|
||||||
|
modalContent.className = 'mgc-settings-content';
|
||||||
|
|
||||||
|
// Modal title
|
||||||
|
const title = document.createElement('h3');
|
||||||
|
title.textContent = 'Cookie Settings';
|
||||||
|
title.style.marginTop = '0';
|
||||||
|
modalContent.appendChild(title);
|
||||||
|
|
||||||
|
// Description
|
||||||
|
const desc = document.createElement('p');
|
||||||
|
desc.textContent = 'Choose which types of cookies you want to allow:';
|
||||||
|
modalContent.appendChild(desc);
|
||||||
|
|
||||||
|
// Cookie categories
|
||||||
|
const categories = [
|
||||||
|
{
|
||||||
|
id: 'functional',
|
||||||
|
label: 'Functional Cookies',
|
||||||
|
description: 'These cookies are essential for the website to function properly.',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'analytics',
|
||||||
|
label: 'Analytics Cookies',
|
||||||
|
description: 'Help us understand how visitors interact with our website.',
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'marketing',
|
||||||
|
label: 'Marketing Cookies',
|
||||||
|
description: 'Used to track visitors across websites for marketing purposes.',
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const checkboxes = [];
|
||||||
|
categories.forEach(function(cat) {
|
||||||
|
const categoryDiv = document.createElement('div');
|
||||||
|
categoryDiv.className = 'mgc-category';
|
||||||
|
categoryDiv.style.marginBottom = '15px';
|
||||||
|
categoryDiv.style.padding = '10px';
|
||||||
|
categoryDiv.style.border = '1px solid #ddd';
|
||||||
|
categoryDiv.style.borderRadius = '4px';
|
||||||
|
|
||||||
|
const label = document.createElement('label');
|
||||||
|
label.style.display = 'flex';
|
||||||
|
label.style.alignItems = 'flex-start';
|
||||||
|
label.style.cursor = cat.required ? 'not-allowed' : 'pointer';
|
||||||
|
|
||||||
|
const checkbox = document.createElement('input');
|
||||||
|
checkbox.type = 'checkbox';
|
||||||
|
checkbox.id = 'mgc-cat-' + cat.id;
|
||||||
|
checkbox.value = cat.id;
|
||||||
|
checkbox.checked = true;
|
||||||
|
checkbox.disabled = cat.required;
|
||||||
|
checkbox.style.marginRight = '10px';
|
||||||
|
checkbox.style.marginTop = '3px';
|
||||||
|
|
||||||
|
if (!cat.required) {
|
||||||
|
checkboxes.push(checkbox);
|
||||||
|
}
|
||||||
|
|
||||||
|
const textDiv = document.createElement('div');
|
||||||
|
|
||||||
|
const labelText = document.createElement('strong');
|
||||||
|
labelText.textContent = cat.label;
|
||||||
|
if (cat.required) {
|
||||||
|
labelText.textContent += ' (Required)';
|
||||||
|
}
|
||||||
|
|
||||||
|
const descText = document.createElement('div');
|
||||||
|
descText.textContent = cat.description;
|
||||||
|
descText.style.fontSize = '13px';
|
||||||
|
descText.style.color = '#666';
|
||||||
|
descText.style.marginTop = '3px';
|
||||||
|
|
||||||
|
textDiv.appendChild(labelText);
|
||||||
|
textDiv.appendChild(descText);
|
||||||
|
|
||||||
|
label.appendChild(checkbox);
|
||||||
|
label.appendChild(textDiv);
|
||||||
|
categoryDiv.appendChild(label);
|
||||||
|
modalContent.appendChild(categoryDiv);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Buttons
|
||||||
|
const buttonRow = document.createElement('div');
|
||||||
|
buttonRow.className = 'mgc-notice-buttons';
|
||||||
|
buttonRow.style.marginTop = '20px';
|
||||||
|
|
||||||
|
// Accept Selected button
|
||||||
|
const acceptSelected = document.createElement('button');
|
||||||
|
acceptSelected.className = 'mgc-button mgc-button-accept mgc-button-color-' + (window.mgcSettings?.buttonColor || 'blue');
|
||||||
|
acceptSelected.textContent = 'Save Preferences';
|
||||||
|
acceptSelected.onclick = function() {
|
||||||
|
const selectedCategories = ['functional']; // Always include functional
|
||||||
|
checkboxes.forEach(function(cb) {
|
||||||
|
if (cb.checked) {
|
||||||
|
selectedCategories.push(cb.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
handleConsent('accept', selectedCategories);
|
||||||
|
modal.remove();
|
||||||
|
};
|
||||||
|
buttonRow.appendChild(acceptSelected);
|
||||||
|
|
||||||
|
// Accept All button
|
||||||
|
const acceptAll = document.createElement('button');
|
||||||
|
acceptAll.className = 'mgc-button mgc-button-accept mgc-button-color-' + (window.mgcSettings?.buttonColor || 'blue');
|
||||||
|
acceptAll.textContent = 'Accept All';
|
||||||
|
acceptAll.onclick = function() {
|
||||||
|
handleConsent('accept', ['functional', 'analytics', 'marketing']);
|
||||||
|
modal.remove();
|
||||||
|
};
|
||||||
|
buttonRow.appendChild(acceptAll);
|
||||||
|
|
||||||
|
// Reject All button
|
||||||
|
const rejectAll = document.createElement('button');
|
||||||
|
rejectAll.className = 'mgc-button mgc-button-settings mgc-button-color-' + (window.mgcSettings?.buttonColor || 'blue');
|
||||||
|
rejectAll.textContent = 'Reject Optional';
|
||||||
|
rejectAll.onclick = function() {
|
||||||
|
handleConsent('accept', ['functional']);
|
||||||
|
modal.remove();
|
||||||
|
};
|
||||||
|
buttonRow.appendChild(rejectAll);
|
||||||
|
|
||||||
|
modalContent.appendChild(buttonRow);
|
||||||
|
modal.appendChild(modalContent);
|
||||||
|
document.body.appendChild(modal);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleConsent(type, categories) {
|
||||||
|
// Set cookie
|
||||||
|
const expiry = window.mgcSettings?.cookieExpiry || 365;
|
||||||
|
setCookie('mgc_consent', type, expiry);
|
||||||
|
setCookie('mgc_consent_categories', JSON.stringify(categories), expiry);
|
||||||
|
|
||||||
|
// Send to server (fire and forget)
|
||||||
|
if (window.mgcSettings?.ajaxUrl) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('action', 'mgc_save_consent');
|
||||||
|
formData.append('nonce', window.mgcSettings.nonce);
|
||||||
|
formData.append('consent_type', type);
|
||||||
|
|
||||||
|
categories.forEach(function(cat) {
|
||||||
|
formData.append('categories[]', cat);
|
||||||
|
});
|
||||||
|
|
||||||
|
fetch(window.mgcSettings.ajaxUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
}).catch(function() {
|
||||||
|
// Silently fail - consent is saved in cookie anyway
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove notice
|
||||||
|
const notice = document.querySelector('.mgc-notice');
|
||||||
|
if (notice) {
|
||||||
|
notice.classList.add('mgc-hidden');
|
||||||
|
setTimeout(function() {
|
||||||
|
notice.remove();
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger custom event
|
||||||
|
const event = new CustomEvent('mgc_consent_saved', {
|
||||||
|
detail: {
|
||||||
|
type: type,
|
||||||
|
categories: categories
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.dispatchEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCookie(name, value, days) {
|
||||||
|
const d = new Date();
|
||||||
|
d.setTime(d.getTime() + (days * 24 * 60 * 60 * 1000));
|
||||||
|
const expires = 'expires=' + d.toUTCString();
|
||||||
|
document.cookie = name + '=' + value + ';' + expires + ';path=/;SameSite=Lax';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCookie(name) {
|
||||||
|
const nameEQ = name + '=';
|
||||||
|
const ca = document.cookie.split(';');
|
||||||
|
for (let i = 0; i < ca.length; i++) {
|
||||||
|
let c = ca[i];
|
||||||
|
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
|
||||||
|
if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length, c.length);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
})();
|
||||||
12
native/wordpress/maple-gdpr-cookies/public/js/index.php
Normal file
12
native/wordpress/maple-gdpr-cookies/public/js/index.php
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Silence is golden.
|
||||||
|
*
|
||||||
|
* Prevent direct access to this file.
|
||||||
|
*
|
||||||
|
* @package WordPress
|
||||||
|
*/
|
||||||
|
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit; // Exit if accessed directly.
|
||||||
|
}
|
||||||
117
native/wordpress/ticket-tailor-wp-max/LICENSE
Normal file
117
native/wordpress/ticket-tailor-wp-max/LICENSE
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 2, June 1991
|
||||||
|
|
||||||
|
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
|
||||||
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
|
||||||
|
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it.
|
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights.
|
||||||
|
|
||||||
|
We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software.
|
||||||
|
|
||||||
|
Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations.
|
||||||
|
|
||||||
|
Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||||
|
|
||||||
|
0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you".
|
||||||
|
|
||||||
|
Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does.
|
||||||
|
|
||||||
|
1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee.
|
||||||
|
|
||||||
|
2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change.
|
||||||
|
|
||||||
|
b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License.
|
||||||
|
|
||||||
|
c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.)
|
||||||
|
|
||||||
|
These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it.
|
||||||
|
|
||||||
|
Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program.
|
||||||
|
|
||||||
|
In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License.
|
||||||
|
|
||||||
|
3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following:
|
||||||
|
|
||||||
|
a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or,
|
||||||
|
|
||||||
|
b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or,
|
||||||
|
|
||||||
|
c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.)
|
||||||
|
|
||||||
|
The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable.
|
||||||
|
|
||||||
|
If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code.
|
||||||
|
|
||||||
|
4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance.
|
||||||
|
|
||||||
|
5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it.
|
||||||
|
|
||||||
|
6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License.
|
||||||
|
|
||||||
|
7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program.
|
||||||
|
|
||||||
|
If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances.
|
||||||
|
|
||||||
|
It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice.
|
||||||
|
|
||||||
|
This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License.
|
||||||
|
|
||||||
|
8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License.
|
||||||
|
|
||||||
|
9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation.
|
||||||
|
|
||||||
|
10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally.
|
||||||
|
|
||||||
|
NO WARRANTY
|
||||||
|
|
||||||
|
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
one line to give the program's name and an idea of what it does. Copyright (C) yyyy name of author
|
||||||
|
|
||||||
|
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If the program is interactive, make it output a short notice like this when it starts in an interactive mode:
|
||||||
|
|
||||||
|
Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
|
The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program.
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names:
|
||||||
|
|
||||||
|
Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||||
|
|
||||||
|
signature of Ty Coon, 1 April 1989 Ty Coon, President of Vice
|
||||||
2
native/wordpress/ticket-tailor-wp-max/README.md
Normal file
2
native/wordpress/ticket-tailor-wp-max/README.md
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
# ticket-tailor-wp-max
|
||||||
|
|
||||||
850
native/wordpress/ticket-tailor-wp-max/assets/css/admin.css
Normal file
850
native/wordpress/ticket-tailor-wp-max/assets/css/admin.css
Normal file
|
|
@ -0,0 +1,850 @@
|
||||||
|
/**
|
||||||
|
* Ticket Tailor - Admin Styles
|
||||||
|
* Version: 3.1 - Enhanced with Canadian/UK English
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* General Admin Styles */
|
||||||
|
.ticket-tailor-admin h1 {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticket-tailor-admin .description {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #646970;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dashboard Stats Grid - FIXED VERSION */
|
||||||
|
.tt-stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-stat-card {
|
||||||
|
padding: 1.5rem;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #c3c4c7;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-stat-card:hover {
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fixed: Use tt-stat-value for the number */
|
||||||
|
.tt-stat-value {
|
||||||
|
display: block;
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
color: #2271b1;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-stat-label {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #50575e;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Quick Actions Section - FIXED FOR BUTTON DISPLAY */
|
||||||
|
.tt-quick-actions {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #c3c4c7;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin: 2rem 0;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
|
||||||
|
overflow: visible; /* Ensure buttons aren't cut off */
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-quick-actions h2 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
color: #1d2327;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-quick-actions .button-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem; /* Increased gap for better spacing */
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 42px; /* Ensure minimum height for buttons */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ENHANCED BUTTON STYLING FOR ADMIN AREA */
|
||||||
|
.ticket-tailor-admin .button,
|
||||||
|
.tt-quick-actions .button,
|
||||||
|
.ticket-tailor-admin input[type="submit"],
|
||||||
|
.ticket-tailor-admin .page-title-action {
|
||||||
|
border-radius: 6px !important;
|
||||||
|
box-shadow:
|
||||||
|
0 2px 4px rgba(0, 0, 0, 0.08),
|
||||||
|
0 1px 2px rgba(0, 0, 0, 0.06) !important;
|
||||||
|
font-size: 14px !important;
|
||||||
|
padding: 8px 16px !important;
|
||||||
|
font-weight: 500 !important;
|
||||||
|
transition: all 0.2s ease !important;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.1) !important;
|
||||||
|
text-transform: none !important;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
line-height: 1.5 !important;
|
||||||
|
display: inline-block !important;
|
||||||
|
flex: 0 0 auto; /* Prevent flex shrinking */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Primary buttons get special treatment */
|
||||||
|
.ticket-tailor-admin .button-primary,
|
||||||
|
.tt-quick-actions .button-primary {
|
||||||
|
background: #2271b1 !important;
|
||||||
|
border-color: #1e5f9a !important;
|
||||||
|
color: #fff !important;
|
||||||
|
box-shadow:
|
||||||
|
0 2px 4px rgba(34, 113, 177, 0.2),
|
||||||
|
0 1px 2px rgba(34, 113, 177, 0.15) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticket-tailor-admin .button-primary:hover,
|
||||||
|
.tt-quick-actions .button-primary:hover {
|
||||||
|
background: #135e96 !important;
|
||||||
|
border-color: #0a4b7c !important;
|
||||||
|
box-shadow:
|
||||||
|
0 4px 8px rgba(34, 113, 177, 0.25),
|
||||||
|
0 2px 4px rgba(34, 113, 177, 0.2) !important;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Secondary buttons */
|
||||||
|
.ticket-tailor-admin .button:not(.button-primary):hover,
|
||||||
|
.tt-quick-actions .button:not(.button-primary):hover {
|
||||||
|
background: #f6f7f7 !important;
|
||||||
|
border-color: #8c8f94 !important;
|
||||||
|
box-shadow:
|
||||||
|
0 3px 6px rgba(0, 0, 0, 0.12),
|
||||||
|
0 2px 4px rgba(0, 0, 0, 0.08) !important;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Small buttons */
|
||||||
|
.ticket-tailor-admin .button-small {
|
||||||
|
font-size: 13px !important;
|
||||||
|
padding: 6px 12px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Page title actions (like "Sync Events" on the events page) */
|
||||||
|
.ticket-tailor-admin .page-title-action {
|
||||||
|
margin-left: 10px !important;
|
||||||
|
vertical-align: middle !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dashboard Section */
|
||||||
|
.tt-dashboard-section {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #c3c4c7;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin: 2rem 0;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-dashboard-section h2 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
color: #1d2327;
|
||||||
|
border-bottom: 2px solid #f0f0f1;
|
||||||
|
padding-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dashboard Widget */
|
||||||
|
.tt-dashboard-widget {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-dashboard-widget h4 {
|
||||||
|
margin: 1rem 0 0.5rem;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: #1d2327;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-dashboard-widget ul {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-dashboard-widget li {
|
||||||
|
padding: 0.75rem 0;
|
||||||
|
border-top: 1px solid #e1e1e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-dashboard-widget li:first-child {
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-dashboard-widget .description {
|
||||||
|
color: #646970;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-dashboard-links {
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding-top: 1rem;
|
||||||
|
border-top: 1px solid #e1e1e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* TABLE STYLES - FIXED CORNERS */
|
||||||
|
.ticket-tailor-admin .wp-list-table {
|
||||||
|
border: 1px solid #c3c4c7;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
|
||||||
|
overflow: hidden; /* This ensures border-radius clips content */
|
||||||
|
border-collapse: separate; /* Required for border-radius to work */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix for table header background to respect rounded corners */
|
||||||
|
.ticket-tailor-admin .wp-list-table thead {
|
||||||
|
background: #f6f7f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticket-tailor-admin .wp-list-table thead tr:first-child th:first-child {
|
||||||
|
border-top-left-radius: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticket-tailor-admin .wp-list-table thead tr:first-child th:last-child {
|
||||||
|
border-top-right-radius: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix for table body to respect rounded corners */
|
||||||
|
.ticket-tailor-admin .wp-list-table tbody tr:last-child td:first-child {
|
||||||
|
border-bottom-left-radius: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticket-tailor-admin .wp-list-table tbody tr:last-child td:last-child {
|
||||||
|
border-bottom-right-radius: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticket-tailor-admin .wp-list-table th {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
color: #2c3338;
|
||||||
|
border-bottom: 1px solid #c3c4c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticket-tailor-admin .wp-list-table td {
|
||||||
|
padding: 12px 10px;
|
||||||
|
vertical-align: middle;
|
||||||
|
border-bottom: 1px solid #f0f0f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticket-tailor-admin .wp-list-table tbody tr:last-child td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Striped table rows */
|
||||||
|
.ticket-tailor-admin .wp-list-table.striped tbody tr:nth-child(odd) {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table hover effect */
|
||||||
|
.ticket-tailor-admin .wp-list-table tbody tr:hover {
|
||||||
|
background-color: #f0f6fc;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status Badges */
|
||||||
|
.tt-status-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.35rem 0.75rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-status-live,
|
||||||
|
.tt-status-published,
|
||||||
|
.tt-status-on_sale {
|
||||||
|
background: #d1f4e0;
|
||||||
|
color: #0f5132;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-status-draft {
|
||||||
|
background: #fff3cd;
|
||||||
|
color: #856404;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-status-archived,
|
||||||
|
.tt-status-cancelled {
|
||||||
|
background: #e2e3e5;
|
||||||
|
color: #41464b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-status-completed,
|
||||||
|
.tt-status-paid {
|
||||||
|
background: #cfe2ff;
|
||||||
|
color: #084298;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-status-pending {
|
||||||
|
background: #fff3cd;
|
||||||
|
color: #856404;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-status-refunded,
|
||||||
|
.tt-status-sold_out {
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-status-unknown {
|
||||||
|
background: #e7e7e7;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Help Page Styles */
|
||||||
|
.tt-help-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 2rem;
|
||||||
|
margin: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-help-section {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #c3c4c7;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-help-section h2 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
color: #1d2327;
|
||||||
|
border-bottom: 2px solid #2271b1;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-help-section h3 {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: #50575e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-help-section code {
|
||||||
|
display: block;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background: #f6f7f7;
|
||||||
|
border: 1px solid #c3c4c7;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: "Courier New", monospace;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-help-section ul,
|
||||||
|
.tt-help-section ol {
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-help-section li {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ENHANCED SETTINGS PAGE STYLING */
|
||||||
|
|
||||||
|
/* Override WordPress admin background for settings pages */
|
||||||
|
.ticket-tailor_page_ticket-tailor-settings {
|
||||||
|
background: #f0f2f5 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main settings container with white background */
|
||||||
|
.ticket-tailor_page_ticket-tailor-settings .wrap {
|
||||||
|
background: white;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||||
|
margin-top: 20px;
|
||||||
|
max-width: 1200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Settings page title */
|
||||||
|
.ticket-tailor_page_ticket-tailor-settings .wrap h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #1d2327;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
border-bottom: 2px solid #f0f2f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tab wrapper styling */
|
||||||
|
.ticket-tailor-admin .nav-tab-wrapper {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
border-bottom: none;
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 8px;
|
||||||
|
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Individual tab styling - rounded and modern */
|
||||||
|
.ticket-tailor-admin .nav-tab {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #50575e;
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticket-tailor-admin .nav-tab:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
|
color: #2c3338;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Active tab styling */
|
||||||
|
.ticket-tailor-admin .nav-tab-active,
|
||||||
|
.ticket-tailor-admin .nav-tab-active:focus,
|
||||||
|
.ticket-tailor-admin .nav-tab-active:hover {
|
||||||
|
background: white !important;
|
||||||
|
color: #2271b1;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
border: none;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form table improvements */
|
||||||
|
.ticket-tailor-admin .form-table {
|
||||||
|
margin-top: 2rem;
|
||||||
|
background: white;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticket-tailor-admin .form-table tr {
|
||||||
|
border-bottom: 1px solid #f0f2f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticket-tailor-admin .form-table tr:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticket-tailor-admin .form-table th {
|
||||||
|
width: 250px;
|
||||||
|
padding: 20px 20px 20px 0;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1d2327;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticket-tailor-admin .form-table td {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticket-tailor-admin .form-table .description {
|
||||||
|
margin-top: 8px;
|
||||||
|
color: #646970;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form inputs enhancement */
|
||||||
|
.ticket-tailor-admin input[type="text"],
|
||||||
|
.ticket-tailor-admin input[type="number"],
|
||||||
|
.ticket-tailor-admin select {
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #c3c4c7;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
transition: border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticket-tailor-admin input[type="text"]:focus,
|
||||||
|
.ticket-tailor-admin input[type="number"]:focus,
|
||||||
|
.ticket-tailor-admin select:focus {
|
||||||
|
border-color: #2271b1;
|
||||||
|
box-shadow: 0 0 0 3px rgba(34, 113, 177, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Checkbox styling */
|
||||||
|
.ticket-tailor-admin input[type="checkbox"] {
|
||||||
|
border-radius: 3px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Code blocks in settings */
|
||||||
|
.ticket-tailor-admin code {
|
||||||
|
background: #f0f2f5;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: "Monaco", "Courier New", monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Submit buttons section */
|
||||||
|
.ticket-tailor-admin .submit {
|
||||||
|
padding: 20px 0;
|
||||||
|
margin: 20px 0 0;
|
||||||
|
border-top: 2px solid #f0f2f5;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cache management section styling */
|
||||||
|
.ticket-tailor-admin hr {
|
||||||
|
border: none;
|
||||||
|
border-top: 2px solid #f0f2f5;
|
||||||
|
margin: 30px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticket-tailor-admin h2 {
|
||||||
|
font-size: 18px;
|
||||||
|
color: #1d2327;
|
||||||
|
margin: 30px 0 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Notice boxes in settings */
|
||||||
|
.ticket-tailor_page_ticket-tailor-settings .notice {
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 15px 0;
|
||||||
|
padding: 12px 15px;
|
||||||
|
border-left-width: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Settings section cards (for Style tab preview, etc.) */
|
||||||
|
.ticket-tailor-admin .settings-section {
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Live preview section enhancement */
|
||||||
|
.ticket-tailor-admin h3 {
|
||||||
|
font-size: 16px;
|
||||||
|
color: #1d2327;
|
||||||
|
margin: 25px 0 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Colour Picker Enhancements */
|
||||||
|
.wp-picker-container {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-picker-container .wp-color-result {
|
||||||
|
margin-bottom: 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hex value display - styled as inline text label */
|
||||||
|
.tt-hex-display {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: "Monaco", "Courier New", monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
min-width: 70px;
|
||||||
|
text-align: center;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
vertical-align: middle;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide the default "Select Color" button text */
|
||||||
|
.wp-picker-container .wp-color-result .wp-color-result-text {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make the colour square more prominent */
|
||||||
|
.wp-picker-container .wp-color-result {
|
||||||
|
width: 40px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0;
|
||||||
|
border: 2px solid #c3c4c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-picker-container .wp-color-result:hover {
|
||||||
|
border-color: #8c8f94;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-picker-container .wp-color-result:after {
|
||||||
|
display: none; /* Remove the dropdown arrow */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure proper alignment in form table */
|
||||||
|
.ticket-tailor-admin .form-table .wp-picker-container {
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Default/Clear button styling - always visible */
|
||||||
|
.wp-picker-container .wp-picker-clear,
|
||||||
|
.wp-picker-container .wp-picker-default {
|
||||||
|
margin-left: 8px;
|
||||||
|
height: 32px;
|
||||||
|
line-height: 30px;
|
||||||
|
padding: 0 12px !important;
|
||||||
|
font-size: 13px;
|
||||||
|
border-radius: 4px;
|
||||||
|
vertical-align: middle;
|
||||||
|
display: inline-block !important;
|
||||||
|
visibility: visible !important;
|
||||||
|
opacity: 1 !important;
|
||||||
|
background: #f0f0f1;
|
||||||
|
color: #50575e;
|
||||||
|
border: 1px solid #c3c4c7;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wp-picker-container .wp-picker-clear:hover,
|
||||||
|
.wp-picker-container .wp-picker-default:hover {
|
||||||
|
background: #e7e7e7;
|
||||||
|
border-color: #8c8f94;
|
||||||
|
color: #2c3338;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure the clear button is always shown */
|
||||||
|
.wp-picker-container.wp-picker-active .wp-picker-clear,
|
||||||
|
.wp-picker-container:not(.wp-picker-active) .wp-picker-clear {
|
||||||
|
display: inline-block !important;
|
||||||
|
visibility: visible !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Iris colour picker popup positioning */
|
||||||
|
.wp-picker-container .wp-picker-holder {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 100;
|
||||||
|
margin-top: 8px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading States */
|
||||||
|
.tt-loading {
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem;
|
||||||
|
color: #646970;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-loading::before {
|
||||||
|
content: "";
|
||||||
|
display: inline-block;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
border: 2px solid #2271b1;
|
||||||
|
border-radius: 50%;
|
||||||
|
border-top-color: transparent;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Notice Styles */
|
||||||
|
.ticket-tailor-admin .notice {
|
||||||
|
margin: 1rem 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticket-tailor-admin .notice-success {
|
||||||
|
border-left-color: #46b450;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticket-tailor-admin .notice-warning {
|
||||||
|
border-left-color: #ffb900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticket-tailor-admin .notice-error {
|
||||||
|
border-left-color: #dc3232;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticket-tailor-admin .notice-info {
|
||||||
|
border-left-color: #2271b1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Filter/Action Bar Above Tables */
|
||||||
|
.ticket-tailor-admin .tablenav {
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticket-tailor-admin .tablenav select {
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 6px 24px 6px 8px;
|
||||||
|
border: 1px solid #8c8f94;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Styles */
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.tt-stats-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 782px) {
|
||||||
|
.tt-stats-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-help-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-quick-actions .button-group {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch; /* Make buttons full width on mobile */
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-quick-actions .button {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-quick-actions .button:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Settings page mobile adjustments */
|
||||||
|
.ticket-tailor_page_ticket-tailor-settings .wrap {
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticket-tailor-admin .nav-tab-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 6px;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticket-tailor-admin .nav-tab {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
margin: 0;
|
||||||
|
padding: 12px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticket-tailor-admin .form-table th,
|
||||||
|
.ticket-tailor-admin .form-table td {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticket-tailor-admin .form-table th {
|
||||||
|
font-weight: 600;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.tt-stat-value {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-dashboard-section {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticket-tailor-admin .wp-list-table {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticket-tailor-admin .wp-list-table td {
|
||||||
|
padding: 8px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure buttons remain readable on very small screens */
|
||||||
|
.ticket-tailor-admin .button {
|
||||||
|
font-size: 13px !important;
|
||||||
|
padding: 7px 14px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Accessibility Improvements */
|
||||||
|
.ticket-tailor-admin a:focus,
|
||||||
|
.ticket-tailor-admin button:focus,
|
||||||
|
.ticket-tailor-admin input:focus,
|
||||||
|
.ticket-tailor-admin select:focus,
|
||||||
|
.ticket-tailor-admin textarea:focus {
|
||||||
|
outline: 2px solid #2271b1;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticket-tailor-admin .button:focus {
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 2px #fff,
|
||||||
|
0 0 0 4px #2271b1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screen-reader-text {
|
||||||
|
position: absolute !important;
|
||||||
|
clip: rect(1px, 1px, 1px, 1px);
|
||||||
|
padding: 0 !important;
|
||||||
|
border: 0 !important;
|
||||||
|
height: 1px !important;
|
||||||
|
width: 1px !important;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Print Styles */
|
||||||
|
@media print {
|
||||||
|
.ticket-tailor-admin .button,
|
||||||
|
.ticket-tailor-admin .nav-tab-wrapper,
|
||||||
|
.tt-quick-actions {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-stat-card {
|
||||||
|
break-inside: avoid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix for WordPress admin bar overlap */
|
||||||
|
.ticket-tailor-admin {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure proper spacing in admin area */
|
||||||
|
.wrap.ticket-tailor-admin {
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
605
native/wordpress/ticket-tailor-wp-max/assets/css/frontend.css
Normal file
605
native/wordpress/ticket-tailor-wp-max/assets/css/frontend.css
Normal file
|
|
@ -0,0 +1,605 @@
|
||||||
|
/**
|
||||||
|
* Ticket Tailor - Frontend Styles
|
||||||
|
* Version: 4.0.0 - White background for image areas
|
||||||
|
* This version fixes stretched/cut-off logos while maintaining layout
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
CRITICAL OVERRIDES - FORCE 220px CARDS
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
/* Override ALL grid layouts with inline styles */
|
||||||
|
.tt-event-listing.tt-layout-grid,
|
||||||
|
.tt-event-listing.tt-layout-grid.tt-columns-3,
|
||||||
|
.tt-event-listing.tt-layout-grid[style*="grid-template-columns"],
|
||||||
|
.tt-event-listing.tt-columns-responsive {
|
||||||
|
display: grid !important;
|
||||||
|
grid-template-columns: repeat(auto-fit, 220px) !important;
|
||||||
|
gap: 3rem !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
align-items: stretch !important;
|
||||||
|
grid-auto-rows: 1fr !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Force specific column counts */
|
||||||
|
.tt-event-listing.tt-layout-grid.tt-columns-4 {
|
||||||
|
grid-template-columns: repeat(4, 220px) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-event-listing.tt-layout-grid.tt-columns-3 {
|
||||||
|
grid-template-columns: repeat(3, 220px) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-event-listing.tt-layout-grid.tt-columns-2 {
|
||||||
|
grid-template-columns: repeat(2, 220px) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Force card width */
|
||||||
|
.tt-event-card {
|
||||||
|
width: 220px !important;
|
||||||
|
max-width: 220px !important;
|
||||||
|
min-width: 220px !important;
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column !important;
|
||||||
|
background: #ffffff !important;
|
||||||
|
border: 1px solid #e1e1e1 !important;
|
||||||
|
border-radius: 12px !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
transition: all 0.3s ease !important;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08) !important;
|
||||||
|
height: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-event-card:hover {
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15) !important;
|
||||||
|
transform: translateY(-2px) !important;
|
||||||
|
border-color: #d1d1d1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Force image container width - with white background */
|
||||||
|
.tt-event-image {
|
||||||
|
width: 220px !important;
|
||||||
|
max-width: 220px !important;
|
||||||
|
height: 147px !important;
|
||||||
|
position: relative !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
background: #ffffff !important; /* White background for empty space */
|
||||||
|
flex: 0 0 auto !important;
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* FIXED: Use contain for thumbnails to prevent cropping */
|
||||||
|
.tt-event-image.tt-image-thumbnail img {
|
||||||
|
width: auto !important;
|
||||||
|
height: auto !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
max-height: 100% !important;
|
||||||
|
object-fit: contain !important; /* Changed from cover to contain */
|
||||||
|
object-position: center !important;
|
||||||
|
display: block !important;
|
||||||
|
transition: transform 0.3s ease !important;
|
||||||
|
padding: 10px !important; /* Add breathing room for logos */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Keep cover for header/banner images */
|
||||||
|
.tt-event-image.tt-image-header img {
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
object-fit: cover !important; /* Keep cover for banner images */
|
||||||
|
object-position: center !important;
|
||||||
|
display: block !important;
|
||||||
|
transition: transform 0.3s ease !important;
|
||||||
|
padding: 0 !important; /* No padding for banner images */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Default for images without specific class */
|
||||||
|
.tt-event-image img {
|
||||||
|
width: auto !important;
|
||||||
|
height: auto !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
max-height: 100% !important;
|
||||||
|
object-fit: contain !important; /* Default to contain */
|
||||||
|
object-position: center !important;
|
||||||
|
display: block !important;
|
||||||
|
transition: transform 0.3s ease !important;
|
||||||
|
padding: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-event-card:hover .tt-event-image img {
|
||||||
|
transform: scale(1.05) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Force content width */
|
||||||
|
.tt-event-content {
|
||||||
|
width: 220px !important;
|
||||||
|
max-width: 220px !important;
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column !important;
|
||||||
|
padding: 0.875rem !important;
|
||||||
|
flex: 1 !important;
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
EVENT LISTING STYLES
|
||||||
|
============================================ */
|
||||||
|
.tt-event-listing {
|
||||||
|
margin: 2rem auto;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Event Listing Wrapper */
|
||||||
|
.tt-event-listing-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Full Width Wrapper */
|
||||||
|
.tt-full-width {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-full-width .tt-event-listing {
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* List Layout */
|
||||||
|
.tt-event-listing.tt-layout-list {
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column !important;
|
||||||
|
gap: 1.5rem !important;
|
||||||
|
align-items: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-event-listing.tt-layout-list .tt-event-card {
|
||||||
|
display: grid !important;
|
||||||
|
grid-template-columns: 220px 1fr !important;
|
||||||
|
gap: 1.5rem !important;
|
||||||
|
max-width: 900px !important;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* List layout images should also use contain */
|
||||||
|
.tt-event-listing.tt-layout-list .tt-event-image img {
|
||||||
|
object-fit: contain !important;
|
||||||
|
padding: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
EVENT CONTENT ELEMENTS
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
/* Event Title */
|
||||||
|
.tt-event-title {
|
||||||
|
margin: 0 0 0.625rem 0 !important;
|
||||||
|
font-size: 1rem !important;
|
||||||
|
line-height: 1.3 !important;
|
||||||
|
color: #2c3e50 !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
display: -webkit-box !important;
|
||||||
|
-webkit-line-clamp: 2 !important;
|
||||||
|
-webkit-box-orient: vertical !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Date and Venue */
|
||||||
|
.tt-event-date,
|
||||||
|
.tt-event-venue {
|
||||||
|
display: flex !important;
|
||||||
|
align-items: flex-start !important;
|
||||||
|
margin-bottom: 0.4rem !important;
|
||||||
|
font-size: 0.85rem !important;
|
||||||
|
color: #666 !important;
|
||||||
|
line-height: 1.3 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-event-date .dashicons,
|
||||||
|
.tt-event-venue .dashicons {
|
||||||
|
margin-right: 0.375rem !important;
|
||||||
|
color: #2271b1 !important;
|
||||||
|
flex-shrink: 0 !important;
|
||||||
|
width: 14px !important;
|
||||||
|
height: 14px !important;
|
||||||
|
font-size: 14px !important;
|
||||||
|
margin-top: 1px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Spacer to push button down */
|
||||||
|
.tt-event-content::before {
|
||||||
|
content: "" !important;
|
||||||
|
flex: 1 !important;
|
||||||
|
min-height: 0.25rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide PHP spacer */
|
||||||
|
.tt-event-spacer {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Button Container */
|
||||||
|
.tt-event-button-container {
|
||||||
|
margin-top: auto !important;
|
||||||
|
padding-top: 0.625rem !important;
|
||||||
|
text-align: center !important; /* Center the button */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Event Button */
|
||||||
|
.tt-event-button,
|
||||||
|
a.tt-event-button {
|
||||||
|
display: inline-block !important;
|
||||||
|
width: auto !important; /* Auto width instead of 100% */
|
||||||
|
padding: 0.75rem 1.5rem !important; /* Padding determines size */
|
||||||
|
background: #2271b1 !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
text-decoration: none !important;
|
||||||
|
text-align: center !important;
|
||||||
|
border-radius: 6px !important;
|
||||||
|
font-weight: 700 !important; /* Bold text */
|
||||||
|
font-size: 1rem !important; /* Larger text */
|
||||||
|
transition: all 0.3s ease !important;
|
||||||
|
border: none !important;
|
||||||
|
cursor: pointer !important;
|
||||||
|
white-space: nowrap !important; /* Prevent text wrapping */
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-event-button:hover,
|
||||||
|
a.tt-event-button:hover {
|
||||||
|
background: #135e96 !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
transform: translateY(-1px) !important;
|
||||||
|
box-shadow: 0 2px 8px rgba(34, 113, 177, 0.3) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Event excerpt (list view only) */
|
||||||
|
.tt-event-excerpt {
|
||||||
|
font-size: 0.875rem !important;
|
||||||
|
color: #666 !important;
|
||||||
|
line-height: 1.4 !important;
|
||||||
|
margin-bottom: 0.75rem !important;
|
||||||
|
display: -webkit-box !important;
|
||||||
|
-webkit-line-clamp: 2 !important;
|
||||||
|
-webkit-box-orient: vertical !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
SINGLE EVENT STYLES
|
||||||
|
============================================ */
|
||||||
|
.tt-single-event {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e1e1e1;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 2rem 0;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-single-event-wrapper {
|
||||||
|
margin: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-event-header-image {
|
||||||
|
width: 100%;
|
||||||
|
max-height: 400px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #ffffff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Single event header images can use cover */
|
||||||
|
.tt-event-header-image img {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
object-fit: cover; /* Keep cover for single event headers */
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-event-details {
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-event-details .tt-event-title {
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-event-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-meta-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.75rem;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-meta-item strong {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-event-description {
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #444;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-event-description p {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-event-cta {
|
||||||
|
margin-top: 2rem;
|
||||||
|
padding-top: 2rem;
|
||||||
|
border-top: 1px solid #e1e1e1;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-button {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
background: #971320;
|
||||||
|
color: #fff;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-button:hover {
|
||||||
|
background: #dd1c3d;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(34, 113, 177, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
WORDPRESS BLOCK SUPPORT
|
||||||
|
============================================ */
|
||||||
|
.wp-block-ticket-tailor-event-listing,
|
||||||
|
.wp-block-ticket-tailor-single-event,
|
||||||
|
.wp-block-ticket-tailor-category-events {
|
||||||
|
margin: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alignfull .tt-event-listing {
|
||||||
|
padding: 0 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
STATUS, ERROR & WIDGET STYLES
|
||||||
|
============================================ */
|
||||||
|
.tt-status {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 500;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-status-on_sale {
|
||||||
|
background: #d4f4dd;
|
||||||
|
color: #1e7e34;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-status-sold_out {
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-status-cancelled {
|
||||||
|
background: #e7e7e7;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-status-postponed {
|
||||||
|
background: #fff3cd;
|
||||||
|
color: #856404;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-error,
|
||||||
|
.tt-no-events {
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 1px solid #e1e1e1;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #666;
|
||||||
|
margin: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-error {
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
border-color: #f5c6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-block-placeholder {
|
||||||
|
padding: 2rem;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border: 2px dashed #dee2e6;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-align: center;
|
||||||
|
color: #6c757d;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-widget-block {
|
||||||
|
margin: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-widget-error {
|
||||||
|
padding: 1rem;
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
border-radius: 4px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
CATEGORY EVENTS
|
||||||
|
============================================ */
|
||||||
|
.tt-category-events-wrapper {
|
||||||
|
margin: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-category-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-category-title {
|
||||||
|
font-size: 2rem;
|
||||||
|
color: #2c3e50;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
RESPONSIVE DESIGN
|
||||||
|
============================================ */
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.tt-event-listing.tt-layout-grid.tt-columns-5 {
|
||||||
|
grid-template-columns: repeat(4, 220px) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.tt-event-listing.tt-layout-grid.tt-columns-4,
|
||||||
|
.tt-event-listing.tt-layout-grid.tt-columns-3 {
|
||||||
|
grid-template-columns: repeat(3, 220px) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.tt-event-listing.tt-layout-grid.tt-columns-5,
|
||||||
|
.tt-event-listing.tt-layout-grid.tt-columns-4,
|
||||||
|
.tt-event-listing.tt-layout-grid.tt-columns-3 {
|
||||||
|
grid-template-columns: repeat(2, 220px) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-event-listing.tt-layout-list .tt-event-card {
|
||||||
|
grid-template-columns: 1fr !important;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
.tt-event-listing.tt-layout-grid {
|
||||||
|
grid-template-columns: 220px !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 400px) {
|
||||||
|
.tt-event-title {
|
||||||
|
font-size: 0.95rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-event-date,
|
||||||
|
.tt-event-venue {
|
||||||
|
font-size: 0.8rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-event-button {
|
||||||
|
padding: 0.5rem 0.875rem !important;
|
||||||
|
font-size: 0.85rem !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
UTILITY CLASSES
|
||||||
|
============================================ */
|
||||||
|
.tt-loading {
|
||||||
|
text-align: center;
|
||||||
|
padding: 3rem;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-loading::after {
|
||||||
|
content: "";
|
||||||
|
display: inline-block;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
border: 2px solid #ddd;
|
||||||
|
border-top-color: #2271b1;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: tt-spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes tt-spin {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-centered {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
ACCESSIBILITY
|
||||||
|
============================================ */
|
||||||
|
.tt-event-button:focus,
|
||||||
|
.tt-button:focus,
|
||||||
|
.tt-event-card:focus-within {
|
||||||
|
outline: 2px solid #2271b1;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screen-reader-text,
|
||||||
|
.tt-sr-only {
|
||||||
|
border: 0;
|
||||||
|
clip: rect(1px, 1px, 1px, 1px);
|
||||||
|
clip-path: inset(50%);
|
||||||
|
height: 1px;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0;
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
word-wrap: normal !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ============================================
|
||||||
|
PRINT STYLES
|
||||||
|
============================================ */
|
||||||
|
@media print {
|
||||||
|
.tt-event-button,
|
||||||
|
.tt-button,
|
||||||
|
.tt-widget-block {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-event-listing.tt-layout-grid {
|
||||||
|
grid-template-columns: repeat(3, 220px) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-event-card {
|
||||||
|
break-inside: avoid;
|
||||||
|
box-shadow: none;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
<?php
|
||||||
|
// Silence is golden.
|
||||||
|
|
@ -0,0 +1,288 @@
|
||||||
|
/**
|
||||||
|
* Ticket Tailor - Frontend Styles
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Event Listing Styles */
|
||||||
|
.tt-event-listing {
|
||||||
|
margin: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-event-listing.tt-layout-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-event-listing.tt-columns-2 {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-event-listing.tt-columns-3 {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-event-listing.tt-columns-4 {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-event-listing.tt-layout-list .tt-event-card {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Event Card */
|
||||||
|
.tt-event-card {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #e1e1e1;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: box-shadow 0.3s ease, transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-event-card:hover {
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-event-image {
|
||||||
|
position: relative;
|
||||||
|
padding-bottom: 56.25%; /* 16:9 aspect ratio */
|
||||||
|
overflow: hidden;
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-event-image img {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-event-content {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-event-title {
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-event-date,
|
||||||
|
.tt-event-venue {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-event-date .dashicons,
|
||||||
|
.tt-event-venue .dashicons {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
color: #2271b1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-event-excerpt {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
color: #555;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-event-button {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
background: #2271b1;
|
||||||
|
color: #fff !important;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: background 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-event-button:hover {
|
||||||
|
background: #135e96;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Single Event Styles */
|
||||||
|
.tt-single-event {
|
||||||
|
margin: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-event-header-image {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-event-header-image img {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-event-details {
|
||||||
|
max-width: 800px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-event-details .tt-event-title {
|
||||||
|
font-size: 2rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-event-meta {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-meta-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-meta-item:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-meta-item .dashicons {
|
||||||
|
margin-right: 0.75rem;
|
||||||
|
margin-top: 0.1rem;
|
||||||
|
color: #2271b1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-meta-item strong {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-event-description {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
line-height: 1.8;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-event-description p {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-event-cta {
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-button {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-button-primary {
|
||||||
|
background: #2271b1;
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-button-primary:hover {
|
||||||
|
background: #135e96;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 12px rgba(34, 113, 177, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status Badges */
|
||||||
|
.tt-status {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-status[class*="live"] {
|
||||||
|
background: #d1f4e0;
|
||||||
|
color: #0f5132;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-status[class*="draft"] {
|
||||||
|
background: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Error & Empty States */
|
||||||
|
.tt-error,
|
||||||
|
.tt-no-events {
|
||||||
|
padding: 1.5rem;
|
||||||
|
background: #fff3cd;
|
||||||
|
border: 1px solid #ffc107;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: #856404;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-error {
|
||||||
|
background: #f8d7da;
|
||||||
|
border-color: #f5c2c7;
|
||||||
|
color: #721c24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-widget-error {
|
||||||
|
padding: 1rem;
|
||||||
|
background: #f8d7da;
|
||||||
|
border: 1px solid #f5c2c7;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #721c24;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Widget Container */
|
||||||
|
.tt-widget-container {
|
||||||
|
margin: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Styles */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.tt-event-listing.tt-layout-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-event-details .tt-event-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-button {
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading State */
|
||||||
|
.tt-loading {
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tt-loading::after {
|
||||||
|
content: "⏳";
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Accessibility */
|
||||||
|
.tt-event-button:focus,
|
||||||
|
.tt-button:focus {
|
||||||
|
outline: 2px solid #2271b1;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Print Styles */
|
||||||
|
@media print {
|
||||||
|
.tt-event-button,
|
||||||
|
.tt-button {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
2
native/wordpress/ticket-tailor-wp-max/assets/index.php
Normal file
2
native/wordpress/ticket-tailor-wp-max/assets/index.php
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
<?php
|
||||||
|
// Silence is golden.
|
||||||
399
native/wordpress/ticket-tailor-wp-max/assets/js/admin.js
Normal file
399
native/wordpress/ticket-tailor-wp-max/assets/js/admin.js
Normal file
|
|
@ -0,0 +1,399 @@
|
||||||
|
/**
|
||||||
|
* Ticket Tailor Admin Scripts
|
||||||
|
* Version: 3.0 - Enhanced with Color Picker Improvements
|
||||||
|
*/
|
||||||
|
(function ($) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
$(document).ready(function () {
|
||||||
|
// ===========================
|
||||||
|
// EXISTING FUNCTIONALITY
|
||||||
|
// ===========================
|
||||||
|
|
||||||
|
// Confirm actions
|
||||||
|
$(".tt-confirm-action").on("click", function (e) {
|
||||||
|
if (!confirm(ticketTailorAdmin.confirmMessage || "Are you sure?")) {
|
||||||
|
e.preventDefault();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Test API connection
|
||||||
|
$("#tt-test-api").on("click", function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const $button = $(this);
|
||||||
|
const $status = $("#tt-api-status");
|
||||||
|
|
||||||
|
$button.prop("disabled", true).text("Testing...");
|
||||||
|
$status.removeClass("success error").text("");
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: ticketTailorAdmin.ajaxUrl,
|
||||||
|
type: "POST",
|
||||||
|
data: {
|
||||||
|
action: "ticket_tailor_test_api",
|
||||||
|
nonce: ticketTailorAdmin.nonce,
|
||||||
|
},
|
||||||
|
success: function (response) {
|
||||||
|
if (response.success) {
|
||||||
|
$status.addClass("success").text("✔ API connection successful!");
|
||||||
|
} else {
|
||||||
|
$status
|
||||||
|
.addClass("error")
|
||||||
|
.text("✗ API connection failed: " + response.data);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function () {
|
||||||
|
$status.addClass("error").text("✗ Connection error");
|
||||||
|
},
|
||||||
|
complete: function () {
|
||||||
|
$button.prop("disabled", false).text("Test Connection");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-dismiss notices
|
||||||
|
setTimeout(function () {
|
||||||
|
$(".notice.is-dismissible").fadeOut();
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
// Sync progress indicators
|
||||||
|
$(".tt-sync-button").on("click", function () {
|
||||||
|
$(this)
|
||||||
|
.prop("disabled", true)
|
||||||
|
.html(
|
||||||
|
'<span class="dashicons dashicons-update spin"></span> Syncing...',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Table row actions
|
||||||
|
$(".tt-row-actions a").on("click", function (e) {
|
||||||
|
const action = $(this).data("action");
|
||||||
|
if (
|
||||||
|
action === "delete" &&
|
||||||
|
!confirm("Are you sure you want to delete this item?")
|
||||||
|
) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===========================
|
||||||
|
// ENHANCED COLOR PICKER
|
||||||
|
// ===========================
|
||||||
|
|
||||||
|
// Function to calculate relative luminance for contrast ratio
|
||||||
|
function getLuminance(hexColor) {
|
||||||
|
// Convert hex to RGB
|
||||||
|
const hex = hexColor.replace("#", "");
|
||||||
|
const r = parseInt(hex.substr(0, 2), 16) / 255;
|
||||||
|
const g = parseInt(hex.substr(2, 2), 16) / 255;
|
||||||
|
const b = parseInt(hex.substr(4, 2), 16) / 255;
|
||||||
|
|
||||||
|
// Apply gamma correction
|
||||||
|
const gammaCorrect = (channel) => {
|
||||||
|
return channel <= 0.03928
|
||||||
|
? channel / 12.92
|
||||||
|
: Math.pow((channel + 0.055) / 1.055, 2.4);
|
||||||
|
};
|
||||||
|
|
||||||
|
const rLinear = gammaCorrect(r);
|
||||||
|
const gLinear = gammaCorrect(g);
|
||||||
|
const bLinear = gammaCorrect(b);
|
||||||
|
|
||||||
|
// Calculate relative luminance
|
||||||
|
return 0.2126 * rLinear + 0.7152 * gLinear + 0.0722 * bLinear;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to determine if white or black text is more readable
|
||||||
|
function getContrastColor(hexColor) {
|
||||||
|
const luminance = getLuminance(hexColor);
|
||||||
|
// Use white text for dark colors, black for light colors
|
||||||
|
return luminance > 0.5 ? "#000000" : "#ffffff";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to format and display hex value with proper contrast
|
||||||
|
function updateHexDisplay($input, color) {
|
||||||
|
const $container = $input.closest(".wp-picker-container");
|
||||||
|
let $hexDisplay = $container.find(".tt-hex-display");
|
||||||
|
|
||||||
|
// Create hex display if it doesn't exist
|
||||||
|
if ($hexDisplay.length === 0) {
|
||||||
|
$hexDisplay = $('<span class="tt-hex-display"></span>');
|
||||||
|
// Find the color picker button and insert after it
|
||||||
|
const $colorButton = $container.find(".wp-color-result");
|
||||||
|
$hexDisplay.insertAfter($colorButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update hex value and text color
|
||||||
|
$hexDisplay.text(color.toUpperCase());
|
||||||
|
const contrastColor = getContrastColor(color);
|
||||||
|
$hexDisplay.css("color", contrastColor);
|
||||||
|
|
||||||
|
// Update background to match the selected color for visual context
|
||||||
|
$hexDisplay.css("background-color", color);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize color pickers with enhanced functionality
|
||||||
|
if ($(".tt-color-picker").length > 0) {
|
||||||
|
$(".tt-color-picker").each(function () {
|
||||||
|
const $this = $(this);
|
||||||
|
const currentColor = $this.val() || $this.data("default-color");
|
||||||
|
|
||||||
|
// Initialize WordPress color picker
|
||||||
|
$this.wpColorPicker({
|
||||||
|
change: function (event, ui) {
|
||||||
|
const color = ui.color.toString();
|
||||||
|
const field = $(this).attr("id");
|
||||||
|
|
||||||
|
// Update hex display
|
||||||
|
updateHexDisplay($(this), color);
|
||||||
|
|
||||||
|
// Update preview in real-time
|
||||||
|
if (field === "text_color") {
|
||||||
|
$(".preview-title, .preview-description, .preview-venue").css(
|
||||||
|
"color",
|
||||||
|
color,
|
||||||
|
);
|
||||||
|
} else if (field === "border_color") {
|
||||||
|
$(".tt-preview-card").css("border-color", color);
|
||||||
|
} else if (field === "button_bg") {
|
||||||
|
$(".tt-preview-button").css("background-color", color);
|
||||||
|
} else if (field === "button_text") {
|
||||||
|
$(".tt-preview-button").css("color", color);
|
||||||
|
} else if (field === "button_hover") {
|
||||||
|
// Store hover color for later use
|
||||||
|
$(".tt-preview-button").data("hover-color", color);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
clear: function () {
|
||||||
|
const field = $(this).attr("id");
|
||||||
|
const defaultColor = $(this).data("default-color");
|
||||||
|
|
||||||
|
// Update hex display with default color
|
||||||
|
updateHexDisplay($(this), defaultColor);
|
||||||
|
|
||||||
|
// Reset preview to default colors
|
||||||
|
if (field === "text_color") {
|
||||||
|
$(".preview-title, .preview-description, .preview-venue").css(
|
||||||
|
"color",
|
||||||
|
defaultColor,
|
||||||
|
);
|
||||||
|
} else if (field === "border_color") {
|
||||||
|
$(".tt-preview-card").css("border-color", defaultColor);
|
||||||
|
} else if (field === "button_bg") {
|
||||||
|
$(".tt-preview-button").css("background-color", defaultColor);
|
||||||
|
} else if (field === "button_text") {
|
||||||
|
$(".tt-preview-button").css("color", defaultColor);
|
||||||
|
} else if (field === "button_hover") {
|
||||||
|
$(".tt-preview-button").data("hover-color", defaultColor);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize hex display for existing colors
|
||||||
|
if (currentColor) {
|
||||||
|
updateHexDisplay($this, currentColor);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle border radius preview in real-time
|
||||||
|
$("#border_radius").on("input", function () {
|
||||||
|
const radius = $(this).val();
|
||||||
|
$(".tt-preview-card").css("border-radius", radius + "px");
|
||||||
|
});
|
||||||
|
|
||||||
|
// Button hover effect for preview
|
||||||
|
let originalBg =
|
||||||
|
$("#button_bg").val() || $("#button_bg").data("default-color");
|
||||||
|
let hoverBg =
|
||||||
|
$("#button_hover").val() || $("#button_hover").data("default-color");
|
||||||
|
|
||||||
|
$(".tt-preview-button").hover(
|
||||||
|
function () {
|
||||||
|
const currentHover = $("#button_hover").val() || hoverBg;
|
||||||
|
$(this).css("background-color", currentHover);
|
||||||
|
},
|
||||||
|
function () {
|
||||||
|
const currentBg = $("#button_bg").val() || originalBg;
|
||||||
|
$(this).css("background-color", currentBg);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update hover colors when changed
|
||||||
|
$("#button_hover").on("change", function () {
|
||||||
|
hoverBg = $(this).val();
|
||||||
|
updateHexDisplay($(this), hoverBg);
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#button_bg").on("change", function () {
|
||||||
|
originalBg = $(this).val();
|
||||||
|
updateHexDisplay($(this), originalBg);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also update hex display when color picker is opened/closed
|
||||||
|
$(".wp-color-result").on("click", function () {
|
||||||
|
const $input = $(this)
|
||||||
|
.closest(".wp-picker-container")
|
||||||
|
.find(".tt-color-picker");
|
||||||
|
const currentColor = $input.val() || $input.data("default-color");
|
||||||
|
if (currentColor) {
|
||||||
|
updateHexDisplay($input, currentColor);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===========================
|
||||||
|
// DASHBOARD ENHANCEMENTS
|
||||||
|
// ===========================
|
||||||
|
|
||||||
|
// Add smooth scrolling for anchor links
|
||||||
|
$('a[href^="#"]').on("click", function (e) {
|
||||||
|
const target = $(this.getAttribute("href"));
|
||||||
|
if (target.length) {
|
||||||
|
e.preventDefault();
|
||||||
|
$("html, body")
|
||||||
|
.stop()
|
||||||
|
.animate(
|
||||||
|
{
|
||||||
|
scrollTop: target.offset().top - 40,
|
||||||
|
},
|
||||||
|
800,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enhanced loading states
|
||||||
|
$(".button").on("click", function () {
|
||||||
|
const $btn = $(this);
|
||||||
|
if ($btn.hasClass("tt-ajax-button")) {
|
||||||
|
$btn.addClass("tt-loading");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// AJAX complete handler to remove loading states
|
||||||
|
$(document).ajaxComplete(function (event, xhr, settings) {
|
||||||
|
$(".tt-loading").removeClass("tt-loading");
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===========================
|
||||||
|
// ACCESSIBILITY ENHANCEMENTS
|
||||||
|
// ===========================
|
||||||
|
|
||||||
|
// Add keyboard navigation support for custom elements
|
||||||
|
$(".tt-stat-card, .tt-help-section").attr("tabindex", "0");
|
||||||
|
|
||||||
|
// PERFORMANCE FIX: Use event delegation instead of direct binding
|
||||||
|
// This prevents memory leaks when elements are dynamically added/removed
|
||||||
|
$(document)
|
||||||
|
.on("focus", "a, button, input, select, textarea", function () {
|
||||||
|
$(this).addClass("has-focus");
|
||||||
|
})
|
||||||
|
.on("blur", "a, button, input, select, textarea", function () {
|
||||||
|
$(this).removeClass("has-focus");
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===========================
|
||||||
|
// RESPONSIVE IMPROVEMENTS
|
||||||
|
// ===========================
|
||||||
|
|
||||||
|
// Handle responsive table display
|
||||||
|
function checkTableResponsive() {
|
||||||
|
$(".wp-list-table").each(function () {
|
||||||
|
const $table = $(this);
|
||||||
|
const tableWidth = $table.width();
|
||||||
|
const containerWidth = $table.parent().width();
|
||||||
|
|
||||||
|
if (tableWidth > containerWidth) {
|
||||||
|
$table.addClass("tt-responsive-table");
|
||||||
|
} else {
|
||||||
|
$table.removeClass("tt-responsive-table");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check on load and resize
|
||||||
|
checkTableResponsive();
|
||||||
|
$(window).on("resize debounce", checkTableResponsive);
|
||||||
|
|
||||||
|
// ===========================
|
||||||
|
// FORM VALIDATION
|
||||||
|
// ===========================
|
||||||
|
|
||||||
|
// Basic form validation for settings
|
||||||
|
$("form").on("submit", function () {
|
||||||
|
let isValid = true;
|
||||||
|
|
||||||
|
// Check required fields
|
||||||
|
$(this)
|
||||||
|
.find("[required]")
|
||||||
|
.each(function () {
|
||||||
|
if (!$(this).val()) {
|
||||||
|
$(this).addClass("error");
|
||||||
|
isValid = false;
|
||||||
|
} else {
|
||||||
|
$(this).removeClass("error");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validate API key format (if present)
|
||||||
|
const $apiKey = $("#api_key");
|
||||||
|
if ($apiKey.length && $apiKey.val()) {
|
||||||
|
// Basic validation - ensure it's not just whitespace
|
||||||
|
if ($apiKey.val().trim().length < 10) {
|
||||||
|
$apiKey.addClass("error");
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
// Show error message
|
||||||
|
if (!$(".tt-validation-error").length) {
|
||||||
|
$(
|
||||||
|
'<div class="notice notice-error tt-validation-error"><p>Please fill in all required fields correctly.</p></div>',
|
||||||
|
)
|
||||||
|
.insertBefore(this)
|
||||||
|
.delay(3000)
|
||||||
|
.fadeOut(function () {
|
||||||
|
$(this).remove();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove error class on input
|
||||||
|
$("input, select, textarea").on("input change", function () {
|
||||||
|
$(this).removeClass("error");
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===========================
|
||||||
|
// UTILITY FUNCTIONS
|
||||||
|
// ===========================
|
||||||
|
|
||||||
|
// Debounce function for performance
|
||||||
|
function debounce(func, wait, immediate) {
|
||||||
|
let timeout;
|
||||||
|
return function () {
|
||||||
|
const context = this,
|
||||||
|
args = arguments;
|
||||||
|
const later = function () {
|
||||||
|
timeout = null;
|
||||||
|
if (!immediate) func.apply(context, args);
|
||||||
|
};
|
||||||
|
const callNow = immediate && !timeout;
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(later, wait);
|
||||||
|
if (callNow) func.apply(context, args);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add debounced resize event
|
||||||
|
$(window).on(
|
||||||
|
"resize",
|
||||||
|
debounce(function () {
|
||||||
|
$(window).trigger("resize.debounce");
|
||||||
|
}, 250),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
})(jQuery);
|
||||||
719
native/wordpress/ticket-tailor-wp-max/assets/js/blocks.js
Normal file
719
native/wordpress/ticket-tailor-wp-max/assets/js/blocks.js
Normal file
|
|
@ -0,0 +1,719 @@
|
||||||
|
/**
|
||||||
|
* Ticket Tailor Gutenberg Blocks
|
||||||
|
* Enhanced with full-width control and dynamic columns
|
||||||
|
*/
|
||||||
|
(function (wp) {
|
||||||
|
const { registerBlockType } = wp.blocks;
|
||||||
|
const { InspectorControls, BlockControls, AlignmentToolbar } = wp.blockEditor;
|
||||||
|
const { PanelBody, TextControl, ToggleControl, SelectControl, RangeControl } =
|
||||||
|
wp.components;
|
||||||
|
const { __ } = wp.i18n;
|
||||||
|
const { createElement: el, Fragment } = wp.element;
|
||||||
|
|
||||||
|
// Block 1: Event Widget (Original)
|
||||||
|
registerBlockType("ticket-tailor/event-widget", {
|
||||||
|
title: __("Ticket Tailor Event Widget", "ticket-tailor"),
|
||||||
|
description: __("Embed a Ticket Tailor event widget", "ticket-tailor"),
|
||||||
|
icon: "tickets-alt",
|
||||||
|
category: "embed",
|
||||||
|
keywords: [
|
||||||
|
__("ticket", "ticket-tailor"),
|
||||||
|
__("event", "ticket-tailor"),
|
||||||
|
__("ticketing", "ticket-tailor"),
|
||||||
|
],
|
||||||
|
supports: {
|
||||||
|
align: ["wide", "full"],
|
||||||
|
},
|
||||||
|
attributes: {
|
||||||
|
url: { type: "string", default: "" },
|
||||||
|
minimal: { type: "boolean", default: false },
|
||||||
|
bgFill: { type: "boolean", default: true },
|
||||||
|
showLogo: { type: "boolean", default: true },
|
||||||
|
ref: { type: "string", default: "website_widget" },
|
||||||
|
},
|
||||||
|
edit: function (props) {
|
||||||
|
const { attributes, setAttributes } = props;
|
||||||
|
const { url, minimal, bgFill, showLogo, ref } = attributes;
|
||||||
|
|
||||||
|
return el(
|
||||||
|
Fragment,
|
||||||
|
{},
|
||||||
|
el(
|
||||||
|
InspectorControls,
|
||||||
|
{},
|
||||||
|
el(
|
||||||
|
PanelBody,
|
||||||
|
{
|
||||||
|
title: __("Widget Settings", "ticket-tailor"),
|
||||||
|
initialOpen: true,
|
||||||
|
},
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: __("Minimal Design", "ticket-tailor"),
|
||||||
|
checked: minimal,
|
||||||
|
onChange: (value) => setAttributes({ minimal: value }),
|
||||||
|
}),
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: __("Background Fill", "ticket-tailor"),
|
||||||
|
checked: bgFill,
|
||||||
|
onChange: (value) => setAttributes({ bgFill: value }),
|
||||||
|
}),
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: __("Show Logo", "ticket-tailor"),
|
||||||
|
checked: showLogo,
|
||||||
|
onChange: (value) => setAttributes({ showLogo: value }),
|
||||||
|
}),
|
||||||
|
el(TextControl, {
|
||||||
|
label: __("Tracking Reference", "ticket-tailor"),
|
||||||
|
value: ref,
|
||||||
|
onChange: (value) => setAttributes({ ref: value }),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
el(
|
||||||
|
"div",
|
||||||
|
{
|
||||||
|
className: "tt-block-placeholder",
|
||||||
|
style: {
|
||||||
|
border: "2px dashed #ccc",
|
||||||
|
padding: "40px",
|
||||||
|
textAlign: "center",
|
||||||
|
background: "#f9f9f9",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
el("span", {
|
||||||
|
className: "dashicons dashicons-tickets-alt",
|
||||||
|
style: { fontSize: "48px", color: "#666" },
|
||||||
|
}),
|
||||||
|
!url
|
||||||
|
? el(
|
||||||
|
"div",
|
||||||
|
{},
|
||||||
|
el(
|
||||||
|
"p",
|
||||||
|
{ style: { marginBottom: "15px" } },
|
||||||
|
__("Ticket Tailor Event Widget", "ticket-tailor"),
|
||||||
|
),
|
||||||
|
el(TextControl, {
|
||||||
|
label: __("Event URL", "ticket-tailor"),
|
||||||
|
placeholder: "https://www.tickettailor.com/events/...",
|
||||||
|
value: url,
|
||||||
|
onChange: (value) => setAttributes({ url: value }),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
: el(
|
||||||
|
"div",
|
||||||
|
{},
|
||||||
|
el("p", {}, __("Widget URL configured", "ticket-tailor")),
|
||||||
|
el(
|
||||||
|
"button",
|
||||||
|
{
|
||||||
|
className: "button",
|
||||||
|
onClick: () => setAttributes({ url: "" }),
|
||||||
|
},
|
||||||
|
__("Change URL", "ticket-tailor"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
save: function () {
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Block 2: Event Listing - ENHANCED WITH FULL WIDTH OPTIONS
|
||||||
|
registerBlockType("ticket-tailor/event-listing", {
|
||||||
|
title: __("Event Listing", "ticket-tailor"),
|
||||||
|
description: __("Display a list of events", "ticket-tailor"),
|
||||||
|
icon: "calendar-alt",
|
||||||
|
category: "widgets",
|
||||||
|
keywords: [
|
||||||
|
__("events", "ticket-tailor"),
|
||||||
|
__("listing", "ticket-tailor"),
|
||||||
|
__("calendar", "ticket-tailor"),
|
||||||
|
],
|
||||||
|
supports: {
|
||||||
|
align: ["wide", "full"],
|
||||||
|
customClassName: true,
|
||||||
|
},
|
||||||
|
attributes: {
|
||||||
|
limit: { type: "number", default: 10 },
|
||||||
|
layout: { type: "string", default: "grid" },
|
||||||
|
columns: { type: "number", default: 3 },
|
||||||
|
columnsMode: { type: "string", default: "fixed" }, // 'fixed' or 'responsive'
|
||||||
|
showPast: { type: "boolean", default: false },
|
||||||
|
showImage: { type: "boolean", default: true },
|
||||||
|
imageType: { type: "string", default: "header" },
|
||||||
|
fullWidth: { type: "boolean", default: true },
|
||||||
|
maxCardWidth: { type: "string", default: "none" }, // 'none', 'small', 'medium', 'large'
|
||||||
|
},
|
||||||
|
edit: function (props) {
|
||||||
|
const { attributes, setAttributes, className } = props;
|
||||||
|
const {
|
||||||
|
limit,
|
||||||
|
layout,
|
||||||
|
columns,
|
||||||
|
columnsMode,
|
||||||
|
showPast,
|
||||||
|
showImage,
|
||||||
|
imageType,
|
||||||
|
fullWidth,
|
||||||
|
maxCardWidth,
|
||||||
|
} = attributes;
|
||||||
|
|
||||||
|
return el(
|
||||||
|
Fragment,
|
||||||
|
{},
|
||||||
|
el(
|
||||||
|
InspectorControls,
|
||||||
|
{},
|
||||||
|
el(
|
||||||
|
PanelBody,
|
||||||
|
{
|
||||||
|
title: __("Layout Settings", "ticket-tailor"),
|
||||||
|
initialOpen: true,
|
||||||
|
},
|
||||||
|
el(SelectControl, {
|
||||||
|
label: __("Layout", "ticket-tailor"),
|
||||||
|
value: layout,
|
||||||
|
options: [
|
||||||
|
{ label: __("Grid", "ticket-tailor"), value: "grid" },
|
||||||
|
{ label: __("List", "ticket-tailor"), value: "list" },
|
||||||
|
],
|
||||||
|
onChange: (value) => setAttributes({ layout: value }),
|
||||||
|
}),
|
||||||
|
layout === "grid" &&
|
||||||
|
el(
|
||||||
|
Fragment,
|
||||||
|
{},
|
||||||
|
el(SelectControl, {
|
||||||
|
label: __("Columns Mode", "ticket-tailor"),
|
||||||
|
value: columnsMode,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: __("Fixed Columns", "ticket-tailor"),
|
||||||
|
value: "fixed",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __("Responsive (Auto-fit)", "ticket-tailor"),
|
||||||
|
value: "responsive",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onChange: (value) => setAttributes({ columnsMode: value }),
|
||||||
|
help: __(
|
||||||
|
"Responsive mode automatically adjusts columns based on available space",
|
||||||
|
"ticket-tailor",
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
el(RangeControl, {
|
||||||
|
label:
|
||||||
|
columnsMode === "fixed"
|
||||||
|
? __("Number of Columns", "ticket-tailor")
|
||||||
|
: __("Maximum Columns", "ticket-tailor"),
|
||||||
|
value: columns,
|
||||||
|
onChange: (value) => setAttributes({ columns: value }),
|
||||||
|
min: 1,
|
||||||
|
max: 5,
|
||||||
|
help:
|
||||||
|
columnsMode === "responsive"
|
||||||
|
? __(
|
||||||
|
"In responsive mode, columns will adjust automatically but won't exceed this number",
|
||||||
|
"ticket-tailor",
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
}),
|
||||||
|
el(SelectControl, {
|
||||||
|
label: __("Maximum Card Width", "ticket-tailor"),
|
||||||
|
value: maxCardWidth,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: __("No Limit (Full Width)", "ticket-tailor"),
|
||||||
|
value: "none",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __("Small (300px)", "ticket-tailor"),
|
||||||
|
value: "small",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __("Medium (400px)", "ticket-tailor"),
|
||||||
|
value: "medium",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __("Large (500px)", "ticket-tailor"),
|
||||||
|
value: "large",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onChange: (value) => setAttributes({ maxCardWidth: value }),
|
||||||
|
help: __(
|
||||||
|
"Set a maximum width for individual event cards",
|
||||||
|
"ticket-tailor",
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: __("Full Width Container", "ticket-tailor"),
|
||||||
|
checked: fullWidth,
|
||||||
|
onChange: (value) => setAttributes({ fullWidth: value }),
|
||||||
|
help: __(
|
||||||
|
"Make the event listing fill the width of its container",
|
||||||
|
"ticket-tailor",
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
el(
|
||||||
|
PanelBody,
|
||||||
|
{
|
||||||
|
title: __("Display Settings", "ticket-tailor"),
|
||||||
|
initialOpen: false,
|
||||||
|
},
|
||||||
|
el(RangeControl, {
|
||||||
|
label: __("Number of Events", "ticket-tailor"),
|
||||||
|
value: limit,
|
||||||
|
onChange: (value) => setAttributes({ limit: value }),
|
||||||
|
min: 1,
|
||||||
|
max: 50,
|
||||||
|
}),
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: __("Show Past Events", "ticket-tailor"),
|
||||||
|
checked: showPast,
|
||||||
|
onChange: (value) => setAttributes({ showPast: value }),
|
||||||
|
}),
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: __("Show Event Images", "ticket-tailor"),
|
||||||
|
checked: showImage,
|
||||||
|
onChange: (value) => setAttributes({ showImage: value }),
|
||||||
|
}),
|
||||||
|
showImage &&
|
||||||
|
el(SelectControl, {
|
||||||
|
label: __("Image Type", "ticket-tailor"),
|
||||||
|
value: imageType,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: __("Thumbnail (Square)", "ticket-tailor"),
|
||||||
|
value: "thumbnail",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __("Banner (16:9)", "ticket-tailor"),
|
||||||
|
value: "header",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onChange: (value) => setAttributes({ imageType: value }),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
el(
|
||||||
|
"div",
|
||||||
|
{
|
||||||
|
className: "tt-block-placeholder",
|
||||||
|
style: {
|
||||||
|
border: "2px dashed #ccc",
|
||||||
|
padding: "40px",
|
||||||
|
textAlign: "center",
|
||||||
|
background: "#f9f9f9",
|
||||||
|
width: fullWidth ? "100%" : "auto",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
el("span", {
|
||||||
|
className: "dashicons dashicons-calendar-alt",
|
||||||
|
style: { fontSize: "48px", color: "#666" },
|
||||||
|
}),
|
||||||
|
el(
|
||||||
|
"p",
|
||||||
|
{
|
||||||
|
style: {
|
||||||
|
fontSize: "16px",
|
||||||
|
fontWeight: "bold",
|
||||||
|
marginBottom: "10px",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
__("Event Listing", "ticket-tailor"),
|
||||||
|
),
|
||||||
|
el(
|
||||||
|
"p",
|
||||||
|
{ style: { fontSize: "14px", color: "#666" } },
|
||||||
|
__("Showing ", "ticket-tailor") +
|
||||||
|
limit +
|
||||||
|
__(" events", "ticket-tailor"),
|
||||||
|
),
|
||||||
|
el(
|
||||||
|
"p",
|
||||||
|
{ style: { fontSize: "13px", color: "#888", marginTop: "10px" } },
|
||||||
|
layout === "grid"
|
||||||
|
? columnsMode === "responsive"
|
||||||
|
? __("Responsive grid with max ", "ticket-tailor") +
|
||||||
|
columns +
|
||||||
|
__(" columns", "ticket-tailor")
|
||||||
|
: columns + __(" column grid", "ticket-tailor")
|
||||||
|
: __("List layout", "ticket-tailor"),
|
||||||
|
),
|
||||||
|
fullWidth &&
|
||||||
|
el(
|
||||||
|
"p",
|
||||||
|
{
|
||||||
|
style: { fontSize: "12px", color: "#2271b1", marginTop: "5px" },
|
||||||
|
},
|
||||||
|
__("✓ Full width enabled", "ticket-tailor"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
save: function () {
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Block 3: Single Event - ENHANCED WITH WIDTH OPTIONS
|
||||||
|
registerBlockType("ticket-tailor/single-event", {
|
||||||
|
title: __("Single Event", "ticket-tailor"),
|
||||||
|
description: __("Display a specific event", "ticket-tailor"),
|
||||||
|
icon: "megaphone",
|
||||||
|
category: "widgets",
|
||||||
|
keywords: [
|
||||||
|
__("event", "ticket-tailor"),
|
||||||
|
__("single", "ticket-tailor"),
|
||||||
|
__("detail", "ticket-tailor"),
|
||||||
|
],
|
||||||
|
supports: {
|
||||||
|
align: ["wide", "full"],
|
||||||
|
},
|
||||||
|
attributes: {
|
||||||
|
eventId: { type: "string", default: "" },
|
||||||
|
showDescription: { type: "boolean", default: true },
|
||||||
|
showTickets: { type: "boolean", default: true },
|
||||||
|
showImage: { type: "boolean", default: true },
|
||||||
|
imageType: { type: "string", default: "header" },
|
||||||
|
fullWidth: { type: "boolean", default: false },
|
||||||
|
maxWidth: { type: "string", default: "800px" },
|
||||||
|
},
|
||||||
|
edit: function (props) {
|
||||||
|
const { attributes, setAttributes } = props;
|
||||||
|
const {
|
||||||
|
eventId,
|
||||||
|
showDescription,
|
||||||
|
showTickets,
|
||||||
|
showImage,
|
||||||
|
imageType,
|
||||||
|
fullWidth,
|
||||||
|
maxWidth,
|
||||||
|
} = attributes;
|
||||||
|
|
||||||
|
const eventOptions =
|
||||||
|
window.ticketTailorData && window.ticketTailorData.events
|
||||||
|
? window.ticketTailorData.events
|
||||||
|
: [{ value: "", label: __("Loading events...", "ticket-tailor") }];
|
||||||
|
|
||||||
|
return el(
|
||||||
|
Fragment,
|
||||||
|
{},
|
||||||
|
el(
|
||||||
|
InspectorControls,
|
||||||
|
{},
|
||||||
|
el(
|
||||||
|
PanelBody,
|
||||||
|
{ title: __("Event Settings", "ticket-tailor"), initialOpen: true },
|
||||||
|
el(SelectControl, {
|
||||||
|
label: __("Select Event", "ticket-tailor"),
|
||||||
|
value: eventId,
|
||||||
|
options: [
|
||||||
|
{ value: "", label: __("Select an event...", "ticket-tailor") },
|
||||||
|
].concat(eventOptions),
|
||||||
|
onChange: (value) => setAttributes({ eventId: value }),
|
||||||
|
}),
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: __("Full Width Display", "ticket-tailor"),
|
||||||
|
checked: fullWidth,
|
||||||
|
onChange: (value) => setAttributes({ fullWidth: value }),
|
||||||
|
}),
|
||||||
|
!fullWidth &&
|
||||||
|
el(TextControl, {
|
||||||
|
label: __("Maximum Width", "ticket-tailor"),
|
||||||
|
value: maxWidth,
|
||||||
|
onChange: (value) => setAttributes({ maxWidth: value }),
|
||||||
|
help: __("e.g. 800px, 100%, 60rem", "ticket-tailor"),
|
||||||
|
}),
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: __("Show Event Image", "ticket-tailor"),
|
||||||
|
checked: showImage,
|
||||||
|
onChange: (value) => setAttributes({ showImage: value }),
|
||||||
|
}),
|
||||||
|
showImage &&
|
||||||
|
el(SelectControl, {
|
||||||
|
label: __("Image Type", "ticket-tailor"),
|
||||||
|
value: imageType,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: __("Banner (Wide)", "ticket-tailor"),
|
||||||
|
value: "header",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __("Thumbnail (Square)", "ticket-tailor"),
|
||||||
|
value: "thumbnail",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onChange: (value) => setAttributes({ imageType: value }),
|
||||||
|
}),
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: __("Show Description", "ticket-tailor"),
|
||||||
|
checked: showDescription,
|
||||||
|
onChange: (value) => setAttributes({ showDescription: value }),
|
||||||
|
}),
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: __("Show Ticket Button", "ticket-tailor"),
|
||||||
|
checked: showTickets,
|
||||||
|
onChange: (value) => setAttributes({ showTickets: value }),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
el(
|
||||||
|
"div",
|
||||||
|
{
|
||||||
|
className: "tt-block-placeholder",
|
||||||
|
style: {
|
||||||
|
border: "2px dashed #ccc",
|
||||||
|
padding: "40px",
|
||||||
|
textAlign: "center",
|
||||||
|
background: "#f9f9f9",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
el("span", {
|
||||||
|
className: "dashicons dashicons-megaphone",
|
||||||
|
style: { fontSize: "48px", color: "#666" },
|
||||||
|
}),
|
||||||
|
!eventId
|
||||||
|
? el(
|
||||||
|
"p",
|
||||||
|
{},
|
||||||
|
__("Please select an event from the sidebar", "ticket-tailor"),
|
||||||
|
)
|
||||||
|
: el(
|
||||||
|
"div",
|
||||||
|
{},
|
||||||
|
el("p", {}, __("Single Event Display", "ticket-tailor")),
|
||||||
|
el(
|
||||||
|
"p",
|
||||||
|
{ style: { fontSize: "14px", color: "#666" } },
|
||||||
|
__("Event ID: ", "ticket-tailor") + eventId,
|
||||||
|
),
|
||||||
|
fullWidth &&
|
||||||
|
el(
|
||||||
|
"p",
|
||||||
|
{
|
||||||
|
style: {
|
||||||
|
fontSize: "12px",
|
||||||
|
color: "#2271b1",
|
||||||
|
marginTop: "5px",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
__("✓ Full width enabled", "ticket-tailor"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
save: function () {
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Block 4: Category Events - ENHANCED
|
||||||
|
registerBlockType("ticket-tailor/category-events", {
|
||||||
|
title: __("Category Events", "ticket-tailor"),
|
||||||
|
description: __("Display events from a specific category", "ticket-tailor"),
|
||||||
|
icon: "category",
|
||||||
|
category: "widgets",
|
||||||
|
keywords: [
|
||||||
|
__("category", "ticket-tailor"),
|
||||||
|
__("filter", "ticket-tailor"),
|
||||||
|
__("events", "ticket-tailor"),
|
||||||
|
],
|
||||||
|
supports: {
|
||||||
|
align: ["wide", "full"],
|
||||||
|
},
|
||||||
|
attributes: {
|
||||||
|
category: { type: "string", default: "" },
|
||||||
|
limit: { type: "number", default: 10 },
|
||||||
|
layout: { type: "string", default: "grid" },
|
||||||
|
columns: { type: "number", default: 3 },
|
||||||
|
columnsMode: { type: "string", default: "fixed" },
|
||||||
|
showImage: { type: "boolean", default: true },
|
||||||
|
imageType: { type: "string", default: "thumbnail" },
|
||||||
|
fullWidth: { type: "boolean", default: true },
|
||||||
|
},
|
||||||
|
edit: function (props) {
|
||||||
|
const { attributes, setAttributes } = props;
|
||||||
|
const {
|
||||||
|
category,
|
||||||
|
limit,
|
||||||
|
layout,
|
||||||
|
columns,
|
||||||
|
columnsMode,
|
||||||
|
showImage,
|
||||||
|
imageType,
|
||||||
|
fullWidth,
|
||||||
|
} = attributes;
|
||||||
|
|
||||||
|
return el(
|
||||||
|
Fragment,
|
||||||
|
{},
|
||||||
|
el(
|
||||||
|
InspectorControls,
|
||||||
|
{},
|
||||||
|
el(
|
||||||
|
PanelBody,
|
||||||
|
{
|
||||||
|
title: __("Category Settings", "ticket-tailor"),
|
||||||
|
initialOpen: true,
|
||||||
|
},
|
||||||
|
el(TextControl, {
|
||||||
|
label: __("Category", "ticket-tailor"),
|
||||||
|
help: __(
|
||||||
|
"Enter the category name (e.g., Music, Sports, Theatre)",
|
||||||
|
"ticket-tailor",
|
||||||
|
),
|
||||||
|
value: category,
|
||||||
|
onChange: (value) => setAttributes({ category: value }),
|
||||||
|
}),
|
||||||
|
el(RangeControl, {
|
||||||
|
label: __("Number of Events", "ticket-tailor"),
|
||||||
|
value: limit,
|
||||||
|
onChange: (value) => setAttributes({ limit: value }),
|
||||||
|
min: 1,
|
||||||
|
max: 50,
|
||||||
|
}),
|
||||||
|
el(SelectControl, {
|
||||||
|
label: __("Layout", "ticket-tailor"),
|
||||||
|
value: layout,
|
||||||
|
options: [
|
||||||
|
{ label: __("Grid", "ticket-tailor"), value: "grid" },
|
||||||
|
{ label: __("List", "ticket-tailor"), value: "list" },
|
||||||
|
],
|
||||||
|
onChange: (value) => setAttributes({ layout: value }),
|
||||||
|
}),
|
||||||
|
layout === "grid" &&
|
||||||
|
el(
|
||||||
|
Fragment,
|
||||||
|
{},
|
||||||
|
el(SelectControl, {
|
||||||
|
label: __("Columns Mode", "ticket-tailor"),
|
||||||
|
value: columnsMode,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: __("Fixed Columns", "ticket-tailor"),
|
||||||
|
value: "fixed",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __("Responsive (Auto-fit)", "ticket-tailor"),
|
||||||
|
value: "responsive",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onChange: (value) => setAttributes({ columnsMode: value }),
|
||||||
|
}),
|
||||||
|
el(RangeControl, {
|
||||||
|
label:
|
||||||
|
columnsMode === "fixed"
|
||||||
|
? __("Columns", "ticket-tailor")
|
||||||
|
: __("Maximum Columns", "ticket-tailor"),
|
||||||
|
value: columns,
|
||||||
|
onChange: (value) => setAttributes({ columns: value }),
|
||||||
|
min: 1,
|
||||||
|
max: 5,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: __("Full Width Container", "ticket-tailor"),
|
||||||
|
checked: fullWidth,
|
||||||
|
onChange: (value) => setAttributes({ fullWidth: value }),
|
||||||
|
}),
|
||||||
|
el(ToggleControl, {
|
||||||
|
label: __("Show Event Images", "ticket-tailor"),
|
||||||
|
checked: showImage,
|
||||||
|
onChange: (value) => setAttributes({ showImage: value }),
|
||||||
|
}),
|
||||||
|
showImage &&
|
||||||
|
el(SelectControl, {
|
||||||
|
label: __("Image Type", "ticket-tailor"),
|
||||||
|
value: imageType,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: __("Thumbnail (Square)", "ticket-tailor"),
|
||||||
|
value: "thumbnail",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __("Banner (16:9)", "ticket-tailor"),
|
||||||
|
value: "header",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
onChange: (value) => setAttributes({ imageType: value }),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
el(
|
||||||
|
"div",
|
||||||
|
{
|
||||||
|
className: "tt-block-placeholder",
|
||||||
|
style: {
|
||||||
|
border: "2px dashed #ccc",
|
||||||
|
padding: "40px",
|
||||||
|
textAlign: "center",
|
||||||
|
background: "#f9f9f9",
|
||||||
|
width: fullWidth ? "100%" : "auto",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
el("span", {
|
||||||
|
className: "dashicons dashicons-category",
|
||||||
|
style: { fontSize: "48px", color: "#666" },
|
||||||
|
}),
|
||||||
|
!category
|
||||||
|
? el(
|
||||||
|
"div",
|
||||||
|
{},
|
||||||
|
el(
|
||||||
|
"p",
|
||||||
|
{ style: { marginBottom: "15px" } },
|
||||||
|
__("Category Events Display", "ticket-tailor"),
|
||||||
|
),
|
||||||
|
el(
|
||||||
|
"p",
|
||||||
|
{ style: { fontSize: "14px", color: "#666" } },
|
||||||
|
__(
|
||||||
|
"Enter a category name in the sidebar to display filtered events",
|
||||||
|
"ticket-tailor",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: el(
|
||||||
|
"div",
|
||||||
|
{},
|
||||||
|
el(
|
||||||
|
"p",
|
||||||
|
{ style: { marginBottom: "10px", fontWeight: "bold" } },
|
||||||
|
__("Category Events: ", "ticket-tailor") + category,
|
||||||
|
),
|
||||||
|
el(
|
||||||
|
"p",
|
||||||
|
{ style: { fontSize: "14px", color: "#666" } },
|
||||||
|
__("Showing ", "ticket-tailor") +
|
||||||
|
limit +
|
||||||
|
__(" events", "ticket-tailor"),
|
||||||
|
),
|
||||||
|
fullWidth &&
|
||||||
|
el(
|
||||||
|
"p",
|
||||||
|
{
|
||||||
|
style: {
|
||||||
|
fontSize: "12px",
|
||||||
|
color: "#2271b1",
|
||||||
|
marginTop: "5px",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
__("✓ Full width enabled", "ticket-tailor"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
save: function () {
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})(window.wp);
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
<?php
|
||||||
|
// Silence is golden.
|
||||||
1436
native/wordpress/ticket-tailor-wp-max/includes/class-admin.php
Normal file
1436
native/wordpress/ticket-tailor-wp-max/includes/class-admin.php
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,544 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Ticket Tailor API Client
|
||||||
|
*
|
||||||
|
* Handles all communication with the Ticket Tailor API
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Exit if accessed directly
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Ticket_Tailor_API_Client {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API Base URL
|
||||||
|
*/
|
||||||
|
private $base_url = 'https://api.tickettailor.com/v1/';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API Key
|
||||||
|
*/
|
||||||
|
private $api_key;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rate limit tracking
|
||||||
|
*/
|
||||||
|
private $rate_limit_remaining;
|
||||||
|
private $rate_limit_reset;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retry configuration - ENTERPRISE: Exponential backoff
|
||||||
|
*/
|
||||||
|
private $max_retries = 3;
|
||||||
|
private $retry_delay_ms = 1000; // 1 second base delay
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor - SECURITY ENHANCEMENT: Use encrypted API key
|
||||||
|
*/
|
||||||
|
public function __construct() {
|
||||||
|
$this->api_key = $this->get_api_key();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get decrypted API key - SECURITY ENHANCEMENT
|
||||||
|
*/
|
||||||
|
private function get_api_key() {
|
||||||
|
// Check for encrypted key first
|
||||||
|
$encrypted = get_option('ticket_tailor_api_key_encrypted', '');
|
||||||
|
|
||||||
|
if (!empty($encrypted)) {
|
||||||
|
return $this->decrypt_api_key($encrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to plain text key (for migration)
|
||||||
|
$plain_key = get_option('ticket_tailor_api_key', '');
|
||||||
|
|
||||||
|
// If plain key exists, encrypt it and migrate
|
||||||
|
if (!empty($plain_key)) {
|
||||||
|
$this->migrate_api_key($plain_key);
|
||||||
|
return $plain_key;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypt API key - SECURITY ENHANCEMENT
|
||||||
|
*/
|
||||||
|
private function encrypt_api_key($key) {
|
||||||
|
if (!function_exists('openssl_encrypt') || !function_exists('wp_salt')) {
|
||||||
|
// Fallback if OpenSSL or wp_salt not available
|
||||||
|
return base64_encode($key);
|
||||||
|
}
|
||||||
|
|
||||||
|
$method = 'AES-256-CBC';
|
||||||
|
$salt = wp_salt('auth');
|
||||||
|
$encryption_key = substr(hash('sha256', $salt), 0, 32);
|
||||||
|
$iv = substr(hash('sha256', wp_salt('nonce')), 0, 16);
|
||||||
|
|
||||||
|
$encrypted = openssl_encrypt($key, $method, $encryption_key, 0, $iv);
|
||||||
|
|
||||||
|
return base64_encode($encrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypt API key - SECURITY ENHANCEMENT
|
||||||
|
*/
|
||||||
|
private function decrypt_api_key($encrypted) {
|
||||||
|
if (!function_exists('openssl_decrypt') || !function_exists('wp_salt')) {
|
||||||
|
// Fallback if OpenSSL or wp_salt not available
|
||||||
|
return base64_decode($encrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
$method = 'AES-256-CBC';
|
||||||
|
$salt = wp_salt('auth');
|
||||||
|
$encryption_key = substr(hash('sha256', $salt), 0, 32);
|
||||||
|
$iv = substr(hash('sha256', wp_salt('nonce')), 0, 16);
|
||||||
|
|
||||||
|
$decrypted = openssl_decrypt(base64_decode($encrypted), $method, $encryption_key, 0, $iv);
|
||||||
|
|
||||||
|
return $decrypted !== false ? $decrypted : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrate plain text API key to encrypted - SECURITY ENHANCEMENT
|
||||||
|
*/
|
||||||
|
private function migrate_api_key($plain_key) {
|
||||||
|
$encrypted = $this->encrypt_api_key($plain_key);
|
||||||
|
update_option('ticket_tailor_api_key_encrypted', $encrypted);
|
||||||
|
|
||||||
|
// Remove plain text key
|
||||||
|
delete_option('ticket_tailor_api_key');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if API is configured
|
||||||
|
*/
|
||||||
|
public function is_configured() {
|
||||||
|
return !empty($this->api_key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make API request with retry logic - ENTERPRISE: Exponential backoff
|
||||||
|
*/
|
||||||
|
private function request($endpoint, $method = 'GET', $data = array()) {
|
||||||
|
if (!$this->is_configured()) {
|
||||||
|
return new WP_Error('no_api_key', __('API key not configured', 'ticket-tailor'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$attempt = 0;
|
||||||
|
$last_error = null;
|
||||||
|
|
||||||
|
while ($attempt < $this->max_retries) {
|
||||||
|
$attempt++;
|
||||||
|
|
||||||
|
$result = $this->make_request_attempt($endpoint, $method, $data, $attempt);
|
||||||
|
|
||||||
|
// Success - return immediately
|
||||||
|
if (!is_wp_error($result)) {
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
$last_error = $result;
|
||||||
|
$error_data = $result->get_error_data();
|
||||||
|
|
||||||
|
// Don't retry on client errors (4xx except 429)
|
||||||
|
if (isset($error_data['status'])) {
|
||||||
|
$status = $error_data['status'];
|
||||||
|
|
||||||
|
// Retry only on: 429 (rate limit), 5xx (server errors), network errors
|
||||||
|
if ($status >= 400 && $status < 500 && $status !== 429) {
|
||||||
|
// Client error - don't retry
|
||||||
|
error_log(sprintf(
|
||||||
|
'Ticket Tailor API: Client error %d on %s, not retrying',
|
||||||
|
$status,
|
||||||
|
$endpoint
|
||||||
|
));
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have more retries, wait with exponential backoff
|
||||||
|
if ($attempt < $this->max_retries) {
|
||||||
|
$delay = $this->calculate_backoff_delay($attempt, $error_data);
|
||||||
|
|
||||||
|
error_log(sprintf(
|
||||||
|
'Ticket Tailor API: Attempt %d/%d failed for %s, retrying in %dms',
|
||||||
|
$attempt,
|
||||||
|
$this->max_retries,
|
||||||
|
$endpoint,
|
||||||
|
$delay
|
||||||
|
));
|
||||||
|
|
||||||
|
usleep($delay * 1000); // Convert ms to microseconds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// All retries exhausted
|
||||||
|
error_log(sprintf(
|
||||||
|
'Ticket Tailor API: All %d attempts failed for %s: %s',
|
||||||
|
$this->max_retries,
|
||||||
|
$endpoint,
|
||||||
|
$last_error->get_error_message()
|
||||||
|
));
|
||||||
|
|
||||||
|
return $last_error;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make single API request attempt - ENTERPRISE: Separated for retry logic
|
||||||
|
*/
|
||||||
|
private function make_request_attempt($endpoint, $method, $data, $attempt_number) {
|
||||||
|
$url = $this->base_url . ltrim($endpoint, '/');
|
||||||
|
|
||||||
|
$args = array(
|
||||||
|
'method' => $method,
|
||||||
|
'headers' => array(
|
||||||
|
'Accept' => 'application/json',
|
||||||
|
'Authorization' => 'Basic ' . base64_encode($this->api_key . ':'),
|
||||||
|
'X-TT-Request-ID' => wp_generate_uuid4(), // ENTERPRISE: Request tracking
|
||||||
|
),
|
||||||
|
'timeout' => 30,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!empty($data)) {
|
||||||
|
if ($method === 'GET') {
|
||||||
|
$url = add_query_arg($data, $url);
|
||||||
|
} else {
|
||||||
|
$args['body'] = wp_json_encode($data);
|
||||||
|
$args['headers']['Content-Type'] = 'application/json';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = wp_remote_request($url, $args);
|
||||||
|
|
||||||
|
// Handle network errors
|
||||||
|
if (is_wp_error($response)) {
|
||||||
|
return new WP_Error(
|
||||||
|
'network_error',
|
||||||
|
sprintf('Network error: %s', $response->get_error_message()),
|
||||||
|
array(
|
||||||
|
'attempt' => $attempt_number,
|
||||||
|
'endpoint' => $endpoint,
|
||||||
|
'original_error' => $response->get_error_code()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track rate limits
|
||||||
|
$headers = wp_remote_retrieve_headers($response);
|
||||||
|
if (isset($headers['x-rate-limit-remaining'])) {
|
||||||
|
$this->rate_limit_remaining = (int) $headers['x-rate-limit-remaining'];
|
||||||
|
}
|
||||||
|
if (isset($headers['x-rate-limit-reset'])) {
|
||||||
|
$this->rate_limit_reset = (int) $headers['x-rate-limit-reset'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$code = wp_remote_retrieve_response_code($response);
|
||||||
|
$body = wp_remote_retrieve_body($response);
|
||||||
|
|
||||||
|
// Handle HTTP errors
|
||||||
|
if ($code < 200 || $code >= 300) {
|
||||||
|
$error_data = json_decode($body, true);
|
||||||
|
$message = isset($error_data['message']) ? $error_data['message'] : 'API request failed';
|
||||||
|
|
||||||
|
return new WP_Error(
|
||||||
|
'api_error',
|
||||||
|
sprintf('%s (HTTP %d)', $message, $code),
|
||||||
|
array(
|
||||||
|
'status' => $code,
|
||||||
|
'response' => $error_data,
|
||||||
|
'attempt' => $attempt_number,
|
||||||
|
'endpoint' => $endpoint,
|
||||||
|
'rate_limit_remaining' => $this->rate_limit_remaining
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$decoded = json_decode($body, true);
|
||||||
|
|
||||||
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||||
|
return new WP_Error(
|
||||||
|
'json_decode_error',
|
||||||
|
sprintf('Failed to decode API response: %s', json_last_error_msg()),
|
||||||
|
array(
|
||||||
|
'attempt' => $attempt_number,
|
||||||
|
'endpoint' => $endpoint,
|
||||||
|
'body_preview' => substr($body, 0, 200)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $decoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate backoff delay - ENTERPRISE: Exponential backoff with jitter
|
||||||
|
*/
|
||||||
|
private function calculate_backoff_delay($attempt, $error_data = array()) {
|
||||||
|
// For rate limits, use the reset time if available
|
||||||
|
if (isset($error_data['status']) && $error_data['status'] === 429) {
|
||||||
|
if ($this->rate_limit_reset) {
|
||||||
|
$wait_time = max(0, $this->rate_limit_reset - time());
|
||||||
|
return min($wait_time * 1000, 60000); // Max 60 seconds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exponential backoff: delay = base * (2 ^ attempt)
|
||||||
|
$delay = $this->retry_delay_ms * pow(2, $attempt - 1);
|
||||||
|
|
||||||
|
// Add jitter (random ±25%)
|
||||||
|
$jitter = $delay * (rand(75, 125) / 100);
|
||||||
|
|
||||||
|
// Cap at 30 seconds
|
||||||
|
return min($jitter, 30000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all events
|
||||||
|
*/
|
||||||
|
public function get_events($args = array()) {
|
||||||
|
$defaults = array(
|
||||||
|
'limit' => 100,
|
||||||
|
);
|
||||||
|
|
||||||
|
$args = wp_parse_args($args, $defaults);
|
||||||
|
|
||||||
|
return $this->request('events', 'GET', $args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get single event - SECURITY FIX: Added length validation
|
||||||
|
*/
|
||||||
|
public function get_event($event_id) {
|
||||||
|
$event_id = sanitize_text_field($event_id);
|
||||||
|
|
||||||
|
// SECURITY FIX: Validate ID length (reasonable limit)
|
||||||
|
if (strlen($event_id) > 100) {
|
||||||
|
return new WP_Error('invalid_id', __('Invalid event ID', 'ticket-tailor'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->request("events/{$event_id}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get event ticket types - SECURITY FIX: Added length validation
|
||||||
|
*/
|
||||||
|
public function get_ticket_types($event_id) {
|
||||||
|
$event_id = sanitize_text_field($event_id);
|
||||||
|
|
||||||
|
// SECURITY FIX: Validate ID length
|
||||||
|
if (strlen($event_id) > 100) {
|
||||||
|
return new WP_Error('invalid_id', __('Invalid event ID', 'ticket-tailor'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->request("events/{$event_id}/ticket_types");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get orders
|
||||||
|
*/
|
||||||
|
public function get_orders($args = array()) {
|
||||||
|
$defaults = array(
|
||||||
|
'limit' => 100,
|
||||||
|
);
|
||||||
|
|
||||||
|
$args = wp_parse_args($args, $defaults);
|
||||||
|
|
||||||
|
return $this->request('orders', 'GET', $args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get single order - SECURITY FIX: Added length validation
|
||||||
|
*/
|
||||||
|
public function get_order($order_id) {
|
||||||
|
$order_id = sanitize_text_field($order_id);
|
||||||
|
|
||||||
|
// SECURITY FIX: Validate ID length
|
||||||
|
if (strlen($order_id) > 100) {
|
||||||
|
return new WP_Error('invalid_id', __('Invalid order ID', 'ticket-tailor'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->request("orders/{$order_id}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get issued tickets for an order - SECURITY FIX: Added length validation
|
||||||
|
*/
|
||||||
|
public function get_order_tickets($order_id) {
|
||||||
|
$order_id = sanitize_text_field($order_id);
|
||||||
|
|
||||||
|
// SECURITY FIX: Validate ID length
|
||||||
|
if (strlen($order_id) > 100) {
|
||||||
|
return new WP_Error('invalid_id', __('Invalid order ID', 'ticket-tailor'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->request("orders/{$order_id}/issued_tickets");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get box office overview
|
||||||
|
*/
|
||||||
|
public function get_overview() {
|
||||||
|
return $this->request('overview');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a hold - SECURITY FIX: Added validation
|
||||||
|
*/
|
||||||
|
public function create_hold($ticket_type_id, $quantity) {
|
||||||
|
$ticket_type_id = sanitize_text_field($ticket_type_id);
|
||||||
|
$quantity = absint($quantity);
|
||||||
|
|
||||||
|
// SECURITY FIX: Validate inputs
|
||||||
|
if (strlen($ticket_type_id) > 100) {
|
||||||
|
return new WP_Error('invalid_id', __('Invalid ticket type ID', 'ticket-tailor'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($quantity < 1 || $quantity > 100) {
|
||||||
|
return new WP_Error('invalid_quantity', __('Invalid quantity (must be 1-100)', 'ticket-tailor'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = array(
|
||||||
|
'ticket_type_id' => $ticket_type_id,
|
||||||
|
'quantity' => $quantity,
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->request('holds', 'POST', $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get voucher codes
|
||||||
|
*/
|
||||||
|
public function get_voucher_codes($args = array()) {
|
||||||
|
return $this->request('voucher_codes', 'GET', $args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get discount codes
|
||||||
|
*/
|
||||||
|
public function get_discount_codes($args = array()) {
|
||||||
|
return $this->request('discount_codes', 'GET', $args);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Paginate through all results - PERFORMANCE FIX: Removed array_merge memory leak
|
||||||
|
*/
|
||||||
|
public function get_all_paginated($endpoint, $args = array()) {
|
||||||
|
$all_results = array();
|
||||||
|
$args['limit'] = 100;
|
||||||
|
$iteration_count = 0;
|
||||||
|
$max_iterations = 100; // 100 * 100 = 10,000 items max
|
||||||
|
$start_time = time();
|
||||||
|
$max_execution_time = 60; // 60 seconds max
|
||||||
|
|
||||||
|
do {
|
||||||
|
$response = $this->request($endpoint, 'GET', $args);
|
||||||
|
|
||||||
|
if (is_wp_error($response)) {
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PERFORMANCE FIX: Use array push instead of array_merge
|
||||||
|
if (isset($response['data']) && is_array($response['data'])) {
|
||||||
|
foreach ($response['data'] as $item) {
|
||||||
|
$all_results[] = $item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for next page
|
||||||
|
$has_more = false;
|
||||||
|
if (isset($response['pagination']['has_more']) && $response['pagination']['has_more']) {
|
||||||
|
$last_item = end($response['data']);
|
||||||
|
if (isset($last_item['id'])) {
|
||||||
|
$args['starting_after'] = $last_item['id'];
|
||||||
|
$has_more = true;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Safety checks to prevent infinite loops
|
||||||
|
$iteration_count++;
|
||||||
|
if ($iteration_count >= $max_iterations) {
|
||||||
|
error_log('Ticket Tailor API: Reached maximum iterations (' . $max_iterations . ')');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timeout protection
|
||||||
|
if ((time() - $start_time) >= $max_execution_time) {
|
||||||
|
error_log('Ticket Tailor API: Reached maximum execution time (' . $max_execution_time . 's)');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Item count protection
|
||||||
|
if (count($all_results) >= 10000) {
|
||||||
|
error_log('Ticket Tailor API: Reached maximum items (10000)');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
} while ($has_more);
|
||||||
|
|
||||||
|
return $all_results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test API connection
|
||||||
|
*/
|
||||||
|
public function test_connection() {
|
||||||
|
$response = $this->request('events', 'GET', array('limit' => 1));
|
||||||
|
|
||||||
|
if (is_wp_error($response)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get rate limit info
|
||||||
|
*/
|
||||||
|
public function get_rate_limit_info() {
|
||||||
|
return array(
|
||||||
|
'remaining' => $this->rate_limit_remaining,
|
||||||
|
'reset' => $this->rate_limit_reset,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set API key - SECURITY ENHANCEMENT: Store encrypted
|
||||||
|
*/
|
||||||
|
public function set_api_key($api_key) {
|
||||||
|
$api_key = sanitize_text_field($api_key);
|
||||||
|
|
||||||
|
// Get old key hash for logging
|
||||||
|
$old_key_hash = hash('sha256', $this->api_key);
|
||||||
|
|
||||||
|
$this->api_key = $api_key;
|
||||||
|
|
||||||
|
// Store encrypted
|
||||||
|
$encrypted = $this->encrypt_api_key($api_key);
|
||||||
|
update_option('ticket_tailor_api_key_encrypted', $encrypted);
|
||||||
|
|
||||||
|
// Remove plain text key if it exists
|
||||||
|
delete_option('ticket_tailor_api_key');
|
||||||
|
|
||||||
|
// Log the change
|
||||||
|
$security_logger = new Ticket_Tailor_Security_Logger();
|
||||||
|
$security_logger->log_api_key_change($old_key_hash, hash('sha256', $api_key));
|
||||||
|
|
||||||
|
// Test the connection
|
||||||
|
return $this->test_connection();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear API key - SECURITY ENHANCEMENT: Clear encrypted key
|
||||||
|
*/
|
||||||
|
public function clear_api_key() {
|
||||||
|
$this->api_key = '';
|
||||||
|
delete_option('ticket_tailor_api_key_encrypted');
|
||||||
|
delete_option('ticket_tailor_api_key');
|
||||||
|
}
|
||||||
|
}
|
||||||
547
native/wordpress/ticket-tailor-wp-max/includes/class-blocks.php
Normal file
547
native/wordpress/ticket-tailor-wp-max/includes/class-blocks.php
Normal file
|
|
@ -0,0 +1,547 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Ticket Tailor Blocks
|
||||||
|
* Enhanced with full-width support and responsive columns
|
||||||
|
* FIXED: Removed spacers that were making cards too tall
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Exit if accessed directly
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Ticket_Tailor_Blocks {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event Manager
|
||||||
|
*/
|
||||||
|
private $events;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*/
|
||||||
|
public function __construct($events) {
|
||||||
|
$this->events = $events;
|
||||||
|
|
||||||
|
add_action('init', array($this, 'register_blocks'));
|
||||||
|
add_action('enqueue_block_editor_assets', array($this, 'enqueue_editor_assets'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register blocks
|
||||||
|
*/
|
||||||
|
public function register_blocks() {
|
||||||
|
// Event Widget Block (Original)
|
||||||
|
register_block_type('ticket-tailor/event-widget', array(
|
||||||
|
'render_callback' => array($this, 'render_event_widget_block'),
|
||||||
|
'attributes' => array(
|
||||||
|
'url' => array('type' => 'string', 'default' => ''),
|
||||||
|
'minimal' => array('type' => 'boolean', 'default' => false),
|
||||||
|
'bgFill' => array('type' => 'boolean', 'default' => true),
|
||||||
|
'showLogo' => array('type' => 'boolean', 'default' => true),
|
||||||
|
'ref' => array('type' => 'string', 'default' => 'website_widget'),
|
||||||
|
'align' => array('type' => 'string', 'default' => ''),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
// Event Listing Block - ENHANCED
|
||||||
|
register_block_type('ticket-tailor/event-listing', array(
|
||||||
|
'render_callback' => array($this, 'render_event_listing_block'),
|
||||||
|
'attributes' => array(
|
||||||
|
'limit' => array('type' => 'number', 'default' => 10),
|
||||||
|
'layout' => array('type' => 'string', 'default' => 'grid'),
|
||||||
|
'columns' => array('type' => 'number', 'default' => 3),
|
||||||
|
'columnsMode' => array('type' => 'string', 'default' => 'fixed'),
|
||||||
|
'showPast' => array('type' => 'boolean', 'default' => false),
|
||||||
|
'showImage' => array('type' => 'boolean', 'default' => true),
|
||||||
|
'imageType' => array('type' => 'string', 'default' => 'thumbnail'),
|
||||||
|
'fullWidth' => array('type' => 'boolean', 'default' => true),
|
||||||
|
'maxCardWidth' => array('type' => 'string', 'default' => 'none'),
|
||||||
|
'align' => array('type' => 'string', 'default' => ''),
|
||||||
|
'className' => array('type' => 'string', 'default' => ''),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
// Single Event Block - ENHANCED
|
||||||
|
register_block_type('ticket-tailor/single-event', array(
|
||||||
|
'render_callback' => array($this, 'render_single_event_block'),
|
||||||
|
'attributes' => array(
|
||||||
|
'eventId' => array('type' => 'string', 'default' => ''),
|
||||||
|
'showDescription' => array('type' => 'boolean', 'default' => true),
|
||||||
|
'showTickets' => array('type' => 'boolean', 'default' => true),
|
||||||
|
'showImage' => array('type' => 'boolean', 'default' => true),
|
||||||
|
'imageType' => array('type' => 'string', 'default' => 'header'),
|
||||||
|
'fullWidth' => array('type' => 'boolean', 'default' => false),
|
||||||
|
'maxWidth' => array('type' => 'string', 'default' => '800px'),
|
||||||
|
'align' => array('type' => 'string', 'default' => ''),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
// Category Events Block - ENHANCED
|
||||||
|
register_block_type('ticket-tailor/category-events', array(
|
||||||
|
'render_callback' => array($this, 'render_category_events_block'),
|
||||||
|
'attributes' => array(
|
||||||
|
'category' => array('type' => 'string', 'default' => ''),
|
||||||
|
'limit' => array('type' => 'number', 'default' => 10),
|
||||||
|
'layout' => array('type' => 'string', 'default' => 'grid'),
|
||||||
|
'columns' => array('type' => 'number', 'default' => 3),
|
||||||
|
'columnsMode' => array('type' => 'string', 'default' => 'fixed'),
|
||||||
|
'showImage' => array('type' => 'boolean', 'default' => true),
|
||||||
|
'imageType' => array('type' => 'string', 'default' => 'thumbnail'),
|
||||||
|
'fullWidth' => array('type' => 'boolean', 'default' => true),
|
||||||
|
'align' => array('type' => 'string', 'default' => ''),
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enqueue editor assets
|
||||||
|
*/
|
||||||
|
public function enqueue_editor_assets() {
|
||||||
|
wp_enqueue_script(
|
||||||
|
'ticket-tailor-blocks',
|
||||||
|
TICKET_TAILOR_PLUGIN_URL . 'assets/js/blocks.js',
|
||||||
|
array('wp-blocks', 'wp-element', 'wp-components', 'wp-block-editor', 'wp-data'),
|
||||||
|
TICKET_TAILOR_VERSION,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
// Pass events data to editor
|
||||||
|
$events = $this->events->get_events();
|
||||||
|
if (!is_wp_error($events)) {
|
||||||
|
$event_options = array();
|
||||||
|
foreach ($events as $event) {
|
||||||
|
$event_options[] = array(
|
||||||
|
'value' => $event['id'],
|
||||||
|
'label' => $event['name'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_localize_script('ticket-tailor-blocks', 'ticketTailorData', array(
|
||||||
|
'events' => $event_options,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_enqueue_style(
|
||||||
|
'ticket-tailor-blocks-editor',
|
||||||
|
TICKET_TAILOR_PLUGIN_URL . 'assets/css/blocks-editor.css',
|
||||||
|
array('wp-edit-blocks'),
|
||||||
|
TICKET_TAILOR_VERSION
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render event widget block
|
||||||
|
*/
|
||||||
|
public function render_event_widget_block($attributes) {
|
||||||
|
$url = $attributes['url'] ?? '';
|
||||||
|
$align = $attributes['align'] ?? '';
|
||||||
|
|
||||||
|
if (empty($url)) {
|
||||||
|
return '<div class="tt-block-placeholder">' . esc_html__('Please enter an event URL', 'ticket-tailor') . '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$minimal = $attributes['minimal'] ?? false;
|
||||||
|
$bg_fill = $attributes['bgFill'] ?? true;
|
||||||
|
$show_logo = $attributes['showLogo'] ?? true;
|
||||||
|
$ref = $attributes['ref'] ?? 'website_widget';
|
||||||
|
|
||||||
|
// Add alignment class
|
||||||
|
$wrapper_class = 'tt-widget-block';
|
||||||
|
if (!empty($align)) {
|
||||||
|
$wrapper_class .= ' align' . esc_attr($align);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use shortcode renderer
|
||||||
|
$shortcode = new Ticket_Tailor_Shortcodes($this->events, null);
|
||||||
|
$widget_html = $shortcode->event_widget_shortcode(array(
|
||||||
|
'url' => $url,
|
||||||
|
'minimal' => $minimal ? 'true' : 'false',
|
||||||
|
'bg_fill' => $bg_fill ? 'true' : 'false',
|
||||||
|
'show_logo' => $show_logo ? 'true' : 'false',
|
||||||
|
'ref' => $ref,
|
||||||
|
));
|
||||||
|
|
||||||
|
return '<div class="' . esc_attr($wrapper_class) . '">' . $widget_html . '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render event listing block - ENHANCED
|
||||||
|
*/
|
||||||
|
public function render_event_listing_block($attributes) {
|
||||||
|
$limit = $attributes['limit'] ?? 10;
|
||||||
|
$layout = $attributes['layout'] ?? 'grid';
|
||||||
|
$columns = $attributes['columns'] ?? 3;
|
||||||
|
$columns_mode = $attributes['columnsMode'] ?? 'fixed';
|
||||||
|
$show_past = $attributes['showPast'] ?? false;
|
||||||
|
$show_image = $attributes['showImage'] ?? true;
|
||||||
|
$image_type = $attributes['imageType'] ?? 'thumbnail';
|
||||||
|
$full_width = $attributes['fullWidth'] ?? true;
|
||||||
|
$max_card_width = $attributes['maxCardWidth'] ?? 'none';
|
||||||
|
$align = $attributes['align'] ?? '';
|
||||||
|
$className = $attributes['className'] ?? '';
|
||||||
|
|
||||||
|
// Sanitize image type
|
||||||
|
if (!in_array($image_type, array('thumbnail', 'header'))) {
|
||||||
|
$image_type = 'thumbnail';
|
||||||
|
}
|
||||||
|
|
||||||
|
$events = $show_past ? $this->events->get_past_events($limit) : $this->events->get_upcoming_events($limit);
|
||||||
|
|
||||||
|
if (is_wp_error($events)) {
|
||||||
|
return '<div class="tt-error">' . esc_html($events->get_error_message()) . '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($events)) {
|
||||||
|
return '<div class="tt-no-events">' . esc_html__('No events found', 'ticket-tailor') . '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
|
||||||
|
// Build wrapper classes
|
||||||
|
$wrapper_classes = array('tt-event-listing-wrapper');
|
||||||
|
if ($full_width) {
|
||||||
|
$wrapper_classes[] = 'tt-full-width';
|
||||||
|
}
|
||||||
|
if (!empty($align)) {
|
||||||
|
$wrapper_classes[] = 'align' . esc_attr($align);
|
||||||
|
}
|
||||||
|
if (!empty($className)) {
|
||||||
|
$wrapper_classes[] = esc_attr($className);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build listing classes
|
||||||
|
$class = 'tt-event-listing tt-layout-' . esc_attr($layout);
|
||||||
|
if ($layout === 'grid') {
|
||||||
|
if ($columns_mode === 'responsive') {
|
||||||
|
$class .= ' tt-columns-responsive tt-max-columns-' . esc_attr($columns);
|
||||||
|
} else {
|
||||||
|
$class .= ' tt-columns-' . esc_attr($columns);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add inline styles ONLY for responsive mode, not for fixed mode
|
||||||
|
$style = '';
|
||||||
|
if ($layout === 'grid' && $columns_mode === 'responsive') {
|
||||||
|
$min_width = $max_card_width === 'small' ? '280px' :
|
||||||
|
($max_card_width === 'medium' ? '350px' :
|
||||||
|
($max_card_width === 'large' ? '450px' : '320px'));
|
||||||
|
$style = 'style="grid-template-columns: repeat(auto-fit, minmax(' . esc_attr($min_width) . ', 1fr));"';
|
||||||
|
}
|
||||||
|
// No inline styles for fixed mode - let CSS classes handle it
|
||||||
|
?>
|
||||||
|
<div class="<?php echo esc_attr(implode(' ', $wrapper_classes)); ?>">
|
||||||
|
<div class="<?php echo esc_attr($class); ?>" <?php echo $style; ?>>
|
||||||
|
<?php foreach ($events as $event) : ?>
|
||||||
|
<div class="tt-event-card">
|
||||||
|
<?php if ($show_image && !empty($event['images'][$image_type])) : ?>
|
||||||
|
<div class="tt-event-image tt-image-<?php echo esc_attr($image_type); ?>">
|
||||||
|
<img src="<?php echo esc_url($event['images'][$image_type]); ?>"
|
||||||
|
alt="<?php echo esc_attr($event['name']); ?>"
|
||||||
|
loading="lazy">
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="tt-event-content">
|
||||||
|
<h3 class="tt-event-title"><?php echo esc_html($event['name']); ?></h3>
|
||||||
|
|
||||||
|
<?php if (!empty($event['start']['iso'])) : ?>
|
||||||
|
<div class="tt-event-date">
|
||||||
|
<span class="dashicons dashicons-calendar-alt"></span>
|
||||||
|
<?php echo esc_html(date_i18n(get_option('date_format') . ' ' . get_option('time_format'), strtotime($event['start']['iso']))); ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (!empty($event['venue']['name'])) : ?>
|
||||||
|
<div class="tt-event-venue">
|
||||||
|
<span class="dashicons dashicons-location"></span>
|
||||||
|
<?php echo esc_html($event['venue']['name']); ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($layout === 'list' && !empty($event['description'])) : ?>
|
||||||
|
<div class="tt-event-excerpt">
|
||||||
|
<?php echo esc_html(wp_trim_words($event['description'], 20)); ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php /* REMOVED SPACER - was making cards too tall
|
||||||
|
<!-- Spacer pushes button to bottom -->
|
||||||
|
<div class="tt-event-spacer"></div>
|
||||||
|
*/ ?>
|
||||||
|
|
||||||
|
<!-- Button container -->
|
||||||
|
<?php if (!empty($event['url'])) : ?>
|
||||||
|
<div class="tt-event-button-container">
|
||||||
|
<a href="<?php echo esc_url($event['url']); ?>"
|
||||||
|
class="tt-event-button"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer">
|
||||||
|
<?php esc_html_e('Get Tickets', 'ticket-tailor'); ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return ob_get_clean();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render single event block - ENHANCED
|
||||||
|
*/
|
||||||
|
public function render_single_event_block($attributes) {
|
||||||
|
$event_id = $attributes['eventId'] ?? '';
|
||||||
|
$show_description = $attributes['showDescription'] ?? true;
|
||||||
|
$show_tickets = $attributes['showTickets'] ?? true;
|
||||||
|
$show_image = $attributes['showImage'] ?? true;
|
||||||
|
$image_type = $attributes['imageType'] ?? 'header';
|
||||||
|
$full_width = $attributes['fullWidth'] ?? false;
|
||||||
|
$max_width = $attributes['maxWidth'] ?? '800px';
|
||||||
|
$align = $attributes['align'] ?? '';
|
||||||
|
|
||||||
|
if (empty($event_id)) {
|
||||||
|
return '<div class="tt-block-placeholder">' . esc_html__('Please select an event', 'ticket-tailor') . '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanitize image type
|
||||||
|
if (!in_array($image_type, array('thumbnail', 'header'))) {
|
||||||
|
$image_type = 'thumbnail';
|
||||||
|
}
|
||||||
|
|
||||||
|
$event = $this->events->get_event($event_id);
|
||||||
|
|
||||||
|
if (is_wp_error($event)) {
|
||||||
|
return '<div class="tt-error">' . esc_html($event->get_error_message()) . '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
|
||||||
|
// Build wrapper classes
|
||||||
|
$wrapper_classes = array('tt-single-event-wrapper');
|
||||||
|
if (!empty($align)) {
|
||||||
|
$wrapper_classes[] = 'align' . esc_attr($align);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build inline styles
|
||||||
|
$style = '';
|
||||||
|
if (!$full_width && !empty($max_width)) {
|
||||||
|
$style = 'style="max-width: ' . esc_attr($max_width) . ';"';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<div class="<?php echo esc_attr(implode(' ', $wrapper_classes)); ?>">
|
||||||
|
<div class="tt-single-event" <?php echo $style; ?>>
|
||||||
|
<?php if ($show_image && !empty($event['images'][$image_type])) : ?>
|
||||||
|
<div class="tt-event-header-image">
|
||||||
|
<img src="<?php echo esc_url($event['images'][$image_type]); ?>"
|
||||||
|
alt="<?php echo esc_attr($event['name']); ?>"
|
||||||
|
loading="lazy">
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="tt-event-details">
|
||||||
|
<h2 class="tt-event-title"><?php echo esc_html($event['name']); ?></h2>
|
||||||
|
|
||||||
|
<div class="tt-event-meta">
|
||||||
|
<?php if (!empty($event['start']['iso'])) : ?>
|
||||||
|
<div class="tt-meta-item">
|
||||||
|
<span class="dashicons dashicons-calendar-alt"></span>
|
||||||
|
<strong><?php esc_html_e('Date:', 'ticket-tailor'); ?></strong>
|
||||||
|
<?php echo esc_html(date_i18n(get_option('date_format') . ' ' . get_option('time_format'), strtotime($event['start']['iso']))); ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (!empty($event['venue']['name'])) : ?>
|
||||||
|
<div class="tt-meta-item">
|
||||||
|
<span class="dashicons dashicons-location"></span>
|
||||||
|
<strong><?php esc_html_e('Venue:', 'ticket-tailor'); ?></strong>
|
||||||
|
<?php echo esc_html($event['venue']['name']); ?>
|
||||||
|
<?php if (!empty($event['venue']['address'])) : ?>
|
||||||
|
<br><small style="margin-left: 28px;"><?php echo esc_html($event['venue']['address']); ?></small>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (!empty($event['status'])) : ?>
|
||||||
|
<div class="tt-meta-item">
|
||||||
|
<span class="dashicons dashicons-info"></span>
|
||||||
|
<strong><?php esc_html_e('Status:', 'ticket-tailor'); ?></strong>
|
||||||
|
<span class="tt-status tt-status-<?php echo esc_attr($event['status']); ?>">
|
||||||
|
<?php echo esc_html($event['status']); ?>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($show_description && !empty($event['description'])) : ?>
|
||||||
|
<div class="tt-event-description">
|
||||||
|
<?php echo wp_kses_post(wpautop($event['description'])); ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($show_tickets && !empty($event['url'])) : ?>
|
||||||
|
<div class="tt-event-cta">
|
||||||
|
<a href="<?php echo esc_url($event['url']); ?>"
|
||||||
|
class="tt-button tt-button-primary"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer">
|
||||||
|
<?php esc_html_e('Buy Tickets', 'ticket-tailor'); ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return ob_get_clean();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render category events block - ENHANCED
|
||||||
|
*/
|
||||||
|
public function render_category_events_block($attributes) {
|
||||||
|
$category = $attributes['category'] ?? '';
|
||||||
|
$limit = $attributes['limit'] ?? 10;
|
||||||
|
$layout = $attributes['layout'] ?? 'grid';
|
||||||
|
$columns = $attributes['columns'] ?? 3;
|
||||||
|
$columns_mode = $attributes['columnsMode'] ?? 'fixed';
|
||||||
|
$show_image = $attributes['showImage'] ?? true;
|
||||||
|
$image_type = $attributes['imageType'] ?? 'thumbnail';
|
||||||
|
$full_width = $attributes['fullWidth'] ?? true;
|
||||||
|
$align = $attributes['align'] ?? '';
|
||||||
|
|
||||||
|
if (empty($category)) {
|
||||||
|
return '<div class="tt-block-placeholder">' . esc_html__('Please select a category', 'ticket-tailor') . '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanitize image type
|
||||||
|
if (!in_array($image_type, array('thumbnail', 'header'))) {
|
||||||
|
$image_type = 'thumbnail';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get events by category
|
||||||
|
$events = $this->events->get_events_by_category($category);
|
||||||
|
|
||||||
|
if (is_wp_error($events)) {
|
||||||
|
return '<div class="tt-error">' . esc_html($events->get_error_message()) . '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limit results
|
||||||
|
$events = array_slice($events, 0, $limit);
|
||||||
|
|
||||||
|
if (empty($events)) {
|
||||||
|
return '<div class="tt-no-events">' .
|
||||||
|
sprintf(
|
||||||
|
/* translators: %s: category name */
|
||||||
|
esc_html__('No events found in category: %s', 'ticket-tailor'),
|
||||||
|
esc_html($category)
|
||||||
|
) .
|
||||||
|
'</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
|
||||||
|
// Build wrapper classes
|
||||||
|
$wrapper_classes = array('tt-category-events-wrapper');
|
||||||
|
if ($full_width) {
|
||||||
|
$wrapper_classes[] = 'tt-full-width';
|
||||||
|
}
|
||||||
|
if (!empty($align)) {
|
||||||
|
$wrapper_classes[] = 'align' . esc_attr($align);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build listing classes
|
||||||
|
$class = 'tt-event-listing tt-category-listing tt-layout-' . esc_attr($layout);
|
||||||
|
if ($layout === 'grid') {
|
||||||
|
if ($columns_mode === 'responsive') {
|
||||||
|
$class .= ' tt-columns-responsive tt-max-columns-' . esc_attr($columns);
|
||||||
|
} else {
|
||||||
|
$class .= ' tt-columns-' . esc_attr($columns);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add inline styles ONLY for responsive mode, not for fixed mode
|
||||||
|
$style = '';
|
||||||
|
if ($layout === 'grid' && $columns_mode === 'responsive') {
|
||||||
|
$style = 'style="grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));"';
|
||||||
|
}
|
||||||
|
// No inline styles for fixed mode - let CSS classes handle it
|
||||||
|
?>
|
||||||
|
<div class="<?php echo esc_attr(implode(' ', $wrapper_classes)); ?>" data-category="<?php echo esc_attr($category); ?>">
|
||||||
|
<div class="tt-category-header">
|
||||||
|
<h2 class="tt-category-title">
|
||||||
|
<?php
|
||||||
|
printf(
|
||||||
|
/* translators: %s: category name */
|
||||||
|
esc_html__('%s Events', 'ticket-tailor'),
|
||||||
|
esc_html(ucfirst($category))
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="<?php echo esc_attr($class); ?>" <?php echo $style; ?>>
|
||||||
|
<?php foreach ($events as $event) : ?>
|
||||||
|
<div class="tt-event-card">
|
||||||
|
<?php if ($show_image && !empty($event['images'][$image_type])) : ?>
|
||||||
|
<div class="tt-event-image tt-image-<?php echo esc_attr($image_type); ?>">
|
||||||
|
<img src="<?php echo esc_url($event['images'][$image_type]); ?>"
|
||||||
|
alt="<?php echo esc_attr($event['name']); ?>"
|
||||||
|
loading="lazy">
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="tt-event-content">
|
||||||
|
<h3 class="tt-event-title"><?php echo esc_html($event['name']); ?></h3>
|
||||||
|
|
||||||
|
<?php if (!empty($event['start']['iso'])) : ?>
|
||||||
|
<div class="tt-event-date">
|
||||||
|
<span class="dashicons dashicons-calendar-alt"></span>
|
||||||
|
<?php echo esc_html(date_i18n(get_option('date_format') . ' ' . get_option('time_format'), strtotime($event['start']['iso']))); ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (!empty($event['venue']['name'])) : ?>
|
||||||
|
<div class="tt-event-venue">
|
||||||
|
<span class="dashicons dashicons-location"></span>
|
||||||
|
<?php echo esc_html($event['venue']['name']); ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($layout === 'list' && !empty($event['description'])) : ?>
|
||||||
|
<div class="tt-event-excerpt">
|
||||||
|
<?php echo esc_html(wp_trim_words($event['description'], 20)); ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php /* REMOVED SPACER - was making cards too tall
|
||||||
|
<!-- Spacer pushes button to bottom -->
|
||||||
|
<div class="tt-event-spacer"></div>
|
||||||
|
*/ ?>
|
||||||
|
|
||||||
|
<!-- Button container -->
|
||||||
|
<?php if (!empty($event['url'])) : ?>
|
||||||
|
<div class="tt-event-button-container">
|
||||||
|
<a href="<?php echo esc_url($event['url']); ?>"
|
||||||
|
class="tt-event-button"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer">
|
||||||
|
<?php esc_html_e('Get Tickets', 'ticket-tailor'); ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return ob_get_clean();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,516 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Ticket Tailor Event Manager
|
||||||
|
*
|
||||||
|
* Handles event caching, syncing, and retrieval
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Exit if accessed directly
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Ticket_Tailor_Event_Manager {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API Client
|
||||||
|
*/
|
||||||
|
private $api;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Table name
|
||||||
|
*/
|
||||||
|
private $table;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache duration (in seconds)
|
||||||
|
*/
|
||||||
|
private $cache_duration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*/
|
||||||
|
public function __construct($api) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$this->api = $api;
|
||||||
|
$this->table = $wpdb->prefix . 'ticket_tailor_events';
|
||||||
|
$this->cache_duration = get_option('ticket_tailor_cache_duration', 3600); // 1 hour default
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all events (from cache or API) - PERFORMANCE FIX: Added limit parameter
|
||||||
|
*/
|
||||||
|
public function get_events($force_refresh = false, $limit = null) {
|
||||||
|
if ($force_refresh) {
|
||||||
|
return $this->sync_all_events();
|
||||||
|
}
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
// Check if cache is still valid
|
||||||
|
$cache_valid_time = gmdate('Y-m-d H:i:s', time() - $this->cache_duration);
|
||||||
|
|
||||||
|
// Build query with optional limit
|
||||||
|
$query = $wpdb->prepare(
|
||||||
|
"SELECT event_data FROM {$this->table} WHERE last_synced > %s ORDER BY last_synced DESC",
|
||||||
|
$cache_valid_time
|
||||||
|
);
|
||||||
|
|
||||||
|
// PERFORMANCE FIX: Add LIMIT if specified
|
||||||
|
if ($limit !== null && is_numeric($limit)) {
|
||||||
|
$query .= $wpdb->prepare(" LIMIT %d", absint($limit));
|
||||||
|
}
|
||||||
|
|
||||||
|
$cached_events = $wpdb->get_results($query);
|
||||||
|
|
||||||
|
if (!empty($cached_events)) {
|
||||||
|
$events = array();
|
||||||
|
foreach ($cached_events as $row) {
|
||||||
|
$event_data = json_decode($row->event_data, true);
|
||||||
|
if ($event_data) {
|
||||||
|
$events[] = $event_data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $events;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache is expired or empty, fetch from API
|
||||||
|
$all_events = $this->sync_all_events();
|
||||||
|
|
||||||
|
// Apply limit to synced events if specified
|
||||||
|
if ($limit !== null && is_array($all_events)) {
|
||||||
|
return array_slice($all_events, 0, absint($limit));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $all_events;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get single event
|
||||||
|
*/
|
||||||
|
public function get_event($event_id, $force_refresh = false) {
|
||||||
|
$event_id = sanitize_text_field($event_id);
|
||||||
|
|
||||||
|
if (!$force_refresh) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$cache_valid_time = gmdate('Y-m-d H:i:s', time() - $this->cache_duration);
|
||||||
|
|
||||||
|
$cached = $wpdb->get_var(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT event_data FROM {$this->table} WHERE event_id = %s AND last_synced > %s",
|
||||||
|
$event_id,
|
||||||
|
$cache_valid_time
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($cached) {
|
||||||
|
$event_data = json_decode($cached, true);
|
||||||
|
if ($event_data) {
|
||||||
|
return $event_data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch from API
|
||||||
|
$event = $this->api->get_event($event_id);
|
||||||
|
|
||||||
|
if (is_wp_error($event)) {
|
||||||
|
return $event;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache it
|
||||||
|
$this->cache_event($event);
|
||||||
|
|
||||||
|
return $event;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync all events from API - ENTERPRISE: Race condition protection
|
||||||
|
*/
|
||||||
|
public function sync_all_events() {
|
||||||
|
if (!$this->api->is_configured()) {
|
||||||
|
return new WP_Error('no_api_key', __('API key not configured', 'ticket-tailor'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ENTERPRISE: Acquire lock to prevent concurrent syncs
|
||||||
|
$lock_key = 'ticket_tailor_sync_events_lock';
|
||||||
|
$lock_acquired = $this->acquire_sync_lock($lock_key);
|
||||||
|
|
||||||
|
if (!$lock_acquired) {
|
||||||
|
return new WP_Error(
|
||||||
|
'sync_in_progress',
|
||||||
|
__('Event sync already in progress. Please wait.', 'ticket-tailor'),
|
||||||
|
array('lock_key' => $lock_key)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$events = $this->api->get_all_paginated('events');
|
||||||
|
|
||||||
|
if (is_wp_error($events)) {
|
||||||
|
$this->release_sync_lock($lock_key);
|
||||||
|
return $events;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ENTERPRISE: Use transaction for atomic cache updates
|
||||||
|
global $wpdb;
|
||||||
|
$wpdb->query('START TRANSACTION');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Cache all events
|
||||||
|
$cached_count = 0;
|
||||||
|
foreach ($events as $event) {
|
||||||
|
if ($this->cache_event($event)) {
|
||||||
|
$cached_count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update last sync time
|
||||||
|
update_option('ticket_tailor_last_event_sync', time());
|
||||||
|
|
||||||
|
$wpdb->query('COMMIT');
|
||||||
|
|
||||||
|
error_log(sprintf(
|
||||||
|
'Ticket Tailor: Successfully synced %d events (%d cached)',
|
||||||
|
count($events),
|
||||||
|
$cached_count
|
||||||
|
));
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$wpdb->query('ROLLBACK');
|
||||||
|
$this->release_sync_lock($lock_key);
|
||||||
|
|
||||||
|
return new WP_Error(
|
||||||
|
'sync_transaction_failed',
|
||||||
|
sprintf('Event sync transaction failed: %s', $e->getMessage())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->release_sync_lock($lock_key);
|
||||||
|
return $events;
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$this->release_sync_lock($lock_key);
|
||||||
|
|
||||||
|
return new WP_Error(
|
||||||
|
'sync_failed',
|
||||||
|
sprintf('Event sync failed: %s', $e->getMessage())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Acquire sync lock - ENTERPRISE: Prevent concurrent syncs
|
||||||
|
*/
|
||||||
|
private function acquire_sync_lock($lock_key, $timeout = 300) {
|
||||||
|
// Try to set transient with 5-minute expiry
|
||||||
|
// If it already exists, returns false
|
||||||
|
$acquired = set_transient($lock_key, time(), $timeout);
|
||||||
|
|
||||||
|
if (!$acquired) {
|
||||||
|
// Check if lock is stale (older than timeout)
|
||||||
|
$lock_time = get_transient($lock_key);
|
||||||
|
if ($lock_time && (time() - $lock_time) > $timeout) {
|
||||||
|
// Stale lock, force release and reacquire
|
||||||
|
delete_transient($lock_key);
|
||||||
|
return set_transient($lock_key, time(), $timeout);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Release sync lock - ENTERPRISE
|
||||||
|
*/
|
||||||
|
private function release_sync_lock($lock_key) {
|
||||||
|
return delete_transient($lock_key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache a single event
|
||||||
|
*/
|
||||||
|
private function cache_event($event) {
|
||||||
|
if (empty($event['id'])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$event_id = sanitize_text_field($event['id']);
|
||||||
|
$event_data = wp_json_encode($event);
|
||||||
|
|
||||||
|
// Check if exists
|
||||||
|
$exists = $wpdb->get_var(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT id FROM {$this->table} WHERE event_id = %s",
|
||||||
|
$event_id
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($exists) {
|
||||||
|
// Update existing
|
||||||
|
return $wpdb->update(
|
||||||
|
$this->table,
|
||||||
|
array(
|
||||||
|
'event_data' => $event_data,
|
||||||
|
'last_synced' => current_time('mysql', 1),
|
||||||
|
),
|
||||||
|
array('event_id' => $event_id),
|
||||||
|
array('%s', '%s'),
|
||||||
|
array('%s')
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Insert new
|
||||||
|
return $wpdb->insert(
|
||||||
|
$this->table,
|
||||||
|
array(
|
||||||
|
'event_id' => $event_id,
|
||||||
|
'event_data' => $event_data,
|
||||||
|
'last_synced' => current_time('mysql', 1),
|
||||||
|
),
|
||||||
|
array('%s', '%s', '%s')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get upcoming events
|
||||||
|
*/
|
||||||
|
public function get_upcoming_events($limit = 10) {
|
||||||
|
$events = $this->get_events();
|
||||||
|
|
||||||
|
if (is_wp_error($events)) {
|
||||||
|
return $events;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter and sort by start date
|
||||||
|
$upcoming = array_filter($events, function($event) {
|
||||||
|
if (!isset($event['start']['iso'])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$start_time = strtotime($event['start']['iso']);
|
||||||
|
return $start_time >= time();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort by start date
|
||||||
|
usort($upcoming, function($a, $b) {
|
||||||
|
$time_a = strtotime($a['start']['iso'] ?? 0);
|
||||||
|
$time_b = strtotime($b['start']['iso'] ?? 0);
|
||||||
|
return $time_a - $time_b;
|
||||||
|
});
|
||||||
|
|
||||||
|
return array_slice($upcoming, 0, $limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get past events
|
||||||
|
*/
|
||||||
|
public function get_past_events($limit = 10) {
|
||||||
|
$events = $this->get_events();
|
||||||
|
|
||||||
|
if (is_wp_error($events)) {
|
||||||
|
return $events;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter and sort by start date
|
||||||
|
$past = array_filter($events, function($event) {
|
||||||
|
if (!isset($event['start']['iso'])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$start_time = strtotime($event['start']['iso']);
|
||||||
|
return $start_time < time();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Sort by start date (newest first)
|
||||||
|
usort($past, function($a, $b) {
|
||||||
|
$time_a = strtotime($a['start']['iso'] ?? 0);
|
||||||
|
$time_b = strtotime($b['start']['iso'] ?? 0);
|
||||||
|
return $time_b - $time_a;
|
||||||
|
});
|
||||||
|
|
||||||
|
return array_slice($past, 0, $limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search events
|
||||||
|
*/
|
||||||
|
public function search_events($search_term) {
|
||||||
|
$events = $this->get_events();
|
||||||
|
|
||||||
|
if (is_wp_error($events)) {
|
||||||
|
return $events;
|
||||||
|
}
|
||||||
|
|
||||||
|
$search_term = strtolower($search_term);
|
||||||
|
|
||||||
|
return array_filter($events, function($event) use ($search_term) {
|
||||||
|
$name = strtolower($event['name'] ?? '');
|
||||||
|
$description = strtolower($event['description'] ?? '');
|
||||||
|
|
||||||
|
return strpos($name, $search_term) !== false ||
|
||||||
|
strpos($description, $search_term) !== false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get events by category
|
||||||
|
*/
|
||||||
|
public function get_events_by_category($category) {
|
||||||
|
$events = $this->get_events();
|
||||||
|
|
||||||
|
if (is_wp_error($events)) {
|
||||||
|
return $events;
|
||||||
|
}
|
||||||
|
|
||||||
|
$category = strtolower($category);
|
||||||
|
|
||||||
|
return array_filter($events, function($event) use ($category) {
|
||||||
|
$event_category = strtolower($event['category'] ?? '');
|
||||||
|
return $event_category === $category;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get event ticket types
|
||||||
|
*/
|
||||||
|
public function get_event_tickets($event_id) {
|
||||||
|
$event_id = sanitize_text_field($event_id);
|
||||||
|
|
||||||
|
// Try to get from transient first
|
||||||
|
$cache_key = 'tt_tickets_' . $event_id;
|
||||||
|
$cached = get_transient($cache_key);
|
||||||
|
|
||||||
|
if ($cached !== false) {
|
||||||
|
return $cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch from API
|
||||||
|
$tickets = $this->api->get_ticket_types($event_id);
|
||||||
|
|
||||||
|
if (is_wp_error($tickets)) {
|
||||||
|
return $tickets;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache for 15 minutes (tickets change less frequently)
|
||||||
|
set_transient($cache_key, $tickets, 900);
|
||||||
|
|
||||||
|
return $tickets;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear event cache - SECURITY FIX: Use prepared statements
|
||||||
|
*/
|
||||||
|
public function clear_cache($event_id = null) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
if ($event_id) {
|
||||||
|
$event_id = sanitize_text_field($event_id);
|
||||||
|
$wpdb->delete($this->table, array('event_id' => $event_id), array('%s'));
|
||||||
|
|
||||||
|
// Also clear ticket cache
|
||||||
|
delete_transient('tt_tickets_' . $event_id);
|
||||||
|
} else {
|
||||||
|
// SECURITY FIX: Validate table name before TRUNCATE
|
||||||
|
if (preg_match('/^[a-zA-Z0-9_]+$/', $this->table)) {
|
||||||
|
$wpdb->query("TRUNCATE TABLE `{$this->table}`");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear all ticket transients - SECURITY FIX: Use prepared statement
|
||||||
|
$wpdb->query($wpdb->prepare(
|
||||||
|
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
|
||||||
|
$wpdb->esc_like('_transient_tt_tickets_') . '%'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cache statistics
|
||||||
|
*/
|
||||||
|
public function get_cache_stats() {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$total_events = $wpdb->get_var("SELECT COUNT(*) FROM {$this->table}");
|
||||||
|
|
||||||
|
$cache_valid_time = gmdate('Y-m-d H:i:s', time() - $this->cache_duration);
|
||||||
|
$valid_events = $wpdb->get_var(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT COUNT(*) FROM {$this->table} WHERE last_synced > %s",
|
||||||
|
$cache_valid_time
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$last_sync = get_option('ticket_tailor_last_event_sync', 0);
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'total_cached' => (int) $total_events,
|
||||||
|
'valid_cached' => (int) $valid_events,
|
||||||
|
'expired_cached' => (int) $total_events - (int) $valid_events,
|
||||||
|
'last_sync' => $last_sync,
|
||||||
|
'cache_duration' => $this->cache_duration,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get event statistics efficiently - PERFORMANCE OPTIMIZATION
|
||||||
|
* Uses aggregated database queries instead of loading all events
|
||||||
|
*/
|
||||||
|
public function get_event_statistics() {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$cache_key = 'tt_event_stats';
|
||||||
|
$cached = wp_cache_get($cache_key);
|
||||||
|
|
||||||
|
if ($cached !== false) {
|
||||||
|
return $cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
$cache_valid_time = gmdate('Y-m-d H:i:s', time() - $this->cache_duration);
|
||||||
|
|
||||||
|
// Get total events count
|
||||||
|
$total_events = $wpdb->get_var(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT COUNT(*) FROM {$this->table} WHERE last_synced > %s",
|
||||||
|
$cache_valid_time
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get upcoming events count (parse JSON to check dates)
|
||||||
|
$all_events = $wpdb->get_results(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT event_data FROM {$this->table} WHERE last_synced > %s",
|
||||||
|
$cache_valid_time
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$upcoming_count = 0;
|
||||||
|
$now = time();
|
||||||
|
|
||||||
|
foreach ($all_events as $row) {
|
||||||
|
$event = json_decode($row->event_data, true);
|
||||||
|
if (!empty($event['start']['iso'])) {
|
||||||
|
$start_time = strtotime($event['start']['iso']);
|
||||||
|
if ($start_time >= $now) {
|
||||||
|
$upcoming_count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$stats = array(
|
||||||
|
'total_events' => (int) $total_events,
|
||||||
|
'upcoming_events' => $upcoming_count,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Cache for 5 minutes
|
||||||
|
wp_cache_set($cache_key, $stats, '', 300);
|
||||||
|
|
||||||
|
return $stats;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,253 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Ticket Tailor Health Check
|
||||||
|
*
|
||||||
|
* Enterprise-grade health check endpoint for monitoring
|
||||||
|
* ENTERPRISE: Provides comprehensive health status for monitoring systems
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Exit if accessed directly
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Ticket_Tailor_Health_Check {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API Client
|
||||||
|
*/
|
||||||
|
private $api;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event Manager
|
||||||
|
*/
|
||||||
|
private $events;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Order Manager
|
||||||
|
*/
|
||||||
|
private $orders;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*/
|
||||||
|
public function __construct($api, $events, $orders) {
|
||||||
|
$this->api = $api;
|
||||||
|
$this->events = $events;
|
||||||
|
$this->orders = $orders;
|
||||||
|
|
||||||
|
// Register health check endpoint
|
||||||
|
add_action('rest_api_init', array($this, 'register_health_endpoint'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register health check REST endpoint
|
||||||
|
*/
|
||||||
|
public function register_health_endpoint() {
|
||||||
|
register_rest_route('ticket-tailor/v1', '/health', array(
|
||||||
|
'methods' => 'GET',
|
||||||
|
'callback' => array($this, 'get_health_status'),
|
||||||
|
'permission_callback' => array($this, 'health_check_permission'),
|
||||||
|
));
|
||||||
|
|
||||||
|
register_rest_route('ticket-tailor/v1', '/health/detailed', array(
|
||||||
|
'methods' => 'GET',
|
||||||
|
'callback' => array($this, 'get_detailed_health_status'),
|
||||||
|
'permission_callback' => array($this, 'detailed_health_check_permission'),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Permission callback for basic health check
|
||||||
|
* ENTERPRISE: Can be accessed without authentication for monitoring
|
||||||
|
*/
|
||||||
|
public function health_check_permission() {
|
||||||
|
// Allow unauthenticated access for monitoring tools
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Permission callback for detailed health check
|
||||||
|
* ENTERPRISE: Requires authentication
|
||||||
|
*/
|
||||||
|
public function detailed_health_check_permission() {
|
||||||
|
return current_user_can('manage_options');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get basic health status
|
||||||
|
* ENTERPRISE: Fast check for monitoring systems
|
||||||
|
*/
|
||||||
|
public function get_health_status() {
|
||||||
|
$status = 'healthy';
|
||||||
|
$checks = array();
|
||||||
|
|
||||||
|
// Check API configuration
|
||||||
|
$checks['api_configured'] = $this->api->is_configured();
|
||||||
|
if (!$checks['api_configured']) {
|
||||||
|
$status = 'degraded';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check database connectivity
|
||||||
|
global $wpdb;
|
||||||
|
$checks['database'] = $wpdb->check_connection();
|
||||||
|
if (!$checks['database']) {
|
||||||
|
$status = 'unhealthy';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if syncs are running (not stuck)
|
||||||
|
$event_sync = get_transient('ticket_tailor_sync_events_lock');
|
||||||
|
$order_sync = get_transient('ticket_tailor_sync_orders_lock');
|
||||||
|
|
||||||
|
$checks['sync_status'] = array(
|
||||||
|
'events_syncing' => !empty($event_sync),
|
||||||
|
'orders_syncing' => !empty($order_sync),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if locks are stale (stuck for > 10 minutes)
|
||||||
|
if ($event_sync && (time() - $event_sync) > 600) {
|
||||||
|
$status = 'degraded';
|
||||||
|
$checks['sync_status']['events_stuck'] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($order_sync && (time() - $order_sync) > 600) {
|
||||||
|
$status = 'degraded';
|
||||||
|
$checks['sync_status']['orders_stuck'] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = array(
|
||||||
|
'status' => $status,
|
||||||
|
'timestamp' => current_time('mysql'),
|
||||||
|
'checks' => $checks,
|
||||||
|
);
|
||||||
|
|
||||||
|
$http_status = ($status === 'healthy') ? 200 : (($status === 'degraded') ? 200 : 503);
|
||||||
|
|
||||||
|
return new WP_REST_Response($response, $http_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get detailed health status
|
||||||
|
* ENTERPRISE: Comprehensive diagnostics for administrators
|
||||||
|
*/
|
||||||
|
public function get_detailed_health_status() {
|
||||||
|
$status = 'healthy';
|
||||||
|
$diagnostics = array();
|
||||||
|
|
||||||
|
// 1. API Configuration
|
||||||
|
$diagnostics['api'] = array(
|
||||||
|
'configured' => $this->api->is_configured(),
|
||||||
|
'rate_limit' => $this->api->get_rate_limit_info(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test API connectivity
|
||||||
|
$api_test = $this->api->test_connection();
|
||||||
|
$diagnostics['api']['connectivity'] = !is_wp_error($api_test);
|
||||||
|
if (is_wp_error($api_test)) {
|
||||||
|
$diagnostics['api']['error'] = $api_test->get_error_message();
|
||||||
|
$status = 'unhealthy';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Database Health
|
||||||
|
global $wpdb;
|
||||||
|
$diagnostics['database'] = array(
|
||||||
|
'connected' => $wpdb->check_connection(),
|
||||||
|
'events_table_exists' => $this->check_table_exists($wpdb->prefix . 'ticket_tailor_events'),
|
||||||
|
'orders_table_exists' => $this->check_table_exists($wpdb->prefix . 'ticket_tailor_orders'),
|
||||||
|
'security_log_table_exists' => $this->check_table_exists($wpdb->prefix . 'ticket_tailor_security_log'),
|
||||||
|
'rate_limit_table_exists' => $this->check_table_exists($wpdb->prefix . 'ticket_tailor_rate_limits'),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. Cache Statistics
|
||||||
|
$diagnostics['cache'] = array(
|
||||||
|
'events' => $this->events->get_cache_stats(),
|
||||||
|
'orders' => $this->orders->get_cache_stats(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4. Sync Status
|
||||||
|
$last_event_sync = get_option('ticket_tailor_last_event_sync', 0);
|
||||||
|
$last_order_sync = get_option('ticket_tailor_last_order_sync', 0);
|
||||||
|
|
||||||
|
$diagnostics['sync'] = array(
|
||||||
|
'last_event_sync' => $last_event_sync ? gmdate('Y-m-d H:i:s', $last_event_sync) : 'never',
|
||||||
|
'last_order_sync' => $last_order_sync ? gmdate('Y-m-d H:i:s', $last_order_sync) : 'never',
|
||||||
|
'event_sync_age_hours' => $last_event_sync ? round((time() - $last_event_sync) / 3600, 1) : null,
|
||||||
|
'order_sync_age_hours' => $last_order_sync ? round((time() - $last_order_sync) / 3600, 1) : null,
|
||||||
|
'events_lock' => get_transient('ticket_tailor_sync_events_lock'),
|
||||||
|
'orders_lock' => get_transient('ticket_tailor_sync_orders_lock'),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check for stale syncs
|
||||||
|
if ($last_event_sync && (time() - $last_event_sync) > 86400) {
|
||||||
|
$diagnostics['sync']['event_sync_stale'] = true;
|
||||||
|
$status = 'degraded';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($last_order_sync && (time() - $last_order_sync) > 86400) {
|
||||||
|
$diagnostics['sync']['order_sync_stale'] = true;
|
||||||
|
$status = 'degraded';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Cron Jobs
|
||||||
|
$diagnostics['cron'] = array(
|
||||||
|
'event_sync_scheduled' => wp_next_scheduled('ticket_tailor_sync_events') ? true : false,
|
||||||
|
'order_sync_scheduled' => wp_next_scheduled('ticket_tailor_sync_orders') ? true : false,
|
||||||
|
'security_cleanup_scheduled' => wp_next_scheduled('ticket_tailor_cleanup_security_logs') ? true : false,
|
||||||
|
'rate_limit_cleanup_scheduled' => wp_next_scheduled('ticket_tailor_cleanup_rate_limits') ? true : false,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 6. PHP Environment
|
||||||
|
$diagnostics['environment'] = array(
|
||||||
|
'php_version' => PHP_VERSION,
|
||||||
|
'wordpress_version' => get_bloginfo('version'),
|
||||||
|
'plugin_version' => TICKET_TAILOR_VERSION,
|
||||||
|
'openssl_enabled' => function_exists('openssl_encrypt'),
|
||||||
|
'curl_enabled' => function_exists('curl_init'),
|
||||||
|
'memory_limit' => ini_get('memory_limit'),
|
||||||
|
'max_execution_time' => ini_get('max_execution_time'),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 7. Security Configuration
|
||||||
|
$diagnostics['security'] = array(
|
||||||
|
'api_key_encrypted' => !empty(get_option('ticket_tailor_api_key_encrypted')),
|
||||||
|
'webhook_secret_configured' => !empty(get_option('ticket_tailor_webhook_secret')),
|
||||||
|
'security_alerts_enabled' => get_option('ticket_tailor_security_alerts', true),
|
||||||
|
);
|
||||||
|
|
||||||
|
$response = array(
|
||||||
|
'status' => $status,
|
||||||
|
'timestamp' => current_time('mysql'),
|
||||||
|
'diagnostics' => $diagnostics,
|
||||||
|
);
|
||||||
|
|
||||||
|
$http_status = ($status === 'healthy') ? 200 : (($status === 'degraded') ? 200 : 503);
|
||||||
|
|
||||||
|
return new WP_REST_Response($response, $http_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if table exists
|
||||||
|
*/
|
||||||
|
private function check_table_exists($table_name) {
|
||||||
|
global $wpdb;
|
||||||
|
$result = $wpdb->get_var($wpdb->prepare(
|
||||||
|
"SHOW TABLES LIKE %s",
|
||||||
|
$wpdb->esc_like($table_name)
|
||||||
|
));
|
||||||
|
return !empty($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get health check URL
|
||||||
|
*/
|
||||||
|
public function get_health_check_url() {
|
||||||
|
return rest_url('ticket-tailor/v1/health');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get detailed health check URL
|
||||||
|
*/
|
||||||
|
public function get_detailed_health_check_url() {
|
||||||
|
return rest_url('ticket-tailor/v1/health/detailed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,490 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Ticket Tailor Order Manager
|
||||||
|
*
|
||||||
|
* Handles order caching, syncing, and retrieval
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Exit if accessed directly
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Ticket_Tailor_Order_Manager {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API Client
|
||||||
|
*/
|
||||||
|
private $api;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Table name
|
||||||
|
*/
|
||||||
|
private $table;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache duration (in seconds)
|
||||||
|
*/
|
||||||
|
private $cache_duration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*/
|
||||||
|
public function __construct($api) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$this->api = $api;
|
||||||
|
$this->table = $wpdb->prefix . 'ticket_tailor_orders';
|
||||||
|
$this->cache_duration = get_option('ticket_tailor_cache_duration', 3600); // 1 hour default
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all orders - PERFORMANCE FIX: Improved query building
|
||||||
|
*/
|
||||||
|
public function get_orders($args = array(), $force_refresh = false) {
|
||||||
|
if ($force_refresh) {
|
||||||
|
return $this->sync_all_orders();
|
||||||
|
}
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$cache_valid_time = gmdate('Y-m-d H:i:s', time() - $this->cache_duration);
|
||||||
|
|
||||||
|
$query = "SELECT order_data FROM {$this->table} WHERE last_synced > %s";
|
||||||
|
$query_args = array($cache_valid_time);
|
||||||
|
|
||||||
|
// Filter by event if specified
|
||||||
|
if (!empty($args['event_id'])) {
|
||||||
|
$query .= " AND event_id = %s";
|
||||||
|
$query_args[] = sanitize_text_field($args['event_id']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$query .= " ORDER BY last_synced DESC";
|
||||||
|
|
||||||
|
// PERFORMANCE FIX: Always apply a reasonable default limit if not specified
|
||||||
|
if (!empty($args['limit'])) {
|
||||||
|
$query .= " LIMIT %d";
|
||||||
|
$query_args[] = absint($args['limit']);
|
||||||
|
} else {
|
||||||
|
// Default limit of 1000 to prevent loading unlimited orders
|
||||||
|
$query .= " LIMIT 1000";
|
||||||
|
}
|
||||||
|
|
||||||
|
$cached_orders = $wpdb->get_results(
|
||||||
|
$wpdb->prepare($query, $query_args)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!empty($cached_orders)) {
|
||||||
|
$orders = array();
|
||||||
|
foreach ($cached_orders as $row) {
|
||||||
|
$order_data = json_decode($row->order_data, true);
|
||||||
|
if ($order_data) {
|
||||||
|
$orders[] = $order_data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $orders;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache is expired or empty, fetch from API
|
||||||
|
$all_orders = $this->sync_all_orders();
|
||||||
|
|
||||||
|
// Apply limit to synced orders if specified
|
||||||
|
if (!empty($args['limit']) && is_array($all_orders)) {
|
||||||
|
return array_slice($all_orders, 0, absint($args['limit']));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply default limit
|
||||||
|
if (is_array($all_orders)) {
|
||||||
|
return array_slice($all_orders, 0, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $all_orders;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get single order
|
||||||
|
*/
|
||||||
|
public function get_order($order_id, $force_refresh = false) {
|
||||||
|
$order_id = sanitize_text_field($order_id);
|
||||||
|
|
||||||
|
if (!$force_refresh) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$cache_valid_time = gmdate('Y-m-d H:i:s', time() - $this->cache_duration);
|
||||||
|
|
||||||
|
$cached = $wpdb->get_var(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT order_data FROM {$this->table} WHERE order_id = %s AND last_synced > %s",
|
||||||
|
$order_id,
|
||||||
|
$cache_valid_time
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($cached) {
|
||||||
|
$order_data = json_decode($cached, true);
|
||||||
|
if ($order_data) {
|
||||||
|
return $order_data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch from API
|
||||||
|
$order = $this->api->get_order($order_id);
|
||||||
|
|
||||||
|
if (is_wp_error($order)) {
|
||||||
|
return $order;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache it
|
||||||
|
$this->cache_order($order);
|
||||||
|
|
||||||
|
return $order;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync all orders from API - ENTERPRISE: Race condition protection
|
||||||
|
*/
|
||||||
|
public function sync_all_orders() {
|
||||||
|
if (!$this->api->is_configured()) {
|
||||||
|
return new WP_Error('no_api_key', __('API key not configured', 'ticket-tailor'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ENTERPRISE: Acquire lock to prevent concurrent syncs
|
||||||
|
$lock_key = 'ticket_tailor_sync_orders_lock';
|
||||||
|
$lock_acquired = $this->acquire_sync_lock($lock_key);
|
||||||
|
|
||||||
|
if (!$lock_acquired) {
|
||||||
|
return new WP_Error(
|
||||||
|
'sync_in_progress',
|
||||||
|
__('Order sync already in progress. Please wait.', 'ticket-tailor'),
|
||||||
|
array('lock_key' => $lock_key)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$orders = $this->api->get_all_paginated('orders');
|
||||||
|
|
||||||
|
if (is_wp_error($orders)) {
|
||||||
|
$this->release_sync_lock($lock_key);
|
||||||
|
return $orders;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ENTERPRISE: Use transaction for atomic cache updates
|
||||||
|
global $wpdb;
|
||||||
|
$wpdb->query('START TRANSACTION');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Cache all orders
|
||||||
|
$cached_count = 0;
|
||||||
|
foreach ($orders as $order) {
|
||||||
|
if ($this->cache_order($order)) {
|
||||||
|
$cached_count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update last sync time
|
||||||
|
update_option('ticket_tailor_last_order_sync', time());
|
||||||
|
|
||||||
|
$wpdb->query('COMMIT');
|
||||||
|
|
||||||
|
error_log(sprintf(
|
||||||
|
'Ticket Tailor: Successfully synced %d orders (%d cached)',
|
||||||
|
count($orders),
|
||||||
|
$cached_count
|
||||||
|
));
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$wpdb->query('ROLLBACK');
|
||||||
|
$this->release_sync_lock($lock_key);
|
||||||
|
|
||||||
|
return new WP_Error(
|
||||||
|
'sync_transaction_failed',
|
||||||
|
sprintf('Order sync transaction failed: %s', $e->getMessage())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->release_sync_lock($lock_key);
|
||||||
|
return $orders;
|
||||||
|
|
||||||
|
} catch (Exception $e) {
|
||||||
|
$this->release_sync_lock($lock_key);
|
||||||
|
|
||||||
|
return new WP_Error(
|
||||||
|
'sync_failed',
|
||||||
|
sprintf('Order sync failed: %s', $e->getMessage())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Acquire sync lock - ENTERPRISE: Prevent concurrent syncs
|
||||||
|
*/
|
||||||
|
private function acquire_sync_lock($lock_key, $timeout = 300) {
|
||||||
|
$acquired = set_transient($lock_key, time(), $timeout);
|
||||||
|
|
||||||
|
if (!$acquired) {
|
||||||
|
$lock_time = get_transient($lock_key);
|
||||||
|
if ($lock_time && (time() - $lock_time) > $timeout) {
|
||||||
|
delete_transient($lock_key);
|
||||||
|
return set_transient($lock_key, time(), $timeout);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Release sync lock - ENTERPRISE
|
||||||
|
*/
|
||||||
|
private function release_sync_lock($lock_key) {
|
||||||
|
return delete_transient($lock_key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cache a single order
|
||||||
|
*/
|
||||||
|
private function cache_order($order) {
|
||||||
|
if (empty($order['id'])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$order_id = sanitize_text_field($order['id']);
|
||||||
|
$event_id = !empty($order['event_id']) ? sanitize_text_field($order['event_id']) : '';
|
||||||
|
$order_data = wp_json_encode($order);
|
||||||
|
|
||||||
|
// Check if exists
|
||||||
|
$exists = $wpdb->get_var(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT id FROM {$this->table} WHERE order_id = %s",
|
||||||
|
$order_id
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($exists) {
|
||||||
|
// Update existing
|
||||||
|
return $wpdb->update(
|
||||||
|
$this->table,
|
||||||
|
array(
|
||||||
|
'event_id' => $event_id,
|
||||||
|
'order_data' => $order_data,
|
||||||
|
'last_synced' => current_time('mysql', 1),
|
||||||
|
),
|
||||||
|
array('order_id' => $order_id),
|
||||||
|
array('%s', '%s', '%s'),
|
||||||
|
array('%s')
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Insert new
|
||||||
|
return $wpdb->insert(
|
||||||
|
$this->table,
|
||||||
|
array(
|
||||||
|
'order_id' => $order_id,
|
||||||
|
'event_id' => $event_id,
|
||||||
|
'order_data' => $order_data,
|
||||||
|
'last_synced' => current_time('mysql', 1),
|
||||||
|
),
|
||||||
|
array('%s', '%s', '%s', '%s')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get orders for a specific event
|
||||||
|
*/
|
||||||
|
public function get_event_orders($event_id) {
|
||||||
|
return $this->get_orders(array('event_id' => $event_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get order statistics
|
||||||
|
*/
|
||||||
|
public function get_order_stats($event_id = null) {
|
||||||
|
$orders = $event_id ? $this->get_event_orders($event_id) : $this->get_orders();
|
||||||
|
|
||||||
|
if (is_wp_error($orders)) {
|
||||||
|
return $orders;
|
||||||
|
}
|
||||||
|
|
||||||
|
$stats = array(
|
||||||
|
'total_orders' => count($orders),
|
||||||
|
'total_revenue' => 0,
|
||||||
|
'total_tickets' => 0,
|
||||||
|
'by_status' => array(),
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($orders as $order) {
|
||||||
|
// Count tickets
|
||||||
|
if (isset($order['total_quantity'])) {
|
||||||
|
$stats['total_tickets'] += (int) $order['total_quantity'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sum revenue
|
||||||
|
if (isset($order['total'])) {
|
||||||
|
$stats['total_revenue'] += (int) $order['total'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count by status
|
||||||
|
$status = $order['status'] ?? 'unknown';
|
||||||
|
if (!isset($stats['by_status'][$status])) {
|
||||||
|
$stats['by_status'][$status] = 0;
|
||||||
|
}
|
||||||
|
$stats['by_status'][$status]++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format revenue (convert from cents to dollars/pounds)
|
||||||
|
$currency = get_option('ticket_tailor_currency', 'USD');
|
||||||
|
$stats['total_revenue_formatted'] = $this->format_currency($stats['total_revenue'], $currency);
|
||||||
|
|
||||||
|
return $stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format currency
|
||||||
|
*/
|
||||||
|
private function format_currency($amount_cents, $currency = 'USD') {
|
||||||
|
$amount = $amount_cents / 100;
|
||||||
|
|
||||||
|
$symbols = array(
|
||||||
|
'USD' => '$',
|
||||||
|
'GBP' => '£',
|
||||||
|
'EUR' => '€',
|
||||||
|
'CAD' => 'C$',
|
||||||
|
'AUD' => 'A$',
|
||||||
|
);
|
||||||
|
|
||||||
|
$symbol = $symbols[$currency] ?? $currency . ' ';
|
||||||
|
|
||||||
|
return $symbol . number_format($amount, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search orders
|
||||||
|
*/
|
||||||
|
public function search_orders($search_term) {
|
||||||
|
$orders = $this->get_orders();
|
||||||
|
|
||||||
|
if (is_wp_error($orders)) {
|
||||||
|
return $orders;
|
||||||
|
}
|
||||||
|
|
||||||
|
$search_term = strtolower($search_term);
|
||||||
|
|
||||||
|
return array_filter($orders, function($order) use ($search_term) {
|
||||||
|
$email = strtolower($order['email'] ?? '');
|
||||||
|
$name = strtolower($order['customer_name'] ?? '');
|
||||||
|
$order_id = strtolower($order['id'] ?? '');
|
||||||
|
|
||||||
|
return strpos($email, $search_term) !== false ||
|
||||||
|
strpos($name, $search_term) !== false ||
|
||||||
|
strpos($order_id, $search_term) !== false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get recent orders
|
||||||
|
*/
|
||||||
|
public function get_recent_orders($limit = 10) {
|
||||||
|
return $this->get_orders(array('limit' => $limit));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear order cache - SECURITY FIX: Validate table name
|
||||||
|
*/
|
||||||
|
public function clear_cache($order_id = null) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
if ($order_id) {
|
||||||
|
$order_id = sanitize_text_field($order_id);
|
||||||
|
$wpdb->delete($this->table, array('order_id' => $order_id), array('%s'));
|
||||||
|
} else {
|
||||||
|
// SECURITY FIX: Validate table name before TRUNCATE
|
||||||
|
if (preg_match('/^[a-zA-Z0-9_]+$/', $this->table)) {
|
||||||
|
$wpdb->query("TRUNCATE TABLE `{$this->table}`");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cache statistics
|
||||||
|
*/
|
||||||
|
public function get_cache_stats() {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$total_orders = $wpdb->get_var("SELECT COUNT(*) FROM {$this->table}");
|
||||||
|
|
||||||
|
$cache_valid_time = gmdate('Y-m-d H:i:s', time() - $this->cache_duration);
|
||||||
|
$valid_orders = $wpdb->get_var(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT COUNT(*) FROM {$this->table} WHERE last_synced > %s",
|
||||||
|
$cache_valid_time
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$last_sync = get_option('ticket_tailor_last_order_sync', 0);
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'total_cached' => (int) $total_orders,
|
||||||
|
'valid_cached' => (int) $valid_orders,
|
||||||
|
'expired_cached' => (int) $total_orders - (int) $valid_orders,
|
||||||
|
'last_sync' => $last_sync,
|
||||||
|
'cache_duration' => $this->cache_duration,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get order statistics efficiently - PERFORMANCE OPTIMIZATION
|
||||||
|
* Uses aggregated database queries instead of loading all orders
|
||||||
|
*/
|
||||||
|
public function get_order_statistics() {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$cache_key = 'tt_order_stats';
|
||||||
|
$cached = wp_cache_get($cache_key);
|
||||||
|
|
||||||
|
if ($cached !== false) {
|
||||||
|
return $cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
$cache_valid_time = gmdate('Y-m-d H:i:s', time() - $this->cache_duration);
|
||||||
|
|
||||||
|
// Get total orders count
|
||||||
|
$total_orders = $wpdb->get_var(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT COUNT(*) FROM {$this->table} WHERE last_synced > %s",
|
||||||
|
$cache_valid_time
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get all order data to calculate revenue (we need to parse JSON)
|
||||||
|
$orders_data = $wpdb->get_results(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT order_data FROM {$this->table} WHERE last_synced > %s",
|
||||||
|
$cache_valid_time
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$total_revenue = 0;
|
||||||
|
|
||||||
|
foreach ($orders_data as $row) {
|
||||||
|
$order = json_decode($row->order_data, true);
|
||||||
|
if (!empty($order['total'])) {
|
||||||
|
$total_revenue += floatval($order['total']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$stats = array(
|
||||||
|
'total_orders' => (int) $total_orders,
|
||||||
|
'total_revenue' => $total_revenue,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Cache for 5 minutes
|
||||||
|
wp_cache_set($cache_key, $stats, '', 300);
|
||||||
|
|
||||||
|
return $stats;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,370 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Ticket Tailor Security Logger
|
||||||
|
*
|
||||||
|
* Logs security events and sends alerts
|
||||||
|
* SECURITY ENHANCEMENT: Complete audit trail for compliance
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Exit if accessed directly
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Ticket_Tailor_Security_Logger {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Table name
|
||||||
|
*/
|
||||||
|
private $table;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*/
|
||||||
|
public function __construct() {
|
||||||
|
global $wpdb;
|
||||||
|
$this->table = $wpdb->prefix . 'ticket_tailor_security_log';
|
||||||
|
|
||||||
|
// Create table if needed
|
||||||
|
$this->maybe_create_table();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create security log table
|
||||||
|
*/
|
||||||
|
private function maybe_create_table() {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
// Check if table already exists
|
||||||
|
$table_exists = $wpdb->get_var($wpdb->prepare(
|
||||||
|
"SHOW TABLES LIKE %s",
|
||||||
|
$this->table
|
||||||
|
));
|
||||||
|
|
||||||
|
if ($table_exists) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$charset_collate = $wpdb->get_charset_collate();
|
||||||
|
|
||||||
|
$sql = "CREATE TABLE IF NOT EXISTS {$this->table} (
|
||||||
|
id bigint(20) NOT NULL AUTO_INCREMENT,
|
||||||
|
event_type varchar(50) NOT NULL,
|
||||||
|
user_id bigint(20) DEFAULT 0,
|
||||||
|
ip_address varchar(45) NOT NULL,
|
||||||
|
details longtext,
|
||||||
|
timestamp datetime NOT NULL,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY event_type (event_type),
|
||||||
|
KEY timestamp (timestamp),
|
||||||
|
KEY ip_address (ip_address)
|
||||||
|
) {$charset_collate};";
|
||||||
|
|
||||||
|
// Only require upgrade.php if we actually need to create the table
|
||||||
|
if (!function_exists('dbDelta')) {
|
||||||
|
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
||||||
|
}
|
||||||
|
dbDelta($sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log security event
|
||||||
|
*/
|
||||||
|
public function log_event($event_type, $details = array()) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$wpdb->insert(
|
||||||
|
$this->table,
|
||||||
|
array(
|
||||||
|
'event_type' => sanitize_key($event_type),
|
||||||
|
'user_id' => get_current_user_id(),
|
||||||
|
'ip_address' => $this->get_ip(),
|
||||||
|
'details' => wp_json_encode($details),
|
||||||
|
'timestamp' => current_time('mysql', 1),
|
||||||
|
),
|
||||||
|
array('%s', '%d', '%s', '%s', '%s')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log failed webhook
|
||||||
|
*/
|
||||||
|
public function log_failed_webhook($ip, $reason) {
|
||||||
|
$this->log_event('webhook_failed', array(
|
||||||
|
'ip' => $ip,
|
||||||
|
'reason' => $reason,
|
||||||
|
));
|
||||||
|
|
||||||
|
// Check for multiple failures
|
||||||
|
$recent_failures = $this->count_recent_events('webhook_failed', 300);
|
||||||
|
if ($recent_failures >= 5) {
|
||||||
|
$this->send_security_alert('Multiple Failed Webhook Attempts', array(
|
||||||
|
'count' => $recent_failures,
|
||||||
|
'ip' => $ip,
|
||||||
|
'timeframe' => '5 minutes',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log successful webhook
|
||||||
|
*/
|
||||||
|
public function log_successful_webhook($ip, $event_type) {
|
||||||
|
$this->log_event('webhook_success', array(
|
||||||
|
'ip' => $ip,
|
||||||
|
'webhook_type' => $event_type,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log API key change
|
||||||
|
*/
|
||||||
|
public function log_api_key_change($old_key_hash, $new_key_hash) {
|
||||||
|
$this->log_event('api_key_changed', array(
|
||||||
|
'old_hash' => substr($old_key_hash, 0, 10),
|
||||||
|
'new_hash' => substr($new_key_hash, 0, 10),
|
||||||
|
'user' => wp_get_current_user()->user_login,
|
||||||
|
));
|
||||||
|
|
||||||
|
$this->send_security_alert('API Key Changed', array(
|
||||||
|
'changed_by' => wp_get_current_user()->user_login,
|
||||||
|
'ip' => $this->get_ip(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log settings change
|
||||||
|
*/
|
||||||
|
public function log_settings_change($setting_name, $old_value = null, $new_value = null) {
|
||||||
|
$this->log_event('settings_changed', array(
|
||||||
|
'setting' => $setting_name,
|
||||||
|
'changed_by' => wp_get_current_user()->user_login,
|
||||||
|
'has_old' => !is_null($old_value),
|
||||||
|
'has_new' => !is_null($new_value),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log failed login attempt
|
||||||
|
*/
|
||||||
|
public function log_failed_login($username) {
|
||||||
|
$this->log_event('login_failed', array(
|
||||||
|
'username' => $username,
|
||||||
|
));
|
||||||
|
|
||||||
|
// Check for brute force
|
||||||
|
$recent_failures = $this->count_recent_events('login_failed', 900, $this->get_ip());
|
||||||
|
if ($recent_failures >= 5) {
|
||||||
|
$this->send_security_alert('Multiple Failed Login Attempts', array(
|
||||||
|
'count' => $recent_failures,
|
||||||
|
'ip' => $this->get_ip(),
|
||||||
|
'username' => $username,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log rate limit exceeded
|
||||||
|
*/
|
||||||
|
public function log_rate_limit_exceeded($ip, $context = 'webhook') {
|
||||||
|
$this->log_event('rate_limit_exceeded', array(
|
||||||
|
'ip' => $ip,
|
||||||
|
'context' => $context,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log unauthorized access attempt
|
||||||
|
*/
|
||||||
|
public function log_unauthorized_access($resource) {
|
||||||
|
$this->log_event('unauthorized_access', array(
|
||||||
|
'resource' => $resource,
|
||||||
|
'user_id' => get_current_user_id(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count recent events
|
||||||
|
*/
|
||||||
|
private function count_recent_events($event_type, $seconds, $ip = null) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$time_threshold = gmdate('Y-m-d H:i:s', time() - $seconds);
|
||||||
|
|
||||||
|
if ($ip) {
|
||||||
|
return $wpdb->get_var($wpdb->prepare(
|
||||||
|
"SELECT COUNT(*) FROM {$this->table}
|
||||||
|
WHERE event_type = %s AND timestamp > %s AND ip_address = %s",
|
||||||
|
$event_type,
|
||||||
|
$time_threshold,
|
||||||
|
$ip
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
return $wpdb->get_var($wpdb->prepare(
|
||||||
|
"SELECT COUNT(*) FROM {$this->table}
|
||||||
|
WHERE event_type = %s AND timestamp > %s",
|
||||||
|
$event_type,
|
||||||
|
$time_threshold
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send security alert email
|
||||||
|
*/
|
||||||
|
public function send_security_alert($event_type, $details) {
|
||||||
|
// Check if alerts are enabled
|
||||||
|
if (!get_option('ticket_tailor_security_alerts', true)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$admin_email = get_option('admin_email');
|
||||||
|
$site_name = get_bloginfo('name');
|
||||||
|
$site_url = get_bloginfo('url');
|
||||||
|
|
||||||
|
$subject = sprintf(
|
||||||
|
'[%s] Security Alert: %s',
|
||||||
|
$site_name,
|
||||||
|
$event_type
|
||||||
|
);
|
||||||
|
|
||||||
|
$message = sprintf(
|
||||||
|
"Security Event Detected on %s\n\n" .
|
||||||
|
"Event Type: %s\n" .
|
||||||
|
"Time: %s UTC\n" .
|
||||||
|
"IP Address: %s\n" .
|
||||||
|
"User: %s\n\n" .
|
||||||
|
"Details:\n%s\n\n" .
|
||||||
|
"---\n" .
|
||||||
|
"This is an automated security alert from Ticket Tailor plugin.\n" .
|
||||||
|
"To disable these alerts, go to: %s/wp-admin/admin.php?page=ticket-tailor-settings",
|
||||||
|
$site_name,
|
||||||
|
$event_type,
|
||||||
|
current_time('Y-m-d H:i:s'),
|
||||||
|
$this->get_ip(),
|
||||||
|
wp_get_current_user()->user_login ?: 'Guest',
|
||||||
|
$this->format_details($details),
|
||||||
|
$site_url
|
||||||
|
);
|
||||||
|
|
||||||
|
wp_mail($admin_email, $subject, $message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format details for email
|
||||||
|
*/
|
||||||
|
private function format_details($details) {
|
||||||
|
$formatted = '';
|
||||||
|
foreach ($details as $key => $value) {
|
||||||
|
$formatted .= ' ' . ucfirst(str_replace('_', ' ', $key)) . ': ' . $value . "\n";
|
||||||
|
}
|
||||||
|
return $formatted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get client IP
|
||||||
|
*/
|
||||||
|
private function get_ip() {
|
||||||
|
$ip_keys = array(
|
||||||
|
'HTTP_CF_CONNECTING_IP',
|
||||||
|
'HTTP_X_FORWARDED_FOR',
|
||||||
|
'HTTP_X_REAL_IP',
|
||||||
|
'REMOTE_ADDR',
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($ip_keys as $key) {
|
||||||
|
if (!empty($_SERVER[$key])) {
|
||||||
|
$ip = $_SERVER[$key];
|
||||||
|
|
||||||
|
if (strpos($ip, ',') !== false) {
|
||||||
|
$ip = trim(explode(',', $ip)[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter_var($ip, FILTER_VALIDATE_IP)) {
|
||||||
|
return $ip;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '0.0.0.0';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get recent security events
|
||||||
|
*/
|
||||||
|
public function get_recent_events($limit = 50, $event_type = null) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
if ($event_type) {
|
||||||
|
return $wpdb->get_results($wpdb->prepare(
|
||||||
|
"SELECT * FROM {$this->table}
|
||||||
|
WHERE event_type = %s
|
||||||
|
ORDER BY timestamp DESC
|
||||||
|
LIMIT %d",
|
||||||
|
$event_type,
|
||||||
|
$limit
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
return $wpdb->get_results($wpdb->prepare(
|
||||||
|
"SELECT * FROM {$this->table}
|
||||||
|
ORDER BY timestamp DESC
|
||||||
|
LIMIT %d",
|
||||||
|
$limit
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean old logs (older than 90 days)
|
||||||
|
*/
|
||||||
|
public function cleanup_old_logs() {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$threshold = gmdate('Y-m-d H:i:s', time() - (90 * DAY_IN_SECONDS));
|
||||||
|
|
||||||
|
$wpdb->query($wpdb->prepare(
|
||||||
|
"DELETE FROM {$this->table} WHERE timestamp < %s",
|
||||||
|
$threshold
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get security statistics
|
||||||
|
*/
|
||||||
|
public function get_statistics($days = 30) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$threshold = gmdate('Y-m-d H:i:s', time() - ($days * DAY_IN_SECONDS));
|
||||||
|
|
||||||
|
$stats = array();
|
||||||
|
|
||||||
|
// Total events
|
||||||
|
$stats['total_events'] = $wpdb->get_var($wpdb->prepare(
|
||||||
|
"SELECT COUNT(*) FROM {$this->table} WHERE timestamp > %s",
|
||||||
|
$threshold
|
||||||
|
));
|
||||||
|
|
||||||
|
// Events by type
|
||||||
|
$stats['by_type'] = $wpdb->get_results($wpdb->prepare(
|
||||||
|
"SELECT event_type, COUNT(*) as count
|
||||||
|
FROM {$this->table}
|
||||||
|
WHERE timestamp > %s
|
||||||
|
GROUP BY event_type
|
||||||
|
ORDER BY count DESC",
|
||||||
|
$threshold
|
||||||
|
), ARRAY_A);
|
||||||
|
|
||||||
|
// Top IPs
|
||||||
|
$stats['top_ips'] = $wpdb->get_results($wpdb->prepare(
|
||||||
|
"SELECT ip_address, COUNT(*) as count
|
||||||
|
FROM {$this->table}
|
||||||
|
WHERE timestamp > %s
|
||||||
|
GROUP BY ip_address
|
||||||
|
ORDER BY count DESC
|
||||||
|
LIMIT 10",
|
||||||
|
$threshold
|
||||||
|
), ARRAY_A);
|
||||||
|
|
||||||
|
return $stats;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,304 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Ticket Tailor Shortcodes
|
||||||
|
* Updated with equal-height card support
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Exit if accessed directly
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Ticket_Tailor_Shortcodes {
|
||||||
|
|
||||||
|
private $events;
|
||||||
|
private $orders;
|
||||||
|
|
||||||
|
public function __construct($events, $orders) {
|
||||||
|
$this->events = $events;
|
||||||
|
$this->orders = $orders;
|
||||||
|
|
||||||
|
$this->register_shortcodes();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function register_shortcodes() {
|
||||||
|
add_shortcode('tt-event', array($this, 'event_widget_shortcode'));
|
||||||
|
add_shortcode('tt-events', array($this, 'event_listing_shortcode'));
|
||||||
|
add_shortcode('tt-single-event', array($this, 'single_event_shortcode'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event widget shortcode (original, enhanced)
|
||||||
|
*/
|
||||||
|
public function event_widget_shortcode($atts) {
|
||||||
|
$atts = shortcode_atts(
|
||||||
|
array(
|
||||||
|
'url' => '',
|
||||||
|
'minimal' => 'false',
|
||||||
|
'bg_fill' => 'true',
|
||||||
|
'show_logo' => 'true',
|
||||||
|
'inherit_ref_from_url_param' => '',
|
||||||
|
'ref' => 'website_widget',
|
||||||
|
),
|
||||||
|
$atts,
|
||||||
|
'tt-event'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (empty($atts['url'])) {
|
||||||
|
return '<div class="tt-widget-error"><p>' .
|
||||||
|
esc_html__('Error: No URL set for Ticket Tailor widget', 'ticket-tailor') .
|
||||||
|
'</p></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$url = esc_url($atts['url'], array('http', 'https'));
|
||||||
|
if (empty($url)) {
|
||||||
|
return '<div class="tt-widget-error"><p>' .
|
||||||
|
esc_html__('Error: Invalid URL provided', 'ticket-tailor') .
|
||||||
|
'</p></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$minimal = $this->sanitize_boolean($atts['minimal']);
|
||||||
|
$bg_fill = $this->sanitize_boolean($atts['bg_fill']);
|
||||||
|
$show_logo = $this->sanitize_boolean($atts['show_logo']);
|
||||||
|
$inherit_ref = sanitize_text_field($atts['inherit_ref_from_url_param']);
|
||||||
|
$ref = sanitize_text_field($atts['ref']);
|
||||||
|
|
||||||
|
static $widget_count = 0;
|
||||||
|
$widget_count++;
|
||||||
|
$widget_id = 'tt-widget-' . $widget_count;
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
?>
|
||||||
|
<div id="<?php echo esc_attr($widget_id); ?>" class="tt-widget-container">
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
var script = document.createElement('script');
|
||||||
|
script.src = 'https://cdn.tickettailor.com/js/widgets/min/widget.js';
|
||||||
|
script.setAttribute('data-url', <?php echo wp_json_encode($url); ?>);
|
||||||
|
script.setAttribute('data-type', 'inline');
|
||||||
|
script.setAttribute('data-inline-minimal', <?php echo wp_json_encode($minimal ? 'true' : 'false'); ?>);
|
||||||
|
script.setAttribute('data-inline-show-logo', <?php echo wp_json_encode($show_logo ? 'true' : 'false'); ?>);
|
||||||
|
script.setAttribute('data-inline-bg-fill', <?php echo wp_json_encode($bg_fill ? 'true' : 'false'); ?>);
|
||||||
|
<?php if (!empty($inherit_ref)) : ?>
|
||||||
|
script.setAttribute('data-inline-inherit-ref-from-url-param', <?php echo wp_json_encode($inherit_ref); ?>);
|
||||||
|
<?php endif; ?>
|
||||||
|
script.setAttribute('data-inline-ref', <?php echo wp_json_encode($ref); ?>);
|
||||||
|
|
||||||
|
var container = document.getElementById(<?php echo wp_json_encode($widget_id); ?>);
|
||||||
|
if (container) {
|
||||||
|
container.appendChild(script);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
return ob_get_clean();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event listing shortcode - UPDATED for equal heights and wider cards
|
||||||
|
*/
|
||||||
|
public function event_listing_shortcode($atts) {
|
||||||
|
$atts = shortcode_atts(
|
||||||
|
array(
|
||||||
|
'limit' => 10,
|
||||||
|
'layout' => 'grid',
|
||||||
|
'columns' => 3,
|
||||||
|
'show_past' => 'false',
|
||||||
|
'show_image' => 'true',
|
||||||
|
),
|
||||||
|
$atts,
|
||||||
|
'tt-events'
|
||||||
|
);
|
||||||
|
|
||||||
|
$limit = absint($atts['limit']);
|
||||||
|
$layout = sanitize_key($atts['layout']);
|
||||||
|
$columns = absint($atts['columns']);
|
||||||
|
$show_past = $this->sanitize_boolean($atts['show_past']);
|
||||||
|
$show_image = $this->sanitize_boolean($atts['show_image']);
|
||||||
|
|
||||||
|
$events = $show_past ?
|
||||||
|
$this->events->get_past_events($limit) :
|
||||||
|
$this->events->get_upcoming_events($limit);
|
||||||
|
|
||||||
|
if (is_wp_error($events)) {
|
||||||
|
return '<div class="tt-error">' . esc_html($events->get_error_message()) . '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($events)) {
|
||||||
|
return '<div class="tt-no-events">' . esc_html__('No events found', 'ticket-tailor') . '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
|
||||||
|
$class = 'tt-event-listing tt-layout-' . esc_attr($layout);
|
||||||
|
if ($layout === 'grid') {
|
||||||
|
$class .= ' tt-columns-' . esc_attr($columns);
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<div class="<?php echo esc_attr($class); ?>">
|
||||||
|
<?php foreach ($events as $event) : ?>
|
||||||
|
<div class="tt-event-card">
|
||||||
|
<?php if ($show_image && !empty($event['images']['header'])) : ?>
|
||||||
|
<div class="tt-event-image">
|
||||||
|
<img src="<?php echo esc_url($event['images']['header']); ?>"
|
||||||
|
alt="<?php echo esc_attr($event['name']); ?>">
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="tt-event-content">
|
||||||
|
<h3 class="tt-event-title"><?php echo esc_html($event['name']); ?></h3>
|
||||||
|
|
||||||
|
<?php if (!empty($event['start']['iso'])) : ?>
|
||||||
|
<div class="tt-event-date">
|
||||||
|
<span class="dashicons dashicons-calendar-alt"></span>
|
||||||
|
<?php echo esc_html(date_i18n(get_option('date_format') . ' ' . get_option('time_format'), strtotime($event['start']['iso']))); ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (!empty($event['venue']['name'])) : ?>
|
||||||
|
<div class="tt-event-venue">
|
||||||
|
<span class="dashicons dashicons-location"></span>
|
||||||
|
<?php echo esc_html($event['venue']['name']); ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
// Only show description in LIST layout, never in GRID
|
||||||
|
if ($layout === 'list' && !empty($event['description'])) :
|
||||||
|
?>
|
||||||
|
<div class="tt-event-excerpt">
|
||||||
|
<?php echo esc_html(wp_trim_words($event['description'], 20)); ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<!-- Spacer pushes button to bottom -->
|
||||||
|
<div class="tt-event-spacer"></div>
|
||||||
|
|
||||||
|
<!-- Button container at bottom of card -->
|
||||||
|
<?php if (!empty($event['url'])) : ?>
|
||||||
|
<div class="tt-event-button-container">
|
||||||
|
<a href="<?php echo esc_url($event['url']); ?>"
|
||||||
|
class="tt-event-button"
|
||||||
|
target="_blank">
|
||||||
|
<?php esc_html_e('Get Tickets', 'ticket-tailor'); ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return ob_get_clean();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Single event shortcode
|
||||||
|
*/
|
||||||
|
public function single_event_shortcode($atts) {
|
||||||
|
$atts = shortcode_atts(
|
||||||
|
array(
|
||||||
|
'id' => '',
|
||||||
|
'show_description' => 'true',
|
||||||
|
'show_tickets' => 'true',
|
||||||
|
'show_image' => 'true',
|
||||||
|
),
|
||||||
|
$atts,
|
||||||
|
'tt-single-event'
|
||||||
|
);
|
||||||
|
|
||||||
|
$event_id = sanitize_text_field($atts['id']);
|
||||||
|
|
||||||
|
if (empty($event_id)) {
|
||||||
|
return '<div class="tt-error">' . esc_html__('Error: No event ID specified', 'ticket-tailor') . '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$show_description = $this->sanitize_boolean($atts['show_description']);
|
||||||
|
$show_tickets = $this->sanitize_boolean($atts['show_tickets']);
|
||||||
|
$show_image = $this->sanitize_boolean($atts['show_image']);
|
||||||
|
|
||||||
|
$event = $this->events->get_event($event_id);
|
||||||
|
|
||||||
|
if (is_wp_error($event)) {
|
||||||
|
return '<div class="tt-error">' . esc_html($event->get_error_message()) . '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
?>
|
||||||
|
<div class="tt-single-event">
|
||||||
|
<?php if ($show_image && !empty($event['images']['header'])) : ?>
|
||||||
|
<div class="tt-event-header-image">
|
||||||
|
<img src="<?php echo esc_url($event['images']['header']); ?>"
|
||||||
|
alt="<?php echo esc_attr($event['name']); ?>">
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="tt-event-details">
|
||||||
|
<h2 class="tt-event-title"><?php echo esc_html($event['name']); ?></h2>
|
||||||
|
|
||||||
|
<div class="tt-event-meta">
|
||||||
|
<?php if (!empty($event['start']['iso'])) : ?>
|
||||||
|
<div class="tt-meta-item">
|
||||||
|
<span class="dashicons dashicons-calendar-alt"></span>
|
||||||
|
<strong><?php esc_html_e('Date:', 'ticket-tailor'); ?></strong>
|
||||||
|
<?php echo esc_html(date_i18n(get_option('date_format') . ' ' . get_option('time_format'), strtotime($event['start']['iso']))); ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (!empty($event['venue']['name'])) : ?>
|
||||||
|
<div class="tt-meta-item">
|
||||||
|
<span class="dashicons dashicons-location"></span>
|
||||||
|
<strong><?php esc_html_e('Venue:', 'ticket-tailor'); ?></strong>
|
||||||
|
<?php echo esc_html($event['venue']['name']); ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if (!empty($event['status'])) : ?>
|
||||||
|
<div class="tt-meta-item">
|
||||||
|
<span class="dashicons dashicons-info"></span>
|
||||||
|
<strong><?php esc_html_e('Status:', 'ticket-tailor'); ?></strong>
|
||||||
|
<span class="tt-status"><?php echo esc_html($event['status']); ?></span>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($show_description && !empty($event['description'])) : ?>
|
||||||
|
<div class="tt-event-description">
|
||||||
|
<?php echo wp_kses_post(wpautop($event['description'])); ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php if ($show_tickets && !empty($event['url'])) : ?>
|
||||||
|
<div class="tt-event-cta">
|
||||||
|
<a href="<?php echo esc_url($event['url']); ?>"
|
||||||
|
class="tt-button tt-button-primary"
|
||||||
|
target="_blank">
|
||||||
|
<?php esc_html_e('Buy Tickets', 'ticket-tailor'); ?>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
|
||||||
|
return ob_get_clean();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitize boolean values
|
||||||
|
*/
|
||||||
|
private function sanitize_boolean($value) {
|
||||||
|
if (is_bool($value)) {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_string($value)) {
|
||||||
|
$value = strtolower(trim($value));
|
||||||
|
return in_array($value, array('1', 'true', 'yes', 'on'), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (bool) $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Ticket Tailor Template Loader
|
||||||
|
*
|
||||||
|
* Handles template loading with theme override support
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Exit if accessed directly
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Ticket_Tailor_Template_Loader {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get template path
|
||||||
|
*
|
||||||
|
* Checks theme folder first, then plugin folder
|
||||||
|
*/
|
||||||
|
public static function get_template_path($template_name) {
|
||||||
|
$template = '';
|
||||||
|
|
||||||
|
// Check in theme
|
||||||
|
$theme_template = locate_template(array(
|
||||||
|
'ticket-tailor/' . $template_name,
|
||||||
|
$template_name,
|
||||||
|
));
|
||||||
|
|
||||||
|
if ($theme_template) {
|
||||||
|
$template = $theme_template;
|
||||||
|
} else {
|
||||||
|
// Use plugin template
|
||||||
|
$template = TICKET_TAILOR_PLUGIN_DIR . 'templates/' . $template_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
return apply_filters('ticket_tailor_template_path', $template, $template_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load template
|
||||||
|
*/
|
||||||
|
public static function load_template($template_name, $args = array()) {
|
||||||
|
$template_path = self::get_template_path($template_name);
|
||||||
|
|
||||||
|
if (!file_exists($template_path)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract variables
|
||||||
|
if (!empty($args) && is_array($args)) {
|
||||||
|
extract($args);
|
||||||
|
}
|
||||||
|
|
||||||
|
include $template_path;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get template HTML
|
||||||
|
*/
|
||||||
|
public static function get_template_html($template_name, $args = array()) {
|
||||||
|
ob_start();
|
||||||
|
self::load_template($template_name, $args);
|
||||||
|
return ob_get_clean();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,560 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Ticket Tailor Webhook Handler
|
||||||
|
*
|
||||||
|
* Handles real-time updates via webhooks from Ticket Tailor
|
||||||
|
* FIXED VERSION: Added rate limiting (100 requests/hour)
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Exit if accessed directly
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Ticket_Tailor_Webhook_Handler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event Manager
|
||||||
|
*/
|
||||||
|
private $events;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Order Manager
|
||||||
|
*/
|
||||||
|
private $orders;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Webhook secret
|
||||||
|
*/
|
||||||
|
private $webhook_secret;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rate limiting settings
|
||||||
|
*/
|
||||||
|
private $rate_limit = 100; // Max requests per hour
|
||||||
|
private $rate_limit_window = 3600; // 1 hour in seconds
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allowed webhook IP ranges - SECURITY ENHANCEMENT
|
||||||
|
* Note: Update this array with actual Ticket Tailor IP ranges
|
||||||
|
*/
|
||||||
|
private $default_webhook_ips = array(
|
||||||
|
// These are placeholder IPs - replace with actual Ticket Tailor IPs
|
||||||
|
// Format: 'x.x.x.x' or 'x.x.x.x/24' for CIDR ranges
|
||||||
|
'0.0.0.0/0', // Allow all by default - should be configured by admin
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Security logger
|
||||||
|
*/
|
||||||
|
private $security_logger;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor - SECURITY ENHANCEMENT: Added security logger
|
||||||
|
*/
|
||||||
|
public function __construct($events, $orders) {
|
||||||
|
$this->events = $events;
|
||||||
|
$this->orders = $orders;
|
||||||
|
$this->webhook_secret = get_option('ticket_tailor_webhook_secret', '');
|
||||||
|
$this->security_logger = new Ticket_Tailor_Security_Logger();
|
||||||
|
|
||||||
|
// Register webhook endpoint
|
||||||
|
add_action('rest_api_init', array($this, 'register_webhook_endpoint'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register REST API endpoint for webhooks
|
||||||
|
*/
|
||||||
|
public function register_webhook_endpoint() {
|
||||||
|
register_rest_route('ticket-tailor/v1', '/webhook', array(
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => array($this, 'handle_webhook'),
|
||||||
|
'permission_callback' => '__return_true', // Public endpoint (verified via secret)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle incoming webhook - SECURITY ENHANCEMENT: IP whitelisting & logging
|
||||||
|
*/
|
||||||
|
public function handle_webhook($request) {
|
||||||
|
$ip_address = $this->get_client_ip();
|
||||||
|
|
||||||
|
// SECURITY ENHANCEMENT: Check IP whitelist first
|
||||||
|
if (!$this->verify_webhook_ip($ip_address)) {
|
||||||
|
$this->security_logger->log_failed_webhook($ip_address, 'IP not whitelisted');
|
||||||
|
return new WP_Error(
|
||||||
|
'ip_not_allowed',
|
||||||
|
__('IP address not authorized for webhooks', 'ticket-tailor'),
|
||||||
|
array('status' => 403)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// SECURITY FIX: Verify webhook secret is configured (mandatory)
|
||||||
|
if (empty($this->webhook_secret)) {
|
||||||
|
$this->security_logger->log_failed_webhook($ip_address, 'Webhook secret not configured');
|
||||||
|
return new WP_Error(
|
||||||
|
'webhook_not_configured',
|
||||||
|
__('Webhook secret not configured. Please set up webhook authentication.', 'ticket-tailor'),
|
||||||
|
array('status' => 503)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check rate limit
|
||||||
|
if (!$this->check_rate_limit($ip_address)) {
|
||||||
|
$this->security_logger->log_rate_limit_exceeded($ip_address, 'webhook');
|
||||||
|
return new WP_Error(
|
||||||
|
'rate_limit_exceeded',
|
||||||
|
__('Rate limit exceeded. Please try again later.', 'ticket-tailor'),
|
||||||
|
array('status' => 429)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get raw body with size limit (SECURITY FIX: Prevent DoS)
|
||||||
|
$body = $request->get_body();
|
||||||
|
if (strlen($body) > 102400) { // 100KB limit
|
||||||
|
return new WP_Error(
|
||||||
|
'payload_too_large',
|
||||||
|
__('Webhook payload too large', 'ticket-tailor'),
|
||||||
|
array('status' => 413)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode($body, true);
|
||||||
|
|
||||||
|
// SECURITY FIX: Validate JSON
|
||||||
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||||
|
return new WP_Error(
|
||||||
|
'invalid_json',
|
||||||
|
__('Invalid JSON payload', 'ticket-tailor'),
|
||||||
|
array('status' => 400)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// SECURITY FIX: Verify webhook signature (now mandatory)
|
||||||
|
$signature = $request->get_header('X-TT-Signature');
|
||||||
|
|
||||||
|
if (!$this->verify_signature($body, $signature)) {
|
||||||
|
$this->security_logger->log_failed_webhook($ip_address, 'Invalid signature');
|
||||||
|
return new WP_Error(
|
||||||
|
'invalid_signature',
|
||||||
|
__('Invalid webhook signature', 'ticket-tailor'),
|
||||||
|
array('status' => 401)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ENTERPRISE: Idempotency check - prevent duplicate processing
|
||||||
|
$webhook_id = $data['id'] ?? md5($body);
|
||||||
|
if ($this->is_webhook_processed($webhook_id)) {
|
||||||
|
// Already processed this webhook - return success to ack receipt
|
||||||
|
return new WP_REST_Response(array(
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Webhook already processed (idempotent)',
|
||||||
|
'webhook_id' => $webhook_id,
|
||||||
|
), 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log webhook for debugging (optional)
|
||||||
|
$this->log_webhook($data);
|
||||||
|
|
||||||
|
// SECURITY ENHANCEMENT: Log successful webhook
|
||||||
|
$event_type = $data['type'] ?? 'unknown';
|
||||||
|
$this->security_logger->log_successful_webhook($ip_address, $event_type);
|
||||||
|
|
||||||
|
// ENTERPRISE: Mark webhook as processed
|
||||||
|
$this->mark_webhook_processed($webhook_id, $event_type, $ip_address);
|
||||||
|
|
||||||
|
// Process webhook based on type
|
||||||
|
$event_type = $data['type'] ?? '';
|
||||||
|
|
||||||
|
switch ($event_type) {
|
||||||
|
case 'order.created':
|
||||||
|
case 'order.updated':
|
||||||
|
$this->handle_order_webhook($data);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'issued_ticket.created':
|
||||||
|
case 'issued_ticket.updated':
|
||||||
|
case 'issued_ticket.voided':
|
||||||
|
$this->handle_ticket_webhook($data);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'event.created':
|
||||||
|
case 'event.updated':
|
||||||
|
$this->handle_event_webhook($data);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Unknown webhook type
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger custom action for developers
|
||||||
|
do_action('ticket_tailor_webhook_received', $event_type, $data);
|
||||||
|
|
||||||
|
return new WP_REST_Response(array(
|
||||||
|
'success' => true,
|
||||||
|
'message' => 'Webhook processed',
|
||||||
|
), 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check rate limit - SECURITY FIX: IP-based rate limiting with database storage
|
||||||
|
*/
|
||||||
|
private function check_rate_limit($ip_address) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
// Sanitize IP address
|
||||||
|
$ip_address = filter_var($ip_address, FILTER_VALIDATE_IP);
|
||||||
|
if (!$ip_address) {
|
||||||
|
return false; // Invalid IP
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create rate limit table if it doesn't exist
|
||||||
|
$table_name = $wpdb->prefix . 'ticket_tailor_rate_limits';
|
||||||
|
$this->maybe_create_rate_limit_table($table_name);
|
||||||
|
|
||||||
|
// Clean up old entries (older than rate limit window)
|
||||||
|
$wpdb->query($wpdb->prepare(
|
||||||
|
"DELETE FROM {$table_name} WHERE timestamp < %s",
|
||||||
|
gmdate('Y-m-d H:i:s', time() - $this->rate_limit_window)
|
||||||
|
));
|
||||||
|
|
||||||
|
// Count requests from this IP in the current window
|
||||||
|
$request_count = $wpdb->get_var($wpdb->prepare(
|
||||||
|
"SELECT COUNT(*) FROM {$table_name} WHERE ip_address = %s AND timestamp > %s",
|
||||||
|
$ip_address,
|
||||||
|
gmdate('Y-m-d H:i:s', time() - $this->rate_limit_window)
|
||||||
|
));
|
||||||
|
|
||||||
|
if ($request_count >= $this->rate_limit) {
|
||||||
|
return false; // Rate limit exceeded
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record this request
|
||||||
|
$wpdb->insert(
|
||||||
|
$table_name,
|
||||||
|
array(
|
||||||
|
'ip_address' => $ip_address,
|
||||||
|
'timestamp' => current_time('mysql', 1),
|
||||||
|
),
|
||||||
|
array('%s', '%s')
|
||||||
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create rate limit table if it doesn't exist - SECURITY FIX
|
||||||
|
*/
|
||||||
|
private function maybe_create_rate_limit_table($table_name) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$charset_collate = $wpdb->get_charset_collate();
|
||||||
|
|
||||||
|
$sql = "CREATE TABLE IF NOT EXISTS {$table_name} (
|
||||||
|
id bigint(20) NOT NULL AUTO_INCREMENT,
|
||||||
|
ip_address varchar(45) NOT NULL,
|
||||||
|
timestamp datetime NOT NULL,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY ip_timestamp (ip_address, timestamp)
|
||||||
|
) {$charset_collate};";
|
||||||
|
|
||||||
|
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
||||||
|
dbDelta($sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get client IP address safely - SECURITY FIX
|
||||||
|
*/
|
||||||
|
private function get_client_ip() {
|
||||||
|
$ip_keys = array(
|
||||||
|
'HTTP_CF_CONNECTING_IP', // CloudFlare
|
||||||
|
'HTTP_X_FORWARDED_FOR', // Proxy
|
||||||
|
'HTTP_X_REAL_IP', // Nginx proxy
|
||||||
|
'REMOTE_ADDR', // Direct connection
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ($ip_keys as $key) {
|
||||||
|
if (!empty($_SERVER[$key])) {
|
||||||
|
$ip = $_SERVER[$key];
|
||||||
|
|
||||||
|
// Handle comma-separated list (X-Forwarded-For)
|
||||||
|
if (strpos($ip, ',') !== false) {
|
||||||
|
$ip = trim(explode(',', $ip)[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate IP
|
||||||
|
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
|
||||||
|
return $ip;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to REMOTE_ADDR even if private
|
||||||
|
return isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '0.0.0.0';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current rate limit status - NEW IN THIS VERSION
|
||||||
|
*/
|
||||||
|
public function get_rate_limit_status() {
|
||||||
|
$transient_key = 'tt_webhook_rate_limit';
|
||||||
|
$requests = get_transient($transient_key);
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'requests_made' => $requests !== false ? $requests : 0,
|
||||||
|
'requests_remaining' => $this->rate_limit - ($requests !== false ? $requests : 0),
|
||||||
|
'limit' => $this->rate_limit,
|
||||||
|
'window' => $this->rate_limit_window,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify webhook IP address - SECURITY ENHANCEMENT
|
||||||
|
*/
|
||||||
|
private function verify_webhook_ip($ip) {
|
||||||
|
// Get configured IP whitelist (defaults to allow all if not configured)
|
||||||
|
$allowed_ips = get_option('ticket_tailor_webhook_ips', $this->default_webhook_ips);
|
||||||
|
|
||||||
|
// If whitelist is empty, allow all (for backward compatibility)
|
||||||
|
if (empty($allowed_ips)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($allowed_ips as $allowed_range) {
|
||||||
|
if ($this->ip_in_range($ip, $allowed_range)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if IP is in CIDR range - SECURITY ENHANCEMENT
|
||||||
|
*/
|
||||||
|
private function ip_in_range($ip, $range) {
|
||||||
|
// Handle exact match
|
||||||
|
if (strpos($range, '/') === false) {
|
||||||
|
return $ip === $range;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle CIDR notation
|
||||||
|
list($subnet, $bits) = explode('/', $range);
|
||||||
|
|
||||||
|
// Convert IPs to long integers
|
||||||
|
$ip_long = ip2long($ip);
|
||||||
|
$subnet_long = ip2long($subnet);
|
||||||
|
|
||||||
|
if ($ip_long === false || $subnet_long === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create netmask
|
||||||
|
$mask = -1 << (32 - (int)$bits);
|
||||||
|
$subnet_long &= $mask;
|
||||||
|
|
||||||
|
return ($ip_long & $mask) == $subnet_long;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify webhook signature
|
||||||
|
*/
|
||||||
|
private function verify_signature($payload, $signature) {
|
||||||
|
if (empty($signature) || empty($this->webhook_secret)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$expected_signature = hash_hmac('sha256', $payload, $this->webhook_secret);
|
||||||
|
|
||||||
|
return hash_equals($expected_signature, $signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle order webhook
|
||||||
|
*/
|
||||||
|
private function handle_order_webhook($data) {
|
||||||
|
if (empty($data['data']['id'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$order_id = $data['data']['id'];
|
||||||
|
|
||||||
|
// Force refresh this specific order
|
||||||
|
$order = $this->orders->get_order($order_id, true);
|
||||||
|
|
||||||
|
if (!is_wp_error($order)) {
|
||||||
|
// Trigger action for custom handling
|
||||||
|
do_action('ticket_tailor_order_webhook', $data['type'], $order);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle ticket webhook
|
||||||
|
*/
|
||||||
|
private function handle_ticket_webhook($data) {
|
||||||
|
if (empty($data['data']['order_id'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$order_id = $data['data']['order_id'];
|
||||||
|
|
||||||
|
// Refresh the related order
|
||||||
|
$order = $this->orders->get_order($order_id, true);
|
||||||
|
|
||||||
|
if (!is_wp_error($order)) {
|
||||||
|
// Trigger action for custom handling
|
||||||
|
do_action('ticket_tailor_ticket_webhook', $data['type'], $data['data'], $order);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle event webhook
|
||||||
|
*/
|
||||||
|
private function handle_event_webhook($data) {
|
||||||
|
if (empty($data['data']['id'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$event_id = $data['data']['id'];
|
||||||
|
|
||||||
|
// Force refresh this specific event
|
||||||
|
$event = $this->events->get_event($event_id, true);
|
||||||
|
|
||||||
|
if (!is_wp_error($event)) {
|
||||||
|
// Trigger action for custom handling
|
||||||
|
do_action('ticket_tailor_event_webhook', $data['type'], $event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log webhook for debugging
|
||||||
|
*/
|
||||||
|
private function log_webhook($data) {
|
||||||
|
// Only log if debug mode is enabled
|
||||||
|
if (!get_option('ticket_tailor_debug_mode', false)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$log_entry = array(
|
||||||
|
'timestamp' => current_time('mysql'),
|
||||||
|
'type' => $data['type'] ?? 'unknown',
|
||||||
|
'data' => $data,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Store in transient (keep last 50 webhooks)
|
||||||
|
$logs = get_transient('ticket_tailor_webhook_logs') ?: array();
|
||||||
|
array_unshift($logs, $log_entry);
|
||||||
|
$logs = array_slice($logs, 0, 50);
|
||||||
|
set_transient('ticket_tailor_webhook_logs', $logs, DAY_IN_SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get webhook URL
|
||||||
|
*/
|
||||||
|
public function get_webhook_url() {
|
||||||
|
return rest_url('ticket-tailor/v1/webhook');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate webhook secret
|
||||||
|
*/
|
||||||
|
public function generate_webhook_secret() {
|
||||||
|
$secret = wp_generate_password(64, false);
|
||||||
|
update_option('ticket_tailor_webhook_secret', $secret);
|
||||||
|
$this->webhook_secret = $secret;
|
||||||
|
return $secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set webhook secret
|
||||||
|
*/
|
||||||
|
public function set_webhook_secret($secret) {
|
||||||
|
$secret = sanitize_text_field($secret);
|
||||||
|
update_option('ticket_tailor_webhook_secret', $secret);
|
||||||
|
$this->webhook_secret = $secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear webhook secret
|
||||||
|
*/
|
||||||
|
public function clear_webhook_secret() {
|
||||||
|
delete_option('ticket_tailor_webhook_secret');
|
||||||
|
$this->webhook_secret = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get webhook logs
|
||||||
|
*/
|
||||||
|
public function get_logs() {
|
||||||
|
return get_transient('ticket_tailor_webhook_logs') ?: array();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear webhook logs
|
||||||
|
*/
|
||||||
|
public function clear_logs() {
|
||||||
|
delete_transient('ticket_tailor_webhook_logs');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test webhook
|
||||||
|
*/
|
||||||
|
public function test_webhook() {
|
||||||
|
$test_data = array(
|
||||||
|
'type' => 'test.webhook',
|
||||||
|
'created_at' => current_time('mysql'),
|
||||||
|
'data' => array(
|
||||||
|
'message' => 'This is a test webhook',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->log_webhook($test_data);
|
||||||
|
|
||||||
|
do_action('ticket_tailor_webhook_received', 'test.webhook', $test_data);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if webhook has been processed - ENTERPRISE: Idempotency
|
||||||
|
*/
|
||||||
|
private function is_webhook_processed($webhook_id) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$table = $wpdb->prefix . 'ticket_tailor_webhook_log';
|
||||||
|
|
||||||
|
$exists = $wpdb->get_var($wpdb->prepare(
|
||||||
|
"SELECT COUNT(*) FROM {$table} WHERE webhook_id = %s",
|
||||||
|
$webhook_id
|
||||||
|
));
|
||||||
|
|
||||||
|
return (bool) $exists;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mark webhook as processed - ENTERPRISE: Idempotency
|
||||||
|
*/
|
||||||
|
private function mark_webhook_processed($webhook_id, $event_type, $ip_address) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$table = $wpdb->prefix . 'ticket_tailor_webhook_log';
|
||||||
|
|
||||||
|
$wpdb->insert(
|
||||||
|
$table,
|
||||||
|
array(
|
||||||
|
'webhook_id' => $webhook_id,
|
||||||
|
'event_type' => $event_type,
|
||||||
|
'processed_at' => current_time('mysql', 1),
|
||||||
|
'ip_address' => $ip_address,
|
||||||
|
),
|
||||||
|
array('%s', '%s', '%s', '%s')
|
||||||
|
);
|
||||||
|
|
||||||
|
// ENTERPRISE: Cleanup old webhook logs (older than 30 days)
|
||||||
|
$wpdb->query($wpdb->prepare(
|
||||||
|
"DELETE FROM {$table} WHERE processed_at < %s",
|
||||||
|
gmdate('Y-m-d H:i:s', time() - (30 * DAY_IN_SECONDS))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
2
native/wordpress/ticket-tailor-wp-max/includes/index.php
Normal file
2
native/wordpress/ticket-tailor-wp-max/includes/index.php
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
<?php
|
||||||
|
// Silence is golden.
|
||||||
2
native/wordpress/ticket-tailor-wp-max/index.php
Normal file
2
native/wordpress/ticket-tailor-wp-max/index.php
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
<?php
|
||||||
|
// Silence is golden.
|
||||||
783
native/wordpress/ticket-tailor-wp-max/ticket-tailor-max.php
Normal file
783
native/wordpress/ticket-tailor-wp-max/ticket-tailor-max.php
Normal file
|
|
@ -0,0 +1,783 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Plugin Name: Ticket Tailor MAX
|
||||||
|
* Plugin URI: https://www.tickettailor.com/
|
||||||
|
* Description: Integrate Ticket Tailor events and ticketing with WordPress. Display events, sell tickets, and manage orders seamlessly.
|
||||||
|
* Version: 4.0.0
|
||||||
|
* Author: SSP Media
|
||||||
|
* Author URI: https://sspmedia.ca/wordpress/
|
||||||
|
* License: GPL v2 or later
|
||||||
|
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
||||||
|
* Text Domain: ticket-tailor
|
||||||
|
* Domain Path: /languages
|
||||||
|
* Requires at least: 5.0
|
||||||
|
* Requires PHP: 7.2
|
||||||
|
* WC tested up to: 8.0
|
||||||
|
* WC requires at least: 3.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Exit if accessed directly
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define plugin constants
|
||||||
|
define('TICKET_TAILOR_VERSION', '4.0');
|
||||||
|
define('TICKET_TAILOR_PLUGIN_FILE', __FILE__);
|
||||||
|
define('TICKET_TAILOR_PLUGIN_DIR', plugin_dir_path(__FILE__));
|
||||||
|
define('TICKET_TAILOR_PLUGIN_URL', plugin_dir_url(__FILE__));
|
||||||
|
define('TICKET_TAILOR_PLUGIN_BASENAME', plugin_basename(__FILE__));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main Ticket Tailor Plugin Class
|
||||||
|
*/
|
||||||
|
class Ticket_Tailor_Plugin {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Single instance of the class
|
||||||
|
*/
|
||||||
|
private static $instance = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API Client
|
||||||
|
*/
|
||||||
|
public $api;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event Manager
|
||||||
|
*/
|
||||||
|
public $events;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Order Manager
|
||||||
|
*/
|
||||||
|
public $orders;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Webhook Handler
|
||||||
|
*/
|
||||||
|
public $webhooks;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Admin Handler
|
||||||
|
*/
|
||||||
|
public $admin;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get single instance
|
||||||
|
*/
|
||||||
|
public static function get_instance() {
|
||||||
|
if (null === self::$instance) {
|
||||||
|
self::$instance = new self();
|
||||||
|
}
|
||||||
|
return self::$instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*/
|
||||||
|
private function __construct() {
|
||||||
|
$this->load_dependencies();
|
||||||
|
$this->init_components();
|
||||||
|
$this->init_hooks();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load required files
|
||||||
|
*/
|
||||||
|
private function load_dependencies() {
|
||||||
|
// Security classes - SECURITY ENHANCEMENT
|
||||||
|
require_once TICKET_TAILOR_PLUGIN_DIR . 'includes/class-security-logger.php';
|
||||||
|
|
||||||
|
// Core classes
|
||||||
|
require_once TICKET_TAILOR_PLUGIN_DIR . 'includes/class-api-client.php';
|
||||||
|
require_once TICKET_TAILOR_PLUGIN_DIR . 'includes/class-event-manager.php';
|
||||||
|
require_once TICKET_TAILOR_PLUGIN_DIR . 'includes/class-order-manager.php';
|
||||||
|
require_once TICKET_TAILOR_PLUGIN_DIR . 'includes/class-webhook-handler.php';
|
||||||
|
require_once TICKET_TAILOR_PLUGIN_DIR . 'includes/class-admin.php';
|
||||||
|
require_once TICKET_TAILOR_PLUGIN_DIR . 'includes/class-blocks.php';
|
||||||
|
require_once TICKET_TAILOR_PLUGIN_DIR . 'includes/class-shortcodes.php';
|
||||||
|
require_once TICKET_TAILOR_PLUGIN_DIR . 'includes/class-template-loader.php';
|
||||||
|
|
||||||
|
// ENTERPRISE: Health check endpoint
|
||||||
|
require_once TICKET_TAILOR_PLUGIN_DIR . 'includes/class-health-check.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize components
|
||||||
|
*/
|
||||||
|
private function init_components() {
|
||||||
|
$this->api = new Ticket_Tailor_API_Client();
|
||||||
|
$this->events = new Ticket_Tailor_Event_Manager($this->api);
|
||||||
|
$this->orders = new Ticket_Tailor_Order_Manager($this->api);
|
||||||
|
$this->webhooks = new Ticket_Tailor_Webhook_Handler($this->events, $this->orders);
|
||||||
|
$this->admin = new Ticket_Tailor_Admin($this->api, $this->events, $this->orders);
|
||||||
|
|
||||||
|
// ENTERPRISE: Health check for monitoring
|
||||||
|
new Ticket_Tailor_Health_Check($this->api, $this->events, $this->orders);
|
||||||
|
|
||||||
|
new Ticket_Tailor_Blocks($this->events);
|
||||||
|
new Ticket_Tailor_Shortcodes($this->events, $this->orders);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize hooks
|
||||||
|
*/
|
||||||
|
private function init_hooks() {
|
||||||
|
register_activation_hook(TICKET_TAILOR_PLUGIN_FILE, array($this, 'activate'));
|
||||||
|
register_deactivation_hook(TICKET_TAILOR_PLUGIN_FILE, array($this, 'deactivate'));
|
||||||
|
|
||||||
|
add_action('plugins_loaded', array($this, 'load_textdomain'));
|
||||||
|
add_action('init', array($this, 'register_post_types'));
|
||||||
|
add_action('wp_enqueue_scripts', array($this, 'enqueue_frontend_assets'));
|
||||||
|
|
||||||
|
// Cron jobs for syncing
|
||||||
|
add_action('ticket_tailor_sync_events', array($this->events, 'sync_all_events'));
|
||||||
|
add_action('ticket_tailor_sync_orders', array($this->orders, 'sync_all_orders'));
|
||||||
|
|
||||||
|
// PERFORMANCE FIX: Cron jobs for cleanup
|
||||||
|
add_action('ticket_tailor_cleanup_security_logs', array($this, 'cleanup_security_logs'));
|
||||||
|
add_action('ticket_tailor_cleanup_rate_limits', array($this, 'cleanup_rate_limits'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enqueue frontend assets
|
||||||
|
*/
|
||||||
|
public function enqueue_frontend_assets() {
|
||||||
|
// Enqueue frontend CSS
|
||||||
|
wp_enqueue_style(
|
||||||
|
'ticket-tailor-frontend',
|
||||||
|
TICKET_TAILOR_PLUGIN_URL . 'assets/css/frontend.css',
|
||||||
|
array(),
|
||||||
|
TICKET_TAILOR_VERSION
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin activation - SECURITY ENHANCEMENT: Added custom capabilities
|
||||||
|
*/
|
||||||
|
public function activate() {
|
||||||
|
// Create custom tables
|
||||||
|
$this->create_tables();
|
||||||
|
|
||||||
|
// SECURITY ENHANCEMENT: Add custom capabilities
|
||||||
|
$this->add_custom_capabilities();
|
||||||
|
|
||||||
|
// Schedule cron jobs
|
||||||
|
if (!wp_next_scheduled('ticket_tailor_sync_events')) {
|
||||||
|
wp_schedule_event(time(), 'hourly', 'ticket_tailor_sync_events');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!wp_next_scheduled('ticket_tailor_sync_orders')) {
|
||||||
|
wp_schedule_event(time(), 'hourly', 'ticket_tailor_sync_orders');
|
||||||
|
}
|
||||||
|
|
||||||
|
// PERFORMANCE FIX: Schedule cleanup cron jobs
|
||||||
|
if (!wp_next_scheduled('ticket_tailor_cleanup_security_logs')) {
|
||||||
|
wp_schedule_event(time(), 'daily', 'ticket_tailor_cleanup_security_logs');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!wp_next_scheduled('ticket_tailor_cleanup_rate_limits')) {
|
||||||
|
wp_schedule_event(time(), 'hourly', 'ticket_tailor_cleanup_rate_limits');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set activation time
|
||||||
|
if (!get_option('ticket_tailor_activated_time')) {
|
||||||
|
add_option('ticket_tailor_activated_time', time());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set welcome transient
|
||||||
|
set_transient('ticket_tailor_welcome_notice', true, 30);
|
||||||
|
|
||||||
|
// Flush rewrite rules for custom post types
|
||||||
|
flush_rewrite_rules();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add custom capabilities - SECURITY ENHANCEMENT
|
||||||
|
*/
|
||||||
|
private function add_custom_capabilities() {
|
||||||
|
// Administrator gets all capabilities
|
||||||
|
$admin_role = get_role('administrator');
|
||||||
|
if ($admin_role) {
|
||||||
|
$admin_role->add_cap('manage_ticket_tailor');
|
||||||
|
$admin_role->add_cap('view_ticket_tailor_dashboard');
|
||||||
|
$admin_role->add_cap('view_ticket_tailor_events');
|
||||||
|
$admin_role->add_cap('view_ticket_tailor_orders');
|
||||||
|
$admin_role->add_cap('sync_ticket_tailor_events');
|
||||||
|
$admin_role->add_cap('configure_ticket_tailor_settings');
|
||||||
|
$admin_role->add_cap('configure_ticket_tailor_webhooks');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Editor can view but not configure
|
||||||
|
$editor_role = get_role('editor');
|
||||||
|
if ($editor_role) {
|
||||||
|
$editor_role->add_cap('view_ticket_tailor_dashboard');
|
||||||
|
$editor_role->add_cap('view_ticket_tailor_events');
|
||||||
|
$editor_role->add_cap('view_ticket_tailor_orders');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shop Manager (WooCommerce) gets order access
|
||||||
|
$shop_manager_role = get_role('shop_manager');
|
||||||
|
if ($shop_manager_role) {
|
||||||
|
$shop_manager_role->add_cap('view_ticket_tailor_dashboard');
|
||||||
|
$shop_manager_role->add_cap('view_ticket_tailor_events');
|
||||||
|
$shop_manager_role->add_cap('view_ticket_tailor_orders');
|
||||||
|
$shop_manager_role->add_cap('sync_ticket_tailor_events');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin deactivation - FIXED VERSION (No security errors)
|
||||||
|
*/
|
||||||
|
public function deactivate() {
|
||||||
|
// Prevent any admin actions from running during deactivation
|
||||||
|
remove_all_actions('admin_init');
|
||||||
|
|
||||||
|
// Clear scheduled events safely
|
||||||
|
$timestamp_events = wp_next_scheduled('ticket_tailor_sync_events');
|
||||||
|
if ($timestamp_events) {
|
||||||
|
wp_unschedule_event($timestamp_events, 'ticket_tailor_sync_events');
|
||||||
|
}
|
||||||
|
|
||||||
|
$timestamp_orders = wp_next_scheduled('ticket_tailor_sync_orders');
|
||||||
|
if ($timestamp_orders) {
|
||||||
|
wp_unschedule_event($timestamp_orders, 'ticket_tailor_sync_orders');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear all scheduled hooks
|
||||||
|
wp_clear_scheduled_hook('ticket_tailor_sync_events');
|
||||||
|
wp_clear_scheduled_hook('ticket_tailor_sync_orders');
|
||||||
|
wp_clear_scheduled_hook('ticket_tailor_cleanup_security_logs');
|
||||||
|
wp_clear_scheduled_hook('ticket_tailor_cleanup_rate_limits');
|
||||||
|
|
||||||
|
// Clear transients
|
||||||
|
delete_transient('ticket_tailor_events_cache');
|
||||||
|
delete_transient('ticket_tailor_orders_cache');
|
||||||
|
delete_transient('ticket_tailor_welcome_notice');
|
||||||
|
|
||||||
|
// Clear any user notices - SECURITY FIX: Use prepared statements
|
||||||
|
global $wpdb;
|
||||||
|
$wpdb->query($wpdb->prepare(
|
||||||
|
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
|
||||||
|
$wpdb->esc_like('_transient_ticket_tailor_notice_') . '%'
|
||||||
|
));
|
||||||
|
$wpdb->query($wpdb->prepare(
|
||||||
|
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
|
||||||
|
$wpdb->esc_like('_transient_timeout_ticket_tailor_notice_') . '%'
|
||||||
|
));
|
||||||
|
|
||||||
|
// Flush rewrite rules
|
||||||
|
flush_rewrite_rules();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create custom database tables - ENTERPRISE: Optimized indexes
|
||||||
|
*/
|
||||||
|
private function create_tables() {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$charset_collate = $wpdb->get_charset_collate();
|
||||||
|
|
||||||
|
// Events cache table - ENTERPRISE: Added composite index
|
||||||
|
$table_events = $wpdb->prefix . 'ticket_tailor_events';
|
||||||
|
$sql_events = "CREATE TABLE IF NOT EXISTS $table_events (
|
||||||
|
id bigint(20) NOT NULL AUTO_INCREMENT,
|
||||||
|
event_id varchar(255) NOT NULL,
|
||||||
|
event_data longtext NOT NULL,
|
||||||
|
last_synced datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE KEY event_id (event_id),
|
||||||
|
KEY last_synced (last_synced),
|
||||||
|
KEY event_id_synced (event_id, last_synced)
|
||||||
|
) $charset_collate;";
|
||||||
|
|
||||||
|
// Orders cache table - ENTERPRISE: Added composite indexes
|
||||||
|
$table_orders = $wpdb->prefix . 'ticket_tailor_orders';
|
||||||
|
$sql_orders = "CREATE TABLE IF NOT EXISTS $table_orders (
|
||||||
|
id bigint(20) NOT NULL AUTO_INCREMENT,
|
||||||
|
order_id varchar(255) NOT NULL,
|
||||||
|
event_id varchar(255) NOT NULL,
|
||||||
|
order_data longtext NOT NULL,
|
||||||
|
last_synced datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE KEY order_id (order_id),
|
||||||
|
KEY event_id (event_id),
|
||||||
|
KEY last_synced (last_synced),
|
||||||
|
KEY event_synced (event_id, last_synced),
|
||||||
|
KEY order_synced (order_id, last_synced)
|
||||||
|
) $charset_collate;";
|
||||||
|
|
||||||
|
// Webhook idempotency table - ENTERPRISE: Prevent duplicate webhook processing
|
||||||
|
$table_webhooks = $wpdb->prefix . 'ticket_tailor_webhook_log';
|
||||||
|
$sql_webhooks = "CREATE TABLE IF NOT EXISTS $table_webhooks (
|
||||||
|
id bigint(20) NOT NULL AUTO_INCREMENT,
|
||||||
|
webhook_id varchar(255) NOT NULL,
|
||||||
|
event_type varchar(100) NOT NULL,
|
||||||
|
processed_at datetime NOT NULL,
|
||||||
|
ip_address varchar(45) NOT NULL,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
UNIQUE KEY webhook_id (webhook_id),
|
||||||
|
KEY processed_at (processed_at),
|
||||||
|
KEY event_type (event_type)
|
||||||
|
) $charset_collate;";
|
||||||
|
|
||||||
|
// Security log table
|
||||||
|
$table_security = $wpdb->prefix . 'ticket_tailor_security_log';
|
||||||
|
$sql_security = "CREATE TABLE IF NOT EXISTS $table_security (
|
||||||
|
id bigint(20) NOT NULL AUTO_INCREMENT,
|
||||||
|
event_type varchar(50) NOT NULL,
|
||||||
|
user_id bigint(20) DEFAULT 0,
|
||||||
|
ip_address varchar(45) NOT NULL,
|
||||||
|
details longtext,
|
||||||
|
timestamp datetime NOT NULL,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY event_type (event_type),
|
||||||
|
KEY timestamp (timestamp),
|
||||||
|
KEY ip_address (ip_address)
|
||||||
|
) $charset_collate;";
|
||||||
|
|
||||||
|
// Rate limits table
|
||||||
|
$table_rate_limits = $wpdb->prefix . 'ticket_tailor_rate_limits';
|
||||||
|
$sql_rate_limits = "CREATE TABLE IF NOT EXISTS $table_rate_limits (
|
||||||
|
id bigint(20) NOT NULL AUTO_INCREMENT,
|
||||||
|
ip_address varchar(45) NOT NULL,
|
||||||
|
endpoint varchar(255) NOT NULL,
|
||||||
|
request_count int(11) NOT NULL DEFAULT 1,
|
||||||
|
timestamp datetime NOT NULL,
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
KEY ip_endpoint (ip_address, endpoint),
|
||||||
|
KEY timestamp (timestamp)
|
||||||
|
) $charset_collate;";
|
||||||
|
|
||||||
|
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
||||||
|
dbDelta($sql_events);
|
||||||
|
dbDelta($sql_orders);
|
||||||
|
dbDelta($sql_webhooks);
|
||||||
|
dbDelta($sql_security);
|
||||||
|
dbDelta($sql_rate_limits);
|
||||||
|
|
||||||
|
// Store database version
|
||||||
|
update_option('ticket_tailor_db_version', '2.0');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register custom post types
|
||||||
|
* FIXED: Added show_in_menu => false to prevent duplicate menu
|
||||||
|
*/
|
||||||
|
public function register_post_types() {
|
||||||
|
// Register Event post type for better WordPress integration
|
||||||
|
register_post_type('tt_event', array(
|
||||||
|
'labels' => array(
|
||||||
|
'name' => __('Events', 'ticket-tailor'),
|
||||||
|
'singular_name' => __('Event', 'ticket-tailor'),
|
||||||
|
'add_new' => __('Add New Event', 'ticket-tailor'),
|
||||||
|
'add_new_item' => __('Add New Event', 'ticket-tailor'),
|
||||||
|
'edit_item' => __('Edit Event', 'ticket-tailor'),
|
||||||
|
'new_item' => __('New Event', 'ticket-tailor'),
|
||||||
|
'view_item' => __('View Event', 'ticket-tailor'),
|
||||||
|
'search_items' => __('Search Events', 'ticket-tailor'),
|
||||||
|
'not_found' => __('No events found', 'ticket-tailor'),
|
||||||
|
'not_found_in_trash' => __('No events found in trash', 'ticket-tailor'),
|
||||||
|
'all_items' => __('All Events', 'ticket-tailor'),
|
||||||
|
'menu_name' => __('Events', 'ticket-tailor'),
|
||||||
|
),
|
||||||
|
'public' => true,
|
||||||
|
'has_archive' => true,
|
||||||
|
'show_in_rest' => true,
|
||||||
|
'supports' => array('title', 'editor', 'thumbnail', 'excerpt', 'custom-fields'),
|
||||||
|
'menu_icon' => 'dashicons-tickets-alt',
|
||||||
|
'rewrite' => array(
|
||||||
|
'slug' => 'events',
|
||||||
|
'with_front' => false,
|
||||||
|
),
|
||||||
|
// FIX: Hide the automatic menu item since we have our own custom menu structure
|
||||||
|
'show_in_menu' => false, // This removes the duplicate "Events" menu
|
||||||
|
// The events are still accessible through our custom Ticket Tailor menu
|
||||||
|
'capability_type' => 'post',
|
||||||
|
'capabilities' => array(
|
||||||
|
'publish_posts' => 'publish_tt_events',
|
||||||
|
'edit_posts' => 'edit_tt_events',
|
||||||
|
'edit_others_posts' => 'edit_others_tt_events',
|
||||||
|
'delete_posts' => 'delete_tt_events',
|
||||||
|
'delete_others_posts' => 'delete_others_tt_events',
|
||||||
|
'read_private_posts' => 'read_private_tt_events',
|
||||||
|
'edit_post' => 'edit_tt_event',
|
||||||
|
'delete_post' => 'delete_tt_event',
|
||||||
|
'read_post' => 'read_tt_event',
|
||||||
|
),
|
||||||
|
'map_meta_cap' => true,
|
||||||
|
'taxonomies' => array('tt_event_category', 'tt_event_tag'),
|
||||||
|
'show_ui' => true,
|
||||||
|
'show_in_nav_menus' => true,
|
||||||
|
'can_export' => true,
|
||||||
|
'exclude_from_search' => false,
|
||||||
|
'publicly_queryable' => true,
|
||||||
|
));
|
||||||
|
|
||||||
|
// Register Event Category taxonomy
|
||||||
|
register_taxonomy('tt_event_category', 'tt_event', array(
|
||||||
|
'labels' => array(
|
||||||
|
'name' => __('Event Categories', 'ticket-tailor'),
|
||||||
|
'singular_name' => __('Event Category', 'ticket-tailor'),
|
||||||
|
'search_items' => __('Search Event Categories', 'ticket-tailor'),
|
||||||
|
'all_items' => __('All Event Categories', 'ticket-tailor'),
|
||||||
|
'parent_item' => __('Parent Event Category', 'ticket-tailor'),
|
||||||
|
'parent_item_colon' => __('Parent Event Category:', 'ticket-tailor'),
|
||||||
|
'edit_item' => __('Edit Event Category', 'ticket-tailor'),
|
||||||
|
'update_item' => __('Update Event Category', 'ticket-tailor'),
|
||||||
|
'add_new_item' => __('Add New Event Category', 'ticket-tailor'),
|
||||||
|
'new_item_name' => __('New Event Category Name', 'ticket-tailor'),
|
||||||
|
'menu_name' => __('Categories', 'ticket-tailor'),
|
||||||
|
),
|
||||||
|
'hierarchical' => true,
|
||||||
|
'public' => true,
|
||||||
|
'show_ui' => true,
|
||||||
|
'show_admin_column' => true,
|
||||||
|
'show_in_nav_menus' => true,
|
||||||
|
'show_tagcloud' => true,
|
||||||
|
'show_in_rest' => true,
|
||||||
|
'rewrite' => array(
|
||||||
|
'slug' => 'event-category',
|
||||||
|
'with_front' => false,
|
||||||
|
'hierarchical' => true,
|
||||||
|
),
|
||||||
|
));
|
||||||
|
|
||||||
|
// Register Event Tag taxonomy
|
||||||
|
register_taxonomy('tt_event_tag', 'tt_event', array(
|
||||||
|
'labels' => array(
|
||||||
|
'name' => __('Event Tags', 'ticket-tailor'),
|
||||||
|
'singular_name' => __('Event Tag', 'ticket-tailor'),
|
||||||
|
'search_items' => __('Search Event Tags', 'ticket-tailor'),
|
||||||
|
'popular_items' => __('Popular Event Tags', 'ticket-tailor'),
|
||||||
|
'all_items' => __('All Event Tags', 'ticket-tailor'),
|
||||||
|
'edit_item' => __('Edit Event Tag', 'ticket-tailor'),
|
||||||
|
'update_item' => __('Update Event Tag', 'ticket-tailor'),
|
||||||
|
'add_new_item' => __('Add New Event Tag', 'ticket-tailor'),
|
||||||
|
'new_item_name' => __('New Event Tag Name', 'ticket-tailor'),
|
||||||
|
'separate_items_with_commas' => __('Separate event tags with commas', 'ticket-tailor'),
|
||||||
|
'add_or_remove_items' => __('Add or remove event tags', 'ticket-tailor'),
|
||||||
|
'choose_from_most_used' => __('Choose from the most used event tags', 'ticket-tailor'),
|
||||||
|
'menu_name' => __('Tags', 'ticket-tailor'),
|
||||||
|
),
|
||||||
|
'hierarchical' => false,
|
||||||
|
'public' => true,
|
||||||
|
'show_ui' => true,
|
||||||
|
'show_admin_column' => true,
|
||||||
|
'show_in_nav_menus' => true,
|
||||||
|
'show_tagcloud' => true,
|
||||||
|
'show_in_rest' => true,
|
||||||
|
'rewrite' => array(
|
||||||
|
'slug' => 'event-tag',
|
||||||
|
'with_front' => false,
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load text domain for translations
|
||||||
|
*/
|
||||||
|
public function load_textdomain() {
|
||||||
|
load_plugin_textdomain(
|
||||||
|
'ticket-tailor',
|
||||||
|
false,
|
||||||
|
dirname(plugin_basename(TICKET_TAILOR_PLUGIN_FILE)) . '/languages/'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check WooCommerce and HPOS status
|
||||||
|
*/
|
||||||
|
public function get_woocommerce_status() {
|
||||||
|
if (!class_exists('WooCommerce')) {
|
||||||
|
return array(
|
||||||
|
'active' => false,
|
||||||
|
'version' => null,
|
||||||
|
'hpos_enabled' => false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$hpos_enabled = false;
|
||||||
|
if (class_exists('\Automattic\WooCommerce\Utilities\OrderUtil')) {
|
||||||
|
$hpos_enabled = \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'active' => true,
|
||||||
|
'version' => defined('WC_VERSION') ? WC_VERSION : WC()->version,
|
||||||
|
'hpos_enabled' => $hpos_enabled,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the plugin is properly configured
|
||||||
|
*/
|
||||||
|
public function is_configured() {
|
||||||
|
$api_key = get_option('ticket_tailor_api_key', '');
|
||||||
|
return !empty($api_key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get plugin version
|
||||||
|
*/
|
||||||
|
public function get_version() {
|
||||||
|
return TICKET_TAILOR_VERSION;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get plugin URL
|
||||||
|
*/
|
||||||
|
public function get_plugin_url() {
|
||||||
|
return TICKET_TAILOR_PLUGIN_URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get plugin path
|
||||||
|
*/
|
||||||
|
public function get_plugin_path() {
|
||||||
|
return TICKET_TAILOR_PLUGIN_DIR;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log debug messages - SECURITY FIX: Store logs outside web root or with protection
|
||||||
|
*/
|
||||||
|
public function log($message, $level = 'info') {
|
||||||
|
if (!get_option('ticket_tailor_debug_mode', false)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_array($message) || is_object($message)) {
|
||||||
|
$message = print_r($message, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanitize message to prevent log injection
|
||||||
|
$message = str_replace(array("\r", "\n"), ' ', $message);
|
||||||
|
|
||||||
|
$log_entry = sprintf(
|
||||||
|
'[%s] [%s] %s',
|
||||||
|
date('Y-m-d H:i:s'),
|
||||||
|
strtoupper(sanitize_key($level)),
|
||||||
|
$message
|
||||||
|
);
|
||||||
|
|
||||||
|
// Use WordPress uploads directory with .htaccess protection
|
||||||
|
$upload_dir = wp_upload_dir();
|
||||||
|
$log_dir = $upload_dir['basedir'] . '/ticket-tailor-logs';
|
||||||
|
|
||||||
|
// Create log directory if it doesn't exist
|
||||||
|
if (!file_exists($log_dir)) {
|
||||||
|
wp_mkdir_p($log_dir);
|
||||||
|
|
||||||
|
// Create .htaccess to deny web access
|
||||||
|
$htaccess_content = "deny from all\n";
|
||||||
|
file_put_contents($log_dir . '/.htaccess', $htaccess_content);
|
||||||
|
|
||||||
|
// Create index.php for additional protection
|
||||||
|
file_put_contents($log_dir . '/index.php', '<?php // Silence is golden');
|
||||||
|
}
|
||||||
|
|
||||||
|
$log_file = $log_dir . '/debug.log';
|
||||||
|
|
||||||
|
// Implement log rotation (max 5MB)
|
||||||
|
if (file_exists($log_file) && filesize($log_file) > 5242880) {
|
||||||
|
rename($log_file, $log_dir . '/debug-' . date('Y-m-d-His') . '.log');
|
||||||
|
}
|
||||||
|
|
||||||
|
error_log($log_entry . PHP_EOL, 3, $log_file);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleanup security logs - PERFORMANCE FIX
|
||||||
|
* Called by daily cron job
|
||||||
|
*/
|
||||||
|
public function cleanup_security_logs() {
|
||||||
|
$security_logger = new Ticket_Tailor_Security_Logger();
|
||||||
|
$security_logger->cleanup_old_logs();
|
||||||
|
|
||||||
|
$this->log('Security logs cleanup completed', 'info');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cleanup rate limit table - PERFORMANCE FIX
|
||||||
|
* Called by hourly cron job
|
||||||
|
*/
|
||||||
|
public function cleanup_rate_limits() {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$table_name = $wpdb->prefix . 'ticket_tailor_rate_limits';
|
||||||
|
|
||||||
|
// Delete entries older than 24 hours
|
||||||
|
$threshold = gmdate('Y-m-d H:i:s', time() - DAY_IN_SECONDS);
|
||||||
|
|
||||||
|
$deleted = $wpdb->query($wpdb->prepare(
|
||||||
|
"DELETE FROM {$table_name} WHERE timestamp < %s",
|
||||||
|
$threshold
|
||||||
|
));
|
||||||
|
|
||||||
|
if ($deleted !== false) {
|
||||||
|
$this->log('Rate limit cleanup completed: ' . $deleted . ' entries removed', 'info');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle plugin upgrade
|
||||||
|
*/
|
||||||
|
public function maybe_upgrade() {
|
||||||
|
$current_version = get_option('ticket_tailor_version', '0.0.0');
|
||||||
|
|
||||||
|
if (version_compare($current_version, TICKET_TAILOR_VERSION, '<')) {
|
||||||
|
// Perform upgrade tasks here if needed
|
||||||
|
|
||||||
|
// Update version
|
||||||
|
update_option('ticket_tailor_version', TICKET_TAILOR_VERSION);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the plugin
|
||||||
|
*/
|
||||||
|
function ticket_tailor() {
|
||||||
|
return Ticket_Tailor_Plugin::get_instance();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Declare WooCommerce HPOS compatibility
|
||||||
|
add_action('before_woocommerce_init', function() {
|
||||||
|
if (class_exists(\Automattic\WooCommerce\Utilities\FeaturesUtil::class)) {
|
||||||
|
\Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility(
|
||||||
|
'custom_order_tables',
|
||||||
|
TICKET_TAILOR_PLUGIN_FILE,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check plugin dependencies
|
||||||
|
add_action('admin_init', function() {
|
||||||
|
// Skip checks during plugin activation/deactivation
|
||||||
|
if (defined('WP_UNINSTALL_PLUGIN') ||
|
||||||
|
(isset($_REQUEST['action']) && in_array($_REQUEST['action'], array('activate', 'deactivate', 'delete')))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_plugin_active('ticket-tailor/ticket-tailor.php')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check PHP version
|
||||||
|
if (version_compare(PHP_VERSION, '7.2', '<')) {
|
||||||
|
deactivate_plugins(plugin_basename(TICKET_TAILOR_PLUGIN_FILE));
|
||||||
|
wp_die(
|
||||||
|
esc_html__('Ticket Tailor requires PHP version 7.2 or higher. Please upgrade your PHP version.', 'ticket-tailor'),
|
||||||
|
esc_html__('Plugin Activation Error', 'ticket-tailor'),
|
||||||
|
array('back_link' => true)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check WordPress version
|
||||||
|
if (version_compare(get_bloginfo('version'), '5.0', '<')) {
|
||||||
|
deactivate_plugins(plugin_basename(TICKET_TAILOR_PLUGIN_FILE));
|
||||||
|
wp_die(
|
||||||
|
esc_html__('Ticket Tailor requires WordPress version 5.0 or higher. Please upgrade WordPress.', 'ticket-tailor'),
|
||||||
|
esc_html__('Plugin Activation Error', 'ticket-tailor'),
|
||||||
|
array('back_link' => true)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle uninstall
|
||||||
|
register_uninstall_hook(TICKET_TAILOR_PLUGIN_FILE, 'ticket_tailor_uninstall');
|
||||||
|
|
||||||
|
function ticket_tailor_uninstall() {
|
||||||
|
// Only run if explicitly uninstalling
|
||||||
|
if (!defined('WP_UNINSTALL_PLUGIN')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove options
|
||||||
|
delete_option('ticket_tailor_api_key');
|
||||||
|
delete_option('ticket_tailor_cache_duration');
|
||||||
|
delete_option('ticket_tailor_currency');
|
||||||
|
delete_option('ticket_tailor_debug_mode');
|
||||||
|
delete_option('ticket_tailor_webhook_secret');
|
||||||
|
delete_option('ticket_tailor_version');
|
||||||
|
delete_option('ticket_tailor_db_version');
|
||||||
|
delete_option('ticket_tailor_activated_time');
|
||||||
|
|
||||||
|
// Remove style options
|
||||||
|
delete_option('ticket_tailor_text_color');
|
||||||
|
delete_option('ticket_tailor_border_color');
|
||||||
|
delete_option('ticket_tailor_border_radius');
|
||||||
|
delete_option('ticket_tailor_button_bg');
|
||||||
|
delete_option('ticket_tailor_button_hover');
|
||||||
|
delete_option('ticket_tailor_button_text');
|
||||||
|
|
||||||
|
// Remove all transients - SECURITY FIX: Use prepared statements
|
||||||
|
global $wpdb;
|
||||||
|
$wpdb->query($wpdb->prepare(
|
||||||
|
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
|
||||||
|
$wpdb->esc_like('_transient_ticket_tailor_') . '%'
|
||||||
|
));
|
||||||
|
$wpdb->query($wpdb->prepare(
|
||||||
|
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
|
||||||
|
$wpdb->esc_like('_transient_timeout_ticket_tailor_') . '%'
|
||||||
|
));
|
||||||
|
|
||||||
|
// Remove custom tables - SECURITY FIX: Validate table names
|
||||||
|
$events_table = $wpdb->prefix . 'ticket_tailor_events';
|
||||||
|
$orders_table = $wpdb->prefix . 'ticket_tailor_orders';
|
||||||
|
$webhooks_table = $wpdb->prefix . 'ticket_tailor_webhook_log';
|
||||||
|
$security_table = $wpdb->prefix . 'ticket_tailor_security_log';
|
||||||
|
$rate_limits_table = $wpdb->prefix . 'ticket_tailor_rate_limits';
|
||||||
|
|
||||||
|
// Validate table names match expected pattern
|
||||||
|
if (preg_match('/^[a-zA-Z0-9_]+$/', $events_table)) {
|
||||||
|
$wpdb->query("DROP TABLE IF EXISTS `{$events_table}`");
|
||||||
|
}
|
||||||
|
if (preg_match('/^[a-zA-Z0-9_]+$/', $orders_table)) {
|
||||||
|
$wpdb->query("DROP TABLE IF EXISTS `{$orders_table}`");
|
||||||
|
}
|
||||||
|
if (preg_match('/^[a-zA-Z0-9_]+$/', $webhooks_table)) {
|
||||||
|
$wpdb->query("DROP TABLE IF EXISTS `{$webhooks_table}`");
|
||||||
|
}
|
||||||
|
if (preg_match('/^[a-zA-Z0-9_]+$/', $security_table)) {
|
||||||
|
$wpdb->query("DROP TABLE IF EXISTS `{$security_table}`");
|
||||||
|
}
|
||||||
|
if (preg_match('/^[a-zA-Z0-9_]+$/', $rate_limits_table)) {
|
||||||
|
$wpdb->query("DROP TABLE IF EXISTS `{$rate_limits_table}`");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear scheduled hooks
|
||||||
|
wp_clear_scheduled_hook('ticket_tailor_sync_events');
|
||||||
|
wp_clear_scheduled_hook('ticket_tailor_sync_orders');
|
||||||
|
wp_clear_scheduled_hook('ticket_tailor_cleanup_security_logs');
|
||||||
|
wp_clear_scheduled_hook('ticket_tailor_cleanup_rate_limits');
|
||||||
|
|
||||||
|
// Remove all posts of custom post type
|
||||||
|
$posts = get_posts(array(
|
||||||
|
'post_type' => 'tt_event',
|
||||||
|
'numberposts' => -1,
|
||||||
|
'post_status' => 'any'
|
||||||
|
));
|
||||||
|
|
||||||
|
foreach ($posts as $post) {
|
||||||
|
wp_delete_post($post->ID, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove all terms from custom taxonomies
|
||||||
|
$terms = get_terms(array(
|
||||||
|
'taxonomy' => array('tt_event_category', 'tt_event_tag'),
|
||||||
|
'hide_empty' => false,
|
||||||
|
));
|
||||||
|
|
||||||
|
foreach ($terms as $term) {
|
||||||
|
wp_delete_term($term->term_id, $term->taxonomy);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush rewrite rules
|
||||||
|
flush_rewrite_rules();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the plugin - FIXED: Delay initialization until WordPress is fully loaded
|
||||||
|
add_action('plugins_loaded', 'ticket_tailor');
|
||||||
117
native/wordpress/wpforms-mailjet-automations/LICENSE
Normal file
117
native/wordpress/wpforms-mailjet-automations/LICENSE
Normal file
|
|
@ -0,0 +1,117 @@
|
||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 2, June 1991
|
||||||
|
|
||||||
|
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
|
||||||
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
|
||||||
|
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it.
|
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights.
|
||||||
|
|
||||||
|
We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software.
|
||||||
|
|
||||||
|
Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations.
|
||||||
|
|
||||||
|
Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||||
|
|
||||||
|
0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you".
|
||||||
|
|
||||||
|
Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does.
|
||||||
|
|
||||||
|
1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee.
|
||||||
|
|
||||||
|
2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change.
|
||||||
|
|
||||||
|
b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License.
|
||||||
|
|
||||||
|
c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.)
|
||||||
|
|
||||||
|
These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it.
|
||||||
|
|
||||||
|
Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program.
|
||||||
|
|
||||||
|
In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License.
|
||||||
|
|
||||||
|
3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following:
|
||||||
|
|
||||||
|
a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or,
|
||||||
|
|
||||||
|
b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or,
|
||||||
|
|
||||||
|
c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.)
|
||||||
|
|
||||||
|
The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable.
|
||||||
|
|
||||||
|
If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code.
|
||||||
|
|
||||||
|
4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance.
|
||||||
|
|
||||||
|
5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it.
|
||||||
|
|
||||||
|
6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License.
|
||||||
|
|
||||||
|
7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program.
|
||||||
|
|
||||||
|
If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances.
|
||||||
|
|
||||||
|
It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice.
|
||||||
|
|
||||||
|
This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License.
|
||||||
|
|
||||||
|
8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License.
|
||||||
|
|
||||||
|
9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation.
|
||||||
|
|
||||||
|
10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally.
|
||||||
|
|
||||||
|
NO WARRANTY
|
||||||
|
|
||||||
|
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
one line to give the program's name and an idea of what it does. Copyright (C) yyyy name of author
|
||||||
|
|
||||||
|
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If the program is interactive, make it output a short notice like this when it starts in an interactive mode:
|
||||||
|
|
||||||
|
Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
|
The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program.
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names:
|
||||||
|
|
||||||
|
Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||||
|
|
||||||
|
signature of Ty Coon, 1 April 1989 Ty Coon, President of Vice
|
||||||
3
native/wordpress/wpforms-mailjet-automations/README.md
Normal file
3
native/wordpress/wpforms-mailjet-automations/README.md
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# wpforms-mailjet-automations
|
||||||
|
|
||||||
|
This plugin creates automations between WP Forms and mailjet, to make capture and management of customer data easier.
|
||||||
|
|
@ -0,0 +1,481 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Admin-specific functionality.
|
||||||
|
*
|
||||||
|
* @package WPFMJ
|
||||||
|
* @subpackage WPFMJ/admin
|
||||||
|
*/
|
||||||
|
|
||||||
|
class WPFMJ_Admin {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ID of this plugin.
|
||||||
|
*/
|
||||||
|
private $plugin_name;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The version of this plugin.
|
||||||
|
*/
|
||||||
|
private $version;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the class.
|
||||||
|
*/
|
||||||
|
public function __construct($plugin_name, $version) {
|
||||||
|
$this->plugin_name = $plugin_name;
|
||||||
|
$this->version = $version;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register the stylesheets for the admin area.
|
||||||
|
*/
|
||||||
|
public function enqueue_styles() {
|
||||||
|
$screen = get_current_screen();
|
||||||
|
|
||||||
|
if ($screen && strpos($screen->id, 'wpfmj') !== false) {
|
||||||
|
wp_enqueue_style(
|
||||||
|
$this->plugin_name,
|
||||||
|
WPFMJ_PLUGIN_URL . 'admin/css/wpfmj-admin.css',
|
||||||
|
array(),
|
||||||
|
$this->version,
|
||||||
|
'all'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register the JavaScript for the admin area.
|
||||||
|
*/
|
||||||
|
public function enqueue_scripts() {
|
||||||
|
$screen = get_current_screen();
|
||||||
|
|
||||||
|
if ($screen && strpos($screen->id, 'wpfmj') !== false) {
|
||||||
|
// Enqueue WordPress components
|
||||||
|
$asset_file = include(WPFMJ_PLUGIN_DIR . 'admin/js/wpfmj-wizard.asset.php');
|
||||||
|
|
||||||
|
if (!$asset_file) {
|
||||||
|
$asset_file = array(
|
||||||
|
'dependencies' => array(
|
||||||
|
'wp-element',
|
||||||
|
'wp-components',
|
||||||
|
'wp-i18n',
|
||||||
|
'wp-api-fetch',
|
||||||
|
),
|
||||||
|
'version' => $this->version
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_enqueue_script(
|
||||||
|
$this->plugin_name,
|
||||||
|
WPFMJ_PLUGIN_URL . 'admin/js/wpfmj-wizard.js',
|
||||||
|
$asset_file['dependencies'],
|
||||||
|
$asset_file['version'],
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
wp_localize_script(
|
||||||
|
$this->plugin_name,
|
||||||
|
'wpfmjData',
|
||||||
|
array(
|
||||||
|
'restUrl' => rest_url(),
|
||||||
|
'nonce' => wp_create_nonce('wp_rest'),
|
||||||
|
'ajaxUrl' => admin_url('admin-ajax.php'),
|
||||||
|
'ajaxNonce' => wp_create_nonce('wpfmj_ajax'),
|
||||||
|
'editId' => isset($_GET['edit']) ? intval($_GET['edit']) : 0,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX: Get all WPForms.
|
||||||
|
*/
|
||||||
|
public function ajax_get_forms() {
|
||||||
|
check_ajax_referer('wpfmj_ajax', 'nonce');
|
||||||
|
|
||||||
|
if (!current_user_can('manage_options')) {
|
||||||
|
wp_send_json_error('Permission denied');
|
||||||
|
}
|
||||||
|
|
||||||
|
$forms = wpforms()->form->get('', array('orderby' => 'title'));
|
||||||
|
$formatted_forms = array();
|
||||||
|
|
||||||
|
foreach ($forms as $form) {
|
||||||
|
$formatted_forms[] = array(
|
||||||
|
'id' => $form->ID,
|
||||||
|
'title' => $form->post_title,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_send_json_success($formatted_forms);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX: Get form fields.
|
||||||
|
*/
|
||||||
|
public function ajax_get_form_fields() {
|
||||||
|
check_ajax_referer('wpfmj_ajax', 'nonce');
|
||||||
|
|
||||||
|
if (!current_user_can('manage_options')) {
|
||||||
|
wp_send_json_error('Permission denied');
|
||||||
|
}
|
||||||
|
|
||||||
|
$form_id = isset($_POST['form_id']) ? intval($_POST['form_id']) : 0;
|
||||||
|
|
||||||
|
if (!$form_id) {
|
||||||
|
wp_send_json_error('Invalid form ID');
|
||||||
|
}
|
||||||
|
|
||||||
|
$form = wpforms()->form->get($form_id);
|
||||||
|
|
||||||
|
if (!$form) {
|
||||||
|
wp_send_json_error('Form not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
$form_data = wpforms_decode($form->post_content);
|
||||||
|
|
||||||
|
// Validate form_data structure
|
||||||
|
if (!is_array($form_data) || !isset($form_data['fields']) || !is_array($form_data['fields'])) {
|
||||||
|
wp_send_json_error('Invalid form data structure');
|
||||||
|
}
|
||||||
|
|
||||||
|
$fields = array();
|
||||||
|
|
||||||
|
foreach ($form_data['fields'] as $field) {
|
||||||
|
// Validate field structure
|
||||||
|
if (!is_array($field) || !isset($field['type']) || !isset($field['label'])) {
|
||||||
|
continue; // Skip invalid fields
|
||||||
|
}
|
||||||
|
|
||||||
|
$field_type = sanitize_text_field($field['type']);
|
||||||
|
$field_id = isset($field['id']) ? sanitize_text_field($field['id']) : '';
|
||||||
|
$field_label = sanitize_text_field($field['label']);
|
||||||
|
|
||||||
|
// Get choices for fields that have them
|
||||||
|
$choices = array();
|
||||||
|
if (in_array($field_type, array('checkbox', 'radio', 'select', 'payment-checkbox', 'payment-multiple', 'payment-select'))) {
|
||||||
|
if (isset($field['choices']) && is_array($field['choices'])) {
|
||||||
|
foreach ($field['choices'] as $choice) {
|
||||||
|
if (is_array($choice) && isset($choice['label'])) {
|
||||||
|
$choices[] = array(
|
||||||
|
'label' => sanitize_text_field($choice['label']),
|
||||||
|
'value' => isset($choice['value']) ? sanitize_text_field($choice['value']) : sanitize_text_field($choice['label']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$fields[] = array(
|
||||||
|
'id' => $field_id,
|
||||||
|
'label' => $field_label,
|
||||||
|
'type' => $field_type,
|
||||||
|
'choices' => $choices,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_send_json_success($fields);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX: Test Mailjet connection.
|
||||||
|
*/
|
||||||
|
public function ajax_test_mailjet() {
|
||||||
|
check_ajax_referer('wpfmj_ajax', 'nonce');
|
||||||
|
|
||||||
|
if (!current_user_can('manage_options')) {
|
||||||
|
wp_send_json_error('Permission denied');
|
||||||
|
}
|
||||||
|
|
||||||
|
$api_key = isset($_POST['api_key']) ? sanitize_text_field($_POST['api_key']) : '';
|
||||||
|
$api_secret = isset($_POST['api_secret']) ? sanitize_text_field($_POST['api_secret']) : '';
|
||||||
|
|
||||||
|
if (empty($api_key) || empty($api_secret)) {
|
||||||
|
wp_send_json_error('API credentials required');
|
||||||
|
}
|
||||||
|
|
||||||
|
$api = new WPFMJ_Mailjet_API($api_key, $api_secret);
|
||||||
|
$result = $api->test_connection();
|
||||||
|
|
||||||
|
if ($result) {
|
||||||
|
wp_send_json_success('Connection successful');
|
||||||
|
} else {
|
||||||
|
wp_send_json_error('Connection failed. Please check your credentials.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX: Get Mailjet lists.
|
||||||
|
*/
|
||||||
|
public function ajax_get_mailjet_lists() {
|
||||||
|
check_ajax_referer('wpfmj_ajax', 'nonce');
|
||||||
|
|
||||||
|
if (!current_user_can('manage_options')) {
|
||||||
|
wp_send_json_error('Permission denied');
|
||||||
|
}
|
||||||
|
|
||||||
|
$api_key = isset($_POST['api_key']) ? sanitize_text_field($_POST['api_key']) : '';
|
||||||
|
$api_secret = isset($_POST['api_secret']) ? sanitize_text_field($_POST['api_secret']) : '';
|
||||||
|
|
||||||
|
if (empty($api_key) || empty($api_secret)) {
|
||||||
|
wp_send_json_error('API credentials required');
|
||||||
|
}
|
||||||
|
|
||||||
|
$api = new WPFMJ_Mailjet_API($api_key, $api_secret);
|
||||||
|
$lists = $api->get_lists();
|
||||||
|
|
||||||
|
if (is_wp_error($lists)) {
|
||||||
|
wp_send_json_error($lists->get_error_message());
|
||||||
|
}
|
||||||
|
|
||||||
|
$formatted_lists = array();
|
||||||
|
foreach ($lists as $list) {
|
||||||
|
$formatted_lists[] = array(
|
||||||
|
'id' => $list['ID'],
|
||||||
|
'name' => $list['Name'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_send_json_success($formatted_lists);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX: Save automation.
|
||||||
|
*/
|
||||||
|
public function ajax_save_automation() {
|
||||||
|
check_ajax_referer('wpfmj_ajax', 'nonce');
|
||||||
|
|
||||||
|
if (!current_user_can('manage_options')) {
|
||||||
|
wp_send_json_error('Permission denied');
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = isset($_POST['data']) ? $_POST['data'] : array();
|
||||||
|
$edit_id = isset($_POST['edit_id']) ? intval($_POST['edit_id']) : 0;
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
$required = array('title', 'form_id', 'field_mapping', 'trigger_field_id', 'api_key', 'api_secret', 'list_mappings', 'activate');
|
||||||
|
foreach ($required as $field) {
|
||||||
|
if (!isset($data[$field]) || (is_string($data[$field]) && empty($data[$field]))) {
|
||||||
|
wp_send_json_error("Missing required field: {$field}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanitize and validate field_mapping
|
||||||
|
if (!is_array($data['field_mapping']) || !isset($data['field_mapping']['email'])) {
|
||||||
|
wp_send_json_error('Invalid field mapping structure');
|
||||||
|
}
|
||||||
|
$field_mapping = array(
|
||||||
|
'email' => sanitize_text_field($data['field_mapping']['email']),
|
||||||
|
'firstname' => isset($data['field_mapping']['firstname']) ? sanitize_text_field($data['field_mapping']['firstname']) : '',
|
||||||
|
'lastname' => isset($data['field_mapping']['lastname']) ? sanitize_text_field($data['field_mapping']['lastname']) : '',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Sanitize and validate list_mappings
|
||||||
|
if (!is_array($data['list_mappings'])) {
|
||||||
|
wp_send_json_error('Invalid list mappings structure');
|
||||||
|
}
|
||||||
|
$list_mappings = array();
|
||||||
|
foreach ($data['list_mappings'] as $key => $value) {
|
||||||
|
$list_mappings[sanitize_text_field($key)] = sanitize_text_field($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanitize API credentials
|
||||||
|
$api_key = sanitize_text_field($data['api_key']);
|
||||||
|
$api_secret = sanitize_text_field($data['api_secret']);
|
||||||
|
|
||||||
|
// Encrypt API credentials
|
||||||
|
$encrypted_key = WPFMJ_Encryption::encrypt($api_key);
|
||||||
|
$encrypted_secret = WPFMJ_Encryption::encrypt($api_secret);
|
||||||
|
|
||||||
|
if (empty($encrypted_key) || empty($encrypted_secret)) {
|
||||||
|
wp_send_json_error('Failed to encrypt API credentials');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare config
|
||||||
|
$config = array(
|
||||||
|
'form_id' => intval($data['form_id']),
|
||||||
|
'field_mapping' => $field_mapping,
|
||||||
|
'trigger_field_id' => sanitize_text_field($data['trigger_field_id']),
|
||||||
|
'api_key' => $encrypted_key,
|
||||||
|
'api_secret' => $encrypted_secret,
|
||||||
|
'list_mappings' => $list_mappings,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create or update post
|
||||||
|
$post_data = array(
|
||||||
|
'post_title' => sanitize_text_field($data['title']),
|
||||||
|
'post_type' => 'wpfmj_automation',
|
||||||
|
'post_status' => $data['activate'] ? 'publish' : 'draft',
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($edit_id) {
|
||||||
|
$post_data['ID'] = $edit_id;
|
||||||
|
$post_id = wp_update_post($post_data);
|
||||||
|
} else {
|
||||||
|
$post_id = wp_insert_post($post_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_wp_error($post_id)) {
|
||||||
|
wp_send_json_error($post_id->get_error_message());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save config
|
||||||
|
update_post_meta($post_id, '_wpfmj_config', $config);
|
||||||
|
update_post_meta($post_id, '_wpfmj_form_id', $config['form_id']);
|
||||||
|
|
||||||
|
wp_send_json_success(array(
|
||||||
|
'id' => $post_id,
|
||||||
|
'message' => 'Automation saved successfully',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX: Get automation.
|
||||||
|
*/
|
||||||
|
public function ajax_get_automation() {
|
||||||
|
check_ajax_referer('wpfmj_ajax', 'nonce');
|
||||||
|
|
||||||
|
if (!current_user_can('manage_options')) {
|
||||||
|
wp_send_json_error('Permission denied');
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = isset($_POST['id']) ? intval($_POST['id']) : 0;
|
||||||
|
|
||||||
|
if (!$id) {
|
||||||
|
wp_send_json_error('Invalid ID');
|
||||||
|
}
|
||||||
|
|
||||||
|
$post = get_post($id);
|
||||||
|
$config = get_post_meta($id, '_wpfmj_config', true);
|
||||||
|
|
||||||
|
if (!$post || !$config) {
|
||||||
|
wp_send_json_error('Automation not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt API credentials
|
||||||
|
$decrypted_key = WPFMJ_Encryption::decrypt($config['api_key']);
|
||||||
|
$decrypted_secret = WPFMJ_Encryption::decrypt($config['api_secret']);
|
||||||
|
|
||||||
|
// Check for decryption failures
|
||||||
|
if ($decrypted_key === false || $decrypted_secret === false) {
|
||||||
|
wp_send_json_error('Failed to decrypt API credentials. The encryption key may have changed.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$config['api_key'] = $decrypted_key;
|
||||||
|
$config['api_secret'] = $decrypted_secret;
|
||||||
|
|
||||||
|
wp_send_json_success(array(
|
||||||
|
'title' => $post->post_title,
|
||||||
|
'status' => $post->post_status,
|
||||||
|
'config' => $config,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX: Toggle automation status.
|
||||||
|
*/
|
||||||
|
public function ajax_toggle_automation() {
|
||||||
|
check_ajax_referer('wpfmj_ajax', 'nonce');
|
||||||
|
|
||||||
|
if (!current_user_can('manage_options')) {
|
||||||
|
wp_send_json_error('Permission denied');
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = isset($_POST['id']) ? intval($_POST['id']) : 0;
|
||||||
|
|
||||||
|
if (!$id) {
|
||||||
|
wp_send_json_error('Invalid ID');
|
||||||
|
}
|
||||||
|
|
||||||
|
$post = get_post($id);
|
||||||
|
|
||||||
|
if (!$post) {
|
||||||
|
wp_send_json_error('Automation not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
$new_status = $post->post_status === 'publish' ? 'draft' : 'publish';
|
||||||
|
|
||||||
|
wp_update_post(array(
|
||||||
|
'ID' => $id,
|
||||||
|
'post_status' => $new_status,
|
||||||
|
));
|
||||||
|
|
||||||
|
wp_send_json_success(array(
|
||||||
|
'status' => $new_status,
|
||||||
|
'message' => 'Automation ' . ($new_status === 'publish' ? 'activated' : 'paused'),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX: Delete automation.
|
||||||
|
*/
|
||||||
|
public function ajax_delete_automation() {
|
||||||
|
check_ajax_referer('wpfmj_ajax', 'nonce');
|
||||||
|
|
||||||
|
if (!current_user_can('manage_options')) {
|
||||||
|
wp_send_json_error('Permission denied');
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = isset($_POST['id']) ? intval($_POST['id']) : 0;
|
||||||
|
|
||||||
|
if (!$id) {
|
||||||
|
wp_send_json_error('Invalid ID');
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = wp_delete_post($id, true);
|
||||||
|
|
||||||
|
if ($result) {
|
||||||
|
wp_send_json_success('Automation deleted');
|
||||||
|
} else {
|
||||||
|
wp_send_json_error('Failed to delete automation');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX: Get dashboard data.
|
||||||
|
*/
|
||||||
|
public function ajax_get_dashboard_data() {
|
||||||
|
check_ajax_referer('wpfmj_ajax', 'nonce');
|
||||||
|
|
||||||
|
if (!current_user_can('manage_options')) {
|
||||||
|
wp_send_json_error('Permission denied');
|
||||||
|
}
|
||||||
|
|
||||||
|
$automations = get_posts(array(
|
||||||
|
'post_type' => 'wpfmj_automation',
|
||||||
|
'post_status' => array('publish', 'draft'),
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
));
|
||||||
|
|
||||||
|
if (empty($automations)) {
|
||||||
|
wp_send_json_success(array());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre-load all post meta to prevent N+1 queries
|
||||||
|
$automation_ids = wp_list_pluck($automations, 'ID');
|
||||||
|
update_post_meta_cache($automation_ids);
|
||||||
|
|
||||||
|
// Pre-load all error counts in a single query to prevent N+1
|
||||||
|
$logger = new WPFMJ_Error_Logger();
|
||||||
|
$error_counts = $logger->get_error_counts_bulk($automation_ids);
|
||||||
|
|
||||||
|
$data = array();
|
||||||
|
|
||||||
|
foreach ($automations as $automation) {
|
||||||
|
$config = get_post_meta($automation->ID, '_wpfmj_config', true);
|
||||||
|
$form = wpforms()->form->get($config['form_id']);
|
||||||
|
|
||||||
|
$data[] = array(
|
||||||
|
'id' => $automation->ID,
|
||||||
|
'title' => $automation->post_title,
|
||||||
|
'form_name' => $form ? $form->post_title : 'Unknown Form',
|
||||||
|
'status' => $automation->post_status,
|
||||||
|
'error_count' => isset($error_counts[$automation->ID]) ? $error_counts[$automation->ID] : 0,
|
||||||
|
'created' => $automation->post_date,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_send_json_success($data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,241 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Dashboard page functionality.
|
||||||
|
*
|
||||||
|
* @package WPFMJ
|
||||||
|
* @subpackage WPFMJ/admin
|
||||||
|
*/
|
||||||
|
|
||||||
|
class WPFMJ_Dashboard {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add menu page.
|
||||||
|
*/
|
||||||
|
public function add_menu_page() {
|
||||||
|
add_menu_page(
|
||||||
|
__('Mailjet Automations', 'wpforms-mailjet-automation'),
|
||||||
|
__('Mailjet Automations', 'wpforms-mailjet-automation'),
|
||||||
|
'manage_options',
|
||||||
|
'wpfmj-dashboard',
|
||||||
|
array($this, 'render_dashboard'),
|
||||||
|
'dashicons-email-alt',
|
||||||
|
30
|
||||||
|
);
|
||||||
|
|
||||||
|
add_submenu_page(
|
||||||
|
'wpfmj-dashboard',
|
||||||
|
__('All Automations', 'wpforms-mailjet-automation'),
|
||||||
|
__('All Automations', 'wpforms-mailjet-automation'),
|
||||||
|
'manage_options',
|
||||||
|
'wpfmj-dashboard',
|
||||||
|
array($this, 'render_dashboard')
|
||||||
|
);
|
||||||
|
|
||||||
|
add_submenu_page(
|
||||||
|
'wpfmj-dashboard',
|
||||||
|
__('Add New Automation', 'wpforms-mailjet-automation'),
|
||||||
|
__('Add New', 'wpforms-mailjet-automation'),
|
||||||
|
'manage_options',
|
||||||
|
'wpfmj-add-new',
|
||||||
|
array($this, 'render_wizard')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render dashboard page.
|
||||||
|
*/
|
||||||
|
public function render_dashboard() {
|
||||||
|
// Verify user has proper capabilities
|
||||||
|
if (!current_user_can('manage_options')) {
|
||||||
|
wp_die(__('You do not have sufficient permissions to access this page.', 'wpforms-mailjet-automation'));
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<div class="wrap">
|
||||||
|
<h1 class="wp-heading-inline"><?php esc_html_e('Mailjet Automations', 'wpforms-mailjet-automation'); ?></h1>
|
||||||
|
<a href="<?php echo esc_url(admin_url('admin.php?page=wpfmj-add-new')); ?>" class="page-title-action">
|
||||||
|
<?php esc_html_e('Add New', 'wpforms-mailjet-automation'); ?>
|
||||||
|
</a>
|
||||||
|
<hr class="wp-header-end">
|
||||||
|
|
||||||
|
<div id="wpfmj-dashboard-root"></div>
|
||||||
|
|
||||||
|
<div id="wpfmj-dashboard-loading" style="padding: 40px; text-align: center;">
|
||||||
|
<span class="spinner is-active" style="float: none; margin: 0 auto;"></span>
|
||||||
|
<p><?php esc_html_e('Loading automations...', 'wpforms-mailjet-automation'); ?></p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
// Render dashboard table
|
||||||
|
jQuery(document).ready(function($) {
|
||||||
|
function loadDashboard() {
|
||||||
|
$.ajax({
|
||||||
|
url: wpfmjData.ajaxUrl,
|
||||||
|
type: 'POST',
|
||||||
|
data: {
|
||||||
|
action: 'wpfmj_get_dashboard_data',
|
||||||
|
nonce: wpfmjData.ajaxNonce
|
||||||
|
},
|
||||||
|
success: function(response) {
|
||||||
|
$('#wpfmj-dashboard-loading').hide();
|
||||||
|
if (response.success) {
|
||||||
|
renderTable(response.data);
|
||||||
|
} else {
|
||||||
|
var errorMessage = $('<div>').text(response.data).html();
|
||||||
|
$('#wpfmj-dashboard-root').html('<div class="notice notice-error"><p>' + errorMessage + '</p></div>');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
$('#wpfmj-dashboard-loading').hide();
|
||||||
|
$('#wpfmj-dashboard-root').html('<div class="notice notice-error"><p>Failed to load automations.</p></div>');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTable(automations) {
|
||||||
|
if (automations.length === 0) {
|
||||||
|
$('#wpfmj-dashboard-root').html(
|
||||||
|
'<div class="wpfmj-empty-state" style="text-align: center; padding: 60px 20px;">' +
|
||||||
|
'<p style="font-size: 18px; color: #666; margin-bottom: 20px;">No automations yet. Create your first automation to get started!</p>' +
|
||||||
|
'<a href="' + wpfmjData.ajaxUrl.replace('admin-ajax.php', 'admin.php?page=wpfmj-add-new') + '" class="button button-primary button-large">Create Automation</a>' +
|
||||||
|
'</div>'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var html = '<table class="wp-list-table widefat fixed striped">';
|
||||||
|
html += '<thead><tr>';
|
||||||
|
html += '<th>Title</th>';
|
||||||
|
html += '<th>Form</th>';
|
||||||
|
html += '<th>Status</th>';
|
||||||
|
html += '<th>Errors</th>';
|
||||||
|
html += '<th>Created</th>';
|
||||||
|
html += '<th>Actions</th>';
|
||||||
|
html += '</tr></thead><tbody>';
|
||||||
|
|
||||||
|
automations.forEach(function(automation) {
|
||||||
|
var statusClass = automation.status === 'publish' ? 'active' : 'inactive';
|
||||||
|
var statusText = automation.status === 'publish' ? 'Active' : 'Paused';
|
||||||
|
var errorBadge = automation.error_count > 0 ? '<span class="wpfmj-error-badge">' + parseInt(automation.error_count) + '</span>' : '—';
|
||||||
|
|
||||||
|
// Escape data for safe HTML output
|
||||||
|
var title = $('<div>').text(automation.title).html();
|
||||||
|
var formName = $('<div>').text(automation.form_name).html();
|
||||||
|
var automationId = parseInt(automation.id);
|
||||||
|
|
||||||
|
html += '<tr>';
|
||||||
|
html += '<td><strong>' + title + '</strong></td>';
|
||||||
|
html += '<td>' + formName + '</td>';
|
||||||
|
html += '<td><span class="wpfmj-status-badge wpfmj-status-' + statusClass + '">' + statusText + '</span></td>';
|
||||||
|
html += '<td>' + errorBadge + '</td>';
|
||||||
|
html += '<td>' + new Date(automation.created).toLocaleDateString() + '</td>';
|
||||||
|
html += '<td>';
|
||||||
|
html += '<a href="admin.php?page=wpfmj-add-new&edit=' + automationId + '" class="button button-small">Edit</a> ';
|
||||||
|
html += '<button class="button button-small wpfmj-toggle-btn" data-id="' + automationId + '" data-status="' + automation.status + '">' + (automation.status === 'publish' ? 'Pause' : 'Activate') + '</button> ';
|
||||||
|
html += '<button class="button button-small button-link-delete wpfmj-delete-btn" data-id="' + automationId + '">Delete</button>';
|
||||||
|
html += '</td>';
|
||||||
|
html += '</tr>';
|
||||||
|
});
|
||||||
|
|
||||||
|
html += '</tbody></table>';
|
||||||
|
$('#wpfmj-dashboard-root').html(html);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use event delegation to prevent memory leaks
|
||||||
|
// Unbind previous handlers before binding new ones
|
||||||
|
$('#wpfmj-dashboard-root').off('click', '.wpfmj-toggle-btn').on('click', '.wpfmj-toggle-btn', function() {
|
||||||
|
var btn = $(this);
|
||||||
|
var id = btn.data('id');
|
||||||
|
toggleAutomation(id, btn);
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#wpfmj-dashboard-root').off('click', '.wpfmj-delete-btn').on('click', '.wpfmj-delete-btn', function() {
|
||||||
|
var id = $(this).data('id');
|
||||||
|
if (confirm('Are you sure you want to delete this automation? This cannot be undone.')) {
|
||||||
|
deleteAutomation(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function toggleAutomation(id, btn) {
|
||||||
|
btn.prop('disabled', true);
|
||||||
|
$.ajax({
|
||||||
|
url: wpfmjData.ajaxUrl,
|
||||||
|
type: 'POST',
|
||||||
|
data: {
|
||||||
|
action: 'wpfmj_toggle_automation',
|
||||||
|
nonce: wpfmjData.ajaxNonce,
|
||||||
|
id: id
|
||||||
|
},
|
||||||
|
success: function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
loadDashboard();
|
||||||
|
} else {
|
||||||
|
var errorMessage = $('<div>').text(response.data).html();
|
||||||
|
alert('Error: ' + errorMessage);
|
||||||
|
btn.prop('disabled', false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
alert('Failed to toggle automation');
|
||||||
|
btn.prop('disabled', false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteAutomation(id) {
|
||||||
|
$.ajax({
|
||||||
|
url: wpfmjData.ajaxUrl,
|
||||||
|
type: 'POST',
|
||||||
|
data: {
|
||||||
|
action: 'wpfmj_delete_automation',
|
||||||
|
nonce: wpfmjData.ajaxNonce,
|
||||||
|
id: id
|
||||||
|
},
|
||||||
|
success: function(response) {
|
||||||
|
if (response.success) {
|
||||||
|
loadDashboard();
|
||||||
|
} else {
|
||||||
|
var errorMessage = $('<div>').text(response.data).html();
|
||||||
|
alert('Error: ' + errorMessage);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
alert('Failed to delete automation');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadDashboard();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render wizard page.
|
||||||
|
*/
|
||||||
|
public function render_wizard() {
|
||||||
|
// Verify user has proper capabilities
|
||||||
|
if (!current_user_can('manage_options')) {
|
||||||
|
wp_die(__('You do not have sufficient permissions to access this page.', 'wpforms-mailjet-automation'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$edit_id = isset($_GET['edit']) ? intval($_GET['edit']) : 0;
|
||||||
|
|
||||||
|
// If editing, verify the post exists and user can edit it
|
||||||
|
if ($edit_id) {
|
||||||
|
$post = get_post($edit_id);
|
||||||
|
if (!$post || $post->post_type !== 'wpfmj_automation') {
|
||||||
|
wp_die(__('Invalid automation ID.', 'wpforms-mailjet-automation'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$page_title = $edit_id ? __('Edit Automation', 'wpforms-mailjet-automation') : __('Add New Automation', 'wpforms-mailjet-automation');
|
||||||
|
?>
|
||||||
|
<div class="wrap">
|
||||||
|
<h1><?php echo esc_html($page_title); ?></h1>
|
||||||
|
<div id="wpfmj-wizard-root"></div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
<?php
|
||||||
|
// Silence is golden.
|
||||||
|
|
@ -0,0 +1,296 @@
|
||||||
|
/* Dashboard Styles */
|
||||||
|
.wpfmj-status-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpfmj-status-active {
|
||||||
|
background-color: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpfmj-status-inactive {
|
||||||
|
background-color: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpfmj-error-badge {
|
||||||
|
display: inline-block;
|
||||||
|
background-color: #dc3545;
|
||||||
|
color: white;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpfmj-empty-state {
|
||||||
|
background: #f9f9f9;
|
||||||
|
border: 2px dashed #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Wizard Styles */
|
||||||
|
.wpfmj-wizard-container {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 20px 0;
|
||||||
|
background: white;
|
||||||
|
border: 1px solid #ccd0d4;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 1px 1px rgba(0,0,0,.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpfmj-wizard-header {
|
||||||
|
padding: 20px 30px;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpfmj-wizard-progress {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpfmj-wizard-progress li {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpfmj-wizard-progress li:not(:last-child)::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 12px;
|
||||||
|
right: -50%;
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
background: #e0e0e0;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpfmj-wizard-progress li.active {
|
||||||
|
color: #2271b1;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpfmj-wizard-progress li.completed {
|
||||||
|
color: #00a32a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpfmj-wizard-progress li.active::before,
|
||||||
|
.wpfmj-wizard-progress li.completed::before {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
margin: 0 auto 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #2271b1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpfmj-wizard-progress li.completed::before {
|
||||||
|
background: #00a32a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpfmj-wizard-body {
|
||||||
|
padding: 30px;
|
||||||
|
min-height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpfmj-wizard-footer {
|
||||||
|
padding: 20px 30px;
|
||||||
|
border-top: 1px solid #e0e0e0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Step Styles */
|
||||||
|
.wpfmj-step {
|
||||||
|
animation: fadeIn 0.3s ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpfmj-step h2 {
|
||||||
|
margin: 0 0 20px;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpfmj-step p.description {
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpfmj-field-group {
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpfmj-field-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpfmj-field-group input[type="text"],
|
||||||
|
.wpfmj-field-group input[type="password"],
|
||||||
|
.wpfmj-field-group select {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Two Column Layout */
|
||||||
|
.wpfmj-two-column {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 30px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpfmj-column h3 {
|
||||||
|
margin: 0 0 15px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 2px solid #2271b1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpfmj-mapping-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
padding: 12px;
|
||||||
|
background: #f9f9f9;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpfmj-mapping-row .wpfmj-answer {
|
||||||
|
flex: 1;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpfmj-mapping-row select {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpfmj-mapping-row button {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Review Step */
|
||||||
|
.wpfmj-review-section {
|
||||||
|
background: #f9f9f9;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpfmj-review-section h3 {
|
||||||
|
margin: 0 0 15px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #2271b1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpfmj-review-item {
|
||||||
|
display: flex;
|
||||||
|
padding: 8px 0;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpfmj-review-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpfmj-review-label {
|
||||||
|
flex: 0 0 180px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpfmj-review-value {
|
||||||
|
flex: 1;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Alerts */
|
||||||
|
.wpfmj-alert {
|
||||||
|
padding: 12px 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpfmj-alert-success {
|
||||||
|
background-color: #d4edda;
|
||||||
|
border: 1px solid #c3e6cb;
|
||||||
|
color: #155724;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpfmj-alert-error {
|
||||||
|
background-color: #f8d7da;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
color: #721c24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpfmj-alert-info {
|
||||||
|
background-color: #d1ecf1;
|
||||||
|
border: 1px solid #bee5eb;
|
||||||
|
color: #0c5460;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loading States */
|
||||||
|
.wpfmj-loading {
|
||||||
|
display: inline-block;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 3px solid #f3f3f3;
|
||||||
|
border-top: 3px solid #2271b1;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
margin-right: 8px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 782px) {
|
||||||
|
.wpfmj-two-column {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpfmj-wizard-footer {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wpfmj-mapping-row {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
<?php
|
||||||
|
// Silence is golden.
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
<?php
|
||||||
|
// Silence is golden.
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Asset file for the wizard JavaScript bundle.
|
||||||
|
* This file would normally be generated by @wordpress/scripts build process.
|
||||||
|
*
|
||||||
|
* To build the React app:
|
||||||
|
* 1. Install dependencies: npm install @wordpress/scripts --save-dev
|
||||||
|
* 2. Add to package.json scripts: "build": "wp-scripts build assets/src/wizard/App.jsx --output-path=admin/js/wpfmj-wizard.js"
|
||||||
|
* 3. Run: npm run build
|
||||||
|
*/
|
||||||
|
|
||||||
|
return array(
|
||||||
|
'dependencies' => array(
|
||||||
|
'wp-element',
|
||||||
|
'wp-components',
|
||||||
|
'wp-i18n',
|
||||||
|
'wp-api-fetch',
|
||||||
|
),
|
||||||
|
'version' => '1.0.0'
|
||||||
|
);
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
<?php
|
||||||
|
// Silence is golden.
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
<?php
|
||||||
|
// Silence is golden.
|
||||||
|
|
@ -0,0 +1,206 @@
|
||||||
|
import { useState, useEffect } from '@wordpress/element';
|
||||||
|
import { Button, Spinner } from '@wordpress/components';
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import StepOne from './components/StepOne';
|
||||||
|
import StepTwo from './components/StepTwo';
|
||||||
|
import StepThree from './components/StepThree';
|
||||||
|
import StepFour from './components/StepFour';
|
||||||
|
import StepFive from './components/StepFive';
|
||||||
|
import StepSix from './components/StepSix';
|
||||||
|
import { fetchAutomation } from './utils/api';
|
||||||
|
|
||||||
|
const WizardApp = () => {
|
||||||
|
const [currentStep, setCurrentStep] = useState(1);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
title: '',
|
||||||
|
form_id: '',
|
||||||
|
field_mapping: {
|
||||||
|
email: '',
|
||||||
|
firstname: '',
|
||||||
|
lastname: ''
|
||||||
|
},
|
||||||
|
trigger_field_id: '',
|
||||||
|
api_key: '',
|
||||||
|
api_secret: '',
|
||||||
|
list_mappings: {},
|
||||||
|
activate: false
|
||||||
|
});
|
||||||
|
const [forms, setForms] = useState([]);
|
||||||
|
const [fields, setFields] = useState([]);
|
||||||
|
const [triggerChoices, setTriggerChoices] = useState([]);
|
||||||
|
const [mailjetLists, setMailjetLists] = useState([]);
|
||||||
|
|
||||||
|
const totalSteps = 6;
|
||||||
|
const editId = window.wpfmjData?.editId || 0;
|
||||||
|
|
||||||
|
// Load automation if editing
|
||||||
|
useEffect(() => {
|
||||||
|
if (!editId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
fetchAutomation(editId)
|
||||||
|
.then(data => {
|
||||||
|
if (!cancelled) {
|
||||||
|
setFormData({
|
||||||
|
title: data.title,
|
||||||
|
...data.config,
|
||||||
|
activate: data.status === 'publish'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
if (!cancelled) {
|
||||||
|
alert('Error loading automation: ' + error.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (!cancelled) {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [editId]);
|
||||||
|
|
||||||
|
const updateFormData = (field, value) => {
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
[field]: value
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const nextStep = () => {
|
||||||
|
if (currentStep < totalSteps) {
|
||||||
|
setCurrentStep(currentStep + 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const prevStep = () => {
|
||||||
|
if (currentStep > 1) {
|
||||||
|
setCurrentStep(currentStep - 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderStep = () => {
|
||||||
|
const stepProps = {
|
||||||
|
formData,
|
||||||
|
updateFormData,
|
||||||
|
forms,
|
||||||
|
setForms,
|
||||||
|
fields,
|
||||||
|
setFields,
|
||||||
|
triggerChoices,
|
||||||
|
setTriggerChoices,
|
||||||
|
mailjetLists,
|
||||||
|
setMailjetLists,
|
||||||
|
nextStep,
|
||||||
|
setLoading
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (currentStep) {
|
||||||
|
case 1:
|
||||||
|
return <StepOne {...stepProps} />;
|
||||||
|
case 2:
|
||||||
|
return <StepTwo {...stepProps} />;
|
||||||
|
case 3:
|
||||||
|
return <StepThree {...stepProps} />;
|
||||||
|
case 4:
|
||||||
|
return <StepFour {...stepProps} />;
|
||||||
|
case 5:
|
||||||
|
return <StepFive {...stepProps} />;
|
||||||
|
case 6:
|
||||||
|
return <StepSix {...stepProps} editId={editId} />;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderProgress = () => {
|
||||||
|
const steps = [
|
||||||
|
__('Choose Form', 'wpforms-mailjet-automation'),
|
||||||
|
__('Map Fields', 'wpforms-mailjet-automation'),
|
||||||
|
__('Trigger Field', 'wpforms-mailjet-automation'),
|
||||||
|
__('Connect Mailjet', 'wpforms-mailjet-automation'),
|
||||||
|
__('Map Lists', 'wpforms-mailjet-automation'),
|
||||||
|
__('Review', 'wpforms-mailjet-automation')
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul className="wpfmj-wizard-progress">
|
||||||
|
{steps.map((step, index) => {
|
||||||
|
const stepNumber = index + 1;
|
||||||
|
const isActive = currentStep === stepNumber;
|
||||||
|
const isCompleted = currentStep > stepNumber;
|
||||||
|
const className = isActive ? 'active' : isCompleted ? 'completed' : '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li key={stepNumber} className={className}>
|
||||||
|
{step}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '40px', textAlign: 'center' }}>
|
||||||
|
<Spinner />
|
||||||
|
<p>{__('Loading...', 'wpforms-mailjet-automation')}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="wpfmj-wizard-container">
|
||||||
|
<div className="wpfmj-wizard-header">
|
||||||
|
{renderProgress()}
|
||||||
|
</div>
|
||||||
|
<div className="wpfmj-wizard-body">
|
||||||
|
{renderStep()}
|
||||||
|
</div>
|
||||||
|
<div className="wpfmj-wizard-footer">
|
||||||
|
<div>
|
||||||
|
{currentStep > 1 && (
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={prevStep}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{__('Previous', 'wpforms-mailjet-automation')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{currentStep < totalSteps && (
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
onClick={nextStep}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{__('Next', 'wpforms-mailjet-automation')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mount the app
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const rootElement = document.getElementById('wpfmj-wizard-root');
|
||||||
|
if (rootElement) {
|
||||||
|
wp.element.render(<WizardApp />, rootElement);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default WizardApp;
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
import { SelectControl, Button } from '@wordpress/components';
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
|
||||||
|
const StepFive = ({ formData, updateFormData, triggerChoices, mailjetLists, nextStep }) => {
|
||||||
|
|
||||||
|
const handleNext = () => {
|
||||||
|
const hasMapping = Object.keys(formData.list_mappings).length > 0;
|
||||||
|
if (!hasMapping) {
|
||||||
|
alert(__('Please map at least one answer to a Mailjet list', 'wpforms-mailjet-automation'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
nextStep();
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateListMapping = (choiceValue, listId) => {
|
||||||
|
const newMappings = { ...formData.list_mappings };
|
||||||
|
|
||||||
|
if (listId === '') {
|
||||||
|
// Remove mapping if empty
|
||||||
|
delete newMappings[choiceValue];
|
||||||
|
} else {
|
||||||
|
newMappings[choiceValue] = listId;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFormData('list_mappings', newMappings);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get list of already selected lists to prevent duplicates
|
||||||
|
const getAvailableLists = (currentChoiceValue) => {
|
||||||
|
const usedListIds = Object.entries(formData.list_mappings)
|
||||||
|
.filter(([key]) => key !== currentChoiceValue)
|
||||||
|
.map(([, listId]) => listId);
|
||||||
|
|
||||||
|
return mailjetLists.filter(list => !usedListIds.includes(list.id.toString()));
|
||||||
|
};
|
||||||
|
|
||||||
|
const listOptions = (choiceValue) => {
|
||||||
|
const available = getAvailableLists(choiceValue);
|
||||||
|
return [
|
||||||
|
{ label: __('Select a list...', 'wpforms-mailjet-automation'), value: '' },
|
||||||
|
...available.map(list => ({
|
||||||
|
label: list.name,
|
||||||
|
value: list.id.toString()
|
||||||
|
}))
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="wpfmj-step">
|
||||||
|
<h2>{__('Map Answers to Mailjet Lists', 'wpforms-mailjet-automation')}</h2>
|
||||||
|
<p className="description">
|
||||||
|
{__('Map each answer from your trigger field to a Mailjet list. When someone selects an answer, they\'ll be added to the corresponding list. Each list can only be used once.', 'wpforms-mailjet-automation')}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="wpfmj-two-column">
|
||||||
|
<div className="wpfmj-column">
|
||||||
|
<h3>{__('Form Answers', 'wpforms-mailjet-automation')}</h3>
|
||||||
|
{triggerChoices.map((choice) => (
|
||||||
|
<div key={choice.value} className="wpfmj-mapping-row">
|
||||||
|
<div className="wpfmj-answer">{choice.label}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="wpfmj-column">
|
||||||
|
<h3>{__('Mailjet Lists', 'wpforms-mailjet-automation')}</h3>
|
||||||
|
{triggerChoices.map((choice) => (
|
||||||
|
<div key={choice.value} className="wpfmj-mapping-row">
|
||||||
|
<SelectControl
|
||||||
|
value={formData.list_mappings[choice.value] || ''}
|
||||||
|
options={listOptions(choice.value)}
|
||||||
|
onChange={(value) => updateListMapping(choice.value, value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{mailjetLists.length === 0 && (
|
||||||
|
<div className="wpfmj-alert wpfmj-alert-error">
|
||||||
|
{__('No Mailjet lists found. Please create lists in your Mailjet account first.', 'wpforms-mailjet-automation')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="wpfmj-alert wpfmj-alert-info" style={{ marginTop: '20px' }}>
|
||||||
|
<strong>{__('Note:', 'wpforms-mailjet-automation')}</strong> {__('For checkbox fields, users can select multiple answers, and they will be added to multiple lists accordingly.', 'wpforms-mailjet-automation')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
onClick={handleNext}
|
||||||
|
disabled={mailjetLists.length === 0}
|
||||||
|
style={{ marginTop: '20px' }}
|
||||||
|
>
|
||||||
|
{__('Next', 'wpforms-mailjet-automation')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StepFive;
|
||||||
|
|
@ -0,0 +1,112 @@
|
||||||
|
import { useState } from '@wordpress/element';
|
||||||
|
import { TextControl, Button } from '@wordpress/components';
|
||||||
|
import { __ } from '@wordpress/i18n';
|
||||||
|
import { testMailjetConnection, fetchMailjetLists } from '../utils/api';
|
||||||
|
|
||||||
|
const StepFour = ({ formData, updateFormData, setMailjetLists, setLoading, nextStep }) => {
|
||||||
|
const [testing, setTesting] = useState(false);
|
||||||
|
const [tested, setTested] = useState(false);
|
||||||
|
const [testSuccess, setTestSuccess] = useState(false);
|
||||||
|
|
||||||
|
const handleTest = async () => {
|
||||||
|
if (!formData.api_key || !formData.api_secret) {
|
||||||
|
alert(__('Please enter both API Key and API Secret', 'wpforms-mailjet-automation'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTesting(true);
|
||||||
|
try {
|
||||||
|
await testMailjetConnection(formData.api_key, formData.api_secret);
|
||||||
|
setTestSuccess(true);
|
||||||
|
setTested(true);
|
||||||
|
|
||||||
|
// Automatically fetch lists after successful connection
|
||||||
|
const lists = await fetchMailjetLists(formData.api_key, formData.api_secret);
|
||||||
|
setMailjetLists(lists);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
setTestSuccess(false);
|
||||||
|
setTested(true);
|
||||||
|
alert(__('Connection failed: ', 'wpforms-mailjet-automation') + error.message);
|
||||||
|
} finally {
|
||||||
|
setTesting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNext = () => {
|
||||||
|
if (!tested || !testSuccess) {
|
||||||
|
alert(__('Please test your API connection first', 'wpforms-mailjet-automation'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
nextStep();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="wpfmj-step">
|
||||||
|
<h2>{__('Connect to Mailjet', 'wpforms-mailjet-automation')}</h2>
|
||||||
|
<p className="description">
|
||||||
|
{__('Enter your Mailjet API credentials. You can find these in your Mailjet account under Account Settings > REST API.', 'wpforms-mailjet-automation')}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="wpfmj-field-group">
|
||||||
|
<TextControl
|
||||||
|
label={__('API Key', 'wpforms-mailjet-automation')}
|
||||||
|
value={formData.api_key}
|
||||||
|
onChange={(value) => {
|
||||||
|
updateFormData('api_key', value);
|
||||||
|
setTested(false);
|
||||||
|
}}
|
||||||
|
type="text"
|
||||||
|
placeholder={__('Enter your Mailjet API Key', 'wpforms-mailjet-automation')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="wpfmj-field-group">
|
||||||
|
<TextControl
|
||||||
|
label={__('API Secret', 'wpforms-mailjet-automation')}
|
||||||
|
value={formData.api_secret}
|
||||||
|
onChange={(value) => {
|
||||||
|
updateFormData('api_secret', value);
|
||||||
|
setTested(false);
|
||||||
|
}}
|
||||||
|
type="password"
|
||||||
|
placeholder={__('Enter your Mailjet API Secret', 'wpforms-mailjet-automation')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: '20px' }}>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
onClick={handleTest}
|
||||||
|
isBusy={testing}
|
||||||
|
disabled={testing || !formData.api_key || !formData.api_secret}
|
||||||
|
>
|
||||||
|
{testing ? __('Testing...', 'wpforms-mailjet-automation') : __('Test Connection', 'wpforms-mailjet-automation')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{tested && testSuccess && (
|
||||||
|
<div className="wpfmj-alert wpfmj-alert-success">
|
||||||
|
{__('✓ Connection successful! Your Mailjet account is connected.', 'wpforms-mailjet-automation')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{tested && !testSuccess && (
|
||||||
|
<div className="wpfmj-alert wpfmj-alert-error">
|
||||||
|
{__('✗ Connection failed. Please check your credentials and try again.', 'wpforms-mailjet-automation')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
onClick={handleNext}
|
||||||
|
disabled={!tested || !testSuccess}
|
||||||
|
style={{ marginTop: '20px' }}
|
||||||
|
>
|
||||||
|
{__('Next', 'wpforms-mailjet-automation')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default StepFour;
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue