added additional plugins
This commit is contained in:
parent
c85895d306
commit
00e60ec1b7
132 changed files with 27514 additions and 0 deletions
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');
|
||||
Loading…
Add table
Add a link
Reference in a new issue