diff --git a/wp-content/plugins/the-events-calendar/common/README.md b/wp-content/plugins/the-events-calendar/common/README.md new file mode 100644 index 000000000..0eaf4a026 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/README.md @@ -0,0 +1,2 @@ +# tribe-common +Common classes and functions used in our plugins diff --git a/wp-content/plugins/the-events-calendar/common/lang/readme.txt b/wp-content/plugins/the-events-calendar/common/lang/readme.txt new file mode 100644 index 000000000..ac9c376f1 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/lang/readme.txt @@ -0,0 +1,3 @@ +If you are interested in contributing to translations, you can get started at https://translations.theeventscalendar.com/projects/tribe-common/ + +On that site you can also find the .po translation files for doing local translations. We do not include these in the plugin itself to save space. \ No newline at end of file diff --git a/wp-content/plugins/the-events-calendar/common/lang/tribe-common-af.mo b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-af.mo new file mode 100644 index 000000000..809e19d96 Binary files /dev/null and b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-af.mo differ diff --git a/wp-content/plugins/the-events-calendar/common/lang/tribe-common-bg_BG.mo b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-bg_BG.mo new file mode 100644 index 000000000..5aaebd12e Binary files /dev/null and b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-bg_BG.mo differ diff --git a/wp-content/plugins/the-events-calendar/common/lang/tribe-common-ca.mo b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-ca.mo new file mode 100644 index 000000000..a9a0b0d64 Binary files /dev/null and b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-ca.mo differ diff --git a/wp-content/plugins/the-events-calendar/common/lang/tribe-common-cs_CZ.mo b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-cs_CZ.mo new file mode 100644 index 000000000..cc45b395a Binary files /dev/null and b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-cs_CZ.mo differ diff --git a/wp-content/plugins/the-events-calendar/common/lang/tribe-common-da_DK.mo b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-da_DK.mo new file mode 100644 index 000000000..ef0b12517 Binary files /dev/null and b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-da_DK.mo differ diff --git a/wp-content/plugins/the-events-calendar/common/lang/tribe-common-de_DE.mo b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-de_DE.mo new file mode 100644 index 000000000..598644645 Binary files /dev/null and b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-de_DE.mo differ diff --git a/wp-content/plugins/the-events-calendar/common/lang/tribe-common-el.mo b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-el.mo new file mode 100644 index 000000000..e26f18028 Binary files /dev/null and b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-el.mo differ diff --git a/wp-content/plugins/the-events-calendar/common/lang/tribe-common-en_GB.mo b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-en_GB.mo new file mode 100644 index 000000000..71f3c696a Binary files /dev/null and b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-en_GB.mo differ diff --git a/wp-content/plugins/the-events-calendar/common/lang/tribe-common-es_ES.mo b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-es_ES.mo new file mode 100644 index 000000000..01bb5b2ba Binary files /dev/null and b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-es_ES.mo differ diff --git a/wp-content/plugins/the-events-calendar/common/lang/tribe-common-et.mo b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-et.mo new file mode 100644 index 000000000..03dafd29f Binary files /dev/null and b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-et.mo differ diff --git a/wp-content/plugins/the-events-calendar/common/lang/tribe-common-fi.mo b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-fi.mo new file mode 100644 index 000000000..cb12eca51 Binary files /dev/null and b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-fi.mo differ diff --git a/wp-content/plugins/the-events-calendar/common/lang/tribe-common-fr_CA.mo b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-fr_CA.mo new file mode 100644 index 000000000..372edaa67 Binary files /dev/null and b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-fr_CA.mo differ diff --git a/wp-content/plugins/the-events-calendar/common/lang/tribe-common-fr_FR.mo b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-fr_FR.mo new file mode 100644 index 000000000..0fc6d69a2 Binary files /dev/null and b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-fr_FR.mo differ diff --git a/wp-content/plugins/the-events-calendar/common/lang/tribe-common-hu_HU.mo b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-hu_HU.mo new file mode 100644 index 000000000..3b22f6fc6 Binary files /dev/null and b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-hu_HU.mo differ diff --git a/wp-content/plugins/the-events-calendar/common/lang/tribe-common-id_ID.mo b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-id_ID.mo new file mode 100644 index 000000000..74bea5696 Binary files /dev/null and b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-id_ID.mo differ diff --git a/wp-content/plugins/the-events-calendar/common/lang/tribe-common-is_IS.mo b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-is_IS.mo new file mode 100644 index 000000000..6b0d92751 Binary files /dev/null and b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-is_IS.mo differ diff --git a/wp-content/plugins/the-events-calendar/common/lang/tribe-common-it_IT.mo b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-it_IT.mo new file mode 100644 index 000000000..d4830f841 Binary files /dev/null and b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-it_IT.mo differ diff --git a/wp-content/plugins/the-events-calendar/common/lang/tribe-common-ja.mo b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-ja.mo new file mode 100644 index 000000000..c6a487224 Binary files /dev/null and b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-ja.mo differ diff --git a/wp-content/plugins/the-events-calendar/common/lang/tribe-common-lt_LT.mo b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-lt_LT.mo new file mode 100644 index 000000000..e52050e15 Binary files /dev/null and b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-lt_LT.mo differ diff --git a/wp-content/plugins/the-events-calendar/common/lang/tribe-common-lv.mo b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-lv.mo new file mode 100644 index 000000000..1c3d01ec1 Binary files /dev/null and b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-lv.mo differ diff --git a/wp-content/plugins/the-events-calendar/common/lang/tribe-common-nb_NO.mo b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-nb_NO.mo new file mode 100644 index 000000000..fdcfa31a9 Binary files /dev/null and b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-nb_NO.mo differ diff --git a/wp-content/plugins/the-events-calendar/common/lang/tribe-common-nl_NL.mo b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-nl_NL.mo new file mode 100644 index 000000000..6e7f32347 Binary files /dev/null and b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-nl_NL.mo differ diff --git a/wp-content/plugins/the-events-calendar/common/lang/tribe-common-pt_BR.mo b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-pt_BR.mo new file mode 100644 index 000000000..d7f0275b0 Binary files /dev/null and b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-pt_BR.mo differ diff --git a/wp-content/plugins/the-events-calendar/common/lang/tribe-common-pt_PT.mo b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-pt_PT.mo new file mode 100644 index 000000000..0a5c830a0 Binary files /dev/null and b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-pt_PT.mo differ diff --git a/wp-content/plugins/the-events-calendar/common/lang/tribe-common-ro_RO.mo b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-ro_RO.mo new file mode 100644 index 000000000..37c56b99d Binary files /dev/null and b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-ro_RO.mo differ diff --git a/wp-content/plugins/the-events-calendar/common/lang/tribe-common-ru_RU.mo b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-ru_RU.mo new file mode 100644 index 000000000..8b76b9b07 Binary files /dev/null and b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-ru_RU.mo differ diff --git a/wp-content/plugins/the-events-calendar/common/lang/tribe-common-sk_SK.mo b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-sk_SK.mo new file mode 100644 index 000000000..9d4b59002 Binary files /dev/null and b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-sk_SK.mo differ diff --git a/wp-content/plugins/the-events-calendar/common/lang/tribe-common-sl_SI.mo b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-sl_SI.mo new file mode 100644 index 000000000..08d669f93 Binary files /dev/null and b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-sl_SI.mo differ diff --git a/wp-content/plugins/the-events-calendar/common/lang/tribe-common-sr_RS.mo b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-sr_RS.mo new file mode 100644 index 000000000..c26af0747 Binary files /dev/null and b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-sr_RS.mo differ diff --git a/wp-content/plugins/the-events-calendar/common/lang/tribe-common-sv_SE.mo b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-sv_SE.mo new file mode 100644 index 000000000..966059445 Binary files /dev/null and b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-sv_SE.mo differ diff --git a/wp-content/plugins/the-events-calendar/common/lang/tribe-common-tr_TR.mo b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-tr_TR.mo new file mode 100644 index 000000000..bd8664ed7 Binary files /dev/null and b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-tr_TR.mo differ diff --git a/wp-content/plugins/the-events-calendar/common/lang/tribe-common-zh_CN.mo b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-zh_CN.mo new file mode 100644 index 000000000..6d6a007a3 Binary files /dev/null and b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-zh_CN.mo differ diff --git a/wp-content/plugins/the-events-calendar/common/lang/tribe-common-zh_TW.mo b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-zh_TW.mo new file mode 100644 index 000000000..55212b4f3 Binary files /dev/null and b/wp-content/plugins/the-events-calendar/common/lang/tribe-common-zh_TW.mo differ diff --git a/wp-content/plugins/the-events-calendar/common/lang/tribe-common.pot b/wp-content/plugins/the-events-calendar/common/lang/tribe-common.pot new file mode 100644 index 000000000..b2053bbae --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/lang/tribe-common.pot @@ -0,0 +1,3953 @@ +# Copyright (C) 2023 The Events Calendar +# This file is distributed under the GPLv2 or later. +msgid "" +msgstr "" +"Project-Id-Version: Tribe Common 5.1.3\n" +"Report-Msgid-Bugs-To: https://evnt.is/191x\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"POT-Creation-Date: 2023-07-10T08:26:14-07:00\n" +"PO-Revision-Date: 2023-07-10 15:26\n" +"X-Generator: WP-CLI 2.7.1\n" +"X-Domain: tribe-common\n" + +#. Plugin Name of the plugin +msgid "Tribe Common" +msgstr "" + +#. Description of the plugin +msgid "An event settings framework for managing shared options" +msgstr "" + +#. Author of the plugin +#: src/admin-views/help-calendar.php:97 +#: src/Tribe/Admin/Help_Page.php:116 +#: src/Tribe/Customizer.php:664 +#: src/Tribe/Plugins_API.php:25 +msgid "The Events Calendar" +msgstr "" + +#. Author URI of the plugin +msgid "http://evnt.is/1x" +msgstr "" + +#: src/admin-views/app-shop.php:30 +#: src/admin-views/conditional_content/black-friday.php:16 +#: src/admin-views/conditional_content/end-of-year-sale.php:16 +#: src/admin-views/help.php:27 +#: src/admin-views/help.php:86 +#: src/admin-views/troubleshooting/footer-logo.php:12 +#: src/admin-views/troubleshooting/introduction.php:19 +msgid "The Events Calendar brand logo" +msgstr "" + +#: src/admin-views/app-shop.php:35 +msgid "All Solutions" +msgstr "" + +#: src/admin-views/app-shop.php:36 +msgid "Save with Bundles" +msgstr "" + +#: src/admin-views/app-shop.php:37 +msgid "Extensions" +msgstr "" + +#: src/admin-views/app-shop.php:38 +msgid "Stellar Discounts" +msgstr "" + +#: src/admin-views/app-shop.php:47 +#: src/admin-views/app-shop.php:109 +#: src/admin-views/app-shop.php:167 +#: src/admin-views/app-shop.php:201 +msgid "Shapes and lines for visual interest" +msgstr "" + +#: src/admin-views/app-shop.php:53 +msgid "One calendar. Countless ways to make it your own." +msgstr "" + +#: src/admin-views/app-shop.php:54 +msgid "Calendars, ticketing, and powerful WordPress tools to manage your events from start to finish." +msgstr "" + +#: src/admin-views/app-shop.php:56 +msgid "Already Installed" +msgstr "" + +#: src/admin-views/app-shop.php:62 +#: src/admin-views/app-shop.php:119 +#: src/admin-views/app-shop.php:141 +msgid "TEC Logo" +msgstr "" + +#: src/admin-views/app-shop.php:66 +#: src/admin-views/help-calendar.php:44 +#: src/admin-views/help-community.php:44 +#: src/admin-views/help-ticketing.php:44 +msgid "Active" +msgstr "" + +#: src/admin-views/app-shop.php:68 +#: src/admin-views/app-shop.php:177 +msgid "FREE" +msgstr "" + +#: src/admin-views/app-shop.php:93 +msgid "Manage" +msgstr "" + +#: src/admin-views/app-shop.php:95 +#: src/admin-views/help-calendar.php:65 +#: src/admin-views/help-community.php:58 +#: src/admin-views/help-ticketing.php:65 +msgid "Learn More" +msgstr "" + +#: src/admin-views/app-shop.php:113 +msgid "The plugins you need at one discounted price" +msgstr "" + +#: src/admin-views/app-shop.php:114 +msgid "We've packaged our most popular plugins into bundles jam-packed with value." +msgstr "" + +#: src/admin-views/app-shop.php:126 +#: src/admin-views/app-shop.php:153 +msgid "Save With A Bundle" +msgstr "" + +#: src/admin-views/app-shop.php:132 +msgid "Includes" +msgstr "" + +#: src/admin-views/app-shop.php:171 +msgid "Free extensions to power up your plugins" +msgstr "" + +#: src/admin-views/app-shop.php:172 +msgid "Extensions are quick solutions our team came up with to solve specific issues you may need. (Note - extensions are not covered by our support team.)" +msgstr "" + +#: src/admin-views/app-shop.php:186 +msgid "Download" +msgstr "" + +#: src/admin-views/app-shop.php:190 +msgid "Browse Extensions" +msgstr "" + +#: src/admin-views/app-shop.php:209 +msgid "Stellar is a collective of WordPress innovators, and home to WordPress products done right." +msgstr "" + +#: src/admin-views/app-shop.php:213 +msgid "$25 towards any Stellar product using code %s" +msgstr "" + +#: src/admin-views/components/loader.php:18 +msgid "Loading..." +msgstr "" + +#: src/admin-views/conditional_content/black-friday.php:23 +msgid "Our biggest
sale of the
year ends
soon

" +msgstr "" + +#: src/admin-views/conditional_content/black-friday.php:30 +#: src/admin-views/conditional_content/end-of-year-sale.php:31 +msgid "Save now" +msgstr "" + +#: src/admin-views/conditional_content/end-of-year-sale.php:23 +msgid "End of Year Sale!
Save 30% on
all our plugins.
Offer expires soon!" +msgstr "" + +#: src/admin-views/event-log.php:21 +msgid "Logging level" +msgstr "" + +#: src/admin-views/event-log.php:43 +msgid "Method" +msgstr "" + +#: src/admin-views/event-log.php:65 +msgid "View" +msgstr "" + +#: src/admin-views/event-log.php:100 +msgid "The selected log file is empty or has not been generated yet." +msgstr "" + +#: src/admin-views/event-log.php:117 +msgid "Download log" +msgstr "" + +#: src/admin-views/help-calendar.php:16 +#: src/admin-views/help-community.php:16 +#: src/admin-views/help-ticketing.php:16 +msgid "Get help for these products and learn more about products you don't have." +msgstr "" + +#: src/admin-views/help-calendar.php:27 +#: src/admin-views/help-community.php:27 +#: src/admin-views/help-ticketing.php:27 +msgid "logo icon" +msgstr "" + +#: src/admin-views/help-calendar.php:51 +#: src/admin-views/help-ticketing.php:51 +msgid "Add license key" +msgstr "" + +#: src/admin-views/help-calendar.php:58 +#: src/admin-views/help-community.php:51 +#: src/admin-views/help-ticketing.php:58 +msgid "Activate" +msgstr "" + +#: src/admin-views/help-calendar.php:76 +#: src/admin-views/help-community.php:69 +#: src/admin-views/help-ticketing.php:76 +msgid "Start Here" +msgstr "" + +#: src/admin-views/help-calendar.php:80 +#: src/admin-views/help-community.php:73 +#: src/admin-views/help-ticketing.php:80 +msgid "Visit Knowledgebase" +msgstr "" + +#: src/admin-views/help-calendar.php:89 +#: src/admin-views/help-calendar.php:122 +#: src/admin-views/help-calendar.php:155 +msgid "book with The Events Calendar logo" +msgstr "" + +#: src/admin-views/help-calendar.php:92 +#: src/admin-views/help-community.php:85 +#: src/admin-views/help-ticketing.php:92 +msgid "Getting Started Guides" +msgstr "" + +#: src/admin-views/help-calendar.php:102 +#: src/Tribe/Admin/Help_Page.php:285 +#: src/Tribe/Plugins_API.php:45 +msgid "Event Aggregator" +msgstr "" + +#: src/admin-views/help-calendar.php:107 +#: src/Tribe/Admin/Help_Page.php:295 +#: src/Tribe/Plugins_API.php:166 +msgid "Filter Bar" +msgstr "" + +#: src/admin-views/help-calendar.php:112 +#: src/Tribe/Admin/Help_Page.php:303 +#: src/Tribe/Plugins_API.php:266 +msgid "Virtual Events" +msgstr "" + +#: src/admin-views/help-calendar.php:125 +msgid "Customizing" +msgstr "" + +#: src/admin-views/help-calendar.php:130 +msgid "Getting started with customizations" +msgstr "" + +#: src/admin-views/help-calendar.php:135 +msgid "Highlighting events" +msgstr "" + +#: src/admin-views/help-calendar.php:140 +msgid "How to Customize Template Files" +msgstr "" + +#: src/admin-views/help-calendar.php:145 +msgid "Customizing CSS (Video)" +msgstr "" + +#: src/admin-views/help-calendar.php:158 +msgid "Common Issues" +msgstr "" + +#: src/admin-views/help-calendar.php:163 +msgid "Known Issues" +msgstr "" + +#: src/admin-views/help-calendar.php:168 +#: src/admin-views/help-community.php:161 +#: src/admin-views/help-ticketing.php:164 +msgid "Release notes" +msgstr "" + +#: src/admin-views/help-calendar.php:173 +#: src/admin-views/help-community.php:166 +#: src/admin-views/help-ticketing.php:169 +msgid "Integrations" +msgstr "" + +#: src/admin-views/help-calendar.php:178 +#: src/admin-views/help-ticketing.php:140 +#: src/Tribe/Plugins_API.php:96 +msgid "Shortcodes" +msgstr "" + +#: src/admin-views/help-calendar.php:188 +#: src/admin-views/help-community.php:181 +#: src/admin-views/help-ticketing.php:184 +msgid "FAQs" +msgstr "" + +#: src/admin-views/help-calendar.php:192 +#: src/admin-views/help-community.php:185 +#: src/admin-views/help-ticketing.php:188 +msgid "All FAQs" +msgstr "" + +#: src/admin-views/help-calendar.php:202 +#: src/admin-views/help-community.php:195 +#: src/admin-views/help-ticketing.php:198 +#: src/admin-views/troubleshooting/common-issues.php:24 +#: src/admin-views/troubleshooting/first-steps.php:20 +#: src/admin-views/troubleshooting/first-steps.php:40 +msgid "lightbulb icon" +msgstr "" + +#: src/admin-views/help-calendar.php:222 +#: src/admin-views/help-community.php:215 +#: src/admin-views/help-ticketing.php:218 +msgid "Free extensions" +msgstr "" + +#: src/admin-views/help-calendar.php:226 +#: src/admin-views/help-community.php:219 +#: src/admin-views/help-ticketing.php:222 +msgid "All Extensions" +msgstr "" + +#: src/admin-views/help-calendar.php:231 +#: src/admin-views/help-community.php:224 +#: src/admin-views/help-ticketing.php:227 +msgid "Small, lightweight WordPress plugins that add new capabilities to our core plugins. Support is not offered for extensions; however they provide enhanced functionality and bonus features." +msgstr "" + +#: src/admin-views/help-community.php:82 +#: src/admin-views/help-community.php:148 +msgid "book with The Events community logo" +msgstr "" + +#: src/admin-views/help-community.php:90 +#: src/Tribe/Admin/Help_Page.php:277 +#: src/Tribe/Plugins_API.php:186 +msgid "Community Events" +msgstr "" + +#: src/admin-views/help-community.php:95 +#: src/Tribe/Admin/Help_Page.php:320 +#: src/Tribe/Plugins_API.php:206 +msgid "Community Tickets" +msgstr "" + +#: src/admin-views/help-community.php:100 +msgid "Community Events Shortcodes" +msgstr "" + +#: src/admin-views/help-community.php:105 +msgid "Locating Links to Your Community Events Pages" +msgstr "" + +#: src/admin-views/help-community.php:115 +#: src/admin-views/help-ticketing.php:117 +msgid "book with Event Tickets logo" +msgstr "" + +#: src/admin-views/help-community.php:118 +msgid "Managing Submissions" +msgstr "" + +#: src/admin-views/help-community.php:123 +msgid "Managing Submissions Overview" +msgstr "" + +#: src/admin-views/help-community.php:128 +msgid "Setting Notifications" +msgstr "" + +#: src/admin-views/help-community.php:133 +msgid "Auto-Publish Events Submitted By Logged-In Users" +msgstr "" + +#: src/admin-views/help-community.php:138 +msgid "Preventing Spam Submissions" +msgstr "" + +#: src/admin-views/help-community.php:151 +#: src/admin-views/help-ticketing.php:154 +msgid "Plugin Maintenance" +msgstr "" + +#: src/admin-views/help-community.php:156 +#: src/admin-views/help-ticketing.php:159 +msgid "Testing for Conflicts" +msgstr "" + +#: src/admin-views/help-community.php:171 +#: src/admin-views/help-ticketing.php:174 +msgid "Automatic Updates" +msgstr "" + +#: src/admin-views/help-ticketing.php:89 +#: src/admin-views/help-ticketing.php:151 +msgid "book with The Events ticketing logo" +msgstr "" + +#: src/admin-views/help-ticketing.php:97 +#: src/Tribe/Admin/Help_Page.php:135 +#: src/Tribe/Plugins_API.php:106 +msgid "Event Tickets" +msgstr "" + +#: src/admin-views/help-ticketing.php:102 +msgid "Calendar & Ticket Shortcodes" +msgstr "" + +#: src/admin-views/help-ticketing.php:107 +#: src/Tribe/Plugins_API.php:146 +#: src/Tribe/Promoter/PUE.php:28 +#: src/views/promoter/auth.php:30 +#: src/views/promoter/auth.php:74 +msgid "Promoter" +msgstr "" + +#: src/admin-views/help-ticketing.php:120 +msgid "Creating Tickets & RSVPs" +msgstr "" + +#: src/admin-views/help-ticketing.php:125 +msgid "Creating Tickets" +msgstr "" + +#: src/admin-views/help-ticketing.php:130 +msgid "Creating RSVPs" +msgstr "" + +#: src/admin-views/help-ticketing.php:135 +msgid "Configuring Paypal for Tickets" +msgstr "" + +#: src/admin-views/help-ticketing.php:142 +msgid "(Event Tickets Plus)" +msgstr "" + +#: src/admin-views/help.php:30 +msgid "Help" +msgstr "" + +#: src/admin-views/help.php:31 +msgid "We're committed to helping make your calendar spectacular and have a wealth of resources available." +msgstr "" + +#: src/admin-views/help.php:34 +msgid "Calendar" +msgstr "" + +#: src/admin-views/help.php:35 +msgid "Ticketing & RSVP" +msgstr "" + +#: src/admin-views/help.php:36 +msgid "Community" +msgstr "" + +#: src/admin-views/help.php:67 +#: src/admin-views/troubleshooting/support-cta.php:13 +msgid "Graphic with an electrical plug and gears" +msgstr "" + +#: src/admin-views/help.php:72 +msgid "Need additional support?" +msgstr "" + +#: src/admin-views/help.php:77 +msgid "Visit Troubleshooting next" +msgstr "" + +#: src/admin-views/notices/end-of-year-sale.php:16 +msgid "End of year savings!" +msgstr "" + +#: src/admin-views/notices/end-of-year-sale.php:18 +msgid "Get 30% off on all plugins from now through December 31." +msgstr "" + +#: src/admin-views/notices/end-of-year-sale.php:25 +#: src/admin-views/notices/tribe-bf-general.php:26 +msgid "Shop now" +msgstr "" + +#: src/admin-views/notices/tribe-bf-general.php:17 +msgid "Save 40% on every single plugin." +msgstr "" + +#: src/admin-views/notices/tribe-bf-general.php:19 +msgid "Black Friday Sale now through %s." +msgstr "" + +#: src/admin-views/notices/tribe-stellar-sale-premium.php:16 +msgid "Make it stellar." +msgstr "" + +#: src/admin-views/notices/tribe-stellar-sale-premium.php:19 +msgid "Save 30% on all StellarWP products." +msgstr "" + +#: src/admin-views/notices/tribe-stellar-sale-premium.php:24 +#: src/admin-views/notices/tribe-stellar-sale-premium.php:38 +#: src/admin-views/notices/tribe-stellar-sale.php:24 +#: src/admin-views/notices/tribe-stellar-sale.php:38 +msgctxt "Shop now link text" +msgid "Shop now" +msgstr "" + +#: src/admin-views/notices/tribe-stellar-sale-premium.php:32 +#: src/admin-views/notices/tribe-stellar-sale.php:32 +msgid "Purchase any StellarWP product during the sale and get 100% off WP Business Reviews and take 40% off all other brands." +msgstr "" + +#: src/admin-views/notices/tribe-stellar-sale-premium.php:46 +#: src/admin-views/notices/tribe-stellar-sale.php:46 +msgctxt "View all StellarWP Deals link text" +msgid "View all StellarWP Deals" +msgstr "" + +#: src/admin-views/notices/tribe-stellar-sale.php:16 +msgid "Make it yours." +msgstr "" + +#: src/admin-views/notices/tribe-stellar-sale.php:19 +msgid "Save 30% on all The Events Calendar products." +msgstr "" + +#: src/admin-views/notices/upsell/icon.php:19 +msgid "The Events Calendar important notice icon" +msgstr "" + +#: src/admin-views/tribe-options-help.php:11 +msgid "Thank you for using %s! All of us at The Events Calendar sincerely appreciate your support and we’re excited to see you using our plugins." +msgstr "" + +#: src/admin-views/tribe-options-help.php:14 +msgid "Getting Support" +msgstr "" + +#: src/admin-views/tribe-options-help.php:15 +msgid "Our website’s %s is a great place to find tips and tricks for using and customizing our plugins." +msgstr "" + +#: src/admin-views/tribe-options-help.php:15 +msgid "Knowledgebase" +msgstr "" + +#: src/admin-views/tribe-options-help.php:18 +msgid "Want to dive deeper?" +msgstr "" + +#: src/admin-views/tribe-options-help.php:20 +msgid "Check out our %s for developers." +msgstr "" + +#: src/admin-views/tribe-options-help.php:21 +msgid "list of available functions" +msgstr "" + +#: src/admin-views/tribe-options-help.php:26 +msgid "Getting More Help" +msgstr "" + +#: src/admin-views/tribe-options-help.php:27 +msgid "While the resources above help solve a majority of the issues we see, there are times you might be looking for extra support. If you need assistance using our plugins and would like us to take a look, please follow these steps:" +msgstr "" + +#: src/admin-views/tribe-options-help.php:33 +msgid "%s. All of the common (and not-so-common) answers to questions we see are here. It’s often the fastest path to finding an answer!" +msgstr "" + +#: src/admin-views/tribe-options-help.php:37 +msgid "Check our Knowledgebase" +msgstr "" + +#: src/admin-views/tribe-options-help.php:43 +msgid "%s. Testing for an existing conflict is the best start for in-depth troubleshooting. We will often ask you to follow these steps when opening a new thread, so doing this ahead of time will be super helpful." +msgstr "" + +#: src/admin-views/tribe-options-help.php:47 +msgid "Test for a theme or plugin conflict" +msgstr "" + +#: src/admin-views/tribe-options-help.php:53 +msgid "%s. There are very few issues we haven’t seen and it’s likely another user has already asked your question and gotten an answer from our support staff. While posting to the help desk is open only to paid customers, they are open for anyone to search and review." +msgstr "" + +#: src/admin-views/tribe-options-help.php:57 +msgid "Search our support help desk" +msgstr "" + +#: src/admin-views/tribe-options-help.php:67 +msgid "Please note that all hands-on support is provided via the help desk. You can email or tweet at us… ​but we will probably point you back to the help desk 😄" +msgstr "" + +#: src/admin-views/tribe-options-help.php:68 +msgid "Read more about our support policy" +msgstr "" + +#: src/admin-views/tribe-options-help.php:71 +#: src/admin-views/troubleshooting/system-information.php:16 +msgid "System Information" +msgstr "" + +#: src/admin-views/tribe-options-help.php:72 +msgid "The details of your calendar plugin and settings is often needed for you or our staff to help troubleshoot an issue. Please opt-in below to automatically share your system information with our support team. This will allow us to assist you faster if you post in our help desk." +msgstr "" + +#: src/admin-views/tribe-options-help.php:80 +#: src/admin-views/troubleshooting/system-information.php:39 +msgid "Copy to clipboard" +msgstr "" + +#: src/admin-views/tribe-options-help.php:82 +msgid "Recent Template Changes" +msgstr "" + +#: src/admin-views/tribe-options-help.php:85 +msgid "Event Log" +msgstr "" + +#: src/admin-views/tribe-options-help.php:104 +msgid "News and Tutorials" +msgstr "" + +#: src/admin-views/tribe-options-help.php:110 +msgid "More..." +msgstr "" + +#: src/admin-views/tribe-options-licenses.php:5 +msgid "If you've purchased a premium add-on, you'll need to enter your license key here in order to have access to automatic updates when new versions are available." +msgstr "" + +#: src/admin-views/tribe-options-licenses.php:10 +msgid "In order to register a plugin license, you'll first need to %1$sdownload and install%2$s the plugin you purchased. You can download the latest version of your plugin(s) from %3$syour account's downloads page%4$s. Once the plugin is installed and activated on this site, the license key field will appear below." +msgstr "" + +#: src/admin-views/tribe-options-licenses.php:19 +msgid "Each paid add-on has its own unique license key. Paste the key into its appropriate field below, and give it a moment to validate. You know you're set when a green expiration date appears alongside a \"valid\" message. Then click Save Changes." +msgstr "" + +#: src/admin-views/tribe-options-licenses.php:23 +msgid "Helpful Links:" +msgstr "" + +#: src/admin-views/tribe-options-licenses.php:28 +msgid "Why am I being told my license key is out of installs?" +msgstr "" + +#: src/admin-views/tribe-options-licenses.php:31 +msgid "View and manage your license keys" +msgstr "" + +#: src/admin-views/tribe-options-licenses.php:34 +msgid "Moving your license keys" +msgstr "" + +#: src/admin-views/tribe-options-licenses.php:37 +msgid "Expired license keys and subscriptions" +msgstr "" + +#: src/admin-views/tribe-options-licenses.php:43 +msgid "Licenses for Multisites" +msgstr "" + +#: src/admin-views/tribe-options-licenses.php:57 +#: src/Tribe/Settings_Manager.php:296 +msgid "Licenses" +msgstr "" + +#: src/admin-views/troubleshooting/common-issues.php:14 +msgid "Common Problems" +msgstr "" + +#: src/admin-views/troubleshooting/detected-issues.php:14 +msgid "We’ve detected the following issues" +msgstr "" + +#: src/admin-views/troubleshooting/detected-issues.php:30 +msgid "warning-icon" +msgstr "" + +#: src/admin-views/troubleshooting/detected-issues.php:45 +msgid "Learn more" +msgstr "" + +#: src/admin-views/troubleshooting/ea-status.php:24 +msgid "Event Aggregator system status " +msgstr "" + +#: src/admin-views/troubleshooting/ea-status/current-status.php:15 +msgid "Imports Enabled in Settings" +msgstr "" + +#: src/admin-views/troubleshooting/ea-status/current-status.php:20 +msgid "Imports disabled in Settings" +msgstr "" + +#: src/admin-views/troubleshooting/ea-status/current-status.php:25 +msgid "Edit Import Settings" +msgstr "" + +#: src/admin-views/troubleshooting/ea-status/current-status.php:31 +msgid "Enabled" +msgstr "" + +#: src/admin-views/troubleshooting/ea-status/current-usage.php:18 +msgid "You have reached your daily import limit. Scheduled imports will be paused until tomorrow." +msgstr "" + +#: src/admin-views/troubleshooting/ea-status/current-usage.php:21 +msgid "You are approaching your daily import limit. You may want to adjust your Scheduled Import frequencies." +msgstr "" + +#: src/admin-views/troubleshooting/ea-status/current-usage.php:25 +msgid "%1$d import used out of %2$d available today" +msgid_plural "%1$d imports used out of %2$d available today" +msgstr[0] "" +msgstr[1] "" + +#: src/admin-views/troubleshooting/ea-status/current-usage.php:32 +msgid "Current usage" +msgstr "" + +#: src/admin-views/troubleshooting/ea-status/eventbrite.php:11 +msgid "Third Party Accounts" +msgstr "" + +#: src/admin-views/troubleshooting/ea-status/eventbrite.php:24 +msgid "You have not connected Event Aggregator to Eventbrite" +msgstr "" + +#: src/admin-views/troubleshooting/ea-status/eventbrite.php:28 +msgctxt "link for connecting eventbrite" +msgid "Connect to Eventbrite" +msgstr "" + +#: src/admin-views/troubleshooting/ea-status/eventbrite.php:32 +msgid "Limited connectivity with Eventbrite" +msgstr "" + +#: src/admin-views/troubleshooting/ea-status/eventbrite.php:33 +#: src/admin-views/troubleshooting/ea-status/meetup.php:23 +msgid "The service has disabled oAuth. Some types of events may not import." +msgstr "" + +#: src/admin-views/troubleshooting/ea-status/eventbrite.php:38 +msgid "Eventbrite" +msgstr "" + +#: src/admin-views/troubleshooting/ea-status/license-key.php:19 +msgid "Your license is valid" +msgstr "" + +#: src/admin-views/troubleshooting/ea-status/license-key.php:26 +msgid "You do not have a license" +msgstr "" + +#: src/admin-views/troubleshooting/ea-status/license-key.php:28 +msgid "Buy Event Aggregator to access more event sources and automatic imports!" +msgstr "" + +#: src/admin-views/troubleshooting/ea-status/license-key.php:31 +msgid "Your license is invalid" +msgstr "" + +#: src/admin-views/troubleshooting/ea-status/license-key.php:32 +msgid "Check your license key" +msgstr "" + +#: src/admin-views/troubleshooting/ea-status/license-key.php:38 +msgid "License & Usage" +msgstr "" + +#: src/admin-views/troubleshooting/ea-status/license-key.php:44 +#: src/Tribe/PUE/Checker.php:620 +#: src/Tribe/PUE/Checker.php:631 +msgid "License Key" +msgstr "" + +#: src/admin-views/troubleshooting/ea-status/meetup.php:16 +msgid "You have not connected Event Aggregator to Meetup" +msgstr "" + +#: src/admin-views/troubleshooting/ea-status/meetup.php:18 +msgctxt "link for connecting meetup" +msgid "Connect to Meetup" +msgstr "" + +#: src/admin-views/troubleshooting/ea-status/meetup.php:22 +msgid "Limited connectivity with Meetup" +msgstr "" + +#: src/admin-views/troubleshooting/ea-status/meetup.php:29 +msgid "Meetup" +msgstr "" + +#: src/admin-views/troubleshooting/ea-status/scheduler-status.php:14 +msgid "WP Cron not enabled" +msgstr "" + +#: src/admin-views/troubleshooting/ea-status/scheduler-status.php:15 +msgid "Scheduled imports may not run reliably" +msgstr "" + +#: src/admin-views/troubleshooting/ea-status/scheduler-status.php:17 +msgid "WP Cron enabled" +msgstr "" + +#: src/admin-views/troubleshooting/ea-status/scheduler-status.php:23 +msgid "Scheduler Status" +msgstr "" + +#: src/admin-views/troubleshooting/ea-status/server-connection.php:11 +msgid "Import Services" +msgstr "" + +#. translators: %s: Event Aggregator Server URL +#: src/admin-views/troubleshooting/ea-status/server-connection.php:24 +#: src/admin-views/troubleshooting/ea-status/server-connection.php:32 +msgid "Not connected to %s" +msgstr "" + +#: src/admin-views/troubleshooting/ea-status/server-connection.php:25 +msgid "The server is not currently responding" +msgstr "" + +#: src/admin-views/troubleshooting/ea-status/server-connection.php:33 +msgid "The server is responding with an error:" +msgstr "" + +#. translators: %s: Event Aggregator Server URL +#: src/admin-views/troubleshooting/ea-status/server-connection.php:39 +msgid "Connected to %s" +msgstr "" + +#: src/admin-views/troubleshooting/ea-status/server-connection.php:45 +msgid "Server Connection" +msgstr "" + +#: src/admin-views/troubleshooting/event-log.php:13 +msgid "Event log" +msgstr "" + +#: src/admin-views/troubleshooting/first-steps.php:11 +msgid "First Steps" +msgstr "" + +#: src/admin-views/troubleshooting/first-steps.php:25 +msgid "Test for conflicts" +msgstr "" + +#: src/admin-views/troubleshooting/first-steps.php:29 +#: src/admin-views/troubleshooting/first-steps.php:49 +msgid "View article" +msgstr "" + +#: src/admin-views/troubleshooting/first-steps.php:30 +msgid "Most issues are caused by conflicts with the theme or other plugins. Follow these steps as a first point of action. %s" +msgstr "" + +#: src/admin-views/troubleshooting/first-steps.php:45 +msgid "Share your system info" +msgstr "" + +#: src/admin-views/troubleshooting/first-steps.php:50 +msgid "Providing the details of your calendar plugin and settings (located below) helps our support team troubleshoot an issue faster. %s" +msgstr "" + +#: src/admin-views/troubleshooting/introduction.php:21 +#: src/Tribe/Admin/Troubleshooting.php:66 +#: src/Tribe/Admin/Troubleshooting.php:67 +#: src/Tribe/Admin/Troubleshooting.php:142 +msgid "Troubleshooting" +msgstr "" + +#: src/admin-views/troubleshooting/introduction.php:22 +msgid "Sometimes things just don’t work as expected. We’ve created a wealth of resources to get you back on track." +msgstr "" + +#: src/admin-views/troubleshooting/notice.php:12 +msgid "Help page?" +msgstr "" + +#: src/admin-views/troubleshooting/notice.php:17 +msgid "Hey there... did you check out the %s" +msgstr "" + +#: src/admin-views/troubleshooting/recent-template-changes.php:12 +msgid "Recent template changes" +msgstr "" + +#: src/admin-views/troubleshooting/support-cta.php:18 +msgid "Get support from humans" +msgstr "" + +#: src/admin-views/troubleshooting/support-cta.php:22 +msgid "Included with our premium products" +msgstr "" + +#: src/admin-views/troubleshooting/support-cta.php:27 +msgid "Open a ticket" +msgstr "" + +#: src/admin-views/troubleshooting/system-information.php:19 +msgid "Please opt-in below to automatically share your system information with our support team. This will allow us to assist you faster if you post in our help desk." +msgstr "" + +#: src/admin-views/troubleshooting/system-information.php:24 +msgid "Yes, automatically share my system information with The Events Calendar support team*" +msgstr "" + +#: src/admin-views/troubleshooting/system-information.php:28 +msgid "* Your system information will only be used by The Events Calendar support team. All information is stored securely. We do not share this information with any third parties." +msgstr "" + +#. Translators: %1$s the post type label. +#: src/Common/Site_Health/Fields/Post_Status_Count_Field.php:54 +msgid "%1$s counts" +msgstr "" + +#: src/Common/Telemetry/Telemetry.php:270 +msgid "See which plugins you have opted in to tracking for" +msgstr "" + +#: src/Common/Telemetry/Telemetry.php:271 +msgid "We hope you love TEC Common!" +msgstr "" + +#: src/Common/Telemetry/Telemetry.php:356 +msgid "We’re sorry to see you go." +msgstr "" + +#: src/Common/Telemetry/Telemetry.php:357 +msgid "We’d love to know why you’re leaving so we can improve our plugin." +msgstr "" + +#: src/Common/Telemetry/Telemetry.php:361 +msgid "I couldn’t understand how to make it work." +msgstr "" + +#: src/Common/Telemetry/Telemetry.php:365 +msgid "I found a better plugin." +msgstr "" + +#: src/Common/Telemetry/Telemetry.php:370 +msgid "I need a specific feature it doesn’t provide." +msgstr "" + +#: src/Common/Telemetry/Telemetry.php:375 +msgid "The plugin doesn’t work." +msgstr "" + +#: src/Common/Telemetry/Telemetry.php:380 +msgid "Other" +msgstr "" + +#: src/functions/template-tags/html.php:101 +msgctxt "The associated field is required." +msgid "(required)" +msgstr "" + +#: src/Tribe/Admin/Activation_Page.php:113 +msgid "Go to plugins page" +msgstr "" + +#: src/Tribe/Admin/Activation_Page.php:121 +msgid "Go to WordPress Updates page" +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:90 +msgctxt "Copy to clipboard button text." +msgid "Copy to clipboard" +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:91 +msgctxt "Copy to clipboard success message" +msgid "System info copied" +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:92 +msgctxt "Copy to clipboard instructions" +msgid "Press \"Cmd + C\" to copy" +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:93 +msgctxt "Default error message for system info optin" +msgid "Something has gone wrong!" +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:94 +msgctxt "Error code label for system info optin" +msgid "Code:" +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:95 +msgctxt "Error status label for system info optin" +msgid "Status:" +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:120 +msgid "The Events Calendar is a carefully crafted, extensible plugin that lets you easily share your events." +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:139 +msgid "Events Tickets is a carefully crafted, extensible plugin that lets you easily sell tickets for your events." +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:154 +msgid "Advanced Post Manager" +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:158 +msgid "Turbo charge your posts admin for any custom post type with sortable filters and columns, and auto-registration of metaboxes." +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:236 +msgid " and " +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:260 +msgid "Events Calendar PRO" +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:269 +#: src/Tribe/Plugins_API.php:227 +msgid "Eventbrite Tickets" +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:311 +#: src/Tribe/Plugins_API.php:126 +msgid "Event Tickets Plus" +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:481 +msgctxt "not available" +msgid "n/a" +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:489 +msgid "You need to upgrade!" +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:489 +#: src/Tribe/Admin/Help_Page.php:864 +msgid "You are up to date!" +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:854 +msgid "Activate %s" +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:854 +msgid "Activate Plugin" +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:862 +msgid "Upgrade Plugin" +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:878 +msgid "Install %s" +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:878 +msgid "Install Plugin" +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:895 +msgid "Latest Version:" +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:898 +msgid "Requires:" +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:899 +msgid "WordPress " +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:901 +msgid "Active Users:" +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:904 +msgid "Rating:" +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:927 +msgid "Premium Add-Ons" +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:933 +msgid "Plugin Active" +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:935 +msgid "Plugin Inactive" +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:940 +msgid "Visit the Add-on Page" +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:964 +msgid "Can I have more than one calendar?" +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:965 +msgid "No, but you can use event categories or tags to display certain events like having..." +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:969 +msgid "What do I get with Events Calendar Pro?" +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:974 +msgid "How do I sell tickets to events?" +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:975 +msgid "Use our free Event Tickets plugin to get started with tickets and RSVPs." +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:979 +msgid "Where can I find a list of available shortcodes?" +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:980 +msgid "Our plugins include many shortcodes that do everything from embedding the calendar..." +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:998 +msgid "Calendar widget areas" +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:999 +msgid "This extension creates a useful variety of WordPress widget areas (a.k.a. sidebars)." +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:1004 +msgid "Event block patterns" +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:1005 +msgid "This extension adds a set of block patterns for events to the WordPress block editor." +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:1010 +msgid "Alternative photo view" +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:1011 +msgid "This extension replaces photo view with a tiled grid of cards featuring event images." +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:1016 +msgid "The Events Calendar Tweaks" +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:1017 +msgid "This extension is a collection of tweaks and snippets for The Events Calendar." +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:1055 +msgid "How Do I create events with Tickets or RSVP’s?" +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:1056 +msgid "We’ve put together a video tutorial showing how to create events with Tickets using our plugins. Click on the link in the link in the title to learn more." +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:1060 +msgid "How Do I Set Up E-Commerce Plugins for Selling Tickets?" +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:1061 +msgid "You can sell tickets using our built-in e-commerce option, or upgrade to Event Tickets Plus to use ecommerce plugins such as WooCommerce." +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:1065 +msgid "Can I have a seating chart associated with my tickets?" +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:1066 +msgid "Yes! You can easily accomplish this task using the stock options and multiple ticket types available with Event Tickets." +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:1070 +msgid "How do I process refunds for tickets?" +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:1071 +msgid "When it comes to paid tickets, these orders can be refunded through the e-commerce platform in use." +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:1089 +msgid "Ticket Email Settings" +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:1090 +msgid "Adds a new settings panel in Events > Settings that gives more control over the ticket and rsvp emails that are sent to attendees after registration." +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:1095 +msgid "Per Event Check In API" +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:1096 +msgid "This extension shows a meta box with an API key on each Event with Ticket/RSVP." +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:1101 +msgid "Add Event & Attendee Info to WooCommerce Order Details" +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:1102 +msgid "Displays the information collected by “attendee meta fields” in the WooCommerce order screens as well." +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:1107 +msgid "Organizer Notification Email" +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:1108 +msgid "This extension will send an email to event organizers whenever a user registers for their event." +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:1145 +msgid "Add Cost Currency Symbol" +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:1146 +msgid "This extension allows you to set default currency symbols for your users to choose from instead of having a plain text field." +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:1151 +msgid "Add Google Maps Display and Link Options" +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:1152 +msgid "This extension adds the “Show Google Maps” and “Show Google Maps Link” checkboxes when creating a new Venue." +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:1157 +msgid "Hide Others’ Organizers and Venues" +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:1158 +msgid "This extension allows you to hide the Organizers and Venues that a visitor has not created from the Community Events submission form." +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:1163 +msgid "Display Custom HTML" +msgstr "" + +#: src/Tribe/Admin/Help_Page.php:1164 +msgid "This extension allows you to add custom HTML content to the top of the Community Events submission form." +msgstr "" + +#: src/Tribe/Admin/Notice/Php_Version.php:59 +#: src/Tribe/Admin/Notice/Plugin_Download.php:147 +msgctxt "separator used in a list of items" +msgid ", " +msgstr "" + +#: src/Tribe/Admin/Notice/Php_Version.php:60 +#: src/Tribe/Admin/Notice/Plugin_Download.php:148 +msgctxt "the final separator in a list of two or more items" +msgid " and " +msgstr "" + +#. Translators: %1$s list of plugins, %2$s current PHP version, %3$s open anchor html link for read more, %4$s open anchor html link for read more +#: src/Tribe/Admin/Notice/Php_Version.php:96 +msgctxt "Message notifying users they need to upgrade PHP" +msgid "Starting February 2023, %1$s will require PHP 7.4 or later. Currently, your site is using PHP version %2$s. Please update to a newer version. %3$sRead more%4$s." +msgstr "" + +#: src/Tribe/Admin/Notice/Plugin_Download.php:120 +msgid "To begin using %2$s, please install (or upgrade) and activate %3$s." +msgstr "" + +#: src/Tribe/Admin/Notice/Plugin_Download.php:122 +#: src/Tribe/PUE/Update_Prevention.php:181 +msgid "Read more" +msgstr "" + +#: src/Tribe/Admin/Notice/Plugin_Download.php:123 +msgid "There’s a new version of %1$s available, but your license is expired. You’ll need to renew your license to get access to the latest version. If you plan to continue using your current version of the plugin(s), be sure to use a compatible version of The Events Calendar. %2$s" +msgstr "" + +#: src/Tribe/Admin/Notice/WP_Version.php:79 +msgid "You are using WordPress 5.7 which included a major jQuery update that may cause compatibility issues with past versions of The Events Calendar, Event Tickets and other plugins." +msgstr "" + +#: src/Tribe/Admin/Notice/WP_Version.php:81 +msgid "WordPress 5.7 includes a major jQuery update that may cause compatibility issues with past versions of The Events Calendar, Event Tickets and other plugins." +msgstr "" + +#: src/Tribe/Admin/Notice/WP_Version.php:83 +msgid "Read more." +msgstr "" + +#: src/Tribe/Admin/Troubleshooting.php:297 +msgid "Site time zone uses UTC" +msgstr "" + +#: src/Tribe/Admin/Troubleshooting.php:298 +msgid "When using The Events Calendar, we highly recommend that you use a geographic timezone such as \"America/Los_Angeles\" and avoid using a UTC timezone offset such as “UTC+9”. Choosing a UTC timezone for your site or individual events may cause problems when importing events or with Daylight Saving Time. Go to your the General WordPress settings to adjust your site timezone." +msgstr "" + +#: src/Tribe/Admin/Troubleshooting.php:300 +msgid "Adjust your timezone" +msgstr "" + +#: src/Tribe/Admin/Troubleshooting.php:305 +msgid "Install max has been reached" +msgstr "" + +#: src/Tribe/Admin/Troubleshooting.php:306 +msgid "License keys can only be used on a limited number of sites, which varies depending on your license level. You'll need to remove the license from one or more other site's in order to use it on this one." +msgstr "" + +#: src/Tribe/Admin/Troubleshooting.php:308 +msgid "Manage your licenses" +msgstr "" + +#: src/Tribe/Admin/Troubleshooting.php:313 +msgid "Default Google Maps API key" +msgstr "" + +#: src/Tribe/Admin/Troubleshooting.php:314 +msgid "The Events Calendar comes with an API key for basic maps functionality. If you’d like to use more advanced features like custom map pins, dynamic map loads, or Events Calendar Pro's Location Search and advanced Map View, you’ll need to get your own Google Maps API key and add it to Events > Settings > Integrations" +msgstr "" + +#: src/Tribe/Admin/Troubleshooting.php:316 +msgid "Enter a custom API key" +msgstr "" + +#: src/Tribe/Admin/Troubleshooting.php:321 +msgid "Plugin(s) are out of date" +msgstr "" + +#: src/Tribe/Admin/Troubleshooting.php:322 +msgid "It's important to use the most recent versions of our plugins so that you have access to the latest features, bug fixes, and security updates. Plugin functionality can be comprimised if your site is running outdated or mis-matched versions." +msgstr "" + +#: src/Tribe/Admin/Troubleshooting.php:324 +msgid "Check for updates" +msgstr "" + +#: src/Tribe/Admin/Troubleshooting.php:343 +msgid "Common Error Messages" +msgstr "" + +#: src/Tribe/Admin/Troubleshooting.php:344 +msgid "Here’s an overview of %s and what they mean." +msgstr "" + +#: src/Tribe/Admin/Troubleshooting.php:349 +msgid "My calendar doesn’t look right." +msgstr "" + +#: src/Tribe/Admin/Troubleshooting.php:355 +msgid "I installed the calendar and it crashed my site." +msgstr "" + +#: src/Tribe/Admin/Troubleshooting.php:356 +msgid "%s and other common installation issues." +msgstr "" + +#: src/Tribe/Admin/Troubleshooting.php:361 +msgid "I keep getting “Page Not Found” on events." +msgstr "" + +#: src/Tribe/Admin/Troubleshooting.php:362 +msgid "There are a few %s to resolve and prevent 404 errors." +msgstr "" + +#: src/Tribe/Ajax/Dropdown.php:38 +msgid "Cannot look for Terms without a taxonomy" +msgstr "" + +#: src/Tribe/Ajax/Dropdown.php:270 +msgid "Missing data source for this dropdown" +msgstr "" + +#: src/Tribe/Ajax/Dropdown.php:283 +msgid "Empty data set for this dropdown" +msgstr "" + +#: src/Tribe/Ajax/Dropdown.php:336 +msgid "The \"%s\" source is invalid and cannot be reached on \"%s\" instance." +msgstr "" + +#: src/Tribe/App_Shop.php:50 +#: src/Tribe/App_Shop.php:51 +#: src/Tribe/App_Shop.php:82 +msgid "Event Add-Ons" +msgstr "" + +#: src/Tribe/App_Shop.php:177 +msgid "Events Marketing Bundle" +msgstr "" + +#: src/Tribe/App_Shop.php:180 +#: src/Tribe/App_Shop.php:204 +#: src/Tribe/App_Shop.php:223 +#: src/Tribe/App_Shop.php:236 +msgid "Save over 20%" +msgstr "" + +#: src/Tribe/App_Shop.php:181 +msgid "Ticket sales, attendee management, and email marketing for your events" +msgstr "" + +#: src/Tribe/App_Shop.php:189 +msgid "Event Importer Bundle" +msgstr "" + +#: src/Tribe/App_Shop.php:192 +msgid "Save over 25%" +msgstr "" + +#: src/Tribe/App_Shop.php:193 +msgid "Fill your calendar with events from across the web, including Google Calendar, Meetup, and more." +msgstr "" + +#: src/Tribe/App_Shop.php:201 +msgid "Virtual Events Marketing Bundle" +msgstr "" + +#: src/Tribe/App_Shop.php:205 +msgid "Streamline your online events and increase revenue." +msgstr "" + +#: src/Tribe/App_Shop.php:213 +msgid "Sell tickets and earn revenue for online events" +msgstr "" + +#: src/Tribe/App_Shop.php:214 +#: src/Tribe/Plugins_API.php:274 +msgid "Zoom integration" +msgstr "" + +#: src/Tribe/App_Shop.php:215 +msgid "Automated emails optimized for virtual events" +msgstr "" + +#: src/Tribe/App_Shop.php:216 +msgid "Add recurring events" +msgstr "" + +#: src/Tribe/App_Shop.php:220 +msgid "Community Manager Bundle" +msgstr "" + +#: src/Tribe/App_Shop.php:224 +msgid "Handle event submissions with ticket sales and everything you need to build a robust community." +msgstr "" + +#: src/Tribe/App_Shop.php:233 +msgid "Ultimate Bundle" +msgstr "" + +#: src/Tribe/App_Shop.php:237 +msgid "All of our premium events management plugins at a deep discount." +msgstr "" + +#: src/Tribe/App_Shop.php:264 +msgid "Website URL CTA" +msgstr "" + +#: src/Tribe/App_Shop.php:267 +msgid "Create a strong call-to-action for attendees to \"Join Webinar\" instead of only sharing a website address." +msgstr "" + +#: src/Tribe/App_Shop.php:270 +msgid "Link Directly to Webinar" +msgstr "" + +#: src/Tribe/App_Shop.php:273 +msgid "When users click on the event title, they’ll be taken right to the source of your event, offering a direct route to join." +msgstr "" + +#: src/Tribe/App_Shop.php:276 +msgid "Events Happening Now" +msgstr "" + +#: src/Tribe/App_Shop.php:279 +msgid "Use this shortcode to display events that are currently in progress, like webinars and livestreams." +msgstr "" + +#: src/Tribe/App_Shop.php:282 +msgid "Custom Venue Links" +msgstr "" + +#: src/Tribe/App_Shop.php:285 +msgid "Turn the venue name for your event into a clickable URL — a great way to link directly to a venue’s website or a virtual meeting." +msgstr "" + +#: src/Tribe/App_Shop.php:288 +msgid "Adjust Label" +msgstr "" + +#: src/Tribe/App_Shop.php:291 +msgid "Change \"Events\" to \"Webinars,\" or \"Venues\" to \"Livestream,\" or \"Organizers\" to \"Hosts.\" Tailor your calendar for virtual events and meetings." +msgstr "" + +#: src/Tribe/App_Shop.php:294 +msgid "Reach Attendees" +msgstr "" + +#: src/Tribe/App_Shop.php:297 +msgid "From registration to attendance history, view every step of the event lifecycle with this HubSpot integration." +msgstr "" + +#: src/Tribe/App_Shop.php:314 +msgid "The online course platform created by e-learning experts." +msgstr "" + +#: src/Tribe/App_Shop.php:316 +msgid "Add Courses" +msgstr "" + +#: src/Tribe/App_Shop.php:317 +msgid "Trusted to power learning programs for major universities, startups, entrepreneurs, and bloggers worldwide." +msgstr "" + +#: src/Tribe/App_Shop.php:322 +msgid "Foundational favorites: iThemes Security and Developer Toolkit." +msgstr "" + +#: src/Tribe/App_Shop.php:324 +msgid "Add Security" +msgstr "" + +#: src/Tribe/App_Shop.php:325 +msgid "iThemes Security, the WordPress security plugin that’s easy to use. Built with performance in mind." +msgstr "" + +#: src/Tribe/App_Shop.php:330 +msgid "Built with developers in mind." +msgstr "" + +#: src/Tribe/App_Shop.php:332 +msgid "Add Content Restriction" +msgstr "" + +#: src/Tribe/App_Shop.php:333 +msgid "Restrict Content Pro is flexible, easy to extend, and chock full of action hooks and filters, making it easy to modify and tweak to your specific needs." +msgstr "" + +#: src/Tribe/App_Shop.php:338 +msgid "Build better WordPress websites with Kadence." +msgstr "" + +#: src/Tribe/App_Shop.php:340 +msgid "Add Starter Templates" +msgstr "" + +#: src/Tribe/App_Shop.php:341 +msgid "Kadence lets you unlock your creativity in the WordPress Block Editor with expertly designed blocks, a robust theme, and a massive library of starter templates." +msgstr "" + +#: src/Tribe/App_Shop.php:346 +msgid "Sales-boosting WooCommerce plugins." +msgstr "" + +#: src/Tribe/App_Shop.php:348 +msgid "Add Commerce Tools" +msgstr "" + +#: src/Tribe/App_Shop.php:349 +msgid "" +"Easy-to-use WooCommerce plugins work perfectly together, with any theme. Create a fast and profitable eCommerce store without any technical knowledge.\n" +"\t\t\t\t\t" +msgstr "" + +#: src/Tribe/App_Shop.php:355 +msgid "The best WordPress donation plugin." +msgstr "" + +#: src/Tribe/App_Shop.php:357 +msgid "Add Donations" +msgstr "" + +#: src/Tribe/App_Shop.php:358 +msgid "GiveWP makes it easy to raise money online with donation forms, donor databases, and fundraising reporting." +msgstr "" + +#: src/Tribe/Cost_Utils.php:114 +msgid "Free" +msgstr "" + +#: src/Tribe/Credits.php:31 +msgid "This calendar is powered by The Events Calendar." +msgstr "" + +#: src/Tribe/Credits.php:48 +msgid "Rate %1$sThe Events Calendar%2$s %3$s" +msgstr "" + +#: src/Tribe/Credits.php:51 +msgid "If you like %1$sEvent Tickets%2$s please leave us a %3$s. It takes a minute and it helps a lot." +msgstr "" + +#: src/Tribe/Customizer.php:665 +msgid "Use the following panel of your customizer to change the styling of your Calendar and Event pages." +msgstr "" + +#: src/Tribe/Customizer.php:1026 +msgctxt "Page title for the TEC Customizer section." +msgid "Customize The Events Calendar" +msgstr "" + +#: src/Tribe/Customizer.php:1027 +msgctxt "Menu item text for the TEC Customizer section link." +msgid "Customize The Events Calendar" +msgstr "" + +#: src/Tribe/Customizer.php:1052 +msgid "Customizer" +msgstr "" + +#: src/Tribe/Customizer.php:1056 +msgid "Adjust colors, fonts, and more with the WordPress Customizer." +msgstr "" + +#. translators: %1$s: opening anchor tag; %2$s: closing anchor tag +#: src/Tribe/Customizer.php:1062 +msgctxt "Link text added to the TEC->Settings->Display tab." +msgid "%1$sCustomize The Events Calendar%2$s" +msgstr "" + +#: src/Tribe/Debug_Bar/Panels/Context.php:22 +#: src/Tribe/Debug_Bar/Panels/Context.php:50 +msgid "The Events Calendar Context" +msgstr "" + +#: src/Tribe/Debug_Bar/Panels/Context.php:53 +msgid "PHP Render Context" +msgstr "" + +#: src/Tribe/Debug_Bar/Panels/Context.php:61 +msgid "Key" +msgstr "" + +#: src/Tribe/Debug_Bar/Panels/Context.php:62 +msgid "Value" +msgstr "" + +#: src/Tribe/Debug_Bar/Panels/Context.php:63 +msgid "ORM arg" +msgstr "" + +#: src/Tribe/Debug_Bar/Panels/Context.php:64 +msgid "Reads" +msgstr "" + +#: src/Tribe/Debug_Bar/Panels/Context.php:65 +msgid "Writes" +msgstr "" + +#: src/Tribe/Debug_Bar/Panels/Context.php:91 +msgid "State" +msgstr "" + +#: src/Tribe/Debug_Bar/Panels/Json_Ld.php:21 +#: src/Tribe/Debug_Bar/Panels/Json_Ld.php:40 +msgid "The Events Calendar JSON-LD Data" +msgstr "" + +#: src/Tribe/Dialog/View.php:157 +msgid "Open the modal window" +msgstr "" + +#: src/Tribe/Dialog/View.php:158 +msgid "Close this modal window" +msgstr "" + +#: src/Tribe/Dialog/View.php:228 +#: src/Tribe/Dialog/View.php:300 +msgid "Cancel" +msgstr "" + +#: src/Tribe/Dialog/View.php:229 +msgid "Confirm" +msgstr "" + +#: src/Tribe/Dialog/View.php:302 +#: src/Tribe/Dialog/View.php:371 +msgid "OK" +msgstr "" + +#: src/Tribe/Dialog/View.php:443 +msgid "Open the dialog window" +msgstr "" + +#: src/Tribe/Dialog/View.php:460 +msgid "Close this dialog window" +msgstr "" + +#: src/Tribe/Documentation/Swagger/Cost_Details_Definition_Provider.php:24 +msgid "The cost currency symbol" +msgstr "" + +#: src/Tribe/Documentation/Swagger/Cost_Details_Definition_Provider.php:28 +msgid "The position of the currency symbol in the cost string" +msgstr "" + +#: src/Tribe/Documentation/Swagger/Cost_Details_Definition_Provider.php:34 +msgid "A sorted array of all the numeric values for the cost" +msgstr "" + +#: src/Tribe/Documentation/Swagger/Date_Details_Definition_Provider.php:24 +msgid "The date year" +msgstr "" + +#: src/Tribe/Documentation/Swagger/Date_Details_Definition_Provider.php:28 +msgid "The date month" +msgstr "" + +#: src/Tribe/Documentation/Swagger/Date_Details_Definition_Provider.php:32 +msgid "The date day" +msgstr "" + +#: src/Tribe/Documentation/Swagger/Date_Details_Definition_Provider.php:36 +msgid "The date hour" +msgstr "" + +#: src/Tribe/Documentation/Swagger/Date_Details_Definition_Provider.php:40 +msgid "The date minutes" +msgstr "" + +#: src/Tribe/Documentation/Swagger/Date_Details_Definition_Provider.php:44 +msgid "The date seconds" +msgstr "" + +#: src/Tribe/Documentation/Swagger/Image_Definition_Provider.php:25 +msgid "The URL to the full size version of the image" +msgstr "" + +#: src/Tribe/Documentation/Swagger/Image_Definition_Provider.php:29 +msgid "The image WordPress post ID" +msgstr "" + +#: src/Tribe/Documentation/Swagger/Image_Definition_Provider.php:33 +msgid "The image file extension" +msgstr "" + +#: src/Tribe/Documentation/Swagger/Image_Definition_Provider.php:37 +msgid "The image natural width in pixels" +msgstr "" + +#: src/Tribe/Documentation/Swagger/Image_Definition_Provider.php:41 +msgid "The image natural height in pixels" +msgstr "" + +#: src/Tribe/Documentation/Swagger/Image_Definition_Provider.php:45 +msgid "The details about each size available for the image" +msgstr "" + +#: src/Tribe/Documentation/Swagger/Image_Size_Definition_Provider.php:24 +msgid "The image width in pixels in the specified size" +msgstr "" + +#: src/Tribe/Documentation/Swagger/Image_Size_Definition_Provider.php:28 +msgid "The image height in pixels in the specified size" +msgstr "" + +#: src/Tribe/Documentation/Swagger/Image_Size_Definition_Provider.php:32 +msgid "The image mime-type" +msgstr "" + +#: src/Tribe/Documentation/Swagger/Image_Size_Definition_Provider.php:37 +msgid "The link to the image in the specified size on the site" +msgstr "" + +#: src/Tribe/Documentation/Swagger/Term_Definition_Provider.php:24 +msgid "The WordPress term ID" +msgstr "" + +#: src/Tribe/Documentation/Swagger/Term_Definition_Provider.php:28 +msgid "The term name" +msgstr "" + +#: src/Tribe/Documentation/Swagger/Term_Definition_Provider.php:32 +msgid "The term slug" +msgstr "" + +#: src/Tribe/Documentation/Swagger/Term_Definition_Provider.php:36 +msgid "The taxonomy the term belongs to" +msgstr "" + +#: src/Tribe/Documentation/Swagger/Term_Definition_Provider.php:40 +msgid "The term description" +msgstr "" + +#: src/Tribe/Documentation/Swagger/Term_Definition_Provider.php:44 +msgid "The term parent term if any" +msgstr "" + +#: src/Tribe/Documentation/Swagger/Term_Definition_Provider.php:48 +msgid "The number of posts associated with the term" +msgstr "" + +#: src/Tribe/Documentation/Swagger/Term_Definition_Provider.php:52 +msgid "The URL to the term archive page" +msgstr "" + +#: src/Tribe/Documentation/Swagger/Term_Definition_Provider.php:57 +msgid "A list of links to the term own, archive and parent REST URL" +msgstr "" + +#: src/Tribe/Editor/Blocks/Abstract.php:123 +msgid "Problem loading the block, please remove this block to restart." +msgstr "" + +#: src/Tribe/Editor/Meta.php:105 +msgid "Numeric Array" +msgstr "" + +#: src/Tribe/Editor/Meta.php:123 +msgid "Text Array" +msgstr "" + +#: src/Tribe/Extension.php:144 +msgid "Tutorial" +msgstr "" + +#: src/Tribe/Extension.php:406 +msgid "Unable to run Tribe Extensions. Your website host is running PHP 5.2 or older, and has likely disabled or misconfigured debug_backtrace(). You, or your website host, will need to upgrade PHP or properly configure debug_backtrace() for Tribe Extensions to work." +msgstr "" + +#: src/Tribe/Extension.php:421 +msgctxt "extension disallowed" +msgid "This extension has been programmatically disallowed. The most common reason is due to another The Events Calendar plugin having absorbed or replaced this extension's functionality. This extension plugin has been deactivated, and you should likely delete it." +msgstr "" + +#: src/Tribe/Field.php:253 +msgid "Invalid field type specified" +msgstr "" + +#: src/Tribe/Field.php:557 +msgid "No radio options specified" +msgstr "" + +#: src/Tribe/Field.php:593 +msgid "No checkbox options specified" +msgstr "" + +#: src/Tribe/Field.php:655 +msgid "No select options specified" +msgstr "" + +#: src/Tribe/Field.php:757 +msgid "Select Image" +msgstr "" + +#: src/Tribe/Field.php:758 +msgid "Remove Image" +msgstr "" + +#: src/Tribe/Field.php:762 +msgid "Select an image" +msgstr "" + +#: src/Tribe/Field.php:763 +msgid "Use this image" +msgstr "" + +#: src/Tribe/Languages/Locations.php:79 +msgid "United States" +msgstr "" + +#: src/Tribe/Languages/Locations.php:80 +msgid "Afghanistan" +msgstr "" + +#: src/Tribe/Languages/Locations.php:81 +msgid "Åland Islands" +msgstr "" + +#: src/Tribe/Languages/Locations.php:82 +msgid "Albania" +msgstr "" + +#: src/Tribe/Languages/Locations.php:83 +msgid "Algeria" +msgstr "" + +#: src/Tribe/Languages/Locations.php:84 +msgid "American Samoa" +msgstr "" + +#: src/Tribe/Languages/Locations.php:85 +msgid "Andorra" +msgstr "" + +#: src/Tribe/Languages/Locations.php:86 +msgid "Angola" +msgstr "" + +#: src/Tribe/Languages/Locations.php:87 +msgid "Anguilla" +msgstr "" + +#: src/Tribe/Languages/Locations.php:88 +msgid "Antarctica" +msgstr "" + +#: src/Tribe/Languages/Locations.php:89 +msgid "Antigua and Barbuda" +msgstr "" + +#: src/Tribe/Languages/Locations.php:90 +msgid "Argentina" +msgstr "" + +#: src/Tribe/Languages/Locations.php:91 +msgid "Armenia" +msgstr "" + +#: src/Tribe/Languages/Locations.php:92 +msgid "Aruba" +msgstr "" + +#: src/Tribe/Languages/Locations.php:93 +msgid "Australia" +msgstr "" + +#: src/Tribe/Languages/Locations.php:94 +msgid "Austria" +msgstr "" + +#: src/Tribe/Languages/Locations.php:95 +msgid "Azerbaijan" +msgstr "" + +#: src/Tribe/Languages/Locations.php:96 +msgid "Bahamas" +msgstr "" + +#: src/Tribe/Languages/Locations.php:97 +msgid "Bahrain" +msgstr "" + +#: src/Tribe/Languages/Locations.php:98 +msgid "Bangladesh" +msgstr "" + +#: src/Tribe/Languages/Locations.php:99 +msgid "Barbados" +msgstr "" + +#: src/Tribe/Languages/Locations.php:100 +msgid "Belarus" +msgstr "" + +#: src/Tribe/Languages/Locations.php:101 +msgid "Belgium" +msgstr "" + +#: src/Tribe/Languages/Locations.php:102 +msgid "Belize" +msgstr "" + +#: src/Tribe/Languages/Locations.php:103 +msgid "Benin" +msgstr "" + +#: src/Tribe/Languages/Locations.php:104 +msgid "Bermuda" +msgstr "" + +#: src/Tribe/Languages/Locations.php:105 +msgid "Bhutan" +msgstr "" + +#: src/Tribe/Languages/Locations.php:106 +msgid "Bolivia" +msgstr "" + +#: src/Tribe/Languages/Locations.php:107 +msgid "Bosnia and Herzegovina" +msgstr "" + +#: src/Tribe/Languages/Locations.php:108 +msgid "Botswana" +msgstr "" + +#: src/Tribe/Languages/Locations.php:109 +msgid "Bouvet Island" +msgstr "" + +#: src/Tribe/Languages/Locations.php:110 +msgid "Brazil" +msgstr "" + +#: src/Tribe/Languages/Locations.php:111 +msgid "British Indian Ocean Territory" +msgstr "" + +#: src/Tribe/Languages/Locations.php:112 +msgid "Brunei Darussalam" +msgstr "" + +#: src/Tribe/Languages/Locations.php:113 +msgid "Bulgaria" +msgstr "" + +#: src/Tribe/Languages/Locations.php:114 +msgid "Burkina Faso" +msgstr "" + +#: src/Tribe/Languages/Locations.php:115 +msgid "Burundi" +msgstr "" + +#: src/Tribe/Languages/Locations.php:116 +msgid "Cambodia" +msgstr "" + +#: src/Tribe/Languages/Locations.php:117 +msgid "Cameroon" +msgstr "" + +#: src/Tribe/Languages/Locations.php:118 +msgid "Canada" +msgstr "" + +#: src/Tribe/Languages/Locations.php:119 +msgid "Cape Verde" +msgstr "" + +#: src/Tribe/Languages/Locations.php:120 +msgid "Cayman Islands" +msgstr "" + +#: src/Tribe/Languages/Locations.php:121 +msgid "Central African Republic" +msgstr "" + +#: src/Tribe/Languages/Locations.php:122 +msgid "Chad" +msgstr "" + +#: src/Tribe/Languages/Locations.php:123 +msgid "Chile" +msgstr "" + +#: src/Tribe/Languages/Locations.php:124 +msgid "China" +msgstr "" + +#: src/Tribe/Languages/Locations.php:125 +msgid "Christmas Island" +msgstr "" + +#: src/Tribe/Languages/Locations.php:126 +msgid "Cocos (Keeling) Islands" +msgstr "" + +#: src/Tribe/Languages/Locations.php:127 +msgid "Collectivity of Saint Martin" +msgstr "" + +#: src/Tribe/Languages/Locations.php:128 +msgid "Colombia" +msgstr "" + +#: src/Tribe/Languages/Locations.php:129 +msgid "Comoros" +msgstr "" + +#: src/Tribe/Languages/Locations.php:130 +msgid "Congo" +msgstr "" + +#: src/Tribe/Languages/Locations.php:131 +msgid "Congo, Democratic Republic of the" +msgstr "" + +#: src/Tribe/Languages/Locations.php:132 +msgid "Cook Islands" +msgstr "" + +#: src/Tribe/Languages/Locations.php:133 +msgid "Costa Rica" +msgstr "" + +#: src/Tribe/Languages/Locations.php:134 +msgid "Côte d'Ivoire" +msgstr "" + +#: src/Tribe/Languages/Locations.php:135 +msgid "Croatia (Local Name: Hrvatska)" +msgstr "" + +#: src/Tribe/Languages/Locations.php:136 +msgid "Cuba" +msgstr "" + +#: src/Tribe/Languages/Locations.php:137 +msgid "Curaçao" +msgstr "" + +#: src/Tribe/Languages/Locations.php:138 +msgid "Cyprus" +msgstr "" + +#: src/Tribe/Languages/Locations.php:139 +msgid "Czech Republic" +msgstr "" + +#: src/Tribe/Languages/Locations.php:140 +msgid "Denmark" +msgstr "" + +#: src/Tribe/Languages/Locations.php:141 +msgid "Djibouti" +msgstr "" + +#: src/Tribe/Languages/Locations.php:142 +msgid "Dominica" +msgstr "" + +#: src/Tribe/Languages/Locations.php:143 +msgid "Dominican Republic" +msgstr "" + +#: src/Tribe/Languages/Locations.php:144 +msgid "East Timor" +msgstr "" + +#: src/Tribe/Languages/Locations.php:145 +msgid "Ecuador" +msgstr "" + +#: src/Tribe/Languages/Locations.php:146 +msgid "Egypt" +msgstr "" + +#: src/Tribe/Languages/Locations.php:147 +msgid "El Salvador" +msgstr "" + +#: src/Tribe/Languages/Locations.php:148 +msgid "Equatorial Guinea" +msgstr "" + +#: src/Tribe/Languages/Locations.php:149 +msgid "Eritrea" +msgstr "" + +#: src/Tribe/Languages/Locations.php:150 +msgid "Estonia" +msgstr "" + +#: src/Tribe/Languages/Locations.php:151 +msgid "Ethiopia" +msgstr "" + +#: src/Tribe/Languages/Locations.php:152 +msgid "Falkland Islands (Malvinas)" +msgstr "" + +#: src/Tribe/Languages/Locations.php:153 +msgid "Faroe Islands" +msgstr "" + +#: src/Tribe/Languages/Locations.php:154 +msgid "Fiji" +msgstr "" + +#: src/Tribe/Languages/Locations.php:155 +msgid "Finland" +msgstr "" + +#: src/Tribe/Languages/Locations.php:156 +msgid "France" +msgstr "" + +#: src/Tribe/Languages/Locations.php:157 +msgid "French Guiana" +msgstr "" + +#: src/Tribe/Languages/Locations.php:158 +msgid "French Polynesia" +msgstr "" + +#: src/Tribe/Languages/Locations.php:159 +msgid "French Southern Territories" +msgstr "" + +#: src/Tribe/Languages/Locations.php:160 +msgid "Gabon" +msgstr "" + +#: src/Tribe/Languages/Locations.php:161 +msgid "Gambia" +msgstr "" + +#: src/Tribe/Languages/Locations.php:162 +msgctxt "The country" +msgid "Georgia" +msgstr "" + +#: src/Tribe/Languages/Locations.php:163 +msgid "Germany" +msgstr "" + +#: src/Tribe/Languages/Locations.php:164 +msgid "Ghana" +msgstr "" + +#: src/Tribe/Languages/Locations.php:165 +msgid "Gibraltar" +msgstr "" + +#: src/Tribe/Languages/Locations.php:166 +msgid "Greece" +msgstr "" + +#: src/Tribe/Languages/Locations.php:167 +msgid "Greenland" +msgstr "" + +#: src/Tribe/Languages/Locations.php:168 +msgid "Grenada" +msgstr "" + +#: src/Tribe/Languages/Locations.php:169 +msgid "Guadeloupe" +msgstr "" + +#: src/Tribe/Languages/Locations.php:170 +msgid "Guam" +msgstr "" + +#: src/Tribe/Languages/Locations.php:171 +msgid "Guatemala" +msgstr "" + +#: src/Tribe/Languages/Locations.php:172 +msgid "Guinea" +msgstr "" + +#: src/Tribe/Languages/Locations.php:173 +msgid "Guinea-Bissau" +msgstr "" + +#: src/Tribe/Languages/Locations.php:174 +msgid "Guyana" +msgstr "" + +#: src/Tribe/Languages/Locations.php:175 +msgid "Haiti" +msgstr "" + +#: src/Tribe/Languages/Locations.php:176 +msgid "Heard and McDonald Islands" +msgstr "" + +#: src/Tribe/Languages/Locations.php:177 +msgid "Holy See (Vatican City State)" +msgstr "" + +#: src/Tribe/Languages/Locations.php:178 +msgid "Honduras" +msgstr "" + +#: src/Tribe/Languages/Locations.php:179 +msgid "Hong Kong" +msgstr "" + +#: src/Tribe/Languages/Locations.php:180 +msgid "Hungary" +msgstr "" + +#: src/Tribe/Languages/Locations.php:181 +msgid "Iceland" +msgstr "" + +#: src/Tribe/Languages/Locations.php:182 +msgid "India" +msgstr "" + +#: src/Tribe/Languages/Locations.php:183 +msgid "Indonesia" +msgstr "" + +#: src/Tribe/Languages/Locations.php:184 +msgid "Iran, Islamic Republic of" +msgstr "" + +#: src/Tribe/Languages/Locations.php:185 +msgid "Iraq" +msgstr "" + +#: src/Tribe/Languages/Locations.php:186 +msgid "Ireland" +msgstr "" + +#: src/Tribe/Languages/Locations.php:187 +msgid "Israel" +msgstr "" + +#: src/Tribe/Languages/Locations.php:188 +msgid "Italy" +msgstr "" + +#: src/Tribe/Languages/Locations.php:189 +msgid "Jamaica" +msgstr "" + +#: src/Tribe/Languages/Locations.php:190 +msgid "Japan" +msgstr "" + +#: src/Tribe/Languages/Locations.php:191 +msgid "Jordan" +msgstr "" + +#: src/Tribe/Languages/Locations.php:192 +msgid "Kazakhstan" +msgstr "" + +#: src/Tribe/Languages/Locations.php:193 +msgid "Kenya" +msgstr "" + +#: src/Tribe/Languages/Locations.php:194 +msgid "Kiribati" +msgstr "" + +#: src/Tribe/Languages/Locations.php:195 +msgid "Korea, Democratic People's Republic of" +msgstr "" + +#: src/Tribe/Languages/Locations.php:196 +msgid "Korea, Republic of" +msgstr "" + +#: src/Tribe/Languages/Locations.php:197 +msgid "Kuwait" +msgstr "" + +#: src/Tribe/Languages/Locations.php:198 +msgid "Kyrgyzstan" +msgstr "" + +#: src/Tribe/Languages/Locations.php:199 +msgid "Lao People's Democratic Republic" +msgstr "" + +#: src/Tribe/Languages/Locations.php:200 +msgid "Latvia" +msgstr "" + +#: src/Tribe/Languages/Locations.php:201 +msgid "Lebanon" +msgstr "" + +#: src/Tribe/Languages/Locations.php:202 +msgid "Lesotho" +msgstr "" + +#: src/Tribe/Languages/Locations.php:203 +msgid "Liberia" +msgstr "" + +#: src/Tribe/Languages/Locations.php:204 +msgid "Libya" +msgstr "" + +#: src/Tribe/Languages/Locations.php:205 +msgid "Liechtenstein" +msgstr "" + +#: src/Tribe/Languages/Locations.php:206 +msgid "Lithuania" +msgstr "" + +#: src/Tribe/Languages/Locations.php:207 +msgid "Luxembourg" +msgstr "" + +#: src/Tribe/Languages/Locations.php:208 +msgid "Macau" +msgstr "" + +#: src/Tribe/Languages/Locations.php:209 +msgid "Madagascar" +msgstr "" + +#: src/Tribe/Languages/Locations.php:210 +msgid "Malawi" +msgstr "" + +#: src/Tribe/Languages/Locations.php:211 +msgid "Malaysia" +msgstr "" + +#: src/Tribe/Languages/Locations.php:212 +msgid "Maldives" +msgstr "" + +#: src/Tribe/Languages/Locations.php:213 +msgid "Mali" +msgstr "" + +#: src/Tribe/Languages/Locations.php:214 +msgid "Malta" +msgstr "" + +#: src/Tribe/Languages/Locations.php:215 +msgid "Marshall Islands" +msgstr "" + +#: src/Tribe/Languages/Locations.php:216 +msgid "Martinique" +msgstr "" + +#: src/Tribe/Languages/Locations.php:217 +msgid "Mauritania" +msgstr "" + +#: src/Tribe/Languages/Locations.php:218 +msgid "Mauritius" +msgstr "" + +#: src/Tribe/Languages/Locations.php:219 +msgid "Mayotte" +msgstr "" + +#: src/Tribe/Languages/Locations.php:220 +msgid "Mexico" +msgstr "" + +#: src/Tribe/Languages/Locations.php:221 +msgid "Micronesia, Federated States of" +msgstr "" + +#: src/Tribe/Languages/Locations.php:222 +msgid "Moldova, Republic of" +msgstr "" + +#: src/Tribe/Languages/Locations.php:223 +msgid "Monaco" +msgstr "" + +#: src/Tribe/Languages/Locations.php:224 +msgid "Mongolia" +msgstr "" + +#: src/Tribe/Languages/Locations.php:225 +msgid "Montenegro" +msgstr "" + +#: src/Tribe/Languages/Locations.php:226 +msgid "Montserrat" +msgstr "" + +#: src/Tribe/Languages/Locations.php:227 +msgid "Morocco" +msgstr "" + +#: src/Tribe/Languages/Locations.php:228 +msgid "Mozambique" +msgstr "" + +#: src/Tribe/Languages/Locations.php:229 +msgid "Myanmar" +msgstr "" + +#: src/Tribe/Languages/Locations.php:230 +msgid "Namibia" +msgstr "" + +#: src/Tribe/Languages/Locations.php:231 +msgid "Nauru" +msgstr "" + +#: src/Tribe/Languages/Locations.php:232 +msgid "Nepal" +msgstr "" + +#: src/Tribe/Languages/Locations.php:233 +msgid "Netherlands" +msgstr "" + +#: src/Tribe/Languages/Locations.php:234 +msgid "New Caledonia" +msgstr "" + +#: src/Tribe/Languages/Locations.php:235 +msgid "New Zealand" +msgstr "" + +#: src/Tribe/Languages/Locations.php:236 +msgid "Nicaragua" +msgstr "" + +#: src/Tribe/Languages/Locations.php:237 +msgid "Niger" +msgstr "" + +#: src/Tribe/Languages/Locations.php:238 +msgid "Nigeria" +msgstr "" + +#: src/Tribe/Languages/Locations.php:239 +msgid "Niue" +msgstr "" + +#: src/Tribe/Languages/Locations.php:240 +msgid "Norfolk Island" +msgstr "" + +#: src/Tribe/Languages/Locations.php:241 +msgid "North Macedonia" +msgstr "" + +#: src/Tribe/Languages/Locations.php:242 +msgid "Northern Mariana Islands" +msgstr "" + +#: src/Tribe/Languages/Locations.php:243 +msgid "Norway" +msgstr "" + +#: src/Tribe/Languages/Locations.php:244 +msgid "Oman" +msgstr "" + +#: src/Tribe/Languages/Locations.php:245 +msgid "Pakistan" +msgstr "" + +#: src/Tribe/Languages/Locations.php:246 +msgid "Palau" +msgstr "" + +#: src/Tribe/Languages/Locations.php:247 +msgid "Panama" +msgstr "" + +#: src/Tribe/Languages/Locations.php:248 +msgid "Papua New Guinea" +msgstr "" + +#: src/Tribe/Languages/Locations.php:249 +msgid "Paraguay" +msgstr "" + +#: src/Tribe/Languages/Locations.php:250 +msgid "Peru" +msgstr "" + +#: src/Tribe/Languages/Locations.php:251 +msgid "Philippines" +msgstr "" + +#: src/Tribe/Languages/Locations.php:252 +msgid "Pitcairn" +msgstr "" + +#: src/Tribe/Languages/Locations.php:253 +msgid "Poland" +msgstr "" + +#: src/Tribe/Languages/Locations.php:254 +msgid "Portugal" +msgstr "" + +#: src/Tribe/Languages/Locations.php:255 +msgid "Puerto Rico" +msgstr "" + +#: src/Tribe/Languages/Locations.php:256 +msgid "Qatar" +msgstr "" + +#: src/Tribe/Languages/Locations.php:257 +msgid "Reunion" +msgstr "" + +#: src/Tribe/Languages/Locations.php:258 +msgid "Romania" +msgstr "" + +#: src/Tribe/Languages/Locations.php:259 +msgid "Russian Federation" +msgstr "" + +#: src/Tribe/Languages/Locations.php:260 +msgid "Rwanda" +msgstr "" + +#: src/Tribe/Languages/Locations.php:261 +msgid "Saint Barthélemy" +msgstr "" + +#: src/Tribe/Languages/Locations.php:262 +msgid "Saint Helena" +msgstr "" + +#: src/Tribe/Languages/Locations.php:263 +msgid "Saint Kitts and Nevis" +msgstr "" + +#: src/Tribe/Languages/Locations.php:264 +msgid "Saint Lucia" +msgstr "" + +#: src/Tribe/Languages/Locations.php:265 +msgid "Saint Pierre and Miquelon" +msgstr "" + +#: src/Tribe/Languages/Locations.php:266 +msgid "Saint Vincent and The Grenadines" +msgstr "" + +#: src/Tribe/Languages/Locations.php:267 +msgid "Samoa" +msgstr "" + +#: src/Tribe/Languages/Locations.php:268 +msgid "San Marino" +msgstr "" + +#: src/Tribe/Languages/Locations.php:269 +msgid "São Tomé and Príncipe" +msgstr "" + +#: src/Tribe/Languages/Locations.php:270 +msgid "Saudi Arabia" +msgstr "" + +#: src/Tribe/Languages/Locations.php:271 +msgid "Senegal" +msgstr "" + +#: src/Tribe/Languages/Locations.php:272 +msgid "Serbia" +msgstr "" + +#: src/Tribe/Languages/Locations.php:273 +msgid "Seychelles" +msgstr "" + +#: src/Tribe/Languages/Locations.php:274 +msgid "Sierra Leone" +msgstr "" + +#: src/Tribe/Languages/Locations.php:275 +msgid "Singapore" +msgstr "" + +#: src/Tribe/Languages/Locations.php:276 +msgid "Sint Maarten" +msgstr "" + +#: src/Tribe/Languages/Locations.php:277 +msgid "Slovakia (Slovak Republic)" +msgstr "" + +#: src/Tribe/Languages/Locations.php:278 +msgid "Slovenia" +msgstr "" + +#: src/Tribe/Languages/Locations.php:279 +msgid "Solomon Islands" +msgstr "" + +#: src/Tribe/Languages/Locations.php:280 +msgid "Somalia" +msgstr "" + +#: src/Tribe/Languages/Locations.php:281 +msgid "South Africa" +msgstr "" + +#: src/Tribe/Languages/Locations.php:282 +msgid "South Georgia, South Sandwich Islands" +msgstr "" + +#: src/Tribe/Languages/Locations.php:283 +msgid "Spain" +msgstr "" + +#: src/Tribe/Languages/Locations.php:284 +msgid "Sri Lanka" +msgstr "" + +#: src/Tribe/Languages/Locations.php:285 +msgid "Sudan" +msgstr "" + +#: src/Tribe/Languages/Locations.php:286 +msgid "Suriname" +msgstr "" + +#: src/Tribe/Languages/Locations.php:287 +msgid "Svalbard and Jan Mayen Islands" +msgstr "" + +#: src/Tribe/Languages/Locations.php:288 +msgid "Swaziland" +msgstr "" + +#: src/Tribe/Languages/Locations.php:289 +msgid "Sweden" +msgstr "" + +#: src/Tribe/Languages/Locations.php:290 +msgid "Switzerland" +msgstr "" + +#: src/Tribe/Languages/Locations.php:291 +msgid "Syrian Arab Republic" +msgstr "" + +#: src/Tribe/Languages/Locations.php:292 +msgid "Taiwan" +msgstr "" + +#: src/Tribe/Languages/Locations.php:293 +msgid "Tajikistan" +msgstr "" + +#: src/Tribe/Languages/Locations.php:294 +msgid "Tanzania, United Republic of" +msgstr "" + +#: src/Tribe/Languages/Locations.php:295 +msgid "Thailand" +msgstr "" + +#: src/Tribe/Languages/Locations.php:296 +msgid "Togo" +msgstr "" + +#: src/Tribe/Languages/Locations.php:297 +msgid "Tokelau" +msgstr "" + +#: src/Tribe/Languages/Locations.php:298 +msgid "Tonga" +msgstr "" + +#: src/Tribe/Languages/Locations.php:299 +msgid "Trinidad and Tobago" +msgstr "" + +#: src/Tribe/Languages/Locations.php:300 +msgid "Tunisia" +msgstr "" + +#: src/Tribe/Languages/Locations.php:301 +msgid "Turkey" +msgstr "" + +#: src/Tribe/Languages/Locations.php:302 +msgid "Turkmenistan" +msgstr "" + +#: src/Tribe/Languages/Locations.php:303 +msgid "Turks and Caicos Islands" +msgstr "" + +#: src/Tribe/Languages/Locations.php:304 +msgid "Tuvalu" +msgstr "" + +#: src/Tribe/Languages/Locations.php:305 +msgid "Uganda" +msgstr "" + +#: src/Tribe/Languages/Locations.php:306 +msgid "Ukraine" +msgstr "" + +#: src/Tribe/Languages/Locations.php:307 +msgid "United Arab Emirates" +msgstr "" + +#: src/Tribe/Languages/Locations.php:308 +msgid "United Kingdom" +msgstr "" + +#: src/Tribe/Languages/Locations.php:309 +msgid "United States Minor Outlying Islands" +msgstr "" + +#: src/Tribe/Languages/Locations.php:310 +msgid "Uruguay" +msgstr "" + +#: src/Tribe/Languages/Locations.php:311 +msgid "Uzbekistan" +msgstr "" + +#: src/Tribe/Languages/Locations.php:312 +msgid "Vanuatu" +msgstr "" + +#: src/Tribe/Languages/Locations.php:313 +msgid "Venezuela" +msgstr "" + +#: src/Tribe/Languages/Locations.php:314 +msgid "Viet Nam" +msgstr "" + +#: src/Tribe/Languages/Locations.php:315 +msgid "Virgin Islands (British)" +msgstr "" + +#: src/Tribe/Languages/Locations.php:316 +msgid "Virgin Islands (U.S.)" +msgstr "" + +#: src/Tribe/Languages/Locations.php:317 +msgid "Wallis and Futuna Islands" +msgstr "" + +#: src/Tribe/Languages/Locations.php:318 +msgid "Western Sahara" +msgstr "" + +#: src/Tribe/Languages/Locations.php:319 +msgid "Yemen" +msgstr "" + +#: src/Tribe/Languages/Locations.php:320 +msgid "Zambia" +msgstr "" + +#: src/Tribe/Languages/Locations.php:321 +msgid "Zimbabwe" +msgstr "" + +#: src/Tribe/Languages/Locations.php:348 +msgid "Alabama" +msgstr "" + +#: src/Tribe/Languages/Locations.php:349 +msgid "Alaska" +msgstr "" + +#: src/Tribe/Languages/Locations.php:350 +msgid "Arizona" +msgstr "" + +#: src/Tribe/Languages/Locations.php:351 +msgid "Arkansas" +msgstr "" + +#: src/Tribe/Languages/Locations.php:352 +msgid "California" +msgstr "" + +#: src/Tribe/Languages/Locations.php:353 +msgid "Colorado" +msgstr "" + +#: src/Tribe/Languages/Locations.php:354 +msgid "Connecticut" +msgstr "" + +#: src/Tribe/Languages/Locations.php:355 +msgid "Delaware" +msgstr "" + +#: src/Tribe/Languages/Locations.php:356 +msgid "District of Columbia" +msgstr "" + +#: src/Tribe/Languages/Locations.php:357 +msgid "Florida" +msgstr "" + +#: src/Tribe/Languages/Locations.php:358 +msgctxt "The US state Georgia" +msgid "Georgia" +msgstr "" + +#: src/Tribe/Languages/Locations.php:359 +msgid "Hawaii" +msgstr "" + +#: src/Tribe/Languages/Locations.php:360 +msgid "Idaho" +msgstr "" + +#: src/Tribe/Languages/Locations.php:361 +msgid "Illinois" +msgstr "" + +#: src/Tribe/Languages/Locations.php:362 +msgid "Indiana" +msgstr "" + +#: src/Tribe/Languages/Locations.php:363 +msgid "Iowa" +msgstr "" + +#: src/Tribe/Languages/Locations.php:364 +msgid "Kansas" +msgstr "" + +#: src/Tribe/Languages/Locations.php:365 +msgid "Kentucky" +msgstr "" + +#: src/Tribe/Languages/Locations.php:366 +msgid "Louisiana" +msgstr "" + +#: src/Tribe/Languages/Locations.php:367 +msgid "Maine" +msgstr "" + +#: src/Tribe/Languages/Locations.php:368 +msgid "Maryland" +msgstr "" + +#: src/Tribe/Languages/Locations.php:369 +msgid "Massachusetts" +msgstr "" + +#: src/Tribe/Languages/Locations.php:370 +msgid "Michigan" +msgstr "" + +#: src/Tribe/Languages/Locations.php:371 +msgid "Minnesota" +msgstr "" + +#: src/Tribe/Languages/Locations.php:372 +msgid "Mississippi" +msgstr "" + +#: src/Tribe/Languages/Locations.php:373 +msgid "Missouri" +msgstr "" + +#: src/Tribe/Languages/Locations.php:374 +msgid "Montana" +msgstr "" + +#: src/Tribe/Languages/Locations.php:375 +msgid "Nebraska" +msgstr "" + +#: src/Tribe/Languages/Locations.php:376 +msgid "Nevada" +msgstr "" + +#: src/Tribe/Languages/Locations.php:377 +msgid "New Hampshire" +msgstr "" + +#: src/Tribe/Languages/Locations.php:378 +msgid "New Jersey" +msgstr "" + +#: src/Tribe/Languages/Locations.php:379 +msgid "New Mexico" +msgstr "" + +#: src/Tribe/Languages/Locations.php:380 +msgid "New York" +msgstr "" + +#: src/Tribe/Languages/Locations.php:381 +msgid "North Carolina" +msgstr "" + +#: src/Tribe/Languages/Locations.php:382 +msgid "North Dakota" +msgstr "" + +#: src/Tribe/Languages/Locations.php:383 +msgid "Ohio" +msgstr "" + +#: src/Tribe/Languages/Locations.php:384 +msgid "Oklahoma" +msgstr "" + +#: src/Tribe/Languages/Locations.php:385 +msgid "Oregon" +msgstr "" + +#: src/Tribe/Languages/Locations.php:386 +msgid "Pennsylvania" +msgstr "" + +#: src/Tribe/Languages/Locations.php:387 +msgid "Rhode Island" +msgstr "" + +#: src/Tribe/Languages/Locations.php:388 +msgid "South Carolina" +msgstr "" + +#: src/Tribe/Languages/Locations.php:389 +msgid "South Dakota" +msgstr "" + +#: src/Tribe/Languages/Locations.php:390 +msgid "Tennessee" +msgstr "" + +#: src/Tribe/Languages/Locations.php:391 +msgid "Texas" +msgstr "" + +#: src/Tribe/Languages/Locations.php:392 +msgid "Utah" +msgstr "" + +#: src/Tribe/Languages/Locations.php:393 +msgid "Vermont" +msgstr "" + +#: src/Tribe/Languages/Locations.php:394 +msgid "Virginia" +msgstr "" + +#: src/Tribe/Languages/Locations.php:395 +msgid "Washington" +msgstr "" + +#: src/Tribe/Languages/Locations.php:396 +msgid "West Virginia" +msgstr "" + +#: src/Tribe/Languages/Locations.php:397 +msgid "Wisconsin" +msgstr "" + +#: src/Tribe/Languages/Locations.php:398 +msgid "Wyoming" +msgstr "" + +#: src/Tribe/Log.php:288 +msgid "Cannot set %s as the current logging engine" +msgstr "" + +#: src/Tribe/Log.php:387 +msgid "Disabled" +msgstr "" + +#: src/Tribe/Log.php:388 +msgid "Only errors" +msgstr "" + +#: src/Tribe/Log.php:389 +msgid "Warnings and errors" +msgstr "" + +#: src/Tribe/Log.php:390 +msgid "Full debug (all events)" +msgstr "" + +#: src/Tribe/Log/Action_Logger.php:39 +msgid "Action-based Logger" +msgstr "" + +#: src/Tribe/Log/Action_Logger.php:84 +msgid "The Action Logger will dispatch any logging message using the \"tribe_log\" action writing, by default, to the PHP error log." +msgstr "" + +#: src/Tribe/Log/Admin.php:146 +msgctxt "log selector" +msgid "None currently available" +msgstr "" + +#: src/Tribe/Log/Admin.php:161 +msgctxt "log engines" +msgid "None currently available" +msgstr "" + +#: src/Tribe/Log/File_Logger.php:128 +msgid "Default (uses temporary files)" +msgstr "" + +#: src/Tribe/Log/Null_Logger.php:26 +msgid "Null logger (will log nothing)" +msgstr "" + +#: src/Tribe/Main.php:403 +msgid ": activate to sort column ascending" +msgstr "" + +#: src/Tribe/Main.php:404 +msgid ": activate to sort column descending" +msgstr "" + +#: src/Tribe/Main.php:406 +msgid "Show _MENU_ entries" +msgstr "" + +#: src/Tribe/Main.php:407 +msgid "No data available in table" +msgstr "" + +#: src/Tribe/Main.php:408 +msgid "Showing _START_ to _END_ of _TOTAL_ entries" +msgstr "" + +#: src/Tribe/Main.php:409 +msgid "Showing 0 to 0 of 0 entries" +msgstr "" + +#: src/Tribe/Main.php:410 +msgid "(filtered from _MAX_ total entries)" +msgstr "" + +#: src/Tribe/Main.php:411 +msgid "No matching records found" +msgstr "" + +#: src/Tribe/Main.php:412 +msgid "Search:" +msgstr "" + +#: src/Tribe/Main.php:413 +msgid "All items on this page were selected. " +msgstr "" + +#: src/Tribe/Main.php:414 +msgid "Select all pages" +msgstr "" + +#: src/Tribe/Main.php:415 +msgid "Clear Selection." +msgstr "" + +#: src/Tribe/Main.php:417 +msgid "All" +msgstr "" + +#: src/Tribe/Main.php:418 +#: src/Tribe/Main.php:435 +msgid "Next" +msgstr "" + +#: src/Tribe/Main.php:419 +msgid "Previous" +msgstr "" + +#: src/Tribe/Main.php:424 +msgid ": Selected %d rows" +msgstr "" + +#: src/Tribe/Main.php:425 +msgid ": Selected 1 row" +msgstr "" + +#: src/Tribe/Main.php:436 +msgid "Prev" +msgstr "" + +#: src/Tribe/Main.php:437 +#: src/Tribe/Main.php:439 +msgid "Today" +msgstr "" + +#: src/Tribe/Main.php:438 +msgid "Done" +msgstr "" + +#: src/Tribe/Main.php:440 +msgid "Clear" +msgstr "" + +#: src/Tribe/Plugins.php:147 +msgid "Using this function before \"plugins_loaded\" action has fired can return unreliable results." +msgstr "" + +#: src/Tribe/Plugins_API.php:30 +msgid "Our flagship free calendar" +msgstr "" + +#: src/Tribe/Plugins_API.php:31 +#: src/Tribe/Plugins_API.php:91 +msgid "The #1 calendar for WordPress" +msgstr "" + +#: src/Tribe/Plugins_API.php:33 +#: src/Tribe/Plugins_API.php:54 +msgid "Customizable" +msgstr "" + +#: src/Tribe/Plugins_API.php:34 +msgid "Import & export events" +msgstr "" + +#: src/Tribe/Plugins_API.php:35 +msgid "Timezone support" +msgstr "" + +#: src/Tribe/Plugins_API.php:36 +msgid "Multiple views" +msgstr "" + +#: src/Tribe/Plugins_API.php:50 +msgid "Automated imports for your calendar" +msgstr "" + +#: src/Tribe/Plugins_API.php:51 +msgid "Import events from Meetup, Eventbrite, iCal, Google Calendar, and more." +msgstr "" + +#: src/Tribe/Plugins_API.php:53 +msgid "Schedule automated imports" +msgstr "" + +#: src/Tribe/Plugins_API.php:55 +msgid "Works with Google Calendar, Meetup, and more" +msgstr "" + +#: src/Tribe/Plugins_API.php:56 +msgid "Refine by date, location, or keyword" +msgstr "" + +#: src/Tribe/Plugins_API.php:65 +msgid "Event Automator" +msgstr "" + +#: src/Tribe/Plugins_API.php:70 +msgid "Automate your event workflow with Zapier—no code required!" +msgstr "" + +#: src/Tribe/Plugins_API.php:71 +msgid "Connect The Events Calendar and Event Tickets with thousands of the most popular apps." +msgstr "" + +#: src/Tribe/Plugins_API.php:73 +msgid "Lead Management" +msgstr "" + +#: src/Tribe/Plugins_API.php:74 +msgid "Communication Outreach" +msgstr "" + +#: src/Tribe/Plugins_API.php:75 +msgid "Internal Processes" +msgstr "" + +#: src/Tribe/Plugins_API.php:76 +msgid "Data Management" +msgstr "" + +#: src/Tribe/Plugins_API.php:85 +msgid "Events Calendar Pro" +msgstr "" + +#: src/Tribe/Plugins_API.php:90 +msgid "Power up your calendar with Pro" +msgstr "" + +#: src/Tribe/Plugins_API.php:93 +msgid "Premium support" +msgstr "" + +#: src/Tribe/Plugins_API.php:94 +msgid "Recurring events & series" +msgstr "" + +#: src/Tribe/Plugins_API.php:95 +msgid "Additional views" +msgstr "" + +#: src/Tribe/Plugins_API.php:97 +msgid "Duplicate events" +msgstr "" + +#: src/Tribe/Plugins_API.php:111 +msgid "Manage ticketing and RSVPs" +msgstr "" + +#: src/Tribe/Plugins_API.php:112 +#: src/Tribe/Plugins_API.php:132 +msgid "Collect RSVPs and sell tickets" +msgstr "" + +#: src/Tribe/Plugins_API.php:114 +msgid "Add tickets and RSVP to any post" +msgstr "" + +#: src/Tribe/Plugins_API.php:115 +msgid "Paypal integration" +msgstr "" + +#: src/Tribe/Plugins_API.php:116 +msgid "Attendee reports" +msgstr "" + +#: src/Tribe/Plugins_API.php:117 +msgid "Customizable ticket template" +msgstr "" + +#: src/Tribe/Plugins_API.php:131 +msgid "Monetize your events" +msgstr "" + +#: src/Tribe/Plugins_API.php:134 +msgid "Custom registration fields" +msgstr "" + +#: src/Tribe/Plugins_API.php:135 +msgid "WooCommerce compatibility" +msgstr "" + +#: src/Tribe/Plugins_API.php:136 +msgid "Ticket scanning with mobile app" +msgstr "" + +#: src/Tribe/Plugins_API.php:137 +msgid "Custom attendee registration fields" +msgstr "" + +#: src/Tribe/Plugins_API.php:151 +msgid "An email marketing solution for events and the people running them" +msgstr "" + +#: src/Tribe/Plugins_API.php:152 +msgid "Email marketing to promote your events" +msgstr "" + +#: src/Tribe/Plugins_API.php:154 +msgid "Automate email touchpoints" +msgstr "" + +#: src/Tribe/Plugins_API.php:155 +msgid "Customize email templates" +msgstr "" + +#: src/Tribe/Plugins_API.php:156 +msgid "Streamline your email process" +msgstr "" + +#: src/Tribe/Plugins_API.php:157 +msgid "Segment your attendee lists" +msgstr "" + +#: src/Tribe/Plugins_API.php:171 +msgid "Help users find exactly the right event" +msgstr "" + +#: src/Tribe/Plugins_API.php:172 +msgid "Allow users to search for events by category, tag, venue, organizer, day of the week, time of day, and price." +msgstr "" + +#: src/Tribe/Plugins_API.php:174 +msgid "Configurable set of filters" +msgstr "" + +#: src/Tribe/Plugins_API.php:175 +msgid "Horizontal or vertical" +msgstr "" + +#: src/Tribe/Plugins_API.php:176 +msgid "Filter category, price, and more" +msgstr "" + +#: src/Tribe/Plugins_API.php:177 +msgid "Filter distance (for Events Calendar Pro)" +msgstr "" + +#: src/Tribe/Plugins_API.php:191 +msgid "Users submit events to your calendar" +msgstr "" + +#: src/Tribe/Plugins_API.php:192 +msgid "Enable 3rd party event submissions." +msgstr "" + +#: src/Tribe/Plugins_API.php:194 +msgid "Publishing Control" +msgstr "" + +#: src/Tribe/Plugins_API.php:195 +msgid "Event Submission Form" +msgstr "" + +#: src/Tribe/Plugins_API.php:196 +msgid "Registered User Settings" +msgstr "" + +#: src/Tribe/Plugins_API.php:197 +msgid "Email notifications" +msgstr "" + +#: src/Tribe/Plugins_API.php:211 +msgid "Run your own events marketplace" +msgstr "" + +#: src/Tribe/Plugins_API.php:212 +msgid "Let users create and sell tickets for events they submit to your calendar." +msgstr "" + +#: src/Tribe/Plugins_API.php:214 +msgid "Users submit events and sell tickets" +msgstr "" + +#: src/Tribe/Plugins_API.php:215 +msgid "Split commission with users" +msgstr "" + +#: src/Tribe/Plugins_API.php:216 +msgid "No admin access required" +msgstr "" + +#: src/Tribe/Plugins_API.php:217 +msgid "Sales reporting" +msgstr "" + +#: src/Tribe/Plugins_API.php:219 +msgctxt "Names of required plugins for Community Tickets" +msgid "Event Tickets Plus and Community Events" +msgstr "" + +#: src/Tribe/Plugins_API.php:232 +msgid "Unite the power of TEC with the ticketing of Eventbrite" +msgstr "" + +#: src/Tribe/Plugins_API.php:233 +msgid "Create Eventbrite tickets and events right from your WordPress dashboard." +msgstr "" + +#: src/Tribe/Plugins_API.php:235 +msgid "Manage tickets from WordPress" +msgstr "" + +#: src/Tribe/Plugins_API.php:236 +msgid "Ticket availability automatically updates" +msgstr "" + +#: src/Tribe/Plugins_API.php:237 +msgid "Integrated with your events on Eventbrite" +msgstr "" + +#: src/Tribe/Plugins_API.php:238 +msgid "Automatically import your events" +msgstr "" + +#: src/Tribe/Plugins_API.php:247 +msgid "Image Widget Plus" +msgstr "" + +#: src/Tribe/Plugins_API.php:252 +msgid "Beautiful display options for your favorite photos." +msgstr "" + +#: src/Tribe/Plugins_API.php:254 +msgid "Multi-Image Support" +msgstr "" + +#: src/Tribe/Plugins_API.php:255 +msgid "Lightbox" +msgstr "" + +#: src/Tribe/Plugins_API.php:256 +msgid "Slideshow" +msgstr "" + +#: src/Tribe/Plugins_API.php:257 +msgid "Random Images" +msgstr "" + +#: src/Tribe/Plugins_API.php:271 +msgid "Features to optimize your calendar for virtual events." +msgstr "" + +#: src/Tribe/Plugins_API.php:272 +msgid "Highlight virtual events on you calendar and integrate with your favorite online meeting tools." +msgstr "" + +#: src/Tribe/Plugins_API.php:275 +msgid "Virtual event labels" +msgstr "" + +#: src/Tribe/Plugins_API.php:276 +msgid "Status control for canceled or postponed events" +msgstr "" + +#: src/Tribe/Plugins_API.php:277 +msgid "Embed livestreams and videos" +msgstr "" + +#: src/Tribe/Process/Queue.php:1024 +msgid "Every %d Minutes" +msgstr "" + +#: src/Tribe/Promoter/Auth.php:41 +msgid "Promoter Key" +msgstr "" + +#: src/Tribe/PUE/Checker.php:605 +msgid "A valid license key is required for support and updates" +msgstr "" + +#: src/Tribe/PUE/Checker.php:608 +msgid "%1$sBuy a license%2$s for the Event Aggregator service to access additional import features." +msgstr "" + +#: src/Tribe/PUE/Checker.php:640 +#: src/Tribe/PUE/Checker.php:674 +msgid "License Key Status:" +msgstr "" + +#: src/Tribe/PUE/Checker.php:649 +msgid "Override network license key" +msgstr "" + +#: src/Tribe/PUE/Checker.php:650 +msgid "Check this box if you wish to override the network license key with your own" +msgstr "" + +#: src/Tribe/PUE/Checker.php:661 +msgid "Site License Key" +msgstr "" + +#: src/Tribe/PUE/Checker.php:761 +msgid "License key(s) updated." +msgstr "" + +#: src/Tribe/PUE/Checker.php:1015 +msgid "Hmmm... something's wrong with this validator. Please contact %ssupport%s." +msgstr "" + +#: src/Tribe/PUE/Checker.php:1028 +msgid "unknown date" +msgstr "" + +#: src/Tribe/PUE/Checker.php:1034 +msgid "Sorry, key validation server is not available." +msgstr "" + +#: src/Tribe/PUE/Checker.php:1060 +msgid "Valid Key! Expires on %s" +msgstr "" + +#: src/Tribe/PUE/Checker.php:1065 +msgid "Thanks for setting up a valid key. It will expire on %s" +msgstr "" + +#: src/Tribe/PUE/Checker.php:1094 +#: src/Tribe/PUE/Notices.php:360 +msgid "Renew Your License Now" +msgstr "" + +#: src/Tribe/PUE/Checker.php:1096 +#: src/Tribe/PUE/Notices.php:362 +msgid " (opens in a new window)" +msgstr "" + +#: src/Tribe/PUE/Checker.php:1113 +msgid "Please refresh the page and try your request again." +msgstr "" + +#: src/Tribe/PUE/Checker.php:1133 +msgid "There is an update for %s. You'll need to %scheck your license%s to have access to updates, downloads, and support." +msgstr "" + +#: src/Tribe/PUE/Checker.php:1190 +msgid "There is an update for %s. %sRenew your license%s to get access to bug fixes, security updates, and new features." +msgstr "" + +#: src/Tribe/PUE/Checker.php:1220 +msgid "Update now to version %s." +msgstr "" + +#: src/Tribe/PUE/Checker.php:1231 +msgid "There is a new version of %1$s available. %2$s" +msgstr "" + +#: src/Tribe/PUE/Checker.php:1812 +msgid "A valid license has been entered by your network administrator." +msgstr "" + +#: src/Tribe/PUE/Checker.php:1813 +msgid "No license entered. Consult your network administrator." +msgstr "" + +#: src/Tribe/PUE/Checker.php:1814 +msgid "Expired license. Consult your network administrator." +msgstr "" + +#: src/Tribe/PUE/Notices.php:293 +msgid "It looks like you're using %1$s, but the license key is invalid. Please download the latest version %2$sfrom your account%3$s." +msgid_plural "It looks like you're using %1$s, but the license keys are invalid. Please download the latest versions %2$sfrom your account%3$s." +msgstr[0] "" +msgstr[1] "" + +#: src/Tribe/PUE/Notices.php:347 +msgid "There is an update available for %1$s but your license has expired. %2$sVisit the Events Calendar website to renew your license.%3$s" +msgid_plural "Updates are available for %1$s but your license keys have expired. %2$sVisit the Events Calendar website to renew your licenses.%3$s" +msgstr[0] "" +msgstr[1] "" + +#: src/Tribe/PUE/Notices.php:388 +msgid "You have a license key for %1$s but the key is out of installs. %2$sVisit the Events Calendar website%3$s to manage your installs, upgrade your license, or purchase a new one." +msgid_plural "You have license keys for %1$s but your keys are out of installs. %2$sVisit the Events Calendar website%3$s to manage your installs, upgrade your licenses, or purchase new ones." +msgstr[0] "" +msgstr[1] "" + +#: src/Tribe/PUE/Notices.php:430 +msgid "You can always check the status of your licenses by logging in to %1$syour account on theeventscalendar.com%2$s." +msgstr "" + +#: src/Tribe/PUE/Notices.php:485 +#: src/Tribe/PUE/Notices.php:528 +msgctxt "formatted plugin list" +msgid "%1$s and %2$s" +msgstr "" + +#: src/Tribe/PUE/Update_Prevention.php:184 +msgid "Your update failed due to an incompatibility between the version (%1$s) of the %2$s you tried to update to and the version of %3$s that you are using. %4$s" +msgstr "" + +#: src/Tribe/Repository.php:2543 +msgid "Could not delete post with ID " +msgstr "" + +#: src/Tribe/Service_Providers/Onboarding.php:111 +msgid "Got it" +msgstr "" + +#: src/Tribe/Settings.php:200 +msgid "Events" +msgstr "" + +#. Translators: %s is the name of the menu item. +#: src/Tribe/Settings.php:331 +msgid "%s Settings" +msgstr "" + +#: src/Tribe/Settings.php:375 +msgid "You've requested a non-existent tab." +msgstr "" + +#: src/Tribe/Settings.php:383 +msgid "Save Changes" +msgstr "" + +#: src/Tribe/Settings.php:453 +msgid "You don't have permission to do that." +msgstr "" + +#: src/Tribe/Settings.php:459 +msgid "The request was sent insecurely." +msgstr "" + +#: src/Tribe/Settings.php:465 +msgid "The request wasn't sent from this tab." +msgstr "" + +#: src/Tribe/Settings.php:646 +msgid "Your form had the following errors:" +msgstr "" + +#: src/Tribe/Settings.php:657 +msgid "The above setting was not saved. Other settings were successfully saved." +msgid_plural "The above settings were not saved. Other settings were successfully saved." +msgstr[0] "" +msgstr[1] "" + +#: src/Tribe/Settings.php:678 +msgid "Settings saved." +msgstr "" + +#: src/Tribe/Settings_Tab.php:225 +msgid "There are no fields set up for this tab yet." +msgstr "" + +#: src/Tribe/Support.php:194 +msgid "English" +msgstr "" + +#: src/Tribe/Support.php:213 +#: src/Tribe/Support.php:214 +msgid "Unknown or not set" +msgstr "" + +#: src/Tribe/Support.php:224 +msgid "Rewrite rules were purged on load of this help page. Chances are there is a rewrite rule flush occurring in a plugin or theme!" +msgstr "" + +#: src/Tribe/Support.php:336 +msgid "Yes, automatically share my system information with The Events Calendar's support team" +msgstr "" + +#: src/Tribe/Support.php:337 +msgid "Your system information will only be used by The Events Calendar's support team. All information is stored securely. We do not share this information with any third parties." +msgstr "" + +#: src/Tribe/Support.php:356 +#: src/Tribe/Support.php:361 +msgid "Invalid Key" +msgstr "" + +#: src/Tribe/Support.php:394 +#: src/Tribe/Support.php:420 +msgid "Permission Error" +msgstr "" + +#: src/Tribe/Support.php:408 +msgid "Unique System Info Key Generated" +msgstr "" + +#: src/Tribe/Support/Template_Checker_Report.php:78 +msgid "No notable changes detected" +msgstr "" + +#: src/Tribe/Support/Template_Checker_Report.php:82 +msgid "Templates introduced or updated with this release (%s):" +msgstr "" + +#: src/Tribe/Support/Template_Checker_Report.php:92 +msgid "Existing theme overrides that may need revision:" +msgstr "" + +#: src/Tribe/Support/Template_Checker_Report.php:96 +msgid "version data missing from override" +msgstr "" + +#: src/Tribe/Support/Template_Checker_Report.php:97 +msgid "based on %s version" +msgstr "" + +#: src/Tribe/Support/Template_Checker_Report.php:113 +msgid "No notable template changes detected." +msgstr "" + +#: src/Tribe/Support/Template_Checker_Report.php:115 +msgid "Information about recent template changes and potentially impacted template overrides is provided below." +msgstr "" + +#: src/Tribe/Validate.php:72 +#: src/Tribe/Validate.php:84 +msgid "Invalid or incomplete field passed" +msgstr "" + +#: src/Tribe/Validate.php:73 +#: src/Tribe/Validate.php:85 +#: src/Tribe/Validate.php:105 +msgid "Field ID:" +msgstr "" + +#: src/Tribe/Validate.php:104 +msgid "Non-existant field validation function passed" +msgstr "" + +#: src/Tribe/Validate.php:105 +msgctxt "non-existant function name passed for field validation" +msgid "with function name:" +msgstr "" + +#: src/Tribe/Validate.php:118 +#: src/Tribe/Validate.php:132 +msgid "%s must contain numbers and letters only" +msgstr "" + +#: src/Tribe/Validate.php:146 +msgid "%s must contain numbers, letters and dots only" +msgstr "" + +#: src/Tribe/Validate.php:160 +msgid "%s must contain numbers, letters, dashes and undescores only" +msgstr "" + +#: src/Tribe/Validate.php:174 +msgid "%s must not be empty" +msgstr "" + +#: src/Tribe/Validate.php:188 +#: src/Tribe/Validate.php:212 +msgid "%s must be a positive number." +msgstr "" + +#: src/Tribe/Validate.php:200 +msgid "%s must be a positive number or percent." +msgstr "" + +#: src/Tribe/Validate.php:230 +msgid "%s must be a whole number." +msgstr "" + +#: src/Tribe/Validate.php:250 +msgid "%s must be a valid slug (numbers, letters, dashes, and underscores)." +msgstr "" + +#: src/Tribe/Validate.php:263 +msgid "%s must be a valid URL." +msgstr "" + +#: src/Tribe/Validate.php:277 +#: src/Tribe/Validate.php:289 +#: src/Tribe/Validate.php:302 +#: src/Tribe/Validate.php:322 +msgid "%s must have a value that's part of its options." +msgstr "" + +#: src/Tribe/Validate.php:334 +msgid "Comparison validation failed because no comparison value was provided, for field %s" +msgstr "" + +#: src/Tribe/Validate.php:341 +msgid "%s cannot be the same as %s." +msgstr "" + +#: src/Tribe/Validate.php:343 +msgid "%s cannot be a duplicate" +msgstr "" + +#: src/Tribe/Validate.php:357 +msgid "%s must be a number or percentage." +msgstr "" + +#: src/Tribe/Validate.php:401 +msgid "%s must be a number between 0 and 21." +msgstr "" + +#: src/Tribe/Validate.php:415 +msgid "%s must consist of letters, numbers, dashes, apostrophes, and spaces only." +msgstr "" + +#: src/Tribe/Validate.php:429 +msgid "%s must consist of letters, spaces, apostrophes, and dashes." +msgstr "" + +#: src/Tribe/Validate.php:441 +msgid "%s must consist of 5 numbers." +msgstr "" + +#: src/Tribe/Validate.php:453 +msgid "%s must be a phone number." +msgstr "" + +#: src/Tribe/Validate.php:467 +msgid "Country List must be formatted as one country per line in the following format:
US, United States
UK, United Kingdom." +msgstr "" + +#: src/Tribe/Validate.php:496 +msgid "%s must be an email address." +msgstr "" + +#. Translators: %s - Label of the form input field. +#: src/Tribe/Validate.php:534 +msgid "%s must be a list of valid email addresses separated by commas or semicolons." +msgstr "" + +#: src/Tribe/Validate.php:552 +msgid "%s must be a valid HTML color code." +msgstr "" + +#: src/Tribe/View_Helpers.php:59 +msgid "Select a Country:" +msgstr "" + +#: src/Tribe/Widget/Widget_Abstract.php:183 +msgid "Widget" +msgstr "" + +#: src/views/promoter/auth.php:35 +msgid "Promoter would like to sync with your site" +msgstr "" + +#: src/views/promoter/auth.php:42 +msgid "Please log in to continue" +msgstr "" + +#: src/views/promoter/auth.php:47 +msgid "You do not have access to authenticate this site." +msgstr "" + +#: src/views/promoter/auth.php:49 +msgid "Please log out and log back in as an admin account" +msgstr "" + +#: src/views/promoter/auth.php:55 +msgid "Sorry, unable to authenticate your site. Please contact Promoter support." +msgstr "" + +#: src/views/promoter/auth.php:57 +msgid "Please authorize to continue onboarding." +msgstr "" + +#: src/views/promoter/auth.php:65 +msgid "Authorize Promoter" +msgstr "" + +#: src/views/promoter/auth.php:78 +msgid "Promoter All rights reserved." +msgstr "" + +#: src/views/promoter/auth.php:79 +msgid "Privacy" +msgstr "" + +#: src/views/promoter/auth.php:80 +msgid "and" +msgstr "" + +#: src/views/promoter/auth.php:81 +msgid "Terms" +msgstr "" + +#: src/views/v2/components/icons/featured.php:24 +msgid "Featured" +msgstr "" + +#: src/views/v2/components/icons/hybrid.php:24 +msgid "Hybrid" +msgstr "" + +#: src/views/v2/components/icons/recurring.php:25 +msgid "Recurring" +msgstr "" + +#: src/views/v2/components/icons/virtual.php:24 +msgid "Virtual" +msgstr "" + +#: src/modules/elements/image-upload/element.js:48 +msgid "remove" +msgstr "" + +#: src/modules/elements/time-picker/element.js:56 +#: src/modules/elements/time-picker/element.js:149 +msgid "All Day" +msgstr "" + +#: src/modules/utils/date.js:103 +msgid "at" +msgstr "" diff --git a/wp-content/plugins/the-events-calendar/common/node_modules/clipboard/dist/clipboard.min.js b/wp-content/plugins/the-events-calendar/common/node_modules/clipboard/dist/clipboard.min.js new file mode 100644 index 000000000..1103f811e --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/node_modules/clipboard/dist/clipboard.min.js @@ -0,0 +1,7 @@ +/*! + * clipboard.js v2.0.11 + * https://clipboardjs.com/ + * + * Licensed MIT © Zeno Rocha + */ +!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.ClipboardJS=e():t.ClipboardJS=e()}(this,function(){return n={686:function(t,e,n){"use strict";n.d(e,{default:function(){return b}});var e=n(279),i=n.n(e),e=n(370),u=n.n(e),e=n(817),r=n.n(e);function c(t){try{return document.execCommand(t)}catch(t){return}}var a=function(t){t=r()(t);return c("cut"),t};function o(t,e){var n,o,t=(n=t,o="rtl"===document.documentElement.getAttribute("dir"),(t=document.createElement("textarea")).style.fontSize="12pt",t.style.border="0",t.style.padding="0",t.style.margin="0",t.style.position="absolute",t.style[o?"right":"left"]="-9999px",o=window.pageYOffset||document.documentElement.scrollTop,t.style.top="".concat(o,"px"),t.setAttribute("readonly",""),t.value=n,t);return e.container.appendChild(t),e=r()(t),c("copy"),t.remove(),e}var f=function(t){var e=1 1 && arguments[1] !== undefined ? arguments[1] : "introjs-stamp"; + // each group increments from 0 + keys[key] = keys[key] || 0; // stamp only once per object + + if (obj[key] === undefined) { + // increment key for each new object + obj[key] = keys[key]++; + } + + return obj[key]; + }; + }(); + + /** + * Iterates arrays + * + * @param {Array} arr + * @param {Function} forEachFnc + * @param {Function} completeFnc + * @return {Null} + */ + function forEach(arr, forEachFnc, completeFnc) { + // in case arr is an empty query selector node list + if (arr) { + for (var i = 0, len = arr.length; i < len; i++) { + forEachFnc(arr[i], i); + } + } + + if (typeof completeFnc === "function") { + completeFnc(); + } + } + + /** + * DOMEvent Handles all DOM events + * + * methods: + * + * on - add event handler + * off - remove event + */ + + var DOMEvent = function () { + function DOMEvent() { + var events_key = "introjs_event"; + /** + * Gets a unique ID for an event listener + * + * @param obj Object + * @param type event type + * @param listener Function + * @param context Object + * @return String + */ + + this._id = function (obj, type, listener, context) { + return type + stamp(listener) + (context ? "_".concat(stamp(context)) : ""); + }; + /** + * Adds event listener + * + * @param obj Object obj + * @param type String + * @param listener Function + * @param context Object + * @param useCapture Boolean + * @return null + */ + + + this.on = function (obj, type, listener, context, useCapture) { + var id = this._id.apply(this, arguments); + + var handler = function handler(e) { + return listener.call(context || obj, e || window.event); + }; + + if ("addEventListener" in obj) { + obj.addEventListener(type, handler, useCapture); + } else if ("attachEvent" in obj) { + obj.attachEvent("on".concat(type), handler); + } + + obj[events_key] = obj[events_key] || {}; + obj[events_key][id] = handler; + }; + /** + * Removes event listener + * + * @param obj Object + * @param type String + * @param listener Function + * @param context Object + * @param useCapture Boolean + * @return null + */ + + + this.off = function (obj, type, listener, context, useCapture) { + var id = this._id.apply(this, arguments); + + var handler = obj[events_key] && obj[events_key][id]; + + if (!handler) { + return; + } + + if ("removeEventListener" in obj) { + obj.removeEventListener(type, handler, useCapture); + } else if ("detachEvent" in obj) { + obj.detachEvent("on".concat(type), handler); + } + + obj[events_key][id] = null; + }; + } + + return new DOMEvent(); + }(); + + var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; + + function createCommonjsModule(fn, module) { + return module = { exports: {} }, fn(module, module.exports), module.exports; + } + + var check = function (it) { + return it && it.Math == Math && it; + }; + + // https://github.com/zloirock/core-js/issues/86#issuecomment-115759028 + var global_1 = + /* global globalThis -- safe */ + check(typeof globalThis == 'object' && globalThis) || + check(typeof window == 'object' && window) || + check(typeof self == 'object' && self) || + check(typeof commonjsGlobal == 'object' && commonjsGlobal) || + // eslint-disable-next-line no-new-func -- fallback + (function () { return this; })() || Function('return this')(); + + var fails = function (exec) { + try { + return !!exec(); + } catch (error) { + return true; + } + }; + + // Detect IE8's incomplete defineProperty implementation + var descriptors = !fails(function () { + return Object.defineProperty({}, 1, { get: function () { return 7; } })[1] != 7; + }); + + var nativePropertyIsEnumerable = {}.propertyIsEnumerable; + var getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor; + + // Nashorn ~ JDK8 bug + var NASHORN_BUG = getOwnPropertyDescriptor && !nativePropertyIsEnumerable.call({ 1: 2 }, 1); + + // `Object.prototype.propertyIsEnumerable` method implementation + // https://tc39.es/ecma262/#sec-object.prototype.propertyisenumerable + var f = NASHORN_BUG ? function propertyIsEnumerable(V) { + var descriptor = getOwnPropertyDescriptor(this, V); + return !!descriptor && descriptor.enumerable; + } : nativePropertyIsEnumerable; + + var objectPropertyIsEnumerable = { + f: f + }; + + var createPropertyDescriptor = function (bitmap, value) { + return { + enumerable: !(bitmap & 1), + configurable: !(bitmap & 2), + writable: !(bitmap & 4), + value: value + }; + }; + + var toString = {}.toString; + + var classofRaw = function (it) { + return toString.call(it).slice(8, -1); + }; + + var split = ''.split; + + // fallback for non-array-like ES3 and non-enumerable old V8 strings + var indexedObject = fails(function () { + // throws an error in rhino, see https://github.com/mozilla/rhino/issues/346 + // eslint-disable-next-line no-prototype-builtins -- safe + return !Object('z').propertyIsEnumerable(0); + }) ? function (it) { + return classofRaw(it) == 'String' ? split.call(it, '') : Object(it); + } : Object; + + // `RequireObjectCoercible` abstract operation + // https://tc39.es/ecma262/#sec-requireobjectcoercible + var requireObjectCoercible = function (it) { + if (it == undefined) throw TypeError("Can't call method on " + it); + return it; + }; + + // toObject with fallback for non-array-like ES3 strings + + + + var toIndexedObject = function (it) { + return indexedObject(requireObjectCoercible(it)); + }; + + var isObject = function (it) { + return typeof it === 'object' ? it !== null : typeof it === 'function'; + }; + + // `ToPrimitive` abstract operation + // https://tc39.es/ecma262/#sec-toprimitive + // instead of the ES6 spec version, we didn't implement @@toPrimitive case + // and the second argument - flag - preferred type is a string + var toPrimitive = function (input, PREFERRED_STRING) { + if (!isObject(input)) return input; + var fn, val; + if (PREFERRED_STRING && typeof (fn = input.toString) == 'function' && !isObject(val = fn.call(input))) return val; + if (typeof (fn = input.valueOf) == 'function' && !isObject(val = fn.call(input))) return val; + if (!PREFERRED_STRING && typeof (fn = input.toString) == 'function' && !isObject(val = fn.call(input))) return val; + throw TypeError("Can't convert object to primitive value"); + }; + + var hasOwnProperty = {}.hasOwnProperty; + + var has = function (it, key) { + return hasOwnProperty.call(it, key); + }; + + var document$1 = global_1.document; + // typeof document.createElement is 'object' in old IE + var EXISTS = isObject(document$1) && isObject(document$1.createElement); + + var documentCreateElement = function (it) { + return EXISTS ? document$1.createElement(it) : {}; + }; + + // Thank's IE8 for his funny defineProperty + var ie8DomDefine = !descriptors && !fails(function () { + return Object.defineProperty(documentCreateElement('div'), 'a', { + get: function () { return 7; } + }).a != 7; + }); + + var nativeGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor; + + // `Object.getOwnPropertyDescriptor` method + // https://tc39.es/ecma262/#sec-object.getownpropertydescriptor + var f$1 = descriptors ? nativeGetOwnPropertyDescriptor : function getOwnPropertyDescriptor(O, P) { + O = toIndexedObject(O); + P = toPrimitive(P, true); + if (ie8DomDefine) try { + return nativeGetOwnPropertyDescriptor(O, P); + } catch (error) { /* empty */ } + if (has(O, P)) return createPropertyDescriptor(!objectPropertyIsEnumerable.f.call(O, P), O[P]); + }; + + var objectGetOwnPropertyDescriptor = { + f: f$1 + }; + + var anObject = function (it) { + if (!isObject(it)) { + throw TypeError(String(it) + ' is not an object'); + } return it; + }; + + var nativeDefineProperty = Object.defineProperty; + + // `Object.defineProperty` method + // https://tc39.es/ecma262/#sec-object.defineproperty + var f$2 = descriptors ? nativeDefineProperty : function defineProperty(O, P, Attributes) { + anObject(O); + P = toPrimitive(P, true); + anObject(Attributes); + if (ie8DomDefine) try { + return nativeDefineProperty(O, P, Attributes); + } catch (error) { /* empty */ } + if ('get' in Attributes || 'set' in Attributes) throw TypeError('Accessors not supported'); + if ('value' in Attributes) O[P] = Attributes.value; + return O; + }; + + var objectDefineProperty = { + f: f$2 + }; + + var createNonEnumerableProperty = descriptors ? function (object, key, value) { + return objectDefineProperty.f(object, key, createPropertyDescriptor(1, value)); + } : function (object, key, value) { + object[key] = value; + return object; + }; + + var setGlobal = function (key, value) { + try { + createNonEnumerableProperty(global_1, key, value); + } catch (error) { + global_1[key] = value; + } return value; + }; + + var SHARED = '__core-js_shared__'; + var store = global_1[SHARED] || setGlobal(SHARED, {}); + + var sharedStore = store; + + var functionToString = Function.toString; + + // this helper broken in `3.4.1-3.4.4`, so we can't use `shared` helper + if (typeof sharedStore.inspectSource != 'function') { + sharedStore.inspectSource = function (it) { + return functionToString.call(it); + }; + } + + var inspectSource = sharedStore.inspectSource; + + var WeakMap = global_1.WeakMap; + + var nativeWeakMap = typeof WeakMap === 'function' && /native code/.test(inspectSource(WeakMap)); + + var shared = createCommonjsModule(function (module) { + (module.exports = function (key, value) { + return sharedStore[key] || (sharedStore[key] = value !== undefined ? value : {}); + })('versions', []).push({ + version: '3.9.1', + mode: 'global', + copyright: '© 2021 Denis Pushkarev (zloirock.ru)' + }); + }); + + var id = 0; + var postfix = Math.random(); + + var uid = function (key) { + return 'Symbol(' + String(key === undefined ? '' : key) + ')_' + (++id + postfix).toString(36); + }; + + var keys = shared('keys'); + + var sharedKey = function (key) { + return keys[key] || (keys[key] = uid(key)); + }; + + var hiddenKeys = {}; + + var WeakMap$1 = global_1.WeakMap; + var set, get, has$1; + + var enforce = function (it) { + return has$1(it) ? get(it) : set(it, {}); + }; + + var getterFor = function (TYPE) { + return function (it) { + var state; + if (!isObject(it) || (state = get(it)).type !== TYPE) { + throw TypeError('Incompatible receiver, ' + TYPE + ' required'); + } return state; + }; + }; + + if (nativeWeakMap) { + var store$1 = sharedStore.state || (sharedStore.state = new WeakMap$1()); + var wmget = store$1.get; + var wmhas = store$1.has; + var wmset = store$1.set; + set = function (it, metadata) { + metadata.facade = it; + wmset.call(store$1, it, metadata); + return metadata; + }; + get = function (it) { + return wmget.call(store$1, it) || {}; + }; + has$1 = function (it) { + return wmhas.call(store$1, it); + }; + } else { + var STATE = sharedKey('state'); + hiddenKeys[STATE] = true; + set = function (it, metadata) { + metadata.facade = it; + createNonEnumerableProperty(it, STATE, metadata); + return metadata; + }; + get = function (it) { + return has(it, STATE) ? it[STATE] : {}; + }; + has$1 = function (it) { + return has(it, STATE); + }; + } + + var internalState = { + set: set, + get: get, + has: has$1, + enforce: enforce, + getterFor: getterFor + }; + + var redefine = createCommonjsModule(function (module) { + var getInternalState = internalState.get; + var enforceInternalState = internalState.enforce; + var TEMPLATE = String(String).split('String'); + + (module.exports = function (O, key, value, options) { + var unsafe = options ? !!options.unsafe : false; + var simple = options ? !!options.enumerable : false; + var noTargetGet = options ? !!options.noTargetGet : false; + var state; + if (typeof value == 'function') { + if (typeof key == 'string' && !has(value, 'name')) { + createNonEnumerableProperty(value, 'name', key); + } + state = enforceInternalState(value); + if (!state.source) { + state.source = TEMPLATE.join(typeof key == 'string' ? key : ''); + } + } + if (O === global_1) { + if (simple) O[key] = value; + else setGlobal(key, value); + return; + } else if (!unsafe) { + delete O[key]; + } else if (!noTargetGet && O[key]) { + simple = true; + } + if (simple) O[key] = value; + else createNonEnumerableProperty(O, key, value); + // add fake Function#toString for correct work wrapped methods / constructors with methods like LoDash isNative + })(Function.prototype, 'toString', function toString() { + return typeof this == 'function' && getInternalState(this).source || inspectSource(this); + }); + }); + + var path = global_1; + + var aFunction = function (variable) { + return typeof variable == 'function' ? variable : undefined; + }; + + var getBuiltIn = function (namespace, method) { + return arguments.length < 2 ? aFunction(path[namespace]) || aFunction(global_1[namespace]) + : path[namespace] && path[namespace][method] || global_1[namespace] && global_1[namespace][method]; + }; + + var ceil = Math.ceil; + var floor = Math.floor; + + // `ToInteger` abstract operation + // https://tc39.es/ecma262/#sec-tointeger + var toInteger = function (argument) { + return isNaN(argument = +argument) ? 0 : (argument > 0 ? floor : ceil)(argument); + }; + + var min = Math.min; + + // `ToLength` abstract operation + // https://tc39.es/ecma262/#sec-tolength + var toLength = function (argument) { + return argument > 0 ? min(toInteger(argument), 0x1FFFFFFFFFFFFF) : 0; // 2 ** 53 - 1 == 9007199254740991 + }; + + var max = Math.max; + var min$1 = Math.min; + + // Helper for a popular repeating case of the spec: + // Let integer be ? ToInteger(index). + // If integer < 0, let result be max((length + integer), 0); else let result be min(integer, length). + var toAbsoluteIndex = function (index, length) { + var integer = toInteger(index); + return integer < 0 ? max(integer + length, 0) : min$1(integer, length); + }; + + // `Array.prototype.{ indexOf, includes }` methods implementation + var createMethod = function (IS_INCLUDES) { + return function ($this, el, fromIndex) { + var O = toIndexedObject($this); + var length = toLength(O.length); + var index = toAbsoluteIndex(fromIndex, length); + var value; + // Array#includes uses SameValueZero equality algorithm + // eslint-disable-next-line no-self-compare -- NaN check + if (IS_INCLUDES && el != el) while (length > index) { + value = O[index++]; + // eslint-disable-next-line no-self-compare -- NaN check + if (value != value) return true; + // Array#indexOf ignores holes, Array#includes - not + } else for (;length > index; index++) { + if ((IS_INCLUDES || index in O) && O[index] === el) return IS_INCLUDES || index || 0; + } return !IS_INCLUDES && -1; + }; + }; + + var arrayIncludes = { + // `Array.prototype.includes` method + // https://tc39.es/ecma262/#sec-array.prototype.includes + includes: createMethod(true), + // `Array.prototype.indexOf` method + // https://tc39.es/ecma262/#sec-array.prototype.indexof + indexOf: createMethod(false) + }; + + var indexOf = arrayIncludes.indexOf; + + + var objectKeysInternal = function (object, names) { + var O = toIndexedObject(object); + var i = 0; + var result = []; + var key; + for (key in O) !has(hiddenKeys, key) && has(O, key) && result.push(key); + // Don't enum bug & hidden keys + while (names.length > i) if (has(O, key = names[i++])) { + ~indexOf(result, key) || result.push(key); + } + return result; + }; + + // IE8- don't enum bug keys + var enumBugKeys = [ + 'constructor', + 'hasOwnProperty', + 'isPrototypeOf', + 'propertyIsEnumerable', + 'toLocaleString', + 'toString', + 'valueOf' + ]; + + var hiddenKeys$1 = enumBugKeys.concat('length', 'prototype'); + + // `Object.getOwnPropertyNames` method + // https://tc39.es/ecma262/#sec-object.getownpropertynames + var f$3 = Object.getOwnPropertyNames || function getOwnPropertyNames(O) { + return objectKeysInternal(O, hiddenKeys$1); + }; + + var objectGetOwnPropertyNames = { + f: f$3 + }; + + var f$4 = Object.getOwnPropertySymbols; + + var objectGetOwnPropertySymbols = { + f: f$4 + }; + + // all object keys, includes non-enumerable and symbols + var ownKeys = getBuiltIn('Reflect', 'ownKeys') || function ownKeys(it) { + var keys = objectGetOwnPropertyNames.f(anObject(it)); + var getOwnPropertySymbols = objectGetOwnPropertySymbols.f; + return getOwnPropertySymbols ? keys.concat(getOwnPropertySymbols(it)) : keys; + }; + + var copyConstructorProperties = function (target, source) { + var keys = ownKeys(source); + var defineProperty = objectDefineProperty.f; + var getOwnPropertyDescriptor = objectGetOwnPropertyDescriptor.f; + for (var i = 0; i < keys.length; i++) { + var key = keys[i]; + if (!has(target, key)) defineProperty(target, key, getOwnPropertyDescriptor(source, key)); + } + }; + + var replacement = /#|\.prototype\./; + + var isForced = function (feature, detection) { + var value = data[normalize(feature)]; + return value == POLYFILL ? true + : value == NATIVE ? false + : typeof detection == 'function' ? fails(detection) + : !!detection; + }; + + var normalize = isForced.normalize = function (string) { + return String(string).replace(replacement, '.').toLowerCase(); + }; + + var data = isForced.data = {}; + var NATIVE = isForced.NATIVE = 'N'; + var POLYFILL = isForced.POLYFILL = 'P'; + + var isForced_1 = isForced; + + var getOwnPropertyDescriptor$1 = objectGetOwnPropertyDescriptor.f; + + + + + + + /* + options.target - name of the target object + options.global - target is the global object + options.stat - export as static methods of target + options.proto - export as prototype methods of target + options.real - real prototype method for the `pure` version + options.forced - export even if the native feature is available + options.bind - bind methods to the target, required for the `pure` version + options.wrap - wrap constructors to preventing global pollution, required for the `pure` version + options.unsafe - use the simple assignment of property instead of delete + defineProperty + options.sham - add a flag to not completely full polyfills + options.enumerable - export as enumerable property + options.noTargetGet - prevent calling a getter on target + */ + var _export = function (options, source) { + var TARGET = options.target; + var GLOBAL = options.global; + var STATIC = options.stat; + var FORCED, target, key, targetProperty, sourceProperty, descriptor; + if (GLOBAL) { + target = global_1; + } else if (STATIC) { + target = global_1[TARGET] || setGlobal(TARGET, {}); + } else { + target = (global_1[TARGET] || {}).prototype; + } + if (target) for (key in source) { + sourceProperty = source[key]; + if (options.noTargetGet) { + descriptor = getOwnPropertyDescriptor$1(target, key); + targetProperty = descriptor && descriptor.value; + } else targetProperty = target[key]; + FORCED = isForced_1(GLOBAL ? key : TARGET + (STATIC ? '.' : '#') + key, options.forced); + // contained in target + if (!FORCED && targetProperty !== undefined) { + if (typeof sourceProperty === typeof targetProperty) continue; + copyConstructorProperties(sourceProperty, targetProperty); + } + // add a flag to not completely full polyfills + if (options.sham || (targetProperty && targetProperty.sham)) { + createNonEnumerableProperty(sourceProperty, 'sham', true); + } + // extend global + redefine(target, key, sourceProperty, options); + } + }; + + // `RegExp.prototype.flags` getter implementation + // https://tc39.es/ecma262/#sec-get-regexp.prototype.flags + var regexpFlags = function () { + var that = anObject(this); + var result = ''; + if (that.global) result += 'g'; + if (that.ignoreCase) result += 'i'; + if (that.multiline) result += 'm'; + if (that.dotAll) result += 's'; + if (that.unicode) result += 'u'; + if (that.sticky) result += 'y'; + return result; + }; + + // babel-minify transpiles RegExp('a', 'y') -> /a/y and it causes SyntaxError, + // so we use an intermediate function. + function RE(s, f) { + return RegExp(s, f); + } + + var UNSUPPORTED_Y = fails(function () { + // babel-minify transpiles RegExp('a', 'y') -> /a/y and it causes SyntaxError + var re = RE('a', 'y'); + re.lastIndex = 2; + return re.exec('abcd') != null; + }); + + var BROKEN_CARET = fails(function () { + // https://bugzilla.mozilla.org/show_bug.cgi?id=773687 + var re = RE('^r', 'gy'); + re.lastIndex = 2; + return re.exec('str') != null; + }); + + var regexpStickyHelpers = { + UNSUPPORTED_Y: UNSUPPORTED_Y, + BROKEN_CARET: BROKEN_CARET + }; + + var nativeExec = RegExp.prototype.exec; + // This always refers to the native implementation, because the + // String#replace polyfill uses ./fix-regexp-well-known-symbol-logic.js, + // which loads this file before patching the method. + var nativeReplace = String.prototype.replace; + + var patchedExec = nativeExec; + + var UPDATES_LAST_INDEX_WRONG = (function () { + var re1 = /a/; + var re2 = /b*/g; + nativeExec.call(re1, 'a'); + nativeExec.call(re2, 'a'); + return re1.lastIndex !== 0 || re2.lastIndex !== 0; + })(); + + var UNSUPPORTED_Y$1 = regexpStickyHelpers.UNSUPPORTED_Y || regexpStickyHelpers.BROKEN_CARET; + + // nonparticipating capturing group, copied from es5-shim's String#split patch. + // eslint-disable-next-line regexp/no-assertion-capturing-group, regexp/no-empty-group -- required for testing + var NPCG_INCLUDED = /()??/.exec('')[1] !== undefined; + + var PATCH = UPDATES_LAST_INDEX_WRONG || NPCG_INCLUDED || UNSUPPORTED_Y$1; + + if (PATCH) { + patchedExec = function exec(str) { + var re = this; + var lastIndex, reCopy, match, i; + var sticky = UNSUPPORTED_Y$1 && re.sticky; + var flags = regexpFlags.call(re); + var source = re.source; + var charsAdded = 0; + var strCopy = str; + + if (sticky) { + flags = flags.replace('y', ''); + if (flags.indexOf('g') === -1) { + flags += 'g'; + } + + strCopy = String(str).slice(re.lastIndex); + // Support anchored sticky behavior. + if (re.lastIndex > 0 && (!re.multiline || re.multiline && str[re.lastIndex - 1] !== '\n')) { + source = '(?: ' + source + ')'; + strCopy = ' ' + strCopy; + charsAdded++; + } + // ^(? + rx + ) is needed, in combination with some str slicing, to + // simulate the 'y' flag. + reCopy = new RegExp('^(?:' + source + ')', flags); + } + + if (NPCG_INCLUDED) { + reCopy = new RegExp('^' + source + '$(?!\\s)', flags); + } + if (UPDATES_LAST_INDEX_WRONG) lastIndex = re.lastIndex; + + match = nativeExec.call(sticky ? reCopy : re, strCopy); + + if (sticky) { + if (match) { + match.input = match.input.slice(charsAdded); + match[0] = match[0].slice(charsAdded); + match.index = re.lastIndex; + re.lastIndex += match[0].length; + } else re.lastIndex = 0; + } else if (UPDATES_LAST_INDEX_WRONG && match) { + re.lastIndex = re.global ? match.index + match[0].length : lastIndex; + } + if (NPCG_INCLUDED && match && match.length > 1) { + // Fix browsers whose `exec` methods don't consistently return `undefined` + // for NPCG, like IE8. NOTE: This doesn' work for /(.?)?/ + nativeReplace.call(match[0], reCopy, function () { + for (i = 1; i < arguments.length - 2; i++) { + if (arguments[i] === undefined) match[i] = undefined; + } + }); + } + + return match; + }; + } + + var regexpExec = patchedExec; + + // `RegExp.prototype.exec` method + // https://tc39.es/ecma262/#sec-regexp.prototype.exec + _export({ target: 'RegExp', proto: true, forced: /./.exec !== regexpExec }, { + exec: regexpExec + }); + + var engineIsNode = classofRaw(global_1.process) == 'process'; + + var engineUserAgent = getBuiltIn('navigator', 'userAgent') || ''; + + var process = global_1.process; + var versions = process && process.versions; + var v8 = versions && versions.v8; + var match, version; + + if (v8) { + match = v8.split('.'); + version = match[0] + match[1]; + } else if (engineUserAgent) { + match = engineUserAgent.match(/Edge\/(\d+)/); + if (!match || match[1] >= 74) { + match = engineUserAgent.match(/Chrome\/(\d+)/); + if (match) version = match[1]; + } + } + + var engineV8Version = version && +version; + + var nativeSymbol = !!Object.getOwnPropertySymbols && !fails(function () { + /* global Symbol -- required for testing */ + return !Symbol.sham && + // Chrome 38 Symbol has incorrect toString conversion + // Chrome 38-40 symbols are not inherited from DOM collections prototypes to instances + (engineIsNode ? engineV8Version === 38 : engineV8Version > 37 && engineV8Version < 41); + }); + + var useSymbolAsUid = nativeSymbol + /* global Symbol -- safe */ + && !Symbol.sham + && typeof Symbol.iterator == 'symbol'; + + var WellKnownSymbolsStore = shared('wks'); + var Symbol$1 = global_1.Symbol; + var createWellKnownSymbol = useSymbolAsUid ? Symbol$1 : Symbol$1 && Symbol$1.withoutSetter || uid; + + var wellKnownSymbol = function (name) { + if (!has(WellKnownSymbolsStore, name) || !(nativeSymbol || typeof WellKnownSymbolsStore[name] == 'string')) { + if (nativeSymbol && has(Symbol$1, name)) { + WellKnownSymbolsStore[name] = Symbol$1[name]; + } else { + WellKnownSymbolsStore[name] = createWellKnownSymbol('Symbol.' + name); + } + } return WellKnownSymbolsStore[name]; + }; + + // TODO: Remove from `core-js@4` since it's moved to entry points + + + + + + + + var SPECIES = wellKnownSymbol('species'); + + var REPLACE_SUPPORTS_NAMED_GROUPS = !fails(function () { + // #replace needs built-in support for named groups. + // #match works fine because it just return the exec results, even if it has + // a "grops" property. + var re = /./; + re.exec = function () { + var result = []; + result.groups = { a: '7' }; + return result; + }; + return ''.replace(re, '$') !== '7'; + }); + + // IE <= 11 replaces $0 with the whole match, as if it was $& + // https://stackoverflow.com/questions/6024666/getting-ie-to-replace-a-regex-with-the-literal-string-0 + var REPLACE_KEEPS_$0 = (function () { + return 'a'.replace(/./, '$0') === '$0'; + })(); + + var REPLACE = wellKnownSymbol('replace'); + // Safari <= 13.0.3(?) substitutes nth capture where n>m with an empty string + var REGEXP_REPLACE_SUBSTITUTES_UNDEFINED_CAPTURE = (function () { + if (/./[REPLACE]) { + return /./[REPLACE]('a', '$0') === ''; + } + return false; + })(); + + // Chrome 51 has a buggy "split" implementation when RegExp#exec !== nativeExec + // Weex JS has frozen built-in prototypes, so use try / catch wrapper + var SPLIT_WORKS_WITH_OVERWRITTEN_EXEC = !fails(function () { + // eslint-disable-next-line regexp/no-empty-group -- required for testing + var re = /(?:)/; + var originalExec = re.exec; + re.exec = function () { return originalExec.apply(this, arguments); }; + var result = 'ab'.split(re); + return result.length !== 2 || result[0] !== 'a' || result[1] !== 'b'; + }); + + var fixRegexpWellKnownSymbolLogic = function (KEY, length, exec, sham) { + var SYMBOL = wellKnownSymbol(KEY); + + var DELEGATES_TO_SYMBOL = !fails(function () { + // String methods call symbol-named RegEp methods + var O = {}; + O[SYMBOL] = function () { return 7; }; + return ''[KEY](O) != 7; + }); + + var DELEGATES_TO_EXEC = DELEGATES_TO_SYMBOL && !fails(function () { + // Symbol-named RegExp methods call .exec + var execCalled = false; + var re = /a/; + + if (KEY === 'split') { + // We can't use real regex here since it causes deoptimization + // and serious performance degradation in V8 + // https://github.com/zloirock/core-js/issues/306 + re = {}; + // RegExp[@@split] doesn't call the regex's exec method, but first creates + // a new one. We need to return the patched regex when creating the new one. + re.constructor = {}; + re.constructor[SPECIES] = function () { return re; }; + re.flags = ''; + re[SYMBOL] = /./[SYMBOL]; + } + + re.exec = function () { execCalled = true; return null; }; + + re[SYMBOL](''); + return !execCalled; + }); + + if ( + !DELEGATES_TO_SYMBOL || + !DELEGATES_TO_EXEC || + (KEY === 'replace' && !( + REPLACE_SUPPORTS_NAMED_GROUPS && + REPLACE_KEEPS_$0 && + !REGEXP_REPLACE_SUBSTITUTES_UNDEFINED_CAPTURE + )) || + (KEY === 'split' && !SPLIT_WORKS_WITH_OVERWRITTEN_EXEC) + ) { + var nativeRegExpMethod = /./[SYMBOL]; + var methods = exec(SYMBOL, ''[KEY], function (nativeMethod, regexp, str, arg2, forceStringMethod) { + if (regexp.exec === regexpExec) { + if (DELEGATES_TO_SYMBOL && !forceStringMethod) { + // The native String method already delegates to @@method (this + // polyfilled function), leasing to infinite recursion. + // We avoid it by directly calling the native @@method method. + return { done: true, value: nativeRegExpMethod.call(regexp, str, arg2) }; + } + return { done: true, value: nativeMethod.call(str, regexp, arg2) }; + } + return { done: false }; + }, { + REPLACE_KEEPS_$0: REPLACE_KEEPS_$0, + REGEXP_REPLACE_SUBSTITUTES_UNDEFINED_CAPTURE: REGEXP_REPLACE_SUBSTITUTES_UNDEFINED_CAPTURE + }); + var stringMethod = methods[0]; + var regexMethod = methods[1]; + + redefine(String.prototype, KEY, stringMethod); + redefine(RegExp.prototype, SYMBOL, length == 2 + // 21.2.5.8 RegExp.prototype[@@replace](string, replaceValue) + // 21.2.5.11 RegExp.prototype[@@split](string, limit) + ? function (string, arg) { return regexMethod.call(string, this, arg); } + // 21.2.5.6 RegExp.prototype[@@match](string) + // 21.2.5.9 RegExp.prototype[@@search](string) + : function (string) { return regexMethod.call(string, this); } + ); + } + + if (sham) createNonEnumerableProperty(RegExp.prototype[SYMBOL], 'sham', true); + }; + + // `String.prototype.{ codePointAt, at }` methods implementation + var createMethod$1 = function (CONVERT_TO_STRING) { + return function ($this, pos) { + var S = String(requireObjectCoercible($this)); + var position = toInteger(pos); + var size = S.length; + var first, second; + if (position < 0 || position >= size) return CONVERT_TO_STRING ? '' : undefined; + first = S.charCodeAt(position); + return first < 0xD800 || first > 0xDBFF || position + 1 === size + || (second = S.charCodeAt(position + 1)) < 0xDC00 || second > 0xDFFF + ? CONVERT_TO_STRING ? S.charAt(position) : first + : CONVERT_TO_STRING ? S.slice(position, position + 2) : (first - 0xD800 << 10) + (second - 0xDC00) + 0x10000; + }; + }; + + var stringMultibyte = { + // `String.prototype.codePointAt` method + // https://tc39.es/ecma262/#sec-string.prototype.codepointat + codeAt: createMethod$1(false), + // `String.prototype.at` method + // https://github.com/mathiasbynens/String.prototype.at + charAt: createMethod$1(true) + }; + + var charAt = stringMultibyte.charAt; + + // `AdvanceStringIndex` abstract operation + // https://tc39.es/ecma262/#sec-advancestringindex + var advanceStringIndex = function (S, index, unicode) { + return index + (unicode ? charAt(S, index).length : 1); + }; + + // `RegExpExec` abstract operation + // https://tc39.es/ecma262/#sec-regexpexec + var regexpExecAbstract = function (R, S) { + var exec = R.exec; + if (typeof exec === 'function') { + var result = exec.call(R, S); + if (typeof result !== 'object') { + throw TypeError('RegExp exec method returned something other than an Object or null'); + } + return result; + } + + if (classofRaw(R) !== 'RegExp') { + throw TypeError('RegExp#exec called on incompatible receiver'); + } + + return regexpExec.call(R, S); + }; + + // @@match logic + fixRegexpWellKnownSymbolLogic('match', 1, function (MATCH, nativeMatch, maybeCallNative) { + return [ + // `String.prototype.match` method + // https://tc39.es/ecma262/#sec-string.prototype.match + function match(regexp) { + var O = requireObjectCoercible(this); + var matcher = regexp == undefined ? undefined : regexp[MATCH]; + return matcher !== undefined ? matcher.call(regexp, O) : new RegExp(regexp)[MATCH](String(O)); + }, + // `RegExp.prototype[@@match]` method + // https://tc39.es/ecma262/#sec-regexp.prototype-@@match + function (regexp) { + var res = maybeCallNative(nativeMatch, regexp, this); + if (res.done) return res.value; + + var rx = anObject(regexp); + var S = String(this); + + if (!rx.global) return regexpExecAbstract(rx, S); + + var fullUnicode = rx.unicode; + rx.lastIndex = 0; + var A = []; + var n = 0; + var result; + while ((result = regexpExecAbstract(rx, S)) !== null) { + var matchStr = String(result[0]); + A[n] = matchStr; + if (matchStr === '') rx.lastIndex = advanceStringIndex(S, toLength(rx.lastIndex), fullUnicode); + n++; + } + return n === 0 ? null : A; + } + ]; + }); + + // `IsArray` abstract operation + // https://tc39.es/ecma262/#sec-isarray + var isArray = Array.isArray || function isArray(arg) { + return classofRaw(arg) == 'Array'; + }; + + // `ToObject` abstract operation + // https://tc39.es/ecma262/#sec-toobject + var toObject = function (argument) { + return Object(requireObjectCoercible(argument)); + }; + + var createProperty = function (object, key, value) { + var propertyKey = toPrimitive(key); + if (propertyKey in object) objectDefineProperty.f(object, propertyKey, createPropertyDescriptor(0, value)); + else object[propertyKey] = value; + }; + + var SPECIES$1 = wellKnownSymbol('species'); + + // `ArraySpeciesCreate` abstract operation + // https://tc39.es/ecma262/#sec-arrayspeciescreate + var arraySpeciesCreate = function (originalArray, length) { + var C; + if (isArray(originalArray)) { + C = originalArray.constructor; + // cross-realm fallback + if (typeof C == 'function' && (C === Array || isArray(C.prototype))) C = undefined; + else if (isObject(C)) { + C = C[SPECIES$1]; + if (C === null) C = undefined; + } + } return new (C === undefined ? Array : C)(length === 0 ? 0 : length); + }; + + var SPECIES$2 = wellKnownSymbol('species'); + + var arrayMethodHasSpeciesSupport = function (METHOD_NAME) { + // We can't use this feature detection in V8 since it causes + // deoptimization and serious performance degradation + // https://github.com/zloirock/core-js/issues/677 + return engineV8Version >= 51 || !fails(function () { + var array = []; + var constructor = array.constructor = {}; + constructor[SPECIES$2] = function () { + return { foo: 1 }; + }; + return array[METHOD_NAME](Boolean).foo !== 1; + }); + }; + + var IS_CONCAT_SPREADABLE = wellKnownSymbol('isConcatSpreadable'); + var MAX_SAFE_INTEGER = 0x1FFFFFFFFFFFFF; + var MAXIMUM_ALLOWED_INDEX_EXCEEDED = 'Maximum allowed index exceeded'; + + // We can't use this feature detection in V8 since it causes + // deoptimization and serious performance degradation + // https://github.com/zloirock/core-js/issues/679 + var IS_CONCAT_SPREADABLE_SUPPORT = engineV8Version >= 51 || !fails(function () { + var array = []; + array[IS_CONCAT_SPREADABLE] = false; + return array.concat()[0] !== array; + }); + + var SPECIES_SUPPORT = arrayMethodHasSpeciesSupport('concat'); + + var isConcatSpreadable = function (O) { + if (!isObject(O)) return false; + var spreadable = O[IS_CONCAT_SPREADABLE]; + return spreadable !== undefined ? !!spreadable : isArray(O); + }; + + var FORCED = !IS_CONCAT_SPREADABLE_SUPPORT || !SPECIES_SUPPORT; + + // `Array.prototype.concat` method + // https://tc39.es/ecma262/#sec-array.prototype.concat + // with adding support of @@isConcatSpreadable and @@species + _export({ target: 'Array', proto: true, forced: FORCED }, { + // eslint-disable-next-line no-unused-vars -- required for `.length` + concat: function concat(arg) { + var O = toObject(this); + var A = arraySpeciesCreate(O, 0); + var n = 0; + var i, k, length, len, E; + for (i = -1, length = arguments.length; i < length; i++) { + E = i === -1 ? O : arguments[i]; + if (isConcatSpreadable(E)) { + len = toLength(E.length); + if (n + len > MAX_SAFE_INTEGER) throw TypeError(MAXIMUM_ALLOWED_INDEX_EXCEEDED); + for (k = 0; k < len; k++, n++) if (k in E) createProperty(A, n, E[k]); + } else { + if (n >= MAX_SAFE_INTEGER) throw TypeError(MAXIMUM_ALLOWED_INDEX_EXCEEDED); + createProperty(A, n++, E); + } + } + A.length = n; + return A; + } + }); + + var TO_STRING_TAG = wellKnownSymbol('toStringTag'); + var test = {}; + + test[TO_STRING_TAG] = 'z'; + + var toStringTagSupport = String(test) === '[object z]'; + + var TO_STRING_TAG$1 = wellKnownSymbol('toStringTag'); + // ES3 wrong here + var CORRECT_ARGUMENTS = classofRaw(function () { return arguments; }()) == 'Arguments'; + + // fallback for IE11 Script Access Denied error + var tryGet = function (it, key) { + try { + return it[key]; + } catch (error) { /* empty */ } + }; + + // getting tag from ES6+ `Object.prototype.toString` + var classof = toStringTagSupport ? classofRaw : function (it) { + var O, tag, result; + return it === undefined ? 'Undefined' : it === null ? 'Null' + // @@toStringTag case + : typeof (tag = tryGet(O = Object(it), TO_STRING_TAG$1)) == 'string' ? tag + // builtinTag case + : CORRECT_ARGUMENTS ? classofRaw(O) + // ES3 arguments fallback + : (result = classofRaw(O)) == 'Object' && typeof O.callee == 'function' ? 'Arguments' : result; + }; + + // `Object.prototype.toString` method implementation + // https://tc39.es/ecma262/#sec-object.prototype.tostring + var objectToString = toStringTagSupport ? {}.toString : function toString() { + return '[object ' + classof(this) + ']'; + }; + + // `Object.prototype.toString` method + // https://tc39.es/ecma262/#sec-object.prototype.tostring + if (!toStringTagSupport) { + redefine(Object.prototype, 'toString', objectToString, { unsafe: true }); + } + + var TO_STRING = 'toString'; + var RegExpPrototype = RegExp.prototype; + var nativeToString = RegExpPrototype[TO_STRING]; + + var NOT_GENERIC = fails(function () { return nativeToString.call({ source: 'a', flags: 'b' }) != '/a/b'; }); + // FF44- RegExp#toString has a wrong name + var INCORRECT_NAME = nativeToString.name != TO_STRING; + + // `RegExp.prototype.toString` method + // https://tc39.es/ecma262/#sec-regexp.prototype.tostring + if (NOT_GENERIC || INCORRECT_NAME) { + redefine(RegExp.prototype, TO_STRING, function toString() { + var R = anObject(this); + var p = String(R.source); + var rf = R.flags; + var f = String(rf === undefined && R instanceof RegExp && !('flags' in RegExpPrototype) ? regexpFlags.call(R) : rf); + return '/' + p + '/' + f; + }, { unsafe: true }); + } + + var MATCH = wellKnownSymbol('match'); + + // `IsRegExp` abstract operation + // https://tc39.es/ecma262/#sec-isregexp + var isRegexp = function (it) { + var isRegExp; + return isObject(it) && ((isRegExp = it[MATCH]) !== undefined ? !!isRegExp : classofRaw(it) == 'RegExp'); + }; + + var aFunction$1 = function (it) { + if (typeof it != 'function') { + throw TypeError(String(it) + ' is not a function'); + } return it; + }; + + var SPECIES$3 = wellKnownSymbol('species'); + + // `SpeciesConstructor` abstract operation + // https://tc39.es/ecma262/#sec-speciesconstructor + var speciesConstructor = function (O, defaultConstructor) { + var C = anObject(O).constructor; + var S; + return C === undefined || (S = anObject(C)[SPECIES$3]) == undefined ? defaultConstructor : aFunction$1(S); + }; + + var arrayPush = [].push; + var min$2 = Math.min; + var MAX_UINT32 = 0xFFFFFFFF; + + // babel-minify transpiles RegExp('x', 'y') -> /x/y and it causes SyntaxError + var SUPPORTS_Y = !fails(function () { return !RegExp(MAX_UINT32, 'y'); }); + + // @@split logic + fixRegexpWellKnownSymbolLogic('split', 2, function (SPLIT, nativeSplit, maybeCallNative) { + var internalSplit; + if ( + 'abbc'.split(/(b)*/)[1] == 'c' || + // eslint-disable-next-line regexp/no-empty-group -- required for testing + 'test'.split(/(?:)/, -1).length != 4 || + 'ab'.split(/(?:ab)*/).length != 2 || + '.'.split(/(.?)(.?)/).length != 4 || + // eslint-disable-next-line regexp/no-assertion-capturing-group, regexp/no-empty-group -- required for testing + '.'.split(/()()/).length > 1 || + ''.split(/.?/).length + ) { + // based on es5-shim implementation, need to rework it + internalSplit = function (separator, limit) { + var string = String(requireObjectCoercible(this)); + var lim = limit === undefined ? MAX_UINT32 : limit >>> 0; + if (lim === 0) return []; + if (separator === undefined) return [string]; + // If `separator` is not a regex, use native split + if (!isRegexp(separator)) { + return nativeSplit.call(string, separator, lim); + } + var output = []; + var flags = (separator.ignoreCase ? 'i' : '') + + (separator.multiline ? 'm' : '') + + (separator.unicode ? 'u' : '') + + (separator.sticky ? 'y' : ''); + var lastLastIndex = 0; + // Make `global` and avoid `lastIndex` issues by working with a copy + var separatorCopy = new RegExp(separator.source, flags + 'g'); + var match, lastIndex, lastLength; + while (match = regexpExec.call(separatorCopy, string)) { + lastIndex = separatorCopy.lastIndex; + if (lastIndex > lastLastIndex) { + output.push(string.slice(lastLastIndex, match.index)); + if (match.length > 1 && match.index < string.length) arrayPush.apply(output, match.slice(1)); + lastLength = match[0].length; + lastLastIndex = lastIndex; + if (output.length >= lim) break; + } + if (separatorCopy.lastIndex === match.index) separatorCopy.lastIndex++; // Avoid an infinite loop + } + if (lastLastIndex === string.length) { + if (lastLength || !separatorCopy.test('')) output.push(''); + } else output.push(string.slice(lastLastIndex)); + return output.length > lim ? output.slice(0, lim) : output; + }; + // Chakra, V8 + } else if ('0'.split(undefined, 0).length) { + internalSplit = function (separator, limit) { + return separator === undefined && limit === 0 ? [] : nativeSplit.call(this, separator, limit); + }; + } else internalSplit = nativeSplit; + + return [ + // `String.prototype.split` method + // https://tc39.es/ecma262/#sec-string.prototype.split + function split(separator, limit) { + var O = requireObjectCoercible(this); + var splitter = separator == undefined ? undefined : separator[SPLIT]; + return splitter !== undefined + ? splitter.call(separator, O, limit) + : internalSplit.call(String(O), separator, limit); + }, + // `RegExp.prototype[@@split]` method + // https://tc39.es/ecma262/#sec-regexp.prototype-@@split + // + // NOTE: This cannot be properly polyfilled in engines that don't support + // the 'y' flag. + function (regexp, limit) { + var res = maybeCallNative(internalSplit, regexp, this, limit, internalSplit !== nativeSplit); + if (res.done) return res.value; + + var rx = anObject(regexp); + var S = String(this); + var C = speciesConstructor(rx, RegExp); + + var unicodeMatching = rx.unicode; + var flags = (rx.ignoreCase ? 'i' : '') + + (rx.multiline ? 'm' : '') + + (rx.unicode ? 'u' : '') + + (SUPPORTS_Y ? 'y' : 'g'); + + // ^(? + rx + ) is needed, in combination with some S slicing, to + // simulate the 'y' flag. + var splitter = new C(SUPPORTS_Y ? rx : '^(?:' + rx.source + ')', flags); + var lim = limit === undefined ? MAX_UINT32 : limit >>> 0; + if (lim === 0) return []; + if (S.length === 0) return regexpExecAbstract(splitter, S) === null ? [S] : []; + var p = 0; + var q = 0; + var A = []; + while (q < S.length) { + splitter.lastIndex = SUPPORTS_Y ? q : 0; + var z = regexpExecAbstract(splitter, SUPPORTS_Y ? S : S.slice(q)); + var e; + if ( + z === null || + (e = min$2(toLength(splitter.lastIndex + (SUPPORTS_Y ? 0 : q)), S.length)) === p + ) { + q = advanceStringIndex(S, q, unicodeMatching); + } else { + A.push(S.slice(p, q)); + if (A.length === lim) return A; + for (var i = 1; i <= z.length - 1; i++) { + A.push(z[i]); + if (A.length === lim) return A; + } + q = p = e; + } + } + A.push(S.slice(p)); + return A; + } + ]; + }, !SUPPORTS_Y); + + /** + * Append a class to an element + * + * @api private + * @method _addClass + * @param {Object} element + * @param {String} className + * @returns null + */ + + function addClass(element, className) { + if (element instanceof SVGElement) { + // svg + var pre = element.getAttribute("class") || ""; + + if (!pre.match(className)) { + // check if element doesn't already have className + element.setAttribute("class", "".concat(pre, " ").concat(className)); + } + } else { + if (element.classList !== undefined) { + // check for modern classList property + var classes = className.split(" "); + forEach(classes, function (cls) { + element.classList.add(cls); + }); + } else if (!element.className.match(className)) { + // check if element doesn't already have className + element.className += " ".concat(className); + } + } + } + + /** + * Get an element CSS property on the page + * Thanks to JavaScript Kit: http://www.javascriptkit.com/dhtmltutors/dhtmlcascade4.shtml + * + * @api private + * @method _getPropValue + * @param {Object} element + * @param {String} propName + * @returns string property value + */ + function getPropValue(element, propName) { + var propValue = ""; + + if (element.currentStyle) { + //IE + propValue = element.currentStyle[propName]; + } else if (document.defaultView && document.defaultView.getComputedStyle) { + //Others + propValue = document.defaultView.getComputedStyle(element, null).getPropertyValue(propName); + } //Prevent exception in IE + + + if (propValue && propValue.toLowerCase) { + return propValue.toLowerCase(); + } else { + return propValue; + } + } + + /** + * To set the show element + * This function set a relative (in most cases) position and changes the z-index + * + * @api private + * @method _setShowElement + * @param {Object} targetElement + */ + + function setShowElement(_ref) { + var element = _ref.element; + addClass(element, "introjs-showElement"); + var currentElementPosition = getPropValue(element, "position"); + + if (currentElementPosition !== "absolute" && currentElementPosition !== "relative" && currentElementPosition !== "sticky" && currentElementPosition !== "fixed") { + //change to new intro item + addClass(element, "introjs-relativePosition"); + } + } + + /** + * Find the nearest scrollable parent + * copied from https://stackoverflow.com/questions/35939886/find-first-scrollable-parent + * + * @param Element element + * @return Element + */ + function getScrollParent(element) { + var style = window.getComputedStyle(element); + var excludeStaticParent = style.position === "absolute"; + var overflowRegex = /(auto|scroll)/; + if (style.position === "fixed") return document.body; + + for (var parent = element; parent = parent.parentElement;) { + style = window.getComputedStyle(parent); + + if (excludeStaticParent && style.position === "static") { + continue; + } + + if (overflowRegex.test(style.overflow + style.overflowY + style.overflowX)) return parent; + } + + return document.body; + } + + /** + * scroll a scrollable element to a child element + * + * @param {Object} targetElement + */ + + function scrollParentToElement(targetElement) { + var element = targetElement.element; + if (!this._options.scrollToElement) return; + var parent = getScrollParent(element); + if (parent === document.body) return; + parent.scrollTop = element.offsetTop - parent.offsetTop; + } + + /** + * Provides a cross-browser way to get the screen dimensions + * via: http://stackoverflow.com/questions/5864467/internet-explorer-innerheight + * + * @api private + * @method _getWinSize + * @returns {Object} width and height attributes + */ + function getWinSize() { + if (window.innerWidth !== undefined) { + return { + width: window.innerWidth, + height: window.innerHeight + }; + } else { + var D = document.documentElement; + return { + width: D.clientWidth, + height: D.clientHeight + }; + } + } + + /** + * Check to see if the element is in the viewport or not + * http://stackoverflow.com/questions/123999/how-to-tell-if-a-dom-element-is-visible-in-the-current-viewport + * + * @api private + * @method _elementInViewport + * @param {Object} el + */ + function elementInViewport(el) { + var rect = el.getBoundingClientRect(); + return rect.top >= 0 && rect.left >= 0 && rect.bottom + 80 <= window.innerHeight && // add 80 to get the text right + rect.right <= window.innerWidth; + } + + /** + * To change the scroll of `window` after highlighting an element + * + * @api private + * @param {String} scrollTo + * @param {Object} targetElement + * @param {Object} tooltipLayer + */ + + function scrollTo(scrollTo, _ref, tooltipLayer) { + var element = _ref.element; + if (scrollTo === "off") return; + var rect; + if (!this._options.scrollToElement) return; + + if (scrollTo === "tooltip") { + rect = tooltipLayer.getBoundingClientRect(); + } else { + rect = element.getBoundingClientRect(); + } + + if (!elementInViewport(element)) { + var winHeight = getWinSize().height; + var top = rect.bottom - (rect.bottom - rect.top); // TODO (afshinm): do we need scroll padding now? + // I have changed the scroll option and now it scrolls the window to + // the center of the target element or tooltip. + + if (top < 0 || element.clientHeight > winHeight) { + window.scrollBy(0, rect.top - (winHeight / 2 - rect.height / 2) - this._options.scrollPadding); // 30px padding from edge to look nice + //Scroll down + } else { + window.scrollBy(0, rect.top - (winHeight / 2 - rect.height / 2) + this._options.scrollPadding); // 30px padding from edge to look nice + } + } + } + + /** + * Setting anchors to behave like buttons + * + * @api private + * @method _setAnchorAsButton + */ + function setAnchorAsButton(anchor) { + anchor.setAttribute("role", "button"); + anchor.tabIndex = 0; + } + + // `Object.keys` method + // https://tc39.es/ecma262/#sec-object.keys + var objectKeys = Object.keys || function keys(O) { + return objectKeysInternal(O, enumBugKeys); + }; + + var nativeAssign = Object.assign; + var defineProperty = Object.defineProperty; + + // `Object.assign` method + // https://tc39.es/ecma262/#sec-object.assign + var objectAssign = !nativeAssign || fails(function () { + // should have correct order of operations (Edge bug) + if (descriptors && nativeAssign({ b: 1 }, nativeAssign(defineProperty({}, 'a', { + enumerable: true, + get: function () { + defineProperty(this, 'b', { + value: 3, + enumerable: false + }); + } + }), { b: 2 })).b !== 1) return true; + // should work with symbols and should have deterministic property order (V8 bug) + var A = {}; + var B = {}; + /* global Symbol -- required for testing */ + var symbol = Symbol(); + var alphabet = 'abcdefghijklmnopqrst'; + A[symbol] = 7; + alphabet.split('').forEach(function (chr) { B[chr] = chr; }); + return nativeAssign({}, A)[symbol] != 7 || objectKeys(nativeAssign({}, B)).join('') != alphabet; + }) ? function assign(target, source) { // eslint-disable-line no-unused-vars -- required for `.length` + var T = toObject(target); + var argumentsLength = arguments.length; + var index = 1; + var getOwnPropertySymbols = objectGetOwnPropertySymbols.f; + var propertyIsEnumerable = objectPropertyIsEnumerable.f; + while (argumentsLength > index) { + var S = indexedObject(arguments[index++]); + var keys = getOwnPropertySymbols ? objectKeys(S).concat(getOwnPropertySymbols(S)) : objectKeys(S); + var length = keys.length; + var j = 0; + var key; + while (length > j) { + key = keys[j++]; + if (!descriptors || propertyIsEnumerable.call(S, key)) T[key] = S[key]; + } + } return T; + } : nativeAssign; + + // `Object.assign` method + // https://tc39.es/ecma262/#sec-object.assign + _export({ target: 'Object', stat: true, forced: Object.assign !== objectAssign }, { + assign: objectAssign + }); + + /** + * Get an element position on the page relative to another element (or body) + * Thanks to `meouw`: http://stackoverflow.com/a/442474/375966 + * + * @api private + * @method getOffset + * @param {Object} element + * @param {Object} relativeEl + * @returns Element's position info + */ + + function getOffset(element, relativeEl) { + var body = document.body; + var docEl = document.documentElement; + var scrollTop = window.pageYOffset || docEl.scrollTop || body.scrollTop; + var scrollLeft = window.pageXOffset || docEl.scrollLeft || body.scrollLeft; + relativeEl = relativeEl || body; + var x = element.getBoundingClientRect(); + var xr = relativeEl.getBoundingClientRect(); + var relativeElPosition = getPropValue(relativeEl, "position"); + var obj = { + width: x.width, + height: x.height + }; + + if (relativeEl.tagName.toLowerCase() !== "body" && relativeElPosition === "relative" || relativeElPosition === "sticky") { + // when the container of our target element is _not_ body and has either "relative" or "sticky" position, we should not + // consider the scroll position but we need to include the relative x/y of the container element + return Object.assign(obj, { + top: x.top - xr.top, + left: x.left - xr.left + }); + } else { + return Object.assign(obj, { + top: x.top + scrollTop, + left: x.left + scrollLeft + }); + } + } + + /** + * Checks to see if target element (or parents) position is fixed or not + * + * @api private + * @method _isFixed + * @param {Object} element + * @returns Boolean + */ + + function isFixed(element) { + var p = element.parentNode; + + if (!p || p.nodeName === "HTML") { + return false; + } + + if (getPropValue(element, "position") === "fixed") { + return true; + } + + return isFixed(p); + } + + var floor$1 = Math.floor; + var replace = ''.replace; + var SUBSTITUTION_SYMBOLS = /\$([$&'`]|\d{1,2}|<[^>]*>)/g; + var SUBSTITUTION_SYMBOLS_NO_NAMED = /\$([$&'`]|\d{1,2})/g; + + // https://tc39.es/ecma262/#sec-getsubstitution + var getSubstitution = function (matched, str, position, captures, namedCaptures, replacement) { + var tailPos = position + matched.length; + var m = captures.length; + var symbols = SUBSTITUTION_SYMBOLS_NO_NAMED; + if (namedCaptures !== undefined) { + namedCaptures = toObject(namedCaptures); + symbols = SUBSTITUTION_SYMBOLS; + } + return replace.call(replacement, symbols, function (match, ch) { + var capture; + switch (ch.charAt(0)) { + case '$': return '$'; + case '&': return matched; + case '`': return str.slice(0, position); + case "'": return str.slice(tailPos); + case '<': + capture = namedCaptures[ch.slice(1, -1)]; + break; + default: // \d\d? + var n = +ch; + if (n === 0) return match; + if (n > m) { + var f = floor$1(n / 10); + if (f === 0) return match; + if (f <= m) return captures[f - 1] === undefined ? ch.charAt(1) : captures[f - 1] + ch.charAt(1); + return match; + } + capture = captures[n - 1]; + } + return capture === undefined ? '' : capture; + }); + }; + + var max$1 = Math.max; + var min$3 = Math.min; + + var maybeToString = function (it) { + return it === undefined ? it : String(it); + }; + + // @@replace logic + fixRegexpWellKnownSymbolLogic('replace', 2, function (REPLACE, nativeReplace, maybeCallNative, reason) { + var REGEXP_REPLACE_SUBSTITUTES_UNDEFINED_CAPTURE = reason.REGEXP_REPLACE_SUBSTITUTES_UNDEFINED_CAPTURE; + var REPLACE_KEEPS_$0 = reason.REPLACE_KEEPS_$0; + var UNSAFE_SUBSTITUTE = REGEXP_REPLACE_SUBSTITUTES_UNDEFINED_CAPTURE ? '$' : '$0'; + + return [ + // `String.prototype.replace` method + // https://tc39.es/ecma262/#sec-string.prototype.replace + function replace(searchValue, replaceValue) { + var O = requireObjectCoercible(this); + var replacer = searchValue == undefined ? undefined : searchValue[REPLACE]; + return replacer !== undefined + ? replacer.call(searchValue, O, replaceValue) + : nativeReplace.call(String(O), searchValue, replaceValue); + }, + // `RegExp.prototype[@@replace]` method + // https://tc39.es/ecma262/#sec-regexp.prototype-@@replace + function (regexp, replaceValue) { + if ( + (!REGEXP_REPLACE_SUBSTITUTES_UNDEFINED_CAPTURE && REPLACE_KEEPS_$0) || + (typeof replaceValue === 'string' && replaceValue.indexOf(UNSAFE_SUBSTITUTE) === -1) + ) { + var res = maybeCallNative(nativeReplace, regexp, this, replaceValue); + if (res.done) return res.value; + } + + var rx = anObject(regexp); + var S = String(this); + + var functionalReplace = typeof replaceValue === 'function'; + if (!functionalReplace) replaceValue = String(replaceValue); + + var global = rx.global; + if (global) { + var fullUnicode = rx.unicode; + rx.lastIndex = 0; + } + var results = []; + while (true) { + var result = regexpExecAbstract(rx, S); + if (result === null) break; + + results.push(result); + if (!global) break; + + var matchStr = String(result[0]); + if (matchStr === '') rx.lastIndex = advanceStringIndex(S, toLength(rx.lastIndex), fullUnicode); + } + + var accumulatedResult = ''; + var nextSourcePosition = 0; + for (var i = 0; i < results.length; i++) { + result = results[i]; + + var matched = String(result[0]); + var position = max$1(min$3(toInteger(result.index), S.length), 0); + var captures = []; + // NOTE: This is equivalent to + // captures = result.slice(1).map(maybeToString) + // but for some reason `nativeSlice.call(result, 1, result.length)` (called in + // the slice polyfill when slicing native arrays) "doesn't work" in safari 9 and + // causes a crash (https://pastebin.com/N21QzeQA) when trying to debug it. + for (var j = 1; j < result.length; j++) captures.push(maybeToString(result[j])); + var namedCaptures = result.groups; + if (functionalReplace) { + var replacerArgs = [matched].concat(captures, position, S); + if (namedCaptures !== undefined) replacerArgs.push(namedCaptures); + var replacement = String(replaceValue.apply(undefined, replacerArgs)); + } else { + replacement = getSubstitution(matched, S, position, captures, namedCaptures, replaceValue); + } + if (position >= nextSourcePosition) { + accumulatedResult += S.slice(nextSourcePosition, position) + replacement; + nextSourcePosition = position + matched.length; + } + } + return accumulatedResult + S.slice(nextSourcePosition); + } + ]; + }); + + /** + * Remove a class from an element + * + * @api private + * @method _removeClass + * @param {Object} element + * @param {RegExp|String} classNameRegex can be regex or string + * @returns null + */ + function removeClass(element, classNameRegex) { + if (element instanceof SVGElement) { + var pre = element.getAttribute("class") || ""; + element.setAttribute("class", pre.replace(classNameRegex, "").replace(/^\s+|\s+$/g, "")); + } else { + element.className = element.className.replace(classNameRegex, "").replace(/^\s+|\s+$/g, ""); + } + } + + /** + * Sets the style of an DOM element + * + * @param {Object} element + * @param {Object|string} style + * @return null + */ + function setStyle(element, style) { + var cssText = ""; + + if (element.style.cssText) { + cssText += element.style.cssText; + } + + if (typeof style === "string") { + cssText += style; + } else { + for (var rule in style) { + cssText += "".concat(rule, ":").concat(style[rule], ";"); + } + } + + element.style.cssText = cssText; + } + + /** + * Update the position of the helper layer on the screen + * + * @api private + * @method _setHelperLayerPosition + * @param {Object} helperLayer + */ + + function setHelperLayerPosition(helperLayer) { + if (helperLayer) { + //prevent error when `this._currentStep` in undefined + if (!this._introItems[this._currentStep]) return; + var currentElement = this._introItems[this._currentStep]; + var elementPosition = getOffset(currentElement.element, this._targetElement); + var widthHeightPadding = this._options.helperElementPadding; // If the target element is fixed, the tooltip should be fixed as well. + // Otherwise, remove a fixed class that may be left over from the previous + // step. + + if (isFixed(currentElement.element)) { + addClass(helperLayer, "introjs-fixedTooltip"); + } else { + removeClass(helperLayer, "introjs-fixedTooltip"); + } + + if (currentElement.position === "floating") { + widthHeightPadding = 0; + } //set new position to helper layer + + + setStyle(helperLayer, { + width: "".concat(elementPosition.width + widthHeightPadding, "px"), + height: "".concat(elementPosition.height + widthHeightPadding, "px"), + top: "".concat(elementPosition.top - widthHeightPadding / 2, "px"), + left: "".concat(elementPosition.left - widthHeightPadding / 2, "px") + }); + } + } + + // `Object.defineProperties` method + // https://tc39.es/ecma262/#sec-object.defineproperties + var objectDefineProperties = descriptors ? Object.defineProperties : function defineProperties(O, Properties) { + anObject(O); + var keys = objectKeys(Properties); + var length = keys.length; + var index = 0; + var key; + while (length > index) objectDefineProperty.f(O, key = keys[index++], Properties[key]); + return O; + }; + + var html = getBuiltIn('document', 'documentElement'); + + var GT = '>'; + var LT = '<'; + var PROTOTYPE = 'prototype'; + var SCRIPT = 'script'; + var IE_PROTO = sharedKey('IE_PROTO'); + + var EmptyConstructor = function () { /* empty */ }; + + var scriptTag = function (content) { + return LT + SCRIPT + GT + content + LT + '/' + SCRIPT + GT; + }; + + // Create object with fake `null` prototype: use ActiveX Object with cleared prototype + var NullProtoObjectViaActiveX = function (activeXDocument) { + activeXDocument.write(scriptTag('')); + activeXDocument.close(); + var temp = activeXDocument.parentWindow.Object; + activeXDocument = null; // avoid memory leak + return temp; + }; + + // Create object with fake `null` prototype: use iframe Object with cleared prototype + var NullProtoObjectViaIFrame = function () { + // Thrash, waste and sodomy: IE GC bug + var iframe = documentCreateElement('iframe'); + var JS = 'java' + SCRIPT + ':'; + var iframeDocument; + iframe.style.display = 'none'; + html.appendChild(iframe); + // https://github.com/zloirock/core-js/issues/475 + iframe.src = String(JS); + iframeDocument = iframe.contentWindow.document; + iframeDocument.open(); + iframeDocument.write(scriptTag('document.F=Object')); + iframeDocument.close(); + return iframeDocument.F; + }; + + // Check for document.domain and active x support + // No need to use active x approach when document.domain is not set + // see https://github.com/es-shims/es5-shim/issues/150 + // variation of https://github.com/kitcambridge/es5-shim/commit/4f738ac066346 + // avoid IE GC bug + var activeXDocument; + var NullProtoObject = function () { + try { + /* global ActiveXObject -- old IE */ + activeXDocument = document.domain && new ActiveXObject('htmlfile'); + } catch (error) { /* ignore */ } + NullProtoObject = activeXDocument ? NullProtoObjectViaActiveX(activeXDocument) : NullProtoObjectViaIFrame(); + var length = enumBugKeys.length; + while (length--) delete NullProtoObject[PROTOTYPE][enumBugKeys[length]]; + return NullProtoObject(); + }; + + hiddenKeys[IE_PROTO] = true; + + // `Object.create` method + // https://tc39.es/ecma262/#sec-object.create + var objectCreate = Object.create || function create(O, Properties) { + var result; + if (O !== null) { + EmptyConstructor[PROTOTYPE] = anObject(O); + result = new EmptyConstructor(); + EmptyConstructor[PROTOTYPE] = null; + // add "__proto__" for Object.getPrototypeOf polyfill + result[IE_PROTO] = O; + } else result = NullProtoObject(); + return Properties === undefined ? result : objectDefineProperties(result, Properties); + }; + + var UNSCOPABLES = wellKnownSymbol('unscopables'); + var ArrayPrototype = Array.prototype; + + // Array.prototype[@@unscopables] + // https://tc39.es/ecma262/#sec-array.prototype-@@unscopables + if (ArrayPrototype[UNSCOPABLES] == undefined) { + objectDefineProperty.f(ArrayPrototype, UNSCOPABLES, { + configurable: true, + value: objectCreate(null) + }); + } + + // add a key to Array.prototype[@@unscopables] + var addToUnscopables = function (key) { + ArrayPrototype[UNSCOPABLES][key] = true; + }; + + var $includes = arrayIncludes.includes; + + + // `Array.prototype.includes` method + // https://tc39.es/ecma262/#sec-array.prototype.includes + _export({ target: 'Array', proto: true }, { + includes: function includes(el /* , fromIndex = 0 */) { + return $includes(this, el, arguments.length > 1 ? arguments[1] : undefined); + } + }); + + // https://tc39.es/ecma262/#sec-array.prototype-@@unscopables + addToUnscopables('includes'); + + var HAS_SPECIES_SUPPORT = arrayMethodHasSpeciesSupport('slice'); + + var SPECIES$4 = wellKnownSymbol('species'); + var nativeSlice = [].slice; + var max$2 = Math.max; + + // `Array.prototype.slice` method + // https://tc39.es/ecma262/#sec-array.prototype.slice + // fallback for not array-like ES3 strings and DOM objects + _export({ target: 'Array', proto: true, forced: !HAS_SPECIES_SUPPORT }, { + slice: function slice(start, end) { + var O = toIndexedObject(this); + var length = toLength(O.length); + var k = toAbsoluteIndex(start, length); + var fin = toAbsoluteIndex(end === undefined ? length : end, length); + // inline `ArraySpeciesCreate` for usage native `Array#slice` where it's possible + var Constructor, result, n; + if (isArray(O)) { + Constructor = O.constructor; + // cross-realm fallback + if (typeof Constructor == 'function' && (Constructor === Array || isArray(Constructor.prototype))) { + Constructor = undefined; + } else if (isObject(Constructor)) { + Constructor = Constructor[SPECIES$4]; + if (Constructor === null) Constructor = undefined; + } + if (Constructor === Array || Constructor === undefined) { + return nativeSlice.call(O, k, fin); + } + } + result = new (Constructor === undefined ? Array : Constructor)(max$2(fin - k, 0)); + for (n = 0; k < fin; k++, n++) if (k in O) createProperty(result, n, O[k]); + result.length = n; + return result; + } + }); + + var notARegexp = function (it) { + if (isRegexp(it)) { + throw TypeError("The method doesn't accept regular expressions"); + } return it; + }; + + var MATCH$1 = wellKnownSymbol('match'); + + var correctIsRegexpLogic = function (METHOD_NAME) { + var regexp = /./; + try { + '/./'[METHOD_NAME](regexp); + } catch (error1) { + try { + regexp[MATCH$1] = false; + return '/./'[METHOD_NAME](regexp); + } catch (error2) { /* empty */ } + } return false; + }; + + // `String.prototype.includes` method + // https://tc39.es/ecma262/#sec-string.prototype.includes + _export({ target: 'String', proto: true, forced: !correctIsRegexpLogic('includes') }, { + includes: function includes(searchString /* , position = 0 */) { + return !!~String(requireObjectCoercible(this)) + .indexOf(notARegexp(searchString), arguments.length > 1 ? arguments[1] : undefined); + } + }); + + var arrayMethodIsStrict = function (METHOD_NAME, argument) { + var method = [][METHOD_NAME]; + return !!method && fails(function () { + // eslint-disable-next-line no-useless-call,no-throw-literal -- required for testing + method.call(null, argument || function () { throw 1; }, 1); + }); + }; + + var nativeJoin = [].join; + + var ES3_STRINGS = indexedObject != Object; + var STRICT_METHOD = arrayMethodIsStrict('join', ','); + + // `Array.prototype.join` method + // https://tc39.es/ecma262/#sec-array.prototype.join + _export({ target: 'Array', proto: true, forced: ES3_STRINGS || !STRICT_METHOD }, { + join: function join(separator) { + return nativeJoin.call(toIndexedObject(this), separator === undefined ? ',' : separator); + } + }); + + // optional / simple context binding + var functionBindContext = function (fn, that, length) { + aFunction$1(fn); + if (that === undefined) return fn; + switch (length) { + case 0: return function () { + return fn.call(that); + }; + case 1: return function (a) { + return fn.call(that, a); + }; + case 2: return function (a, b) { + return fn.call(that, a, b); + }; + case 3: return function (a, b, c) { + return fn.call(that, a, b, c); + }; + } + return function (/* ...args */) { + return fn.apply(that, arguments); + }; + }; + + var push = [].push; + + // `Array.prototype.{ forEach, map, filter, some, every, find, findIndex, filterOut }` methods implementation + var createMethod$2 = function (TYPE) { + var IS_MAP = TYPE == 1; + var IS_FILTER = TYPE == 2; + var IS_SOME = TYPE == 3; + var IS_EVERY = TYPE == 4; + var IS_FIND_INDEX = TYPE == 6; + var IS_FILTER_OUT = TYPE == 7; + var NO_HOLES = TYPE == 5 || IS_FIND_INDEX; + return function ($this, callbackfn, that, specificCreate) { + var O = toObject($this); + var self = indexedObject(O); + var boundFunction = functionBindContext(callbackfn, that, 3); + var length = toLength(self.length); + var index = 0; + var create = specificCreate || arraySpeciesCreate; + var target = IS_MAP ? create($this, length) : IS_FILTER || IS_FILTER_OUT ? create($this, 0) : undefined; + var value, result; + for (;length > index; index++) if (NO_HOLES || index in self) { + value = self[index]; + result = boundFunction(value, index, O); + if (TYPE) { + if (IS_MAP) target[index] = result; // map + else if (result) switch (TYPE) { + case 3: return true; // some + case 5: return value; // find + case 6: return index; // findIndex + case 2: push.call(target, value); // filter + } else switch (TYPE) { + case 4: return false; // every + case 7: push.call(target, value); // filterOut + } + } + } + return IS_FIND_INDEX ? -1 : IS_SOME || IS_EVERY ? IS_EVERY : target; + }; + }; + + var arrayIteration = { + // `Array.prototype.forEach` method + // https://tc39.es/ecma262/#sec-array.prototype.foreach + forEach: createMethod$2(0), + // `Array.prototype.map` method + // https://tc39.es/ecma262/#sec-array.prototype.map + map: createMethod$2(1), + // `Array.prototype.filter` method + // https://tc39.es/ecma262/#sec-array.prototype.filter + filter: createMethod$2(2), + // `Array.prototype.some` method + // https://tc39.es/ecma262/#sec-array.prototype.some + some: createMethod$2(3), + // `Array.prototype.every` method + // https://tc39.es/ecma262/#sec-array.prototype.every + every: createMethod$2(4), + // `Array.prototype.find` method + // https://tc39.es/ecma262/#sec-array.prototype.find + find: createMethod$2(5), + // `Array.prototype.findIndex` method + // https://tc39.es/ecma262/#sec-array.prototype.findIndex + findIndex: createMethod$2(6), + // `Array.prototype.filterOut` method + // https://github.com/tc39/proposal-array-filtering + filterOut: createMethod$2(7) + }; + + var $filter = arrayIteration.filter; + + + var HAS_SPECIES_SUPPORT$1 = arrayMethodHasSpeciesSupport('filter'); + + // `Array.prototype.filter` method + // https://tc39.es/ecma262/#sec-array.prototype.filter + // with adding support of @@species + _export({ target: 'Array', proto: true, forced: !HAS_SPECIES_SUPPORT$1 }, { + filter: function filter(callbackfn /* , thisArg */) { + return $filter(this, callbackfn, arguments.length > 1 ? arguments[1] : undefined); + } + }); + + /** + * Set tooltip left so it doesn't go off the right side of the window + * + * @return boolean true, if tooltipLayerStyleLeft is ok. false, otherwise. + */ + function checkRight(targetOffset, tooltipLayerStyleLeft, tooltipOffset, windowSize, tooltipLayer) { + if (targetOffset.left + tooltipLayerStyleLeft + tooltipOffset.width > windowSize.width) { + // off the right side of the window + tooltipLayer.style.left = "".concat(windowSize.width - tooltipOffset.width - targetOffset.left, "px"); + return false; + } + + tooltipLayer.style.left = "".concat(tooltipLayerStyleLeft, "px"); + return true; + } + + /** + * Set tooltip right so it doesn't go off the left side of the window + * + * @return boolean true, if tooltipLayerStyleRight is ok. false, otherwise. + */ + function checkLeft(targetOffset, tooltipLayerStyleRight, tooltipOffset, tooltipLayer) { + if (targetOffset.left + targetOffset.width - tooltipLayerStyleRight - tooltipOffset.width < 0) { + // off the left side of the window + tooltipLayer.style.left = "".concat(-targetOffset.left, "px"); + return false; + } + + tooltipLayer.style.right = "".concat(tooltipLayerStyleRight, "px"); + return true; + } + + var HAS_SPECIES_SUPPORT$2 = arrayMethodHasSpeciesSupport('splice'); + + var max$3 = Math.max; + var min$4 = Math.min; + var MAX_SAFE_INTEGER$1 = 0x1FFFFFFFFFFFFF; + var MAXIMUM_ALLOWED_LENGTH_EXCEEDED = 'Maximum allowed length exceeded'; + + // `Array.prototype.splice` method + // https://tc39.es/ecma262/#sec-array.prototype.splice + // with adding support of @@species + _export({ target: 'Array', proto: true, forced: !HAS_SPECIES_SUPPORT$2 }, { + splice: function splice(start, deleteCount /* , ...items */) { + var O = toObject(this); + var len = toLength(O.length); + var actualStart = toAbsoluteIndex(start, len); + var argumentsLength = arguments.length; + var insertCount, actualDeleteCount, A, k, from, to; + if (argumentsLength === 0) { + insertCount = actualDeleteCount = 0; + } else if (argumentsLength === 1) { + insertCount = 0; + actualDeleteCount = len - actualStart; + } else { + insertCount = argumentsLength - 2; + actualDeleteCount = min$4(max$3(toInteger(deleteCount), 0), len - actualStart); + } + if (len + insertCount - actualDeleteCount > MAX_SAFE_INTEGER$1) { + throw TypeError(MAXIMUM_ALLOWED_LENGTH_EXCEEDED); + } + A = arraySpeciesCreate(O, actualDeleteCount); + for (k = 0; k < actualDeleteCount; k++) { + from = actualStart + k; + if (from in O) createProperty(A, k, O[from]); + } + A.length = actualDeleteCount; + if (insertCount < actualDeleteCount) { + for (k = actualStart; k < len - actualDeleteCount; k++) { + from = k + actualDeleteCount; + to = k + insertCount; + if (from in O) O[to] = O[from]; + else delete O[to]; + } + for (k = len; k > len - actualDeleteCount + insertCount; k--) delete O[k - 1]; + } else if (insertCount > actualDeleteCount) { + for (k = len - actualDeleteCount; k > actualStart; k--) { + from = k + actualDeleteCount - 1; + to = k + insertCount - 1; + if (from in O) O[to] = O[from]; + else delete O[to]; + } + } + for (k = 0; k < insertCount; k++) { + O[k + actualStart] = arguments[k + 2]; + } + O.length = len - actualDeleteCount + insertCount; + return A; + } + }); + + /** + * Remove an entry from a string array if it's there, does nothing if it isn't there. + * + * @param {Array} stringArray + * @param {String} stringToRemove + */ + function removeEntry(stringArray, stringToRemove) { + if (stringArray.includes(stringToRemove)) { + stringArray.splice(stringArray.indexOf(stringToRemove), 1); + } + } + + /** + * auto-determine alignment + * @param {Integer} offsetLeft + * @param {Integer} tooltipWidth + * @param {Object} windowSize + * @param {String} desiredAlignment + * @return {String} calculatedAlignment + */ + + function _determineAutoAlignment(offsetLeft, tooltipWidth, _ref, desiredAlignment) { + var width = _ref.width; + var halfTooltipWidth = tooltipWidth / 2; + var winWidth = Math.min(width, window.screen.width); + var possibleAlignments = ["-left-aligned", "-middle-aligned", "-right-aligned"]; + var calculatedAlignment = ""; // valid left must be at least a tooltipWidth + // away from right side + + if (winWidth - offsetLeft < tooltipWidth) { + removeEntry(possibleAlignments, "-left-aligned"); + } // valid middle must be at least half + // width away from both sides + + + if (offsetLeft < halfTooltipWidth || winWidth - offsetLeft < halfTooltipWidth) { + removeEntry(possibleAlignments, "-middle-aligned"); + } // valid right must be at least a tooltipWidth + // width away from left side + + + if (offsetLeft < tooltipWidth) { + removeEntry(possibleAlignments, "-right-aligned"); + } + + if (possibleAlignments.length) { + if (possibleAlignments.includes(desiredAlignment)) { + // the desired alignment is valid + calculatedAlignment = desiredAlignment; + } else { + // pick the first valid position, in order + calculatedAlignment = possibleAlignments[0]; + } + } else { + // if screen width is too small + // for ANY alignment, middle is + // probably the best for visibility + calculatedAlignment = "-middle-aligned"; + } + + return calculatedAlignment; + } + /** + * Determines the position of the tooltip based on the position precedence and availability + * of screen space. + * + * @param {Object} targetElement + * @param {Object} tooltipLayer + * @param {String} desiredTooltipPosition + * @return {String} calculatedPosition + */ + + + function _determineAutoPosition(targetElement, tooltipLayer, desiredTooltipPosition) { + // Take a clone of position precedence. These will be the available + var possiblePositions = this._options.positionPrecedence.slice(); + + var windowSize = getWinSize(); + var tooltipHeight = getOffset(tooltipLayer).height + 10; + var tooltipWidth = getOffset(tooltipLayer).width + 20; + var targetElementRect = targetElement.getBoundingClientRect(); // If we check all the possible areas, and there are no valid places for the tooltip, the element + // must take up most of the screen real estate. Show the tooltip floating in the middle of the screen. + + var calculatedPosition = "floating"; + /* + * auto determine position + */ + // Check for space below + + if (targetElementRect.bottom + tooltipHeight > windowSize.height) { + removeEntry(possiblePositions, "bottom"); + } // Check for space above + + + if (targetElementRect.top - tooltipHeight < 0) { + removeEntry(possiblePositions, "top"); + } // Check for space to the right + + + if (targetElementRect.right + tooltipWidth > windowSize.width) { + removeEntry(possiblePositions, "right"); + } // Check for space to the left + + + if (targetElementRect.left - tooltipWidth < 0) { + removeEntry(possiblePositions, "left"); + } // @var {String} ex: 'right-aligned' + + + var desiredAlignment = function (pos) { + var hyphenIndex = pos.indexOf("-"); + + if (hyphenIndex !== -1) { + // has alignment + return pos.substr(hyphenIndex); + } + + return ""; + }(desiredTooltipPosition || ""); // strip alignment from position + + + if (desiredTooltipPosition) { + // ex: "bottom-right-aligned" + // should return 'bottom' + desiredTooltipPosition = desiredTooltipPosition.split("-")[0]; + } + + if (possiblePositions.length) { + if (possiblePositions.includes(desiredTooltipPosition)) { + // If the requested position is in the list, choose that + calculatedPosition = desiredTooltipPosition; + } else { + // Pick the first valid position, in order + calculatedPosition = possiblePositions[0]; + } + } // only top and bottom positions have optional alignments + + + if (["top", "bottom"].includes(calculatedPosition)) { + calculatedPosition += _determineAutoAlignment(targetElementRect.left, tooltipWidth, windowSize, desiredAlignment); + } + + return calculatedPosition; + } + /** + * Render tooltip box in the page + * + * @api private + * @method placeTooltip + * @param {HTMLElement} targetElement + * @param {HTMLElement} tooltipLayer + * @param {HTMLElement} arrowLayer + * @param {Boolean} hintMode + */ + + + function placeTooltip(targetElement, tooltipLayer, arrowLayer, hintMode) { + var tooltipCssClass = ""; + var currentStepObj; + var tooltipOffset; + var targetOffset; + var windowSize; + var currentTooltipPosition; + hintMode = hintMode || false; //reset the old style + + tooltipLayer.style.top = null; + tooltipLayer.style.right = null; + tooltipLayer.style.bottom = null; + tooltipLayer.style.left = null; + tooltipLayer.style.marginLeft = null; + tooltipLayer.style.marginTop = null; + arrowLayer.style.display = "inherit"; //prevent error when `this._currentStep` is undefined + + if (!this._introItems[this._currentStep]) return; //if we have a custom css class for each step + + currentStepObj = this._introItems[this._currentStep]; + + if (typeof currentStepObj.tooltipClass === "string") { + tooltipCssClass = currentStepObj.tooltipClass; + } else { + tooltipCssClass = this._options.tooltipClass; + } + + tooltipLayer.className = ["introjs-tooltip", tooltipCssClass].filter(Boolean).join(" "); + tooltipLayer.setAttribute("role", "dialog"); + currentTooltipPosition = this._introItems[this._currentStep].position; // Floating is always valid, no point in calculating + + if (currentTooltipPosition !== "floating" && this._options.autoPosition) { + currentTooltipPosition = _determineAutoPosition.call(this, targetElement, tooltipLayer, currentTooltipPosition); + } + + var tooltipLayerStyleLeft; + targetOffset = getOffset(targetElement); + tooltipOffset = getOffset(tooltipLayer); + windowSize = getWinSize(); + addClass(tooltipLayer, "introjs-".concat(currentTooltipPosition)); + + switch (currentTooltipPosition) { + case "top-right-aligned": + arrowLayer.className = "introjs-arrow bottom-right"; + var tooltipLayerStyleRight = 0; + checkLeft(targetOffset, tooltipLayerStyleRight, tooltipOffset, tooltipLayer); + tooltipLayer.style.bottom = "".concat(targetOffset.height + 20, "px"); + break; + + case "top-middle-aligned": + arrowLayer.className = "introjs-arrow bottom-middle"; + var tooltipLayerStyleLeftRight = targetOffset.width / 2 - tooltipOffset.width / 2; // a fix for middle aligned hints + + if (hintMode) { + tooltipLayerStyleLeftRight += 5; + } + + if (checkLeft(targetOffset, tooltipLayerStyleLeftRight, tooltipOffset, tooltipLayer)) { + tooltipLayer.style.right = null; + checkRight(targetOffset, tooltipLayerStyleLeftRight, tooltipOffset, windowSize, tooltipLayer); + } + + tooltipLayer.style.bottom = "".concat(targetOffset.height + 20, "px"); + break; + + case "top-left-aligned": // top-left-aligned is the same as the default top + + case "top": + arrowLayer.className = "introjs-arrow bottom"; + tooltipLayerStyleLeft = hintMode ? 0 : 15; + checkRight(targetOffset, tooltipLayerStyleLeft, tooltipOffset, windowSize, tooltipLayer); + tooltipLayer.style.bottom = "".concat(targetOffset.height + 20, "px"); + break; + + case "right": + tooltipLayer.style.left = "".concat(targetOffset.width + 20, "px"); + + if (targetOffset.top + tooltipOffset.height > windowSize.height) { + // In this case, right would have fallen below the bottom of the screen. + // Modify so that the bottom of the tooltip connects with the target + arrowLayer.className = "introjs-arrow left-bottom"; + tooltipLayer.style.top = "-".concat(tooltipOffset.height - targetOffset.height - 20, "px"); + } else { + arrowLayer.className = "introjs-arrow left"; + } + + break; + + case "left": + if (!hintMode && this._options.showStepNumbers === true) { + tooltipLayer.style.top = "15px"; + } + + if (targetOffset.top + tooltipOffset.height > windowSize.height) { + // In this case, left would have fallen below the bottom of the screen. + // Modify so that the bottom of the tooltip connects with the target + tooltipLayer.style.top = "-".concat(tooltipOffset.height - targetOffset.height - 20, "px"); + arrowLayer.className = "introjs-arrow right-bottom"; + } else { + arrowLayer.className = "introjs-arrow right"; + } + + tooltipLayer.style.right = "".concat(targetOffset.width + 20, "px"); + break; + + case "floating": + arrowLayer.style.display = "none"; //we have to adjust the top and left of layer manually for intro items without element + + tooltipLayer.style.left = "50%"; + tooltipLayer.style.top = "50%"; + tooltipLayer.style.marginLeft = "-".concat(tooltipOffset.width / 2, "px"); + tooltipLayer.style.marginTop = "-".concat(tooltipOffset.height / 2, "px"); + break; + + case "bottom-right-aligned": + arrowLayer.className = "introjs-arrow top-right"; + tooltipLayerStyleRight = 0; + checkLeft(targetOffset, tooltipLayerStyleRight, tooltipOffset, tooltipLayer); + tooltipLayer.style.top = "".concat(targetOffset.height + 20, "px"); + break; + + case "bottom-middle-aligned": + arrowLayer.className = "introjs-arrow top-middle"; + tooltipLayerStyleLeftRight = targetOffset.width / 2 - tooltipOffset.width / 2; // a fix for middle aligned hints + + if (hintMode) { + tooltipLayerStyleLeftRight += 5; + } + + if (checkLeft(targetOffset, tooltipLayerStyleLeftRight, tooltipOffset, tooltipLayer)) { + tooltipLayer.style.right = null; + checkRight(targetOffset, tooltipLayerStyleLeftRight, tooltipOffset, windowSize, tooltipLayer); + } + + tooltipLayer.style.top = "".concat(targetOffset.height + 20, "px"); + break; + // case 'bottom-left-aligned': + // Bottom-left-aligned is the same as the default bottom + // case 'bottom': + // Bottom going to follow the default behavior + + default: + arrowLayer.className = "introjs-arrow top"; + tooltipLayerStyleLeft = 0; + checkRight(targetOffset, tooltipLayerStyleLeft, tooltipOffset, windowSize, tooltipLayer); + tooltipLayer.style.top = "".concat(targetOffset.height + 20, "px"); + } + } + + /** + * To remove all show element(s) + * + * @api private + * @method _removeShowElement + */ + + function removeShowElement() { + var elms = document.querySelectorAll(".introjs-showElement"); + forEach(elms, function (elm) { + removeClass(elm, /introjs-[a-zA-Z]+/g); + }); + } + + function _createElement(tagname, attrs) { + var element = document.createElement(tagname); + attrs = attrs || {}; // regex for matching attributes that need to be set with setAttribute + + var setAttRegex = /^(?:role|data-|aria-)/; + + for (var k in attrs) { + var v = attrs[k]; + + if (k === "style") { + setStyle(element, v); + } else if (k.match(setAttRegex)) { + element.setAttribute(k, v); + } else { + element[k] = v; + } + } + + return element; + } + + /** + * Appends `element` to `parentElement` + * + * @param {Element} parentElement + * @param {Element} element + * @param {Boolean} [animate=false] + */ + + function appendChild(parentElement, element, animate) { + if (animate) { + var existingOpacity = element.style.opacity || "1"; + setStyle(element, { + opacity: "0" + }); + window.setTimeout(function () { + setStyle(element, { + opacity: existingOpacity + }); + }, 10); + } + + parentElement.appendChild(element); + } + + /** + * Gets the current progress percentage + * + * @api private + * @method _getProgress + * @returns current progress percentage + */ + + function _getProgress() { + // Steps are 0 indexed + var currentStep = parseInt(this._currentStep + 1, 10); + return currentStep / this._introItems.length * 100; + } + /** + * Add disableinteraction layer and adjust the size and position of the layer + * + * @api private + * @method _disableInteraction + */ + + + function _disableInteraction() { + var disableInteractionLayer = document.querySelector(".introjs-disableInteraction"); + + if (disableInteractionLayer === null) { + disableInteractionLayer = _createElement("div", { + className: "introjs-disableInteraction" + }); + + this._targetElement.appendChild(disableInteractionLayer); + } + + setHelperLayerPosition.call(this, disableInteractionLayer); + } + /** + * Show an element on the page + * + * @api private + * @method _showElement + * @param {Object} targetElement + */ + + + function _showElement(targetElement) { + var _this = this; + + if (typeof this._introChangeCallback !== "undefined") { + this._introChangeCallback.call(this, targetElement.element); + } + + var self = this; + var oldHelperLayer = document.querySelector(".introjs-helperLayer"); + var oldReferenceLayer = document.querySelector(".introjs-tooltipReferenceLayer"); + var highlightClass = "introjs-helperLayer"; + var nextTooltipButton; + var prevTooltipButton; + var skipTooltipButton; + + if (typeof targetElement.highlightClass === "string") { + highlightClass += " ".concat(targetElement.highlightClass); + } //check for options highlight class + + + if (typeof this._options.highlightClass === "string") { + highlightClass += " ".concat(this._options.highlightClass); + } + + if (oldHelperLayer !== null) { + var oldHelperNumberLayer = oldReferenceLayer.querySelector(".introjs-helperNumberLayer"); + var oldtooltipLayer = oldReferenceLayer.querySelector(".introjs-tooltiptext"); + var oldTooltipTitleLayer = oldReferenceLayer.querySelector(".introjs-tooltip-title"); + var oldArrowLayer = oldReferenceLayer.querySelector(".introjs-arrow"); + var oldtooltipContainer = oldReferenceLayer.querySelector(".introjs-tooltip"); + skipTooltipButton = oldReferenceLayer.querySelector(".introjs-skipbutton"); + prevTooltipButton = oldReferenceLayer.querySelector(".introjs-prevbutton"); + nextTooltipButton = oldReferenceLayer.querySelector(".introjs-nextbutton"); //update or reset the helper highlight class + + oldHelperLayer.className = highlightClass; //hide the tooltip + + oldtooltipContainer.style.opacity = 0; + oldtooltipContainer.style.display = "none"; // if the target element is within a scrollable element + + scrollParentToElement.call(self, targetElement); // set new position to helper layer + + setHelperLayerPosition.call(self, oldHelperLayer); + setHelperLayerPosition.call(self, oldReferenceLayer); //remove old classes if the element still exist + + removeShowElement(); //we should wait until the CSS3 transition is competed (it's 0.3 sec) to prevent incorrect `height` and `width` calculation + + if (self._lastShowElementTimer) { + window.clearTimeout(self._lastShowElementTimer); + } + + self._lastShowElementTimer = window.setTimeout(function () { + // set current step to the label + if (oldHelperNumberLayer !== null) { + oldHelperNumberLayer.innerHTML = "".concat(targetElement.step, " of ").concat(_this._introItems.length); + } // set current tooltip text + + + oldtooltipLayer.innerHTML = targetElement.intro; // set current tooltip title + + oldTooltipTitleLayer.innerHTML = targetElement.title; //set the tooltip position + + oldtooltipContainer.style.display = "block"; + placeTooltip.call(self, targetElement.element, oldtooltipContainer, oldArrowLayer); //change active bullet + + if (self._options.showBullets) { + oldReferenceLayer.querySelector(".introjs-bullets li > a.active").className = ""; + oldReferenceLayer.querySelector(".introjs-bullets li > a[data-stepnumber=\"".concat(targetElement.step, "\"]")).className = "active"; + } + + oldReferenceLayer.querySelector(".introjs-progress .introjs-progressbar").style.cssText = "width:".concat(_getProgress.call(self), "%;"); + oldReferenceLayer.querySelector(".introjs-progress .introjs-progressbar").setAttribute("aria-valuenow", _getProgress.call(self)); //show the tooltip + + oldtooltipContainer.style.opacity = 1; //reset button focus + + if (typeof nextTooltipButton !== "undefined" && nextTooltipButton !== null && /introjs-donebutton/gi.test(nextTooltipButton.className)) { + // skip button is now "done" button + nextTooltipButton.focus(); + } else if (typeof nextTooltipButton !== "undefined" && nextTooltipButton !== null) { + //still in the tour, focus on next + nextTooltipButton.focus(); + } // change the scroll of the window, if needed + + + scrollTo.call(self, targetElement.scrollTo, targetElement, oldtooltipLayer); + }, 350); // end of old element if-else condition + } else { + var helperLayer = _createElement("div", { + className: highlightClass + }); + var referenceLayer = _createElement("div", { + className: "introjs-tooltipReferenceLayer" + }); + var arrowLayer = _createElement("div", { + className: "introjs-arrow" + }); + var tooltipLayer = _createElement("div", { + className: "introjs-tooltip" + }); + var tooltipTextLayer = _createElement("div", { + className: "introjs-tooltiptext" + }); + var tooltipHeaderLayer = _createElement("div", { + className: "introjs-tooltip-header" + }); + var tooltipTitleLayer = _createElement("h1", { + className: "introjs-tooltip-title" + }); + var bulletsLayer = _createElement("div", { + className: "introjs-bullets" + }); + var progressLayer = _createElement("div"); + var buttonsLayer = _createElement("div"); + setStyle(helperLayer, { + "box-shadow": "0 0 1px 2px rgba(33, 33, 33, 0.8), rgba(33, 33, 33, ".concat(self._options.overlayOpacity.toString(), ") 0 0 0 5000px") + }); // target is within a scrollable element + + scrollParentToElement.call(self, targetElement); //set new position to helper layer + + setHelperLayerPosition.call(self, helperLayer); + setHelperLayerPosition.call(self, referenceLayer); //add helper layer to target element + + appendChild(this._targetElement, helperLayer, true); + appendChild(this._targetElement, referenceLayer); + tooltipTextLayer.innerHTML = targetElement.intro; + tooltipTitleLayer.innerHTML = targetElement.title; + + if (this._options.showBullets === false) { + bulletsLayer.style.display = "none"; + } + + var ulContainer = _createElement("ul"); + ulContainer.setAttribute("role", "tablist"); + + var anchorClick = function anchorClick() { + self.goToStep(this.getAttribute("data-stepnumber")); + }; + + forEach(this._introItems, function (_ref, i) { + var step = _ref.step; + var innerLi = _createElement("li"); + var anchorLink = _createElement("a"); + innerLi.setAttribute("role", "presentation"); + anchorLink.setAttribute("role", "tab"); + anchorLink.onclick = anchorClick; + + if (i === targetElement.step - 1) { + anchorLink.className = "active"; + } + + setAnchorAsButton(anchorLink); + anchorLink.innerHTML = " "; + anchorLink.setAttribute("data-stepnumber", step); + innerLi.appendChild(anchorLink); + ulContainer.appendChild(innerLi); + }); + bulletsLayer.appendChild(ulContainer); + progressLayer.className = "introjs-progress"; + + if (this._options.showProgress === false) { + progressLayer.style.display = "none"; + } + + var progressBar = _createElement("div", { + className: "introjs-progressbar" + }); + + if (this._options.progressBarAdditionalClass) { + progressBar.className += " " + this._options.progressBarAdditionalClass; + } + + progressBar.setAttribute("role", "progress"); + progressBar.setAttribute("aria-valuemin", 0); + progressBar.setAttribute("aria-valuemax", 100); + progressBar.setAttribute("aria-valuenow", _getProgress.call(this)); + progressBar.style.cssText = "width:".concat(_getProgress.call(this), "%;"); + progressLayer.appendChild(progressBar); + buttonsLayer.className = "introjs-tooltipbuttons"; + + if (this._options.showButtons === false) { + buttonsLayer.style.display = "none"; + } + + tooltipHeaderLayer.appendChild(tooltipTitleLayer); + tooltipLayer.appendChild(tooltipHeaderLayer); + tooltipLayer.appendChild(tooltipTextLayer); + tooltipLayer.appendChild(bulletsLayer); + tooltipLayer.appendChild(progressLayer); // add helper layer number + + var helperNumberLayer = _createElement("div"); + + if (this._options.showStepNumbers === true) { + helperNumberLayer.className = "introjs-helperNumberLayer"; + helperNumberLayer.innerHTML = "".concat(targetElement.step, " of ").concat(this._introItems.length); + tooltipLayer.appendChild(helperNumberLayer); + } + + tooltipLayer.appendChild(arrowLayer); + referenceLayer.appendChild(tooltipLayer); //next button + + nextTooltipButton = _createElement("a"); + + nextTooltipButton.onclick = function () { + if (self._introItems.length - 1 !== self._currentStep) { + nextStep.call(self); + } else if (/introjs-donebutton/gi.test(nextTooltipButton.className)) { + if (typeof self._introCompleteCallback === "function") { + self._introCompleteCallback.call(self); + } + + exitIntro.call(self, self._targetElement); + } + }; + + setAnchorAsButton(nextTooltipButton); + nextTooltipButton.innerHTML = this._options.nextLabel; //previous button + + prevTooltipButton = _createElement("a"); + + prevTooltipButton.onclick = function () { + if (self._currentStep !== 0) { + previousStep.call(self); + } + }; + + setAnchorAsButton(prevTooltipButton); + prevTooltipButton.innerHTML = this._options.prevLabel; //skip button + + skipTooltipButton = _createElement("a", { + className: "introjs-skipbutton" + }); + setAnchorAsButton(skipTooltipButton); + skipTooltipButton.innerHTML = this._options.skipLabel; + + skipTooltipButton.onclick = function () { + if (self._introItems.length - 1 === self._currentStep && typeof self._introCompleteCallback === "function") { + self._introCompleteCallback.call(self); + } + + if (typeof self._introSkipCallback === "function") { + self._introSkipCallback.call(self); + } + + exitIntro.call(self, self._targetElement); + }; + + tooltipHeaderLayer.appendChild(skipTooltipButton); //in order to prevent displaying previous button always + + if (this._introItems.length > 1) { + buttonsLayer.appendChild(prevTooltipButton); + } // we always need the next button because this + // button changes to "Done" in the last step of the tour + + + buttonsLayer.appendChild(nextTooltipButton); + tooltipLayer.appendChild(buttonsLayer); //set proper position + + placeTooltip.call(self, targetElement.element, tooltipLayer, arrowLayer); // change the scroll of the window, if needed + + scrollTo.call(this, targetElement.scrollTo, targetElement, tooltipLayer); //end of new element if-else condition + } // removing previous disable interaction layer + + + var disableInteractionLayer = self._targetElement.querySelector(".introjs-disableInteraction"); + + if (disableInteractionLayer) { + disableInteractionLayer.parentNode.removeChild(disableInteractionLayer); + } //disable interaction + + + if (targetElement.disableInteraction) { + _disableInteraction.call(self); + } // when it's the first step of tour + + + if (this._currentStep === 0 && this._introItems.length > 1) { + if (typeof nextTooltipButton !== "undefined" && nextTooltipButton !== null) { + nextTooltipButton.className = "".concat(this._options.buttonClass, " introjs-nextbutton"); + nextTooltipButton.innerHTML = this._options.nextLabel; + } + + if (this._options.hidePrev === true) { + if (typeof prevTooltipButton !== "undefined" && prevTooltipButton !== null) { + prevTooltipButton.className = "".concat(this._options.buttonClass, " introjs-prevbutton introjs-hidden"); + } + + if (typeof nextTooltipButton !== "undefined" && nextTooltipButton !== null) { + addClass(nextTooltipButton, "introjs-fullbutton"); + } + } else { + if (typeof prevTooltipButton !== "undefined" && prevTooltipButton !== null) { + prevTooltipButton.className = "".concat(this._options.buttonClass, " introjs-prevbutton introjs-disabled"); + } + } + } else if (this._introItems.length - 1 === this._currentStep || this._introItems.length === 1) { + // last step of tour + if (typeof prevTooltipButton !== "undefined" && prevTooltipButton !== null) { + prevTooltipButton.className = "".concat(this._options.buttonClass, " introjs-prevbutton"); + } + + if (this._options.hideNext === true) { + if (typeof nextTooltipButton !== "undefined" && nextTooltipButton !== null) { + nextTooltipButton.className = "".concat(this._options.buttonClass, " introjs-nextbutton introjs-hidden"); + } + + if (typeof prevTooltipButton !== "undefined" && prevTooltipButton !== null) { + addClass(prevTooltipButton, "introjs-fullbutton"); + } + } else { + if (typeof nextTooltipButton !== "undefined" && nextTooltipButton !== null) { + if (this._options.nextToDone === true) { + nextTooltipButton.innerHTML = this._options.doneLabel; + addClass(nextTooltipButton, "".concat(this._options.buttonClass, " introjs-nextbutton introjs-donebutton")); + } else { + nextTooltipButton.className = "".concat(this._options.buttonClass, " introjs-nextbutton introjs-disabled"); + } + } + } + } else { + // steps between start and end + if (typeof prevTooltipButton !== "undefined" && prevTooltipButton !== null) { + prevTooltipButton.className = "".concat(this._options.buttonClass, " introjs-prevbutton"); + } + + if (typeof nextTooltipButton !== "undefined" && nextTooltipButton !== null) { + nextTooltipButton.className = "".concat(this._options.buttonClass, " introjs-nextbutton"); + nextTooltipButton.innerHTML = this._options.nextLabel; + } + } + + if (typeof prevTooltipButton !== "undefined" && prevTooltipButton !== null) { + prevTooltipButton.setAttribute("role", "button"); + } + + if (typeof nextTooltipButton !== "undefined" && nextTooltipButton !== null) { + nextTooltipButton.setAttribute("role", "button"); + } + + if (typeof skipTooltipButton !== "undefined" && skipTooltipButton !== null) { + skipTooltipButton.setAttribute("role", "button"); + } //Set focus on "next" button, so that hitting Enter always moves you onto the next step + + + if (typeof nextTooltipButton !== "undefined" && nextTooltipButton !== null) { + nextTooltipButton.focus(); + } + + setShowElement(targetElement); + + if (typeof this._introAfterChangeCallback !== "undefined") { + this._introAfterChangeCallback.call(this, targetElement.element); + } + } + + /** + * Go to specific step of introduction + * + * @api private + * @method _goToStep + */ + + function goToStep(step) { + //because steps starts with zero + this._currentStep = step - 2; + + if (typeof this._introItems !== "undefined") { + nextStep.call(this); + } + } + /** + * Go to the specific step of introduction with the explicit [data-step] number + * + * @api private + * @method _goToStepNumber + */ + + function goToStepNumber(step) { + this._currentStepNumber = step; + + if (typeof this._introItems !== "undefined") { + nextStep.call(this); + } + } + /** + * Go to next step on intro + * + * @api private + * @method _nextStep + */ + + function nextStep() { + var _this = this; + + this._direction = "forward"; + + if (typeof this._currentStepNumber !== "undefined") { + forEach(this._introItems, function (_ref, i) { + var step = _ref.step; + + if (step === _this._currentStepNumber) { + _this._currentStep = i - 1; + _this._currentStepNumber = undefined; + } + }); + } + + if (typeof this._currentStep === "undefined") { + this._currentStep = 0; + } else { + ++this._currentStep; + } + + var nextStep = this._introItems[this._currentStep]; + var continueStep = true; + + if (typeof this._introBeforeChangeCallback !== "undefined") { + continueStep = this._introBeforeChangeCallback.call(this, nextStep && nextStep.element); + } // if `onbeforechange` returned `false`, stop displaying the element + + + if (continueStep === false) { + --this._currentStep; + return false; + } + + if (this._introItems.length <= this._currentStep) { + //end of the intro + //check if any callback is defined + if (typeof this._introCompleteCallback === "function") { + this._introCompleteCallback.call(this); + } + + exitIntro.call(this, this._targetElement); + return; + } + + _showElement.call(this, nextStep); + } + /** + * Go to previous step on intro + * + * @api private + * @method _previousStep + */ + + function previousStep() { + this._direction = "backward"; + + if (this._currentStep === 0) { + return false; + } + + --this._currentStep; + var nextStep = this._introItems[this._currentStep]; + var continueStep = true; + + if (typeof this._introBeforeChangeCallback !== "undefined") { + continueStep = this._introBeforeChangeCallback.call(this, nextStep && nextStep.element); + } // if `onbeforechange` returned `false`, stop displaying the element + + + if (continueStep === false) { + ++this._currentStep; + return false; + } + + _showElement.call(this, nextStep); + } + /** + * Returns the current step of the intro + * + * @returns {number | boolean} + */ + + function currentStep() { + return this._currentStep; + } + + /** + * on keyCode: + * https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode + * This feature has been removed from the Web standards. + * Though some browsers may still support it, it is in + * the process of being dropped. + * Instead, you should use KeyboardEvent.code, + * if it's implemented. + * + * jQuery's approach is to test for + * (1) e.which, then + * (2) e.charCode, then + * (3) e.keyCode + * https://github.com/jquery/jquery/blob/a6b0705294d336ae2f63f7276de0da1195495363/src/event.js#L638 + * + * @param type var + * @return type + */ + + function onKeyDown(e) { + var code = e.code === undefined ? e.which : e.code; // if e.which is null + + if (code === null) { + code = e.charCode === null ? e.keyCode : e.charCode; + } + + if ((code === "Escape" || code === 27) && this._options.exitOnEsc === true) { + //escape key pressed, exit the intro + //check if exit callback is defined + exitIntro.call(this, this._targetElement); + } else if (code === "ArrowLeft" || code === 37) { + //left arrow + previousStep.call(this); + } else if (code === "ArrowRight" || code === 39) { + //right arrow + nextStep.call(this); + } else if (code === "Enter" || code === "NumpadEnter" || code === 13) { + //srcElement === ie + var target = e.target || e.srcElement; + + if (target && target.className.match("introjs-prevbutton")) { + //user hit enter while focusing on previous button + previousStep.call(this); + } else if (target && target.className.match("introjs-skipbutton")) { + //user hit enter while focusing on skip button + if (this._introItems.length - 1 === this._currentStep && typeof this._introCompleteCallback === "function") { + this._introCompleteCallback.call(this); + } + + exitIntro.call(this, this._targetElement); + } else if (target && target.getAttribute("data-stepnumber")) { + // user hit enter while focusing on step bullet + target.click(); + } else { + //default behavior for responding to enter + nextStep.call(this); + } //prevent default behaviour on hitting Enter, to prevent steps being skipped in some browsers + + + if (e.preventDefault) { + e.preventDefault(); + } else { + e.returnValue = false; + } + } + } + + /* + * makes a copy of the object + * @api private + * @method _cloneObject + */ + function cloneObject(object) { + if (object === null || _typeof(object) !== "object" || typeof object.nodeType !== "undefined") { + return object; + } + + var temp = {}; + + for (var key in object) { + if (typeof window.jQuery !== "undefined" && object[key] instanceof window.jQuery) { + temp[key] = object[key]; + } else { + temp[key] = cloneObject(object[key]); + } + } + + return temp; + } + + /** + * Get a queryselector within the hint wrapper + * + * @param {String} selector + * @return {NodeList|Array} + */ + + function hintQuerySelectorAll(selector) { + var hintsWrapper = document.querySelector(".introjs-hints"); + return hintsWrapper ? hintsWrapper.querySelectorAll(selector) : []; + } + /** + * Hide a hint + * + * @api private + * @method hideHint + */ + + function hideHint(stepId) { + var hint = hintQuerySelectorAll(".introjs-hint[data-step=\"".concat(stepId, "\"]"))[0]; + removeHintTooltip.call(this); + + if (hint) { + addClass(hint, "introjs-hidehint"); + } // call the callback function (if any) + + + if (typeof this._hintCloseCallback !== "undefined") { + this._hintCloseCallback.call(this, stepId); + } + } + /** + * Hide all hints + * + * @api private + * @method hideHints + */ + + function hideHints() { + var _this = this; + + var hints = hintQuerySelectorAll(".introjs-hint"); + forEach(hints, function (hint) { + hideHint.call(_this, hint.getAttribute("data-step")); + }); + } + /** + * Show all hints + * + * @api private + * @method _showHints + */ + + function showHints() { + var _this2 = this; + + var hints = hintQuerySelectorAll(".introjs-hint"); + + if (hints && hints.length) { + forEach(hints, function (hint) { + showHint.call(_this2, hint.getAttribute("data-step")); + }); + } else { + populateHints.call(this, this._targetElement); + } + } + /** + * Show a hint + * + * @api private + * @method showHint + */ + + function showHint(stepId) { + var hint = hintQuerySelectorAll(".introjs-hint[data-step=\"".concat(stepId, "\"]"))[0]; + + if (hint) { + removeClass(hint, /introjs-hidehint/g); + } + } + /** + * Removes all hint elements on the page + * Useful when you want to destroy the elements and add them again (e.g. a modal or popup) + * + * @api private + * @method removeHints + */ + + function removeHints() { + var _this3 = this; + + var hints = hintQuerySelectorAll(".introjs-hint"); + forEach(hints, function (hint) { + removeHint.call(_this3, hint.getAttribute("data-step")); + }); + } + /** + * Remove one single hint element from the page + * Useful when you want to destroy the element and add them again (e.g. a modal or popup) + * Use removeHints if you want to remove all elements. + * + * @api private + * @method removeHint + */ + + function removeHint(stepId) { + var hint = hintQuerySelectorAll(".introjs-hint[data-step=\"".concat(stepId, "\"]"))[0]; + + if (hint) { + hint.parentNode.removeChild(hint); + } + } + /** + * Add all available hints to the page + * + * @api private + * @method addHints + */ + + function addHints() { + var _this4 = this; + + var self = this; + var hintsWrapper = document.querySelector(".introjs-hints"); + + if (hintsWrapper === null) { + hintsWrapper = _createElement("div", { + className: "introjs-hints" + }); + } + /** + * Returns an event handler unique to the hint iteration + * + * @param {Integer} i + * @return {Function} + */ + + + var getHintClick = function getHintClick(i) { + return function (e) { + var evt = e ? e : window.event; + + if (evt.stopPropagation) { + evt.stopPropagation(); + } + + if (evt.cancelBubble !== null) { + evt.cancelBubble = true; + } + + showHintDialog.call(self, i); + }; + }; + + forEach(this._introItems, function (item, i) { + // avoid append a hint twice + if (document.querySelector(".introjs-hint[data-step=\"".concat(i, "\"]"))) { + return; + } + + var hint = _createElement("a", { + className: "introjs-hint" + }); + setAnchorAsButton(hint); + hint.onclick = getHintClick(i); + + if (!item.hintAnimation) { + addClass(hint, "introjs-hint-no-anim"); + } // hint's position should be fixed if the target element's position is fixed + + + if (isFixed(item.element)) { + addClass(hint, "introjs-fixedhint"); + } + + var hintDot = _createElement("div", { + className: "introjs-hint-dot" + }); + var hintPulse = _createElement("div", { + className: "introjs-hint-pulse" + }); + hint.appendChild(hintDot); + hint.appendChild(hintPulse); + hint.setAttribute("data-step", i); // we swap the hint element with target element + // because _setHelperLayerPosition uses `element` property + + item.targetElement = item.element; + item.element = hint; // align the hint position + + alignHintPosition.call(_this4, item.hintPosition, hint, item.targetElement); + hintsWrapper.appendChild(hint); + }); // adding the hints wrapper + + document.body.appendChild(hintsWrapper); // call the callback function (if any) + + if (typeof this._hintsAddedCallback !== "undefined") { + this._hintsAddedCallback.call(this); + } + } + /** + * Aligns hint position + * + * @api private + * @method alignHintPosition + * @param {String} position + * @param {Object} hint + * @param {Object} element + */ + + function alignHintPosition(position, _ref, element) { + var style = _ref.style; + // get/calculate offset of target element + var offset = getOffset.call(this, element); + var iconWidth = 20; + var iconHeight = 20; // align the hint element + + switch (position) { + default: + case "top-left": + style.left = "".concat(offset.left, "px"); + style.top = "".concat(offset.top, "px"); + break; + + case "top-right": + style.left = "".concat(offset.left + offset.width - iconWidth, "px"); + style.top = "".concat(offset.top, "px"); + break; + + case "bottom-left": + style.left = "".concat(offset.left, "px"); + style.top = "".concat(offset.top + offset.height - iconHeight, "px"); + break; + + case "bottom-right": + style.left = "".concat(offset.left + offset.width - iconWidth, "px"); + style.top = "".concat(offset.top + offset.height - iconHeight, "px"); + break; + + case "middle-left": + style.left = "".concat(offset.left, "px"); + style.top = "".concat(offset.top + (offset.height - iconHeight) / 2, "px"); + break; + + case "middle-right": + style.left = "".concat(offset.left + offset.width - iconWidth, "px"); + style.top = "".concat(offset.top + (offset.height - iconHeight) / 2, "px"); + break; + + case "middle-middle": + style.left = "".concat(offset.left + (offset.width - iconWidth) / 2, "px"); + style.top = "".concat(offset.top + (offset.height - iconHeight) / 2, "px"); + break; + + case "bottom-middle": + style.left = "".concat(offset.left + (offset.width - iconWidth) / 2, "px"); + style.top = "".concat(offset.top + offset.height - iconHeight, "px"); + break; + + case "top-middle": + style.left = "".concat(offset.left + (offset.width - iconWidth) / 2, "px"); + style.top = "".concat(offset.top, "px"); + break; + } + } + /** + * Triggers when user clicks on the hint element + * + * @api private + * @method _showHintDialog + * @param {Number} stepId + */ + + function showHintDialog(stepId) { + var hintElement = document.querySelector(".introjs-hint[data-step=\"".concat(stepId, "\"]")); + var item = this._introItems[stepId]; // call the callback function (if any) + + if (typeof this._hintClickCallback !== "undefined") { + this._hintClickCallback.call(this, hintElement, item, stepId); + } // remove all open tooltips + + + var removedStep = removeHintTooltip.call(this); // to toggle the tooltip + + if (parseInt(removedStep, 10) === stepId) { + return; + } + + var tooltipLayer = _createElement("div", { + className: "introjs-tooltip" + }); + var tooltipTextLayer = _createElement("div"); + var arrowLayer = _createElement("div"); + var referenceLayer = _createElement("div"); + + tooltipLayer.onclick = function (e) { + //IE9 & Other Browsers + if (e.stopPropagation) { + e.stopPropagation(); + } //IE8 and Lower + else { + e.cancelBubble = true; + } + }; + + tooltipTextLayer.className = "introjs-tooltiptext"; + var tooltipWrapper = _createElement("p"); + tooltipWrapper.innerHTML = item.hint; + var closeButton = _createElement("a"); + closeButton.className = this._options.buttonClass; + closeButton.setAttribute("role", "button"); + closeButton.innerHTML = this._options.hintButtonLabel; + closeButton.onclick = hideHint.bind(this, stepId); + tooltipTextLayer.appendChild(tooltipWrapper); + tooltipTextLayer.appendChild(closeButton); + arrowLayer.className = "introjs-arrow"; + tooltipLayer.appendChild(arrowLayer); + tooltipLayer.appendChild(tooltipTextLayer); // set current step for _placeTooltip function + + this._currentStep = hintElement.getAttribute("data-step"); // align reference layer position + + referenceLayer.className = "introjs-tooltipReferenceLayer introjs-hintReference"; + referenceLayer.setAttribute("data-step", hintElement.getAttribute("data-step")); + setHelperLayerPosition.call(this, referenceLayer); + referenceLayer.appendChild(tooltipLayer); + document.body.appendChild(referenceLayer); //set proper position + + placeTooltip.call(this, hintElement, tooltipLayer, arrowLayer, true); + } + /** + * Removes open hint (tooltip hint) + * + * @api private + * @method _removeHintTooltip + */ + + function removeHintTooltip() { + var tooltip = document.querySelector(".introjs-hintReference"); + + if (tooltip) { + var step = tooltip.getAttribute("data-step"); + tooltip.parentNode.removeChild(tooltip); + return step; + } + } + /** + * Start parsing hint items + * + * @api private + * @param {Object} targetElm + * @method _startHint + */ + + function populateHints(targetElm) { + var _this5 = this; + + this._introItems = []; + + if (this._options.hints) { + forEach(this._options.hints, function (hint) { + var currentItem = cloneObject(hint); + + if (typeof currentItem.element === "string") { + //grab the element with given selector from the page + currentItem.element = document.querySelector(currentItem.element); + } + + currentItem.hintPosition = currentItem.hintPosition || _this5._options.hintPosition; + currentItem.hintAnimation = currentItem.hintAnimation || _this5._options.hintAnimation; + + if (currentItem.element !== null) { + _this5._introItems.push(currentItem); + } + }); + } else { + var hints = targetElm.querySelectorAll("*[data-hint]"); + + if (!hints || !hints.length) { + return false; + } //first add intro items with data-step + + + forEach(hints, function (currentElement) { + // hint animation + var hintAnimation = currentElement.getAttribute("data-hintanimation"); + + if (hintAnimation) { + hintAnimation = hintAnimation === "true"; + } else { + hintAnimation = _this5._options.hintAnimation; + } + + _this5._introItems.push({ + element: currentElement, + hint: currentElement.getAttribute("data-hint"), + hintPosition: currentElement.getAttribute("data-hintposition") || _this5._options.hintPosition, + hintAnimation: hintAnimation, + tooltipClass: currentElement.getAttribute("data-tooltipclass"), + position: currentElement.getAttribute("data-position") || _this5._options.tooltipPosition + }); + }); + } + + addHints.call(this); + /* + todo: + these events should be removed at some point + */ + + DOMEvent.on(document, "click", removeHintTooltip, this, false); + DOMEvent.on(window, "resize", reAlignHints, this, true); + } + /** + * Re-aligns all hint elements + * + * @api private + * @method _reAlignHints + */ + + function reAlignHints() { + var _this6 = this; + + forEach(this._introItems, function (_ref2) { + var targetElement = _ref2.targetElement, + hintPosition = _ref2.hintPosition, + element = _ref2.element; + + if (typeof targetElement === "undefined") { + return; + } + + alignHintPosition.call(_this6, hintPosition, element, targetElement); + }); + } + + /** + * Update placement of the intro objects on the screen + * @api private + */ + + function refresh() { + // re-align intros + setHelperLayerPosition.call(this, document.querySelector(".introjs-helperLayer")); + setHelperLayerPosition.call(this, document.querySelector(".introjs-tooltipReferenceLayer")); + setHelperLayerPosition.call(this, document.querySelector(".introjs-disableInteraction")); // re-align tooltip + + if (this._currentStep !== undefined && this._currentStep !== null) { + var oldArrowLayer = document.querySelector(".introjs-arrow"); + var oldtooltipContainer = document.querySelector(".introjs-tooltip"); + placeTooltip.call(this, this._introItems[this._currentStep].element, oldtooltipContainer, oldArrowLayer); + } //re-align hints + + + reAlignHints.call(this); + return this; + } + + function onResize() { + refresh.call(this); + } + + /** + * Removes `element` from `parentElement` + * + * @param {Element} element + * @param {Boolean} [animate=false] + */ + + function removeChild(element, animate) { + if (!element || !element.parentElement) return; + var parentElement = element.parentElement; + + if (animate) { + setStyle(element, { + opacity: "0" + }); + window.setTimeout(function () { + parentElement.removeChild(element); + }, 500); + } else { + parentElement.removeChild(element); + } + } + + /** + * Exit from intro + * + * @api private + * @method _exitIntro + * @param {Object} targetElement + * @param {Boolean} force - Setting to `true` will skip the result of beforeExit callback + */ + + function exitIntro(targetElement, force) { + var continueExit = true; // calling onbeforeexit callback + // + // If this callback return `false`, it would halt the process + + if (this._introBeforeExitCallback !== undefined) { + continueExit = this._introBeforeExitCallback.call(this); + } // skip this check if `force` parameter is `true` + // otherwise, if `onbeforeexit` returned `false`, don't exit the intro + + + if (!force && continueExit === false) return; // remove overlay layers from the page + + var overlayLayers = targetElement.querySelectorAll(".introjs-overlay"); + + if (overlayLayers && overlayLayers.length) { + forEach(overlayLayers, function (overlayLayer) { + return removeChild(overlayLayer); + }); + } //remove all helper layers + + + var helperLayer = targetElement.querySelector(".introjs-helperLayer"); + removeChild(helperLayer, true); + var referenceLayer = targetElement.querySelector(".introjs-tooltipReferenceLayer"); + removeChild(referenceLayer); //remove disableInteractionLayer + + var disableInteractionLayer = targetElement.querySelector(".introjs-disableInteraction"); + removeChild(disableInteractionLayer); //remove intro floating element + + var floatingElement = document.querySelector(".introjsFloatingElement"); + removeChild(floatingElement); + removeShowElement(); //clean listeners + + DOMEvent.off(window, "keydown", onKeyDown, this, true); + DOMEvent.off(window, "resize", onResize, this, true); //check if any callback is defined + + if (this._introExitCallback !== undefined) { + this._introExitCallback.call(this); + } //set the step to zero + + + this._currentStep = undefined; + } + + /** + * Add overlay layer to the page + * + * @api private + * @method _addOverlayLayer + * @param {Object} targetElm + */ + + function addOverlayLayer(targetElm) { + var _this = this; + + var overlayLayer = _createElement("div", { + className: "introjs-overlay" + }); + setStyle(overlayLayer, { + top: 0, + bottom: 0, + left: 0, + right: 0, + position: "fixed" + }); + targetElm.appendChild(overlayLayer); + + if (this._options.exitOnOverlayClick === true) { + setStyle(overlayLayer, { + cursor: "pointer" + }); + + overlayLayer.onclick = function () { + exitIntro.call(_this, targetElm); + }; + } + + return true; + } + + /** + * Initiate a new introduction/guide from an element in the page + * + * @api private + * @method _introForElement + * @param {Object} targetElm + * @param {String} group + * @returns {Boolean} Success or not? + */ + + function introForElement(targetElm, group) { + var _this = this; + + var allIntroSteps = targetElm.querySelectorAll("*[data-intro]"); + var introItems = []; + + if (this._options.steps) { + //use steps passed programmatically + forEach(this._options.steps, function (step) { + var currentItem = cloneObject(step); //set the step + + currentItem.step = introItems.length + 1; + currentItem.title = currentItem.title || ""; //use querySelector function only when developer used CSS selector + + if (typeof currentItem.element === "string") { + //grab the element with given selector from the page + currentItem.element = document.querySelector(currentItem.element); + } //intro without element + + + if (typeof currentItem.element === "undefined" || currentItem.element === null) { + var floatingElementQuery = document.querySelector(".introjsFloatingElement"); + + if (floatingElementQuery === null) { + floatingElementQuery = _createElement("div", { + className: "introjsFloatingElement" + }); + document.body.appendChild(floatingElementQuery); + } + + currentItem.element = floatingElementQuery; + currentItem.position = "floating"; + } + + currentItem.scrollTo = currentItem.scrollTo || _this._options.scrollTo; + + if (typeof currentItem.disableInteraction === "undefined") { + currentItem.disableInteraction = _this._options.disableInteraction; + } + + if (currentItem.element !== null) { + introItems.push(currentItem); + } + }); + } else { + //use steps from data-* annotations + var elmsLength = allIntroSteps.length; + var disableInteraction; //if there's no element to intro + + if (elmsLength < 1) { + return false; + } + + forEach(allIntroSteps, function (currentElement) { + // PR #80 + // start intro for groups of elements + if (group && currentElement.getAttribute("data-intro-group") !== group) { + return; + } // skip hidden elements + + + if (currentElement.style.display === "none") { + return; + } + + var step = parseInt(currentElement.getAttribute("data-step"), 10); + + if (currentElement.hasAttribute("data-disable-interaction")) { + disableInteraction = !!currentElement.getAttribute("data-disable-interaction"); + } else { + disableInteraction = _this._options.disableInteraction; + } + + if (step > 0) { + introItems[step - 1] = { + element: currentElement, + title: currentElement.getAttribute("data-title") || "", + intro: currentElement.getAttribute("data-intro"), + step: parseInt(currentElement.getAttribute("data-step"), 10), + tooltipClass: currentElement.getAttribute("data-tooltipclass"), + highlightClass: currentElement.getAttribute("data-highlightclass"), + position: currentElement.getAttribute("data-position") || _this._options.tooltipPosition, + scrollTo: currentElement.getAttribute("data-scrollto") || _this._options.scrollTo, + disableInteraction: disableInteraction + }; + } + }); //next add intro items without data-step + //todo: we need a cleanup here, two loops are redundant + + var _nextStep = 0; + forEach(allIntroSteps, function (currentElement) { + // PR #80 + // start intro for groups of elements + if (group && currentElement.getAttribute("data-intro-group") !== group) { + return; + } + + if (currentElement.getAttribute("data-step") === null) { + while (true) { + if (typeof introItems[_nextStep] === "undefined") { + break; + } else { + _nextStep++; + } + } + + if (currentElement.hasAttribute("data-disable-interaction")) { + disableInteraction = !!currentElement.getAttribute("data-disable-interaction"); + } else { + disableInteraction = _this._options.disableInteraction; + } + + introItems[_nextStep] = { + element: currentElement, + title: currentElement.getAttribute("data-title") || "", + intro: currentElement.getAttribute("data-intro"), + step: _nextStep + 1, + tooltipClass: currentElement.getAttribute("data-tooltipclass"), + highlightClass: currentElement.getAttribute("data-highlightclass"), + position: currentElement.getAttribute("data-position") || _this._options.tooltipPosition, + scrollTo: currentElement.getAttribute("data-scrollto") || _this._options.scrollTo, + disableInteraction: disableInteraction + }; + } + }); + } //removing undefined/null elements + + + var tempIntroItems = []; + + for (var z = 0; z < introItems.length; z++) { + if (introItems[z]) { + // copy non-falsy values to the end of the array + tempIntroItems.push(introItems[z]); + } + } + + introItems = tempIntroItems; //Ok, sort all items with given steps + + introItems.sort(function (a, b) { + return a.step - b.step; + }); //set it to the introJs object + + this._introItems = introItems; //add overlay layer to the page + + if (addOverlayLayer.call(this, targetElm)) { + //then, start the show + nextStep.call(this); + + if (this._options.keyboardNavigation) { + DOMEvent.on(window, "keydown", onKeyDown, this, true); + } //for window resize + + + DOMEvent.on(window, "resize", onResize, this, true); + } + + return false; + } + + var version$1 = "3.4.0"; + + /** + * IntroJs main class + * + * @class IntroJs + */ + + function IntroJs(obj) { + this._targetElement = obj; + this._introItems = []; + this._options = { + /* Next button label in tooltip box */ + nextLabel: "Next", + + /* Previous button label in tooltip box */ + prevLabel: "Back", + + /* Skip button label in tooltip box */ + skipLabel: "×", + + /* Done button label in tooltip box */ + doneLabel: "Done", + + /* Hide previous button in the first step? Otherwise, it will be disabled button. */ + hidePrev: false, + + /* Hide next button in the last step? Otherwise, it will be disabled button (note: this will also hide the "Done" button) */ + hideNext: false, + + /* Change the Next button to Done in the last step of the intro? otherwise, it will render a disabled button */ + nextToDone: true, + + /* Default tooltip box position */ + tooltipPosition: "bottom", + + /* Next CSS class for tooltip boxes */ + tooltipClass: "", + + /* CSS class that is added to the helperLayer */ + highlightClass: "", + + /* Close introduction when pressing Escape button? */ + exitOnEsc: true, + + /* Close introduction when clicking on overlay layer? */ + exitOnOverlayClick: true, + + /* Show step numbers in introduction? */ + showStepNumbers: false, + + /* Let user use keyboard to navigate the tour? */ + keyboardNavigation: true, + + /* Show tour control buttons? */ + showButtons: true, + + /* Show tour bullets? */ + showBullets: true, + + /* Show tour progress? */ + showProgress: false, + + /* Scroll to highlighted element? */ + scrollToElement: true, + + /* + * Should we scroll the tooltip or target element? + * + * Options are: 'element' or 'tooltip' + */ + scrollTo: "element", + + /* Padding to add after scrolling when element is not in the viewport (in pixels) */ + scrollPadding: 30, + + /* Set the overlay opacity */ + overlayOpacity: 0.5, + + /* To determine the tooltip position automatically based on the window.width/height */ + autoPosition: true, + + /* Precedence of positions, when auto is enabled */ + positionPrecedence: ["bottom", "top", "right", "left"], + + /* Disable an interaction with element? */ + disableInteraction: false, + + /* Set how much padding to be used around helper element */ + helperElementPadding: 10, + + /* Default hint position */ + hintPosition: "top-middle", + + /* Hint button label */ + hintButtonLabel: "Got it", + + /* Adding animation to hints? */ + hintAnimation: true, + + /* additional classes to put on the buttons */ + buttonClass: "introjs-button", + + /* additional classes to put on progress bar */ + progressBarAdditionalClass: false + }; + } + + var introJs = function introJs(targetElm) { + var instance; + + if (_typeof(targetElm) === "object") { + //Ok, create a new instance + instance = new IntroJs(targetElm); + } else if (typeof targetElm === "string") { + //select the target element with query selector + var targetElement = document.querySelector(targetElm); + + if (targetElement) { + instance = new IntroJs(targetElement); + } else { + throw new Error("There is no element with given selector."); + } + } else { + instance = new IntroJs(document.body); + } // add instance to list of _instances + // passing group to stamp to increment + // from 0 onward somewhat reliably + + + introJs.instances[stamp(instance, "introjs-instance")] = instance; + return instance; + }; + /** + * Current IntroJs version + * + * @property version + * @type String + */ + + + introJs.version = version$1; + /** + * key-val object helper for introJs instances + * + * @property instances + * @type Object + */ + + introJs.instances = {}; //Prototype + + introJs.fn = IntroJs.prototype = { + clone: function clone() { + return new IntroJs(this); + }, + setOption: function setOption(option, value) { + this._options[option] = value; + return this; + }, + setOptions: function setOptions(options) { + this._options = mergeOptions(this._options, options); + return this; + }, + start: function start(group) { + introForElement.call(this, this._targetElement, group); + return this; + }, + goToStep: function goToStep$1(step) { + goToStep.call(this, step); + + return this; + }, + addStep: function addStep(options) { + if (!this._options.steps) { + this._options.steps = []; + } + + this._options.steps.push(options); + + return this; + }, + addSteps: function addSteps(steps) { + if (!steps.length) return; + + for (var index = 0; index < steps.length; index++) { + this.addStep(steps[index]); + } + + return this; + }, + goToStepNumber: function goToStepNumber$1(step) { + goToStepNumber.call(this, step); + + return this; + }, + nextStep: function nextStep$1() { + nextStep.call(this); + + return this; + }, + previousStep: function previousStep$1() { + previousStep.call(this); + + return this; + }, + currentStep: function currentStep$1() { + return currentStep.call(this); + }, + exit: function exit(force) { + exitIntro.call(this, this._targetElement, force); + return this; + }, + refresh: function refresh$1() { + refresh.call(this); + + return this; + }, + onbeforechange: function onbeforechange(providedCallback) { + if (typeof providedCallback === "function") { + this._introBeforeChangeCallback = providedCallback; + } else { + throw new Error("Provided callback for onbeforechange was not a function"); + } + + return this; + }, + onchange: function onchange(providedCallback) { + if (typeof providedCallback === "function") { + this._introChangeCallback = providedCallback; + } else { + throw new Error("Provided callback for onchange was not a function."); + } + + return this; + }, + onafterchange: function onafterchange(providedCallback) { + if (typeof providedCallback === "function") { + this._introAfterChangeCallback = providedCallback; + } else { + throw new Error("Provided callback for onafterchange was not a function"); + } + + return this; + }, + oncomplete: function oncomplete(providedCallback) { + if (typeof providedCallback === "function") { + this._introCompleteCallback = providedCallback; + } else { + throw new Error("Provided callback for oncomplete was not a function."); + } + + return this; + }, + onhintsadded: function onhintsadded(providedCallback) { + if (typeof providedCallback === "function") { + this._hintsAddedCallback = providedCallback; + } else { + throw new Error("Provided callback for onhintsadded was not a function."); + } + + return this; + }, + onhintclick: function onhintclick(providedCallback) { + if (typeof providedCallback === "function") { + this._hintClickCallback = providedCallback; + } else { + throw new Error("Provided callback for onhintclick was not a function."); + } + + return this; + }, + onhintclose: function onhintclose(providedCallback) { + if (typeof providedCallback === "function") { + this._hintCloseCallback = providedCallback; + } else { + throw new Error("Provided callback for onhintclose was not a function."); + } + + return this; + }, + onexit: function onexit(providedCallback) { + if (typeof providedCallback === "function") { + this._introExitCallback = providedCallback; + } else { + throw new Error("Provided callback for onexit was not a function."); + } + + return this; + }, + onskip: function onskip(providedCallback) { + if (typeof providedCallback === "function") { + this._introSkipCallback = providedCallback; + } else { + throw new Error("Provided callback for onskip was not a function."); + } + + return this; + }, + onbeforeexit: function onbeforeexit(providedCallback) { + if (typeof providedCallback === "function") { + this._introBeforeExitCallback = providedCallback; + } else { + throw new Error("Provided callback for onbeforeexit was not a function."); + } + + return this; + }, + addHints: function addHints() { + populateHints.call(this, this._targetElement); + return this; + }, + hideHint: function hideHint$1(stepId) { + hideHint.call(this, stepId); + + return this; + }, + hideHints: function hideHints$1() { + hideHints.call(this); + + return this; + }, + showHint: function showHint$1(stepId) { + showHint.call(this, stepId); + + return this; + }, + showHints: function showHints$1() { + showHints.call(this); + + return this; + }, + removeHints: function removeHints$1() { + removeHints.call(this); + + return this; + }, + removeHint: function removeHint$1(stepId) { + removeHint().call(this, stepId); + + return this; + }, + showHintDialog: function showHintDialog$1(stepId) { + showHintDialog.call(this, stepId); + + return this; + } + }; + + return introJs; + +}))); diff --git a/wp-content/plugins/the-events-calendar/common/node_modules/intro.js/introjs-rtl.css b/wp-content/plugins/the-events-calendar/common/node_modules/intro.js/introjs-rtl.css new file mode 100644 index 000000000..60249e211 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/node_modules/intro.js/introjs-rtl.css @@ -0,0 +1,23 @@ +.introjs-tooltipbuttons { + text-align: left; } + +.introjs-skipbutton { + margin-left: 5px; + float: left; } + +.introjs-tooltip-title { + float: right; } + +.introjs-tooltip { + direction: rtl; } + +.introjs-prevbutton { + border: 1px solid #d4d4d4; + float: right; } + +.introjs-nextbutton { + border: 1px solid #d4d4d4; + float: left; } + +.introjs-bullets ul li { + float: right; } diff --git a/wp-content/plugins/the-events-calendar/common/node_modules/intro.js/introjs.css b/wp-content/plugins/the-events-calendar/common/node_modules/intro.js/introjs.css new file mode 100644 index 000000000..1e8ffe7dd --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/node_modules/intro.js/introjs.css @@ -0,0 +1,436 @@ +/* + Buttons style by http://nicolasgallagher.com/lab/css3-github-buttons/ + Changed by Afshin Mehrabani +*/ +/* overrides extra padding on button elements in Firefox */ +@-webkit-keyframes introjspulse { + 0% { + -webkit-transform: scale(0); + transform: scale(0); + opacity: 0; } + 25% { + -webkit-transform: scale(0); + transform: scale(0); + opacity: 0.1; } + 50% { + -webkit-transform: scale(0.1); + transform: scale(0.1); + opacity: 0.3; } + 75% { + -webkit-transform: scale(0.5); + transform: scale(0.5); + opacity: 0.5; } + 100% { + -webkit-transform: scale(1); + transform: scale(1); + opacity: 0; } } +@keyframes introjspulse { + 0% { + -webkit-transform: scale(0); + transform: scale(0); + opacity: 0; } + 25% { + -webkit-transform: scale(0); + transform: scale(0); + opacity: 0.1; } + 50% { + -webkit-transform: scale(0.1); + transform: scale(0.1); + opacity: 0.3; } + 75% { + -webkit-transform: scale(0.5); + transform: scale(0.5); + opacity: 0.5; } + 100% { + -webkit-transform: scale(1); + transform: scale(1); + opacity: 0; } } + +.introjs-overlay { + position: absolute; + -webkit-box-sizing: content-box; + box-sizing: content-box; + z-index: 999999; + opacity: 0; + -webkit-transition: all 0.3s ease-out; + -o-transition: all 0.3s ease-out; + transition: all 0.3s ease-out; } + +.introjs-showElement { + z-index: 9999999 !important; } + +tr.introjs-showElement > td { + z-index: 9999999 !important; + position: relative; } + +tr.introjs-showElement > th { + z-index: 9999999 !important; + position: relative; } + +.introjs-disableInteraction { + z-index: 99999999 !important; + position: absolute; + background-color: #ffffff; + opacity: 0; + filter: alpha(opacity=0); } + +.introjs-relativePosition { + position: relative; } + +.introjs-helperLayer { + -webkit-box-sizing: content-box; + box-sizing: content-box; + position: absolute; + z-index: 9999998; + border-radius: 4px; + -webkit-transition: all 0.3s ease-out; + -o-transition: all 0.3s ease-out; + transition: all 0.3s ease-out; } + .introjs-helperLayer * { + -webkit-box-sizing: content-box; + box-sizing: content-box; } + .introjs-helperLayer *:before { + -webkit-box-sizing: content-box; + box-sizing: content-box; } + .introjs-helperLayer *:after { + -webkit-box-sizing: content-box; + box-sizing: content-box; } + +.introjs-tooltipReferenceLayer { + font-family: "Helvetica Neue", Inter, ui-sans-serif, "Apple Color Emoji", Helvetica, Arial, sans-serif; + -webkit-box-sizing: content-box; + box-sizing: content-box; + position: absolute; + visibility: hidden; + z-index: 100000000; + background-color: transparent; + -webkit-transition: all 0.3s ease-out; + -o-transition: all 0.3s ease-out; + transition: all 0.3s ease-out; } + .introjs-tooltipReferenceLayer * { + font-family: "Helvetica Neue", Inter, ui-sans-serif, "Apple Color Emoji", Helvetica, Arial, sans-serif; } + +.introjs-helperNumberLayer { + font-family: "Helvetica Neue", Inter, ui-sans-serif, "Apple Color Emoji", Helvetica, Arial, sans-serif; + color: #9e9e9e; + text-align: center; + padding-top: 10px; + padding-bottom: 10px; } + +.introjs-arrow { + border: 5px solid transparent; + content: ""; + position: absolute; } + +.introjs-arrow.top { + top: -10px; + left: 10px; + border-bottom-color: #ffffff; } + +.introjs-arrow.top-right { + top: -10px; + right: 10px; + border-bottom-color: #ffffff; } + +.introjs-arrow.top-middle { + top: -10px; + left: 50%; + margin-left: -5px; + border-bottom-color: #ffffff; } + +.introjs-arrow.right { + right: -10px; + top: 10px; + border-left-color: #ffffff; } + +.introjs-arrow.right-bottom { + bottom: 10px; + right: -10px; + border-left-color: #ffffff; } + +.introjs-arrow.bottom { + bottom: -10px; + left: 10px; + border-top-color: #ffffff; } + +.introjs-arrow.bottom-right { + bottom: -10px; + right: 10px; + border-top-color: #ffffff; } + +.introjs-arrow.bottom-middle { + bottom: -10px; + left: 50%; + margin-left: -5px; + border-top-color: #ffffff; } + +.introjs-arrow.left { + left: -10px; + top: 10px; + border-right-color: #ffffff; } + +.introjs-arrow.left-bottom { + left: -10px; + bottom: 10px; + border-right-color: #ffffff; } + +.introjs-tooltip { + -webkit-box-sizing: content-box; + box-sizing: content-box; + position: absolute; + visibility: visible; + background-color: #ffffff; + min-width: 250px; + max-width: 300px; + border-radius: 5px; + -webkit-box-shadow: 0 3px 30px rgba(33, 33, 33, 0.3); + box-shadow: 0 3px 30px rgba(33, 33, 33, 0.3); + -webkit-transition: opacity 0.1s ease-out; + -o-transition: opacity 0.1s ease-out; + transition: opacity 0.1s ease-out; } + +.introjs-tooltiptext { + padding: 20px; } + +.introjs-tooltip-title { + font-size: 18px; + margin: 0; + padding: 0; + font-weight: 700; + float: left; + line-height: 32px; } + +.introjs-tooltip-header { + padding-left: 20px; + padding-right: 20px; + padding-top: 10px; } + .introjs-tooltip-header:after { + content: "."; + visibility: hidden; + display: block; + height: 0; + clear: both; } + +.introjs-tooltipbuttons { + border-top: 1px solid #e0e0e0; + padding: 10px; + text-align: right; + white-space: nowrap; } + .introjs-tooltipbuttons:after { + content: ""; + visibility: hidden; + display: block; + height: 0; + clear: both; } + +.introjs-button { + -webkit-box-sizing: content-box; + box-sizing: content-box; + position: relative; + overflow: visible; + display: inline-block; + padding: 0.5rem 1rem; + border: 1px solid #bdbdbd; + text-decoration: none; + text-shadow: 1px 1px 0 #ffffff; + font-size: 14px; + color: #424242; + white-space: nowrap; + cursor: pointer; + outline: none; + background-color: #f4f4f4; + border-radius: 0.2em; + zoom: 1; + *display: inline; } + .introjs-button:hover { + outline: none; + text-decoration: none; + border-color: #9e9e9e; + background-color: #e0e0e0; + color: #212121; } + .introjs-button:focus { + outline: none; + text-decoration: none; + background-color: #eeeeee; + -webkit-box-shadow: 0 0 0 0.2rem rgba(158, 158, 158, 0.5); + box-shadow: 0 0 0 0.2rem rgba(158, 158, 158, 0.5); + border: 1px solid #616161; + color: #212121; } + .introjs-button:active { + outline: none; + text-decoration: none; + background-color: #e0e0e0; + border-color: #9e9e9e; + color: #212121; } + .introjs-button::-moz-focus-inner { + padding: 0; + border: 0; } + +.introjs-skipbutton { + -webkit-box-sizing: content-box; + box-sizing: content-box; + color: #616161; + float: right; + font-size: 20px; + cursor: pointer; + font-weight: bold; + line-height: 1; + text-align: center; + padding: 7px 10px; } + .introjs-skipbutton:hover, .introjs-skipbutton:focus { + color: #212121; + outline: none; + text-decoration: none; } + +.introjs-prevbutton { + float: left; } + +.introjs-nextbutton { + float: right; } + +.introjs-disabled { + color: #9e9e9e; + border-color: #bdbdbd; + -webkit-box-shadow: none; + box-shadow: none; + cursor: default; + background-color: #f4f4f4; + background-image: none; + text-decoration: none; } + .introjs-disabled:hover, .introjs-disabled:focus { + color: #9e9e9e; + border-color: #bdbdbd; + -webkit-box-shadow: none; + box-shadow: none; + cursor: default; + background-color: #f4f4f4; + background-image: none; + text-decoration: none; } + +.introjs-hidden { + display: none; } + +.introjs-bullets { + text-align: center; + padding-top: 10px; + padding-bottom: 10px; } + .introjs-bullets ul { + -webkit-box-sizing: content-box; + box-sizing: content-box; + clear: both; + margin: 0 auto 0; + padding: 0; + display: inline-block; } + .introjs-bullets ul li { + -webkit-box-sizing: content-box; + box-sizing: content-box; + list-style: none; + float: left; + margin: 0 2px; } + .introjs-bullets ul li a { + -webkit-transition: width 0.1s ease-in; + -o-transition: width 0.1s ease-in; + transition: width 0.1s ease-in; + -webkit-box-sizing: content-box; + box-sizing: content-box; + display: block; + width: 6px; + height: 6px; + background: #ccc; + border-radius: 10px; + text-decoration: none; + cursor: pointer; } + .introjs-bullets ul li a:hover, .introjs-bullets ul li a:focus { + width: 15px; + background: #999; + text-decoration: none; + outline: none; } + .introjs-bullets ul li a.active { + width: 15px; + background: #999; } + +.introjs-progress { + -webkit-box-sizing: content-box; + box-sizing: content-box; + overflow: hidden; + height: 10px; + margin: 10px; + border-radius: 4px; + background-color: #e0e0e0; } + +.introjs-progressbar { + -webkit-box-sizing: content-box; + box-sizing: content-box; + float: left; + width: 0%; + height: 100%; + font-size: 10px; + line-height: 10px; + text-align: center; + background-color: #08c; } + +.introjsFloatingElement { + position: absolute; + height: 0; + width: 0; + left: 50%; + top: 50%; } + +.introjs-fixedTooltip { + position: fixed; } + +.introjs-hint { + -webkit-box-sizing: content-box; + box-sizing: content-box; + position: absolute; + background: transparent; + width: 20px; + height: 15px; + cursor: pointer; } + .introjs-hint:focus { + border: 0; + outline: 0; } + .introjs-hint:hover > .introjs-hint-pulse { + border: 5px solid rgba(60, 60, 60, 0.57); } + +.introjs-hidehint { + display: none; } + +.introjs-fixedhint { + position: fixed; } + +.introjs-hint-pulse { + -webkit-box-sizing: content-box; + box-sizing: content-box; + width: 10px; + height: 10px; + border: 5px solid rgba(60, 60, 60, 0.27); + border-radius: 30px; + background-color: rgba(136, 136, 136, 0.24); + z-index: 10; + position: absolute; + -webkit-transition: all 0.2s ease-out; + -o-transition: all 0.2s ease-out; + transition: all 0.2s ease-out; } + +.introjs-hint-no-anim .introjs-hint-dot { + -webkit-animation: none; + animation: none; } + +.introjs-hint-dot { + -webkit-box-sizing: content-box; + box-sizing: content-box; + border: 10px solid rgba(146, 146, 146, 0.36); + background: transparent; + border-radius: 60px; + height: 50px; + width: 50px; + -webkit-animation: introjspulse 3s ease-out; + animation: introjspulse 3s ease-out; + -webkit-animation-iteration-count: infinite; + animation-iteration-count: infinite; + position: absolute; + top: -25px; + left: -25px; + z-index: 1; + opacity: 0; } diff --git a/wp-content/plugins/the-events-calendar/common/src/Common/Configuration/Configuration.php b/wp-content/plugins/the-events-calendar/common/src/Common/Configuration/Configuration.php new file mode 100644 index 000000000..6d508c08e --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Common/Configuration/Configuration.php @@ -0,0 +1,77 @@ +loader = $loader; + } + + /** + * @inheritDoc + */ + public function all(): array { + $configs = []; + foreach ( $this->loader->all() as $provider ) { + $configs = array_merge( $configs, $provider->all() ); + } + + return $configs; + } + + /** + * @inheritDoc + */ + public function get( $key ) { + foreach ( $this->loader->all() as $provider ) { + if ( $provider->has( $key ) ) { + return $provider->get( $key ); + } + } + + return null; + } + + /** + * @inheritDoc + */ + public function has( $key ): bool { + foreach ( $this->loader->all() as $provider ) { + if ( $provider->has( $key ) ) { + return true; + } + } + + return false; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Common/Configuration/Configuration_Loader.php b/wp-content/plugins/the-events-calendar/common/src/Common/Configuration/Configuration_Loader.php new file mode 100644 index 000000000..d99fa3d0d --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Common/Configuration/Configuration_Loader.php @@ -0,0 +1,67 @@ + + */ + protected static $providers = []; + + /** + * Add a var provider to the list of providers referenced when accessing a variable + * from within the Configuration object. + * + * @since 5.1.3 + * + * @param Configuration_Provider_Interface $provider + * + * @return $this + */ + public function add( Configuration_Provider_Interface $provider ): self { + if ( is_callable( [ $provider, 'register' ] ) ) { + $provider->register(); + } + self::$providers[] = $provider; + + return $this; + } + + /** + * Retrieve a list of all Configuration_Provider_Interface providers loaded. + * + * @since 5.1.3 + * + * @return Configuration_Provider_Interface[] + */ + public function all(): array { + return self::$providers; + } + + /** + * Remove the providers. + * + * @since 5.1.3 + * + * @return $this + */ + public function reset(): self { + self::$providers = []; + + return $this; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Common/Configuration/Configuration_Provider_Interface.php b/wp-content/plugins/the-events-calendar/common/src/Common/Configuration/Configuration_Provider_Interface.php new file mode 100644 index 000000000..f29645ce7 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Common/Configuration/Configuration_Provider_Interface.php @@ -0,0 +1,50 @@ +has( $key ) ) { + return constant( $key ); + } + return null; + } + + /** + * @inheritDoc + */ + public function all(): array { + return get_defined_constants( false ); + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Common/Configuration/readme.md b/wp-content/plugins/the-events-calendar/common/src/Common/Configuration/readme.md new file mode 100644 index 000000000..2735f1664 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Common/Configuration/readme.md @@ -0,0 +1,60 @@ +# System Configuration + +Provides a system-wide set of configuration values. Easily access feature flags, and other immutable configurations. + +Inspired by systems that load configurations from various sources, like retrieving `conf.ini` or `.env` values. + +## Setup + +Add a configuration loader, so the system knows where to get the configuration values from. + +```php +// Constants_Provider.php +class Constants_Provider implements Configuration_Provider_Interface { + + public function has( $key ): bool { + return defined( $key ); + } + + public function get( $key ) { + if ( $this->has( $key ) ) { + + return constant( $key ); + } + + return null; + } + + public function all(): array { + return get_defined_constants( false ); + } +} +``` +```php +// Provider.php + +class Provider { + protected function register(): void { + // Can add other loaders with other configuration values, such as local vs prod configurations. + tribe( Configuration_Loader::class )->add( new Constants_Provider() ) + } +} +``` + +This is an extensible loader to allow various configuration sources and application specific logic to bind configuration providers in different ways. + +## Retrieve Configuration Value + +```php +// wp-config.php +define('TEC_FEATURE_FLAG', true); +``` +```php +// Model.php +public function tec_magic() { + // Feature enabled? + if ( tribe( Configuration::class )->get( 'TEC_FEATURE_FLAG' ) ) { + // do stuff... + } +} +``` \ No newline at end of file diff --git a/wp-content/plugins/the-events-calendar/common/src/Common/Context/Post_Request_Type.php b/wp-content/plugins/the-events-calendar/common/src/Common/Context/Post_Request_Type.php new file mode 100644 index 000000000..eddd0ebea --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Common/Context/Post_Request_Type.php @@ -0,0 +1,156 @@ + $post_type The post type or post types to check. + * + * @return bool Whether the current request is one to quick edit a single post of the specified post type or not. + */ + public function is_inline_editing_post( $post_type ): bool { + if ( ! ( ! empty( $post_type ) && wp_doing_ajax() && tribe_get_request_var( 'action' ) === 'inline-save' ) ) { + return false; + } + + $post_id = tribe_get_request_var( 'post_ID', null ); + + if ( empty( $post_id ) || ! is_numeric( $post_id ) ) { + return false; + } + + return in_array( get_post_type( $post_id ), (array) $post_type, true ); + } + + /** + * Whether the current request is one to edit a list of the specified post types or not. + * + * The admin edit screen for a post type is the one that lists all the posts of that typ, + * it has the URL `/wp-admin/edit.php?post_type=`. + * + * @since 5.0.13 + * + * @param string|array $post_type The post type or post types to check. + * + * @return bool Whether the current request is one to edit a list of the specified post types or not. + */ + public function is_editing_post_list( $post_type ): bool { + // Quick check: are we on the `/wp-admin/edit.php` page? + global $pagenow; + + if ( $pagenow !== 'edit.php' ) { + return false; + } + + // Run some more thorough checks for the post type(s). + $post_types = array_filter( (array) $post_type ); + + return $this->is_editing_post( $post_types ); + } + + /** + * Whether we are currently creating a new post, a post of post type(s) or not. + * + * @since 4.7.7 + * + * @param null $post_type The optional post type to check. + * + * @return bool Whether we are currently creating a new post, a post of post type(s) or not. + */ + public function is_new_post( $post_type = null ): bool { + global $pagenow; + $is_new = 'post-new.php' === $pagenow; + + return $is_new && $this->is_editing_post( $post_type ); + } + + + /** + * Whether we are currently editing a post(s), post type(s) or not. + * + * @since 4.7.7 + * + * @param null|array|string|int $post_or_type A post ID, post type, an array of post types or post IDs, `null` + * to just make sure we are currently editing a post. + * + * @return bool + */ + public function is_editing_post( $post_or_type = null ): bool { + global $pagenow; + $is_new = 'post-new.php' === $pagenow; + $is_post = 'post.php' === $pagenow; + $is_editing = 'edit.php' === $pagenow; + + if ( ! ( $is_new || $is_post || $is_editing ) ) { + return false; + } + + if ( ! empty( $post_or_type ) ) { + $lookup = []; + // Prevent a slew of warnings every time we call this. + if ( isset( $_REQUEST ) ) { + $lookup[] = (array) $_REQUEST; + } + + if ( isset( $_GET ) ) { + $lookup[] = (array) $_GET; + } + + if ( isset( $_POST ) ) { + $lookup[] = (array) $_POST; + } + + if ( empty( $lookup ) ) { + return false; + } + + $current_post = Arr::get_in_any( $lookup, 'post', get_post() ); + + if ( is_numeric( $post_or_type ) ) { + $post = $is_post ? get_post( $post_or_type ) : null; + + return ! empty( $post ) && $post == $current_post; + } + + $post_types = is_array( $post_or_type ) ? $post_or_type : [ $post_or_type ]; + + $post = $is_post ? get_post( $current_post ) : null; + + if ( count( array_filter( $post_types, 'is_numeric' ) ) === count( $post_types ) ) { + return ! empty( $post ) && in_array( $post->ID, $post_types ); + } + + if ( $is_post && $post instanceof WP_Post ) { + $post_type = $post->post_type; + } else { + $post_type = Arr::get_in_any( $lookup, 'post_type', 'post' ); + } + + return (bool) count( array_intersect( $post_types, [ $post_type ] ) ); + } + + return $is_new || $is_post; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Common/Contracts/Container.php b/wp-content/plugins/the-events-calendar/common/src/Common/Contracts/Container.php new file mode 100644 index 000000000..1ff22f189 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Common/Contracts/Container.php @@ -0,0 +1,30 @@ +getMessage(), $e->getCode() ); + } + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Common/Contracts/Provider/Controller.php b/wp-content/plugins/the-events-calendar/common/src/Common/Contracts/Provider/Controller.php new file mode 100644 index 000000000..5db034f84 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Common/Contracts/Provider/Controller.php @@ -0,0 +1,147 @@ +container->singleton( static::class, $this ); + + if ( ! $this->is_active() ) { + return; + } + + $this->container->setVar( static::class . '_registered', true ); + + $this->do_register(); + } + + /** + * Registers the filters and actions hooks added by the controller. + * + * @since 5.0.17 + * + * @return void + */ + abstract protected function do_register(): void; + + /** + * Removes the filters and actions hooks added by the controller. + * + * Bound implementations should not be removed in this method! + * + * @since 5.0.17 + * + * @return void Filters and actions hooks added by the controller are be removed. + */ + abstract public function unregister(): void; + + /** + * Whether the controller is active or not. + * + * Controllers will be active by default, if that is not the case, the controller should override this method. + * + * @since 5.0.17 + * + * @return bool Whether the controller is active or not. + */ + public function is_active(): bool { + return true; + } + + /** + * Logs a message at the `debug` level. + * + * @since 5.0.17 + * + * @param string $message The message to log. + * @param array $context An array of context to log with the message. + * + * @return void The message is logged. + */ + protected function debug( string $message, array $context = [] ): void { + do_action( 'tribe_log', Log::DEBUG, $message, array_merge( [ + 'controller' => static::class, + ], $context ) ); + } + + /** + * Logs a message at the `warning` level. + * + * @since 5.0.17 + * + * @param string $message The message to log. + * @param array $context An array of context to log with the message. + * + * @return void The message is logged. + */ + protected function warning( string $message, array $context = [] ): void { + do_action( 'tribe_log', Log::WARNING, $message, array_merge( [ + 'controller' => static::class, + ], $context ) ); + } + + /** + * Logs a message at the `error` level. + * + * @since 5.0.17 + * + * @param string $message The message to log. + * @param array $context An array of context to log with the message. + * + * @return void The message is logged. + */ + protected function error( string $message, array $context = [] ): void { + do_action( 'tribe_log', Log::ERROR, $message, array_merge( [ + 'controller' => static::class, + ], $context ) ); + } + + /** + * Returns whether any instance of this controller has been registered or not. + * + * @since 5.0.17 + * + * @return bool Whether any instance of this controller has been registered or not. + */ + public static function is_registered(): bool { + return (bool) tribe()->getVar( static::class . '_registered' ); + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Common/Contracts/Service_Provider.php b/wp-content/plugins/the-events-calendar/common/src/Common/Contracts/Service_Provider.php new file mode 100644 index 000000000..29d3162b0 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Common/Contracts/Service_Provider.php @@ -0,0 +1,9 @@ +> $blocks Array of parsed block objects. + * + * @return array> Block references to the passed blocks and their inner blocks. + */ + public static function flatten_blocks( &$blocks ) { + $all_blocks = []; + $queue = []; + + foreach ( $blocks as &$block ) { + $queue[] = &$block; + } + + $queue_count = count( $queue ); + + while ( $queue_count > 0 ) { + $block = &$queue[0]; + array_shift( $queue ); + $all_blocks[] = &$block; + + if ( ! empty( $block['innerBlocks'] ) ) { + foreach ( $block['innerBlocks'] as &$inner_block ) { + $queue[] = &$inner_block; + } + } + + $queue_count = count( $queue ); + } + + return $all_blocks; + } + + /** + * Parses wp_template content and injects the current theme's stylesheet as a theme attribute into + * each wp_template_part. + * + * @since 4.14.18 + * + * @param string $template_content serialized wp_template content. + * + * @return string Updated wp_template content. + */ + public static function inject_theme_attribute_in_content( $template_content ) { + $has_updated_content = false; + $new_content = ''; + $template_blocks = parse_blocks( $template_content ); + + $blocks = static::flatten_blocks( $template_blocks ); + foreach ( $blocks as &$block ) { + if ( + 'core/template-part' === $block['blockName'] && + ! isset( $block['attrs']['theme'] ) + ) { + $block['attrs']['theme'] = wp_get_theme()->get_stylesheet(); + $has_updated_content = true; + } + } + + if ( $has_updated_content ) { + foreach ( $template_blocks as &$block ) { + $new_content .= serialize_block( $block ); + } + + return $new_content; + } + + return $template_content; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Common/Exceptions/Container_Exception.php b/wp-content/plugins/the-events-calendar/common/src/Common/Exceptions/Container_Exception.php new file mode 100644 index 000000000..679943066 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Common/Exceptions/Container_Exception.php @@ -0,0 +1,16 @@ +container->singleton( self::class, self::class ); + + // Prevents any loading in case we shouldn't load. + if ( ! $this->should_load() ) { + return; + } + + $this->load(); + } + + /** + * Gets the slug for this integration parent, the main plugin that is being integrated to. + * + * @since 5.1.1 + * + * @return string + */ + abstract public static function get_parent(): string; + + /** + * Gets the slug for this integration. + * + * @since 5.1.1 + * + * @return string + */ + abstract public static function get_slug(): string; + + /** + * Determines whether this integration should load. + * + * @since 5.1.1 + * + * @return bool + */ + public function should_load(): bool { + return $this->filter_should_load( $this->load_conditionals() ); + } + + /** + * Filters whether the integration should load. + * + * @since 5.1.1 + * + * @param bool $value Whether the integration should load. + * + * @return bool + */ + protected function filter_should_load( bool $value ): bool { + $parent = static::get_parent(); + $slug = static::get_slug(); + $type = static::get_type(); + + /** + * Filters if integrations should be loaded. + * + * @since 5.1.1 + * + * @param bool $value Whether the integration should load. + * @param string $type Type of integration we are loading. + * @param string $slug Slug of the integration we are loading. + */ + $value = apply_filters( 'tec_integration:should_load', $value, $parent, $type, $slug ); + + /** + * Filters if integrations should be loaded. + * + * @since 5.1.1 + * + * @param bool $value Whether the integration should load. + * @param string $type Type of integration we are loading. + * @param string $slug Slug of the integration we are loading. + */ + $value = apply_filters( "tec_integration:{$parent}/should_load", $value, $type, $slug ); + + /** + * Filters if integrations of the current type should be loaded. + * + * @since 5.1.1 + * + * @param bool $value Whether the integration should load. + * @param string $slug Slug of the integration we are loading. + */ + $value = apply_filters( "tec_integration:{$parent}/{$type}/should_load", $value, $slug ); + + /** + * Filters if a specific integration (by type and slug) should be loaded. + * + * @since 5.1.1 + * + * @param bool $value Whether the integration should load. + */ + return (bool) apply_filters( "tec_integration:{$parent}/{$type}/{$slug}/should_load", $value ); + } + + /** + * Determines if the integration in question should be loaded. + * + * @since 5.1.1 + * + * @return bool + */ + abstract public function load_conditionals(): bool; + + /** + * Loads the integration itself. + * + * @since 5.1.1 + * + * @return void + */ + abstract protected function load(): void; + + /** + * Determines the integration type. + * + * @since 5.1.1 + * + * @return string + */ + abstract public static function get_type(): string; +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Common/Integrations/Provider.php b/wp-content/plugins/the-events-calendar/common/src/Common/Integrations/Provider.php new file mode 100644 index 000000000..60535e663 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Common/Integrations/Provider.php @@ -0,0 +1,27 @@ +container->singleton( static::class, $this ); + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Common/Integrations/Traits/Module_Integration.php b/wp-content/plugins/the-events-calendar/common/src/Common/Integrations/Traits/Module_Integration.php new file mode 100644 index 000000000..f3a053446 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Common/Integrations/Traits/Module_Integration.php @@ -0,0 +1,16 @@ +container->singleton( static::class, $this ); + + $hook_prefix = $this->container->make( Libraries\Provider::class )->get_hook_prefix(); + + try { + Installer\Config::set_hook_prefix( $hook_prefix ); + } catch ( \Exception $e ) { + // Ignore. + } + + add_filter( "stellarwp/installer/{$hook_prefix}/button_classes", [ $this, 'filter_button_classes' ] ); + } + + /** + * Filters the installer button classes. + * + * @since 5.0.10 + * + * @param array|mixed $classes The button classes. + * + * @return array + */ + public function filter_button_classes( $classes ) { + if ( ! is_array( $classes ) ) { + $classes = (array) $classes; + } + + $classes[] = 'components-button'; + $classes[] = 'is-primary'; + $classes[] = 'tec-admin__notice-install-content-button'; + return $classes; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Common/Libraries/Provider.php b/wp-content/plugins/the-events-calendar/common/src/Common/Libraries/Provider.php new file mode 100644 index 000000000..95c7a036e --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Common/Libraries/Provider.php @@ -0,0 +1,43 @@ +container->singleton( static::class, $this ); + + tribe_register_provider( Installer\Provider::class ); + + DB\Config::setHookPrefix( $this->get_hook_prefix() ); + } + + /** + * Gets the hook prefix. + * + * @since 5.0.10 + * + * @return string + */ + public function get_hook_prefix(): string { + return static::$hook_prefix; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Common/Site_Health/Factory.php b/wp-content/plugins/the-events-calendar/common/src/Common/Site_Health/Factory.php new file mode 100644 index 000000000..b8be57896 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Common/Site_Health/Factory.php @@ -0,0 +1,100 @@ + + */ + public function get_sections(): array { + /** + * Allows filtering of the Common Info Sections. + * + * @since 5.1.0 + * + * @param array $sections Which sections exist. + */ + $sections = (array) apply_filters( 'tec_debug_info_sections', [] ); + + return array_filter( $sections, static function ( $section ) { + return $section instanceof Info_Section_Abstract; + } ); + } + + /** + * Gets the section after which we will insert all the factory-generated sections. + * + * @since 5.1.0 + * + * @return string + */ + protected function get_insert_after_section_key(): string { + return 'wp-media'; + } + + /** + * Filters the actual site health data to include our sections. + * + * @since 5.1.0 + * + * @param array $info + * + * @return array + */ + public function filter_include_info_sections( array $info = [] ) { + $sections = []; + foreach ( $this->get_sections() as $key => $section ) { + $sections[ $key ] = $section->to_array(); + } + + $info = Tribe__Main::array_insert_after_key( $this->get_insert_after_section_key(), $info, $sections ); + + return $info; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Common/Site_Health/Fields/Generic_Info_Field.php b/wp-content/plugins/the-events-calendar/common/src/Common/Site_Health/Fields/Generic_Info_Field.php new file mode 100644 index 000000000..96416649d --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Common/Site_Health/Fields/Generic_Info_Field.php @@ -0,0 +1,61 @@ +|string|null $value + * @param int $priority + */ + public function __construct( string $id, string $label, $value = null, int $priority = 50 ) { + $this->id = $id; + $this->label = $label; + $this->value = $value; + $this->priority = $priority; + $this->is_private = true; + $this->debug = false; + } + + /** + * Given an array of configurations sets up a new generic field instance. + * + * @since 5.1.0 + * + * @param array $field + * + * @return Info_Field_Abstract + */ + public static function from_array( array $field ): Info_Field_Abstract { + return new static( $field['id'], $field['label'], $field['value'], $field['priority'] ); + } + + /** + * Given all the arguments create a new Generic Field. + * + * @since 5.1.0 + * + * @param string $id + * @param string $label + * @param string|null $value + * @param int $priority + * + * @return Info_Field_Abstract + */ + public static function from_args( string $id, string $label, ?string $value, int $priority = 50 ): Info_Field_Abstract { + return new static( $id, $label, $value, $priority ); + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Common/Site_Health/Fields/Post_Status_Count_Field.php b/wp-content/plugins/the-events-calendar/common/src/Common/Site_Health/Fields/Post_Status_Count_Field.php new file mode 100644 index 000000000..6809dbe6d --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Common/Site_Health/Fields/Post_Status_Count_Field.php @@ -0,0 +1,102 @@ +|string|null $value + * @param int $priority + */ + public function __construct( string $id, string $post_type = null, int $priority = 50 ) { + $this->id = $id; + $this->post_type = $post_type; + $this->priority = $priority; + $this->is_private = true; + $this->debug = false; + } + + /** + * @inheritDoc + */ + public function get_label(): string { + $post_type_obj = get_post_type_object( $this->post_type ); + $name = $post_type_obj->label; + + if ( ! empty( $post_type_obj->labels->singular_name ) ) { + $name = $post_type_obj->labels->singular_name; + } + + return sprintf( + /* Translators: %1$s the post type label. */ + esc_html__( '%1$s counts', 'tribe-common' ), + $name + ); + } + + /** + * @inheritDoc + */ + public function get_value() { + return $this->get_counts(); + } + + /** + * Converts a post status count object to an array in the format + * [ (string) status_slug => (int) count] + * + * @since 5.1.0 + * + * @param stdClass $obj The object returned from wp_count_posts(). + * + * @return array An array of stati (key) with counts (value). + */ + protected function get_counts(): array { + $counts = (array) wp_count_posts( $this->post_type ); + $stati = [ + 'publish', + 'future', + 'draft', + 'pending', + ]; + + /** + * Allows other plugins to add/remove stati to track. + * + * @param array $stati An array of stati to track. + * @param self $field The field instance. + */ + $stati = apply_filters( 'tec_site_heath_event_stati', $stati, $this ); + + $keys = array_keys( $counts ); + foreach( $keys as $key ) { + if ( ! in_array( $key, $stati ) ) { + unset( $counts[ $key ] ); + } + } + + return $counts; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Common/Site_Health/Info_Field_Abstract.php b/wp-content/plugins/the-events-calendar/common/src/Common/Site_Health/Info_Field_Abstract.php new file mode 100644 index 000000000..6b0d991e5 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Common/Site_Health/Info_Field_Abstract.php @@ -0,0 +1,159 @@ +|array|array + */ + protected $value; + + /** + * Stores the priority for the field, used for sorting. + * + * @since 5.1.0 + * + * @var int + */ + protected int $priority; + + /** + * Stores the flag for if the field is private. + * + * @since 5.1.0 + * + * @var bool + */ + protected bool $is_private; + + /** + * Stores the debug value for the field. + * + * @since 5.1.0 + * + * @var string + */ + protected string $debug; + + /** + * @inheritDoc + */ + public function get_id(): string { + return $this->id; + } + + /** + * @inheritDoc + */ + public function get_label(): string { + return $this->label; + } + + /** + * @inheritDoc + */ + public function get_value() { + return $this->value; + } + + /** + * @inheritDoc + */ + public function get_priority(): int { + return $this->priority; + } + + /** + * @inheritDoc + */ + public function is_private(): bool { + return $this->is_private; + } + + /** + * @inheritDoc + */ + public function get_debug(): string { + return $this->debug; + } + + /** + * @inheritDoc + */ + public function to_array(): array { + return [ + 'id' => $this->filter_param( 'id', $this->get_id() ), + 'label' => $this->filter_param( 'label', $this->get_label() ), + 'value' => $this->filter_param( 'value', $this->get_value() ), + 'priority' => $this->filter_param( 'priority', $this->get_priority() ), + 'private' => $this->filter_param( 'private', $this->is_private() ), + 'debug' => $this->filter_param( 'debug', $this->get_debug() ), + ]; + } + + /** + * Internal method to the Field class, to allow filtering of specific values. + * + * @since 5.1.0 + * + * @param string $param The field parameter we're filtering. + * @param mixed $value Value of the field. + * + * @return mixed + */ + protected function filter_param( string $param, $value = null ) { + $field_id = $this->get_id(); + + /** + * Filters the get of a particular param for all fields. + * + * @since 5.1.0 + * + * @param mixed $value Value of the field. + * @param Info_Field_Abstract $field Current Field. + */ + $value = apply_filters( "tec_debug_info_field_get_{$param}", $value, $this ); + + /** + * Filters the get of a particular param for a specific section + field combination. + * + * @since 5.1.0 + * + * @param mixed $value Value of the field. + * @param Info_Field_Abstract $field Current Field. + */ + return apply_filters( "tec_debug_info_field_{$field_id}_get_{$param}", $value, $this ); + } + +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Common/Site_Health/Info_Field_Interface.php b/wp-content/plugins/the-events-calendar/common/src/Common/Site_Health/Info_Field_Interface.php new file mode 100644 index 000000000..a9e980d95 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Common/Site_Health/Info_Field_Interface.php @@ -0,0 +1,85 @@ +|array|array + */ + public function get_value(); + + /** + * Gets the priority for the field, used to order fields in a section. + * + * @since 5.1.0 + * + * @return int + */ + public function get_priority(): int; + + /** + * Get the debug value for the field. + * + * Optional. The output that is used for this field when the user copies the data. It should be more concise and + * not translated. If not set, the content of $value is used. Note that the array keys are used as labels for + * the copied data. + * + * @since 5.1.0 + * + * @return string + */ + public function get_debug(): string; + + /** + * Determines if the field is private or not. + * + * Optional. If set to true, the field will be excluded from the copied data, allowing you to show, for example, API keys here. Default false. + * + * @since 5.1.0 + * + * @return bool + */ + public function is_private(): bool; + + /** + * Pulls all the params for this field into an array consumable by the site health info page. + * + * @since 5.1.0 + * + * @return array + */ + public function to_array(): array; +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Common/Site_Health/Info_Section_Abstract.php b/wp-content/plugins/the-events-calendar/common/src/Common/Site_Health/Info_Section_Abstract.php new file mode 100644 index 000000000..11db83c99 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Common/Site_Health/Info_Section_Abstract.php @@ -0,0 +1,233 @@ + $fields + */ + protected array $fields = []; + + /** + * @inheritDoc + */ + public static function get_slug(): string { + return static::$slug; + } + + /** + * @inheritDoc + */ + public function to_array(): array { + $fields = []; + foreach ( $this->get_fields() as $key => $field ) { + $fields[ $key ] = $field->to_array(); + } + + return [ + 'label' => $this->filter_param( 'label', $this->get_label() ), + 'description' => $this->filter_param( 'description', $this->get_description() ), + 'private' => $this->filter_param( 'private', $this->is_private() ), + 'show_count' => $this->filter_param( 'show_count', $this->get_show_count() ), + 'fields' => $this->filter_param( 'fields', $fields ), + ]; + } + + /** + * @inheritDoc + */ + public function get_label(): string { + return $this->label; + } + + /** + * @inheritDoc + */ + public function get_description(): string { + return $this->description; + } + + /** + * @inheritDoc + */ + public function get_fields(): array { + return $this->fields; + } + + /** + * @inheritDoc + */ + public function get_show_count(): bool { + return $this->show_count; + } + + /** + * @inheritDoc + */ + public function is_private(): bool { + return $this->is_private; + } + + /** + * @inheritDoc + */ + public function has_field( $field ): bool { + // Assume field is a key first. + $id = $field; + + // When it's a field use its ID. + if ( $field instanceof Info_Field_Abstract ) { + $id = $field->get_id(); + } + + // Keys can only be strings. + if ( ! is_string( $id ) ) { + return false; + } + + $fields = $this->get_fields(); + + return isset( $fields[ $id ] ); + } + + /** + * @inheritDoc + */ + public function get_field( string $id ): ?Info_Field_Abstract { + if ( ! $this->has_field( $id ) ) { + return null; + } + + return $this->get_fields()[ $id ]; + } + + /** + * @inheritDoc + */ + public function add_field( Info_Field_Abstract $field, bool $overwrite = false ): bool { + // Allow the adding of a field to overwrite existing fields. + if ( ! $overwrite && $this->has_field( $field ) ) { + return false; + } + + $this->fields[ $field->get_id() ] = $field; + + $this->sort_fields(); + + return true; + } + + /** + * Internal method to the Section class, to allow filtering of specific values. + * + * @since 5.1.0 + * + * @param string $param + * @param mixed $value + * + * @return mixed + */ + protected function filter_param( string $param, $value = null ) { + $section_slug = static::get_slug(); + /** + * Filters the get of a particular param for all sections. + * + * @since 5.1.0 + * + * @param mixed $value Value of the field. + * @param Info_Section_Abstract $section Current Section. + */ + $value = apply_filters( "tec_debug_info_section_get_{$param}", $value, $this ); + + /** + * Filters the get of a particular param for a specific section. + * + * @since 5.1.0 + * + * @param mixed $value Value of the field. + * @param Info_Section_Abstract $section Current Section. + */ + return apply_filters( "tec_debug_info_section_{$section_slug}_get_{$param}", $value, $this ); + } + + /** + * Sorts the fields stored on this section, will retain keys. + * + * @since 5.1.0 + * + * @return void + */ + protected function sort_fields(): void { + uasort( $this->fields, static function ( $field_a, $field_b ) { + $a = $field_a->get_priority(); + $b = $field_b->get_priority(); + + if ( $a === $b ) { + return 0; + } + + return ( $a < $b ) ? - 1 : 1; + } ); + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Common/Site_Health/Info_Section_Interface.php b/wp-content/plugins/the-events-calendar/common/src/Common/Site_Health/Info_Section_Interface.php new file mode 100644 index 000000000..81ad0b681 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Common/Site_Health/Info_Section_Interface.php @@ -0,0 +1,123 @@ + + */ + public function get_fields(): array; + + /** + * Determines if a given field exists. + * + * @since 5.1.0 + * + * @param string|Info_Field_Abstract $field + * + * @return bool + */ + public function has_field( $field ): bool; + + /** + * Based on the id of the field return the object. + * + * @since 5.1.0 + * + * @param string $id + * + * @return Info_Field_Abstract|null + */ + public function get_field( string $id ): ?Info_Field_Abstract; + + /** + * Adds a field to this section. + * + * @see Generic_Info_Field If you are looking on how to quickly generate a field without creating a new class. + * + * @since 5.1.0 + * + * @param Info_Field_Abstract $field What field we are trying to add. + * @param bool $overwrite (optional) Determines if we will overwrite the field or not, if found. + * + * @return bool Determined by if we found a field with that id already. + */ + public function add_field( Info_Field_Abstract $field, bool $overwrite = false ): bool; +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Common/Site_Health/Provider.php b/wp-content/plugins/the-events-calendar/common/src/Common/Site_Health/Provider.php new file mode 100644 index 000000000..0d09d4e54 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Common/Site_Health/Provider.php @@ -0,0 +1,51 @@ +add_filters(); + } + + /** + * Include the filters related to this module. + * + * @since 5.1.0 + * + * @return void + */ + protected function add_filters(): void { + add_filter( 'debug_information', [ $this, 'filter_include_info_section' ] ); + } + + /** + * Includes the info sections controlled by Common. + * + * @since 5.1.0 + * + * @param array $info Current set of info sections. + * + * @return array + */ + public function filter_include_info_section( $info ): array { + return $this->container->make( Factory::class )->filter_include_info_sections( (array) $info ); + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Common/Storage/Timed_Option.php b/wp-content/plugins/the-events-calendar/common/src/Common/Storage/Timed_Option.php new file mode 100644 index 000000000..5b9d51959 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Common/Storage/Timed_Option.php @@ -0,0 +1,340 @@ +active = false; + } + + /** + * Activate the usage of Database Timed Options. + * + * @since 5.0.6 + * + * @return void + */ + public function activate(): void { + $this->active = true; + } + + /** + * Is the timed options active? + * + * @since 5.0.6 + * + * @return bool + */ + public function is_active(): bool { + /** + * Allows the modification of the state of usage for Timed Options. + * + * @since 5.0.6 + * + * @param bool $active Whether we use Database Timed Options or a glorified Memoization system. + */ + return (bool) apply_filters( 'tec_common_timed_option_is_active', $this->active ); + } + + /** + * Gets the option name for a given timed option, by attaching a prefix and allowing filtering. + * + * @since 5.0.6 + * + * @param string $key Key for the option we are trying to get the option name for. + * + * @return string + */ + public function get_option_name( string $key ): string { + /** + * Allows the modification of where we store the Transient Data. + * + * @since 5.0.6 + * + * @param string $option_name Name of the option where all the transient data will live. + */ + return (string) apply_filters( 'tec_common_timed_option_name', $this->option_name_prefix . $key, $this->option_name_prefix ); + } + + /** + * Fetches the value of a given timed option. + * + * @since 5.0.6 + * + * @param string $key Key for the option we are trying to get. + * @param mixed $default Default value when the option is either expired or not-set. + * @param bool $force If we should expire cache and fetch from the database. + * + * @return mixed|null + */ + public function get( $key, $default = null, bool $force = false ) { + /** + * Allows the filtering the default timed_option value. + * + * @since 5.0.6 + * + * @param mixed $default Default value when the option is either expired or not-set. + * @param string $key Key for the option we are trying to get. + * @param bool $force If we should expire cache and fetch from the database. + */ + $default = apply_filters( 'tec_common_timed_option_default_value', $default, $key, $force ); + + /** + * Allows the filtering to short-circuit the whole fetch logic. + * + * @since 5.0.6 + * + * @param mixed|null $pre If anything diff than null it will short-circuit. + * @param string $key Key for the option we are trying to get. + * @param mixed $default Default value when the option is either expired or not-set. + * @param bool $force If we should expire cache and fetch from the database. + */ + $pre = apply_filters( 'tec_common_timed_option_pre_value', null, $key, $default, $force ); + + if ( null !== $pre ) { + return $pre; + } + + $time = time(); + + // If we have a stored value that is not expired, use it. + if ( + ! $force + && isset( $this->data[ $key ] ) + && is_numeric( $this->data[ $key ]['expiration'] ) + && $time < $this->data[ $key ]['expiration'] + ) { + /** + * Allows the filtering of the cached value of the timed option. + * + * @since 5.0.6 + * + * @param mixed $value If anything diff than null it will short-circuit. + * @param string $key Key for the option we are trying to get. + * @param mixed $default Default value when the option is either expired or not-set. + * @param bool $force If we should expire cache and fetch from the database. + * @param bool $cache If the value was pulled from cache. + */ + return apply_filters( 'tec_common_timed_option_value', $this->data[ $key ]['value'], $key, $default, $force, true ); + } + + $timed_option = null; + + if ( $this->is_active() ) { + $timed_option_name = $this->get_option_name( $key ); + if ( true === $force ) { + wp_cache_delete( $timed_option_name, 'options' ); + } + $timed_option = get_option( $timed_option_name, null ); + } + + // Bail with default when non-existent. + if ( empty( $timed_option ) ) { + if ( $this->is_active() ) { + // Avoids next request check, forces auto-loading. + $this->set( $key, null, 0 ); + } + + return $default; + } + + // Bail with default when expired. + if ( $time >= $timed_option['expiration'] ) { + $this->delete( $key ); + + return $default; + } + + $this->data[ $key ] = $timed_option; + + /** + * Allows the filtering of the value of the timed option. + * + * @since 5.0.6 + * + * @param mixed $value If anything diff than null it will short-circuit. + * @param string $key Key for the option we are trying to get. + * @param mixed $default Default value when the option is either expired or not-set. + * @param bool $force If we should expire cache and fetch from the database. + * @param bool $cache If the value was pulled from cache. + */ + return apply_filters( 'tec_common_timed_option_value', $timed_option['value'], $key, $default, $force, false ); + } + + /** + * Delete a given timed option based on a key. + * Will also clear local cache. + * + * @since 5.0.6 + * + * @param string $key Which timed option we are checking. + * + * @return bool + */ + public function delete( $key ): bool { + $key = (string) $key; + $updated = false; + + if ( $this->is_active() ) { + $timed_option_name = $this->get_option_name( $key ); + $updated = update_option( $timed_option_name, null, true ); + wp_cache_delete( $timed_option_name, 'options' ); + } + + // Bail with default when non-existent. + if ( ! isset( $this->data[ $key ] ) ) { + return $updated; + } + + unset( $this->data[ $key ] ); + + return $updated; + } + + /** + * Checks if a given timed option exists. + * + * @since 5.0.6 + * + * @param string $key Which timed option we are checking. + * @param bool $force Clears the cache before get_option() + * + * @return bool + */ + public function exists( $key, bool $force = false ): bool { + /** + * Allows the filtering to short-circuit the whole exists logic. + * + * @since 5.0.6 + * + * @param mixed|null $pre If anything diff than null it will short-circuit. + * @param string $key Key for the option we are trying to get. + * @param bool $force If we should expire cache and fetch from the database. + */ + $pre = apply_filters( 'tec_common_timed_option_pre_exists', null, $key, $force ); + + if ( null !== $pre ) { + return (bool) $pre; + } + + $time = time(); + $cached = false; + $timed_option = null; + + // If we have a stored value that is not expired, use it. + if ( + ! $force + && isset( $this->data[ $key ] ) + && is_numeric( $this->data[ $key ]['expiration'] ) + && $time < $this->data[ $key ]['expiration'] + ) { + $cached = true; + $timed_option = $this->data[ $key ]; + } elseif ( $this->is_active() ) { + $timed_option_name = $this->get_option_name( $key ); + if ( true === $force ) { + wp_cache_delete( $timed_option_name, 'options' ); + } + $timed_option = get_option( $timed_option_name, null ); + } + + $exists = true; + + if ( null === $timed_option ) { + $exists = false; + } + + if ( ! is_array( $timed_option ) ) { + $exists = false; + } + + if ( ! isset( $timed_option['expiration'] ) || ! is_numeric( $timed_option['expiration'] ) ) { + $exists = false; + } + + /** + * Does a particular timed option key exists. + * + * @since 5.0.6 + * + * @param mixed $exists If anything diff than null it will short-circuit. + * @param string $key Key for the option we are trying to get. + * @param bool $force If we should expire cache and fetch from the database. + * @param bool $cached If the value was pulled from cache. + */ + return (bool) apply_filters( 'tec_common_timed_option_exists', $exists, $key, $force, $cached ); + } + + /** + * Update the value of a timed option on the database and on local cache. + * + * @since 5.0.6 + * + * @param string $key Key for this option. + * @param mixed $value Value stored for this option. + * @param int $expiration Expiration in seconds for this timed option. + * + * @return bool + */ + public function set( $key, $value, int $expiration = DAY_IN_SECONDS ): bool { + $key = (string) $key; + $data = [ + 'key' => $key, + 'value' => $value, + 'expiration' => time() + $expiration, + ]; + + $this->data[ $key ] = $data; + $updated = true; + + if ( $this->is_active() ) { + $updated = update_option( $this->get_option_name( $key ), $data, true ); + } + + return $updated; + } +} \ No newline at end of file diff --git a/wp-content/plugins/the-events-calendar/common/src/Common/Telemetry/Migration.php b/wp-content/plugins/the-events-calendar/common/src/Common/Telemetry/Migration.php new file mode 100644 index 000000000..7c48cab26 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Common/Telemetry/Migration.php @@ -0,0 +1,314 @@ +get_var( "SELECT `option_value` FROM $wpdb->options WHERE `option_name` = 'fs_accounts' LIMIT 1" ); + + + if ( empty( $fs_accounts ) || $fs_accounts instanceof \WP_Error ) { + return []; + } + + // Store original here as backup. + update_option( static::$fs_accounts_slug, $fs_accounts ); + + // Prevent issues with incomplete classes + $fs_accounts = preg_replace_callback( + '/O:(\d+):"([^"]+)":([^:]+):\{/m', + static function( $matches ) { + if ( $matches[2] === 'stdClass' ) { + return $matches[0]; + } + + $key_slug = "tec_fs_key"; + $key_slug_count = strlen( $key_slug ); + $new_size = $matches[3] + 1; + + return "a:{$new_size}:{s:{$key_slug_count}:\"{$key_slug}\";s:{$matches[1]}:\"{$matches[2]}\";"; + }, + $fs_accounts + ); + + $fs_accounts = maybe_unserialize( $fs_accounts ); + + // Store the modified data here. + update_option( static::$fs_accounts_data, $fs_accounts ); + + // return the modified data. + return $fs_accounts; + } + + /** + * Determine if we are opted-in to Freemius + * + * @since 5.1.0 + * + * @return boolean + */ + public function is_opted_in(): bool { + if ( ! is_null( self::$is_opted_in ) ) { + return self::$is_opted_in; + } + + $fs_accounts = $this->get_fs_accounts(); + + $sites = Arr::get( $fs_accounts, 'sites', [] ); + + if ( empty( $sites ) ) { + self::$is_opted_in = false; + return false; + } + + $disconnected = []; + + foreach ( $this->our_plugins as $plugin ) { + if ( ! isset( $sites[ $plugin ] ) ) { + continue; + } + + $disconnected[] = (bool) Arr::get( $sites, [ $plugin, 'is_disconnected' ] ); + } + + if ( 1 > count( $disconnected ) ) { + self::$is_opted_in = false; + return false; + } + + self::$is_opted_in = in_array( false, $disconnected, true ); + return self::$is_opted_in; + } + + /** + * Whether the class should load/run. + * + * @since 5.1.0 + * + * @return boolean + */ + public function should_load(): bool { + // If we've already checked, bail. + if ( get_option( self::$fs_accounts_data ) ) { + return false; + } + + // When we have an archived plugin list we can bail. + if ( get_option( self::$fs_plugins_slug ) ) { + return false; + } + + $fs_active_plugins = get_option( self::$key_fs_active_plugins ); + + // Bail if empty. + if ( empty( $fs_active_plugins ) ) { + return false; + } + + /** + * Allows filtering of whether the class should load/run. + * + * @since 5.1.0 + * + * @param bool $should_load Whether the class should load/run. + */ + return apply_filters( 'tec_telemetry_migration_should_load', true ); + } + + /** + * Detect if the user has opted in to Freemius and auto-opt them in to Telemetry. + * + * @since 5.1.0 + */ + public function migrate_existing_opt_in(): void { + // Let's reduce the amount this triggers. + if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) { + return; + } + + if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) { + return; + } + + if ( ! $this->should_load() ) { + return; + } + + $fs_active_plugins = get_option( self::$key_fs_active_plugins ); + + // Clean up our list. + $this->remove_inactive_plugins( $fs_active_plugins ); + + // Bail if none of our plugins are present. + if ( ! count( $this->our_plugins ) ) { + return; + } + + $this->auto_opt_in(); + + // Remove us from fs_active_plugins. + $this->handle_fs_active_plugins( $fs_active_plugins ); + } + + /** + * Filters our list of plugins to only the ones Freemius shows as active + * + * @since 5.1.0 + * + * @param Object $fs_active_plugins The stored list of active plugins from Freemius. + */ + private function remove_inactive_plugins( $fs_active_plugins ): void { + $freemius_plugins = ! empty( $fs_active_plugins->plugins ) ? (array) $fs_active_plugins->plugins : []; + + foreach ( $this->our_plugins as $plugin ) { + if ( ! isset( $freemius_plugins[ $plugin ] ) ) { + unset( $this->our_plugins[ $plugin ] ); + } + } + } + + /** + * Handles our entries in the fs_active_plugins option. + * Removes them from the Freemius option and stores a backup of the original. + * + * @since 5.1.0 + * + * @param Object $fs_active_plugins + * @return void + */ + private function handle_fs_active_plugins( $fs_active_plugins ): void { + // Store a backup of the original option. + update_option( self::$fs_plugins_slug, $fs_active_plugins ); + + foreach ( $this->our_plugins as $plugin ) { + $plugin .= '/common/vendor/freemius'; + + unset( $fs_active_plugins->plugins[ $plugin ] ); + + if ( ! empty( $fs_active_plugins->newest->sdk_path ) && $fs_active_plugins->newest->sdk_path === $plugin ) { + unset( $fs_active_plugins->newest ); + } + } + + // Update the Freemius option in the database with our edits. + update_option( self::$key_fs_active_plugins, $fs_active_plugins ); + } + + /** + * Opts the user in to Telemetry. + * + * @since 5.1.0 + * + */ + public function auto_opt_in() { + $opt_in = $this->is_opted_in(); + + $opt_in_subscriber = Config::get_container()->get( Opt_In_Subscriber::class ); + $telemetry = tribe( Telemetry::class ); + $slug = Telemetry::get_stellar_slug(); + + $opt_in_subscriber->opt_in( $slug ); + $telemetry->register_tec_telemetry_plugins( $opt_in ); + + /** + * Allows plugins to hook in and perform actions (like display a notice) when + * the user is automatically opted in to Telemetry. + * + * We also use this to trigger the actual auto-opt-in at the default priority. + * + * @since 5.1.0 + */ + do_action( 'tec_telemetry_auto_opt_in' ); + + // Disable the modal on all migrations. + $telemetry::disable_modal( $slug, 0 ); + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Common/Telemetry/Provider.php b/wp-content/plugins/the-events-calendar/common/src/Common/Telemetry/Provider.php new file mode 100644 index 000000000..2963f079c --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Common/Telemetry/Provider.php @@ -0,0 +1,158 @@ +container->bind( Telemetry::class, Telemetry::class ); + + $this->add_actions(); + $this->add_filters(); + } + + /** + * Add the action hooks. + * + * @since 5.1.0 + */ + public function add_actions() { + add_action( 'tribe_plugins_loaded', [ $this, 'boot_telemetry' ], 50 ); + + /** + * All these actions here need to be hooked from `tec_common_telemetry_preload` action to make sure that we have + * all the telemetry code loaded and ready to go. + */ + add_action( 'tec_common_telemetry_preload', [ $this, 'hook_telemetry_init' ], 5 ); + + add_action( 'tec_telemetry_modal', [ $this, 'show_optin_modal' ] ); + add_action( 'tec_common_telemetry_preload', [ $this, 'migrate_existing_opt_in' ], 100 ); + add_action( 'tec_common_telemetry_loaded', [ $this, 'maybe_enqueue_admin_modal_assets' ] ); + } + + /** + * Add the filter hooks. + * + * @since 5.1.0 + */ + public function add_filters() { + add_filter( 'stellarwp/telemetry/optin_args', [ $this, 'filter_optin_args' ] ); + add_filter( 'stellarwp/telemetry/exit_interview_args', [ $this, 'filter_exit_interview_args' ] ); + } + + /** + * It's super important to make sure when hooking to WordPress actions that we don't do before we are sure that + * telemetry was properly booted into the system. + * + * @since 5.1.3 + */ + public function hook_telemetry_init(): void { + add_action( 'admin_init', [ $this, 'initialize_telemetry' ], 5 ); + } + + /** + * Initialize our internal Telemetry code. + * Drivers, start your engines... + * + * @since 5.1.0 + * + * @return void + */ + public function boot_telemetry() { + $this->container->make( Telemetry::class )->boot(); + } + + /** + * Initialize our internal Telemetry code. + * Drivers, start your engines... + * + * @since 5.1.0 + * + * @return void + */ + public function initialize_telemetry() { + $this->container->make( Telemetry::class )->init(); + } + + /** + * Placeholder for eventual Freemius removal hooking in to modify things. + * + * @since 5.1.0 + * + * @return void + */ + public function migrate_existing_opt_in() { + $this->container->make( Migration::class )->migrate_existing_opt_in(); + } + + /** + * Logic for if the opt-in modal should be shown. + * + * @since 5.1.0 + * + * @return void + */ + public function show_optin_modal( $slug ) { + $this->container->make( Telemetry::class )->show_optin_modal( $slug ); + } + + /** + * Filters the default opt-in modal args. + * + * @since 5.1.0 + * + * @param array $args The current optin modal args. + * + * @return array + */ + public function filter_optin_args( $args ): array { + return $this->container->make( Telemetry::class )->filter_optin_args( $args ); + } + + /** + * Filters the exit questionnaire shown during plugin deactivation/uninstall. + * + * @since 5.1.0 + * + * @param array $args The current args. + * + * @return array $args The modified args. + */ + public function filter_exit_interview_args( $args ) { + return $this->container->make( Telemetry::class )->filter_exit_interview_args( $args ); + } + + + /** + * Ensure the assets for the modal are enqueued, if needed. + * + * @since 5.1.0 + */ + public function maybe_enqueue_admin_modal_assets(): void { + $this->container->make( Asset_Subscriber::class )->maybe_enqueue_admin_assets(); + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Common/Telemetry/Telemetry.php b/wp-content/plugins/the-events-calendar/common/src/Common/Telemetry/Telemetry.php new file mode 100644 index 000000000..238bcd883 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Common/Telemetry/Telemetry.php @@ -0,0 +1,592 @@ + + */ + public static $tec_slugs = []; + + + /** + * Gentlefolk, start your engines. + * + * @since 5.1.0 + * + * @return void + */ + public function boot(): void { + /** + * Configure the container. + * + * The container must be compatible with stellarwp/container-contract. + * See here: https://github.com/stellarwp/container-contract#usage. + * + * If you do not have a container, we recommend https://github.com/lucatume/di52 + * and the corresponding wrapper: + * https://github.com/stellarwp/container-contract/blob/main/examples/di52/Container.php + */ + $container = Container::init(); + + Config::set_container( $container ); + + static::clean_up(); + + self::$tec_slugs = self::get_tec_telemetry_slugs(); + self::$plugin_slug = self::get_parent_plugin_slug(); + self::$plugin_path = \Tribe__Main::instance()->get_parent_plugin_file_path(); + $stellar_slug = self::get_stellar_slug(); + + if ( empty( $stellar_slug ) ) { + return; + } + + $telemetry_server = ! defined('TELEMETRY_SERVER') ? 'https://telemetry.stellarwp.com/api/v1': TELEMETRY_SERVER; + + Config::set_server_url( $telemetry_server ); + + // Set a unique prefix for actions & filters. + Config::set_hook_prefix( self::$hook_prefix ); + + // Set a unique plugin slug. + Config::set_stellar_slug( $stellar_slug ); + + self::$plugin_path = \Tribe__Main::instance()->get_parent_plugin_file_path(); + + if ( empty( self::$plugin_path ) ) { + return; + } + + // Initialize the library. + Core::instance()->init( self::$plugin_path ); + + /** + * Allow plugins to hook in and add themselves, + * running their own actions once Telemetry is initiated, + * but before we register all our plugins. + * + * @since 5.1.0 + * + * @param self $telemetry The Telemetry instance. + */ + do_action( 'tec_common_telemetry_preload', $this ); + } + + /** + * Initializes the plugins and triggers the "loaded" action. + * + * @since 5.1.0 + * + * @return void + */ + public function init(): void { + $this->register_tec_telemetry_plugins(); + + /** + * Allow plugins to hook in and add themselves, + * running their own actions once Telemetry is initiated. + * + * @since 5.1.0 + * + * @param self $telemetry The Telemetry instance. + */ + do_action( 'tec_common_telemetry_loaded', $this ); + } + + /** + * Clean up some old data. + * If the "tec" plugin exists, and it has no wp_slug, remove it. + * This prevents a fatal with the Telemetry library when we call get_opted_in_plugins(). + * + * @since 5.1.1.1 + * + * @return void + */ + public static function clean_up(): void { + $status = static::get_status_object(); + $option = $status->get_option(); + if ( ! empty( $option['plugins'][ 'tec' ] ) && empty( $option['plugins'][ 'tec' ]['wp_slug'] ) ) { + $status->remove_plugin( 'tec' ); + } + } + + public static function get_plugin_slug() { + if ( empty( self::$plugin_slug ) ) { + self::$plugin_slug = self::get_parent_plugin_slug(); + } + + return self::$plugin_slug; + } + + /** + * Get the slug of the parent plugin. + * Hydrated lazily. + * + * @since 5.1.0 + * + * @return string + */ + public static function get_parent_plugin_slug(): string { + if ( empty( self::$parent_plugin ) ) { + $file = \Tribe__Main::instance()->get_parent_plugin_file_path(); + self::$parent_plugin = substr( + $file, + ( strrpos( $file, '/' ) + 1 ), + ( strlen( $file ) - ( strrpos( $file, '/' ) + 5 ) ) + ); + } + + return self::$parent_plugin; + } + + /** + * Get the stellar slug based on the parent plugin. + * + * @since 5.1.0 + */ + public static function get_stellar_slug(): string { + $tec_slugs = self::get_tec_telemetry_slugs(); + + foreach( $tec_slugs as $slug => $path ) { + if ( stripos( self::$plugin_path, $path ) ) { + return $slug; + } + } + + return ''; + } + + /** + * Filters the default optin modal args. + * + * @since 5.1.0 + * + * @param array $args The current optin modal args. + * + * @return array + */ + public function filter_optin_args( $args ): array { + $user_name = esc_html( wp_get_current_user()->display_name ); + + /* + if ET only change logo, name to Event Tickets + if TEC only change logo + If both, use The Events Calendar + */ + + $optin_args = [ + 'plugin_logo' => tribe_resource_url( 'images/logo/tec-brand.svg', false, null, \Tribe__Main::instance() ), + 'plugin_logo_width' => 'auto', + 'plugin_logo_height' => 42, + 'plugin_logo_alt' => 'TEC Common Logo', + 'plugin_name' => 'TEC Common', + 'plugin_slug' => self::get_plugin_slug(), + 'user_name' => $user_name, + 'permissions_url' => self::get_permissions_url(), + 'tos_url' => self::get_terms_url(), + 'privacy_url' => self::get_privacy_url(), + 'opted_in_plugins_text' => __( 'See which plugins you have opted in to tracking for', 'tribe-common' ), + 'heading' => __( 'We hope you love TEC Common!', 'tribe-common' ), + 'intro' => __( "Hi, {$user_name}! This is an invitation to help our StellarWP community. If you opt-in, some data about your usage of TEC Common and future StellarWP Products will be shared with our teams (so they can work their butts off to improve). We will also share some helpful info on WordPress, and our products from time to time. And if you skip this, that’s okay! Our products still work just fine.", 'tribe-common' ), + ]; + + /** + * Allows overriding the modal optin args. + * + * @since 5.1.0 + * + * @param array $optin_args The modal arguments to filter. + */ + $this->optin_args = apply_filters( 'tec_common_telemetry_optin_args', $optin_args ); + + return array_merge( $args, $this->optin_args ); + } + + /** + * Get the URL for the permission link in the optin modal. + * + * @since 5.1.0 + * + * @return string + */ + public static function get_permissions_url(): string { + /** + * Allow overriding the permissions URL. + * + * @since 5.1.0 + * + * @param string $url The URL to the permissions page. + */ + return esc_url( apply_filters( 'tec_common_telemetry_permissions_url', 'https://evnt.is/1bcl' ) ); + } + + /** + * Get the URL for the Terms of Service link in the optin modal. + * + * @since 5.1.0 + * + * @return string + */ + public static function get_terms_url(): string { + /** + * Allow overriding the Terms of Service URL. + * + * @since 5.1.0 + * + * @param string $url The URL to the Terms of Service page. + */ + return esc_url( apply_filters( 'tec_common_telemetry_terms_url', 'https://evnt.is/1bcm' ) ); + } + + /** + * Get the URL for the Privacy Policy link in the optin modal. + * + * @since 5.1.0 + * + * @return string + */ + public static function get_privacy_url(): string { + /** + * Allow overriding the Privacy Policy URL. + * + * @since 5.1.0 + * + * @param string $url The URL to the Privacy Policy page. + */ + return esc_url( apply_filters( 'tec_common_telemetry_privacy_url', 'https://evnt.is/1bcn' ) ); + } + + /** + * Filters the exit questionnaire shown during plugin deactivation/uninstall. + * + * @since 5.1.0 + * + * @param array $args The current args. + * + * @return array $args The modified args. + */ + public function filter_exit_interview_args( $args ) { + $new_args = [ + 'plugin_logo' => tribe_resource_url( 'images/logo/tec-brand.svg', false, null, \Tribe__Main::instance() ), + 'plugin_logo_width' => 'auto', + 'plugin_logo_height' => 32, + 'plugin_logo_alt' => 'TEC Common Logo', + 'heading' => __( 'We’re sorry to see you go.', 'tribe-common' ), + 'intro' => __( 'We’d love to know why you’re leaving so we can improve our plugin.', 'tribe-common' ), + 'uninstall_reasons' => [ + [ + 'uninstall_reason_id' => 'confusing', + 'uninstall_reason' => __( 'I couldn’t understand how to make it work.', 'tribe-common' ), + ], + [ + 'uninstall_reason_id' => 'better-plugin', + 'uninstall_reason' => __( 'I found a better plugin.', 'tribe-common' ), + 'show_comment' => true, + ], + [ + 'uninstall_reason_id' => 'no-feature', + 'uninstall_reason' => __( 'I need a specific feature it doesn’t provide.', 'tribe-common' ), + 'show_comment' => true, + ], + [ + 'uninstall_reason_id' => 'broken', + 'uninstall_reason' => __( 'The plugin doesn’t work.', 'tribe-common' ), + 'show_comment' => true, + ], + [ + 'uninstall_reason_id' => 'other', + 'uninstall_reason' => __( 'Other', 'tribe-common' ), + 'show_comment' => true, + ], + ], + ]; + + return array_merge( $args, $new_args ); + } + + /** + * Triggers Telemetry's opt-in modal with our parameters. + * + * @since 5.1.0 + * + * @return void + */ + public function show_optin_modal( $slug ): void { + + /** + * Filter allowing disabling of the optin modal. + * Returning boolean false will disable the modal + * + * @since 5.1.0 + * + * @param bool $show Whether to show the modal or not. + * + */ + $show = (bool) apply_filters( 'tec_common_telemetry_show_optin_modal', true, $slug ); + + if ( ! $show ) { + return; + } + + /** + * Telemetry uses this to determine when/where the optin modal should be shown. + * i.e. the modal is shown when we run this. + * + * @since 5.1.0 + * + * @param string $plugin_slug The slug of the plugin showing the modal. + */ + do_action( 'stellarwp/telemetry/optin', $slug ); + } + + /** + * Sugar function to get the status object from the container. + * + * @since 5.1.0 + * + * @return Status + */ + public static function get_status_object(): Status { + return Config::get_container()->get( Status::class ); + } + + /** + * Allows out plugins to hook in and add themselves, + * automating a lot of the registration and opt in/out process. + * + * @since 5.1.0 + * + * @return array An array of plugins in the format [ 'plugin_slug' => 'plugin_path' ] + */ + public static function get_tec_telemetry_slugs() { + /** + * Filter for plugins to hooked into Telemetry and add themselves. + * This acts a Telemetry "registry" for all TEC plugins. + * Used to ensure TEC plugins get (de)activated as a group. + * + * @since 5.1.0 + * + * @param array $slugs An array of plugins in the format [ 'plugin_slug' => 'plugin_path' ] + */ + return apply_filters( 'tec_telemetry_slugs', [] ); + } + + /** + * Register and opt in/out the plugins that are hooked into `tec_telemetry_slugs`. + * This keeps all TEC plugins in sync and only requires one optin modal response. + * + * @since 5.1.0 + * + * @return void + */ + public function register_tec_telemetry_plugins( $opted = NULL ) { + $new_opted = $opted; + // Let's reduce the amount this triggers. + if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) { + return; + } + + if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) { + return; + } + + $tec_slugs = self::get_tec_telemetry_slugs(); + + // We've got no other plugins? + if ( empty( $tec_slugs ) ) { + return; + } + + // In case we're not specifically passed a status... + $new_opted = $this->calculate_optin_status( $opted ); + + $status = Config::get_container()->get( Status::class ); + $opt_in_subscriber = Config::get_container()->get( Opt_In_Subscriber::class ); + $opt_in_subscriber->initialize_optin_option(); + + foreach ( $tec_slugs as $slug => $path ) { + // Register each plugin with the already instantiated library. + Config::add_stellar_slug( $slug, $path ); + $status->add_plugin( $slug, $new_opted, $path ); + + if ( $new_opted ) { + $opt_in_subscriber->opt_in( $slug ); + $status->set_status( $new_opted, $slug ); + } + + // If we're manually opting in/out, don't show the modal(s). + if ( ! is_null( $opted ) || ! empty( $new_opted ) ) { + static::disable_modal( $slug ); + } + + // If we've already interacted with a modal, don't show another one. + $show = static::calculate_modal_status(); + if ( ! $show ) { + static::disable_modal( $slug, $show ); + } + } + } + + /** + * Calculate the optin status for the TEC plugins from various sources. + * + * @since 6.1.0 + * + * @param bool $opted + * + * @return bool $opted + */ + public function calculate_optin_status( $opted ) { + if ( NULL !== $opted ) { + return $opted; + } + + // If they have opted in to one plugin, opt them in to all TEC ones. + $status_obj = static::get_status_object(); + $stati = []; + $option = $status_obj->get_option(); + + foreach ( static::$base_parent_slugs as $slug ) { + if ( $status_obj->plugin_exists( $slug ) ) { + $stati[ $slug ] = $option['plugins'][ $slug ][ 'optin' ]; + } + } + + $status = array_filter( $stati ); + + return (bool) array_pop( $status ); + } + + /** + * Calculate the optin status for the TEC plugins from various sources. + * + * @since 5.1.1.1 + * + * @return bool $show If the modal should show + */ + public static function calculate_modal_status() { + // If they have already interacted with a modal, find out. + $shows = array_flip( static::$base_parent_slugs ); + $optin = Config::get_container()->get( Opt_In_Template::class ); + + foreach ( static::$base_parent_slugs as $slug ) { + $show = get_option( $optin->get_option_name( $slug ), null ); + // Remove unset entries from the array. + if ( is_null( $show ) ) { + unset( $shows[ $slug ] ); + continue; + } + + $shows[ $slug ] = $show; + } + + // No entries - show modal. + if ( empty( $shows ) ) { + return true; + } + + // Flip the array = duplicate entries will be overwritten. + $shows = array_flip( $shows ); + + // If we have interacted with any modals, don't show this one. + return ! isset( $shows[0] ); + } + + /** + * Sugar function to disable (or enable) the optin modal. + * + * @since 6.1.0 + * + * @param string $slug The plugin slug for Telemetry. + * @param boolean|int $enable Opt out (0|false) or in (1|true). + */ + public static function disable_modal( $slug, $enable = false ) { + // Ensure we have a integer representation of a boolean value. + $enable = tec_bool_to_int( tribe_is_truthy( $enable ) ); + + $option_slug = Config::get_container()->get( Opt_In_Template::class )->get_option_name( $slug ); + update_option( $option_slug, $enable ); + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Common/Translations_Loader.php b/wp-content/plugins/the-events-calendar/common/src/Common/Translations_Loader.php new file mode 100644 index 000000000..3bdcce0ac --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Common/Translations_Loader.php @@ -0,0 +1,198 @@ + + */ + protected $loaded_domains = []; + + /** + * Switches the locale to the one specified. + * + * Note: the method will not check what the current locale is and will just load the + * translations specified. The burden of checking the current locale is on the caller. + * + * @since 5.0.8 + * + * @param string $locale The locale to switch to. + * @param array $domains A map from text domains to the directory containing the translations. + * + * @return bool Whether the locale was switched or not. + */ + public function load( string $locale, array $domains = [] ): bool { + if ( empty( $domains ) ) { + return false; + } + + /** + * Fires before the locale translations are loaded. + * + * @since 5.0.8 + * + * @param string $locale The locale that will be loaded. + * @param array $domains The list of domains translations will be loaded for. + */ + do_action( 'tec_locale_translations_load_before', $locale, $domains ); + + $this->has_loaded_translations = true; + $this->override_locale = $locale; + $this->loaded_domains = $domains; + + /* + * The `plugin_locale` filter will be applied in `load_plugin_textdomain()` to determine + * the language file to load. + */ + add_filter( 'plugin_locale', [ $this, 'override_locale' ] ); + + $this->load_locale_translations( $domains, $locale ); + + remove_filter( 'plugin_locale', [ $this, 'override_locale' ] ); + + /** + * Fires after the locale translations are loaded. + * + * @since 5.0.8 + * + * @param string $locale The locale that has been loaded. + * @param array $domains The list of domains translations have been loaded for. + */ + do_action( 'tec_locale_translations_load_after', $locale, $domains ); + + return true; + } + + /** + * A proxy method to return the current override locale if set, or the input locale otherwise. + * + * Used during filter application. + * + * @since 5.0.8 + * + * @param string $locale The locale to override. + * + * @return string The overridden locale. + * + * @internal This function is public only for the purpose of being used as a filter callback. + */ + public function override_locale( $locale ) { + return $this->override_locale ?: $locale; + } + + /** + * Returns whether the locale has been switched or not. + * + * @since 5.0.8 + * + * @return bool Whether the locale has been switched or not. + */ + public function has_loaded_translations(): bool { + return $this->has_loaded_translations; + } + + /** + * Restored the locale to the previous one and removes the class filters. + * + * @since 5.0.8 + * + * @return void Translations for each domain will be reloaded. + */ + public function restore() { + if ( ! $this->has_loaded_translations ) { + return; + } + + $this->override_locale = ''; + + /** + * Fires before the locale translations are restored. + * + * @since 5.0.8 + * + * @param array $domains The list of domains translations will be loaded for. + */ + do_action( 'tec_locale_translations_restore_before', $this->loaded_domains ); + + // Reload the translations using the currently determined locale. + $this->load_locale_translations( $this->loaded_domains, determine_locale() ); + + /** + * Fires after the locale translations are restored. + * + * @since 5.0.8 + * + * @param array $domains The list of domains translations have been loaded for. + */ + do_action( 'tec_locale_translations_restore_after', $this->loaded_domains ); + + $this->has_loaded_translations = false; + } + + /** + * Load the translations for the map of domains for the current locale. + * + * @since 5.0.8 + * + * @param array $domains A map from text domains to the directory containing the translations. + * @param string $locale The locale to load the translations for. + * + * @return void Translations for each domain will be loaded for the current plugin locale. + */ + protected function load_locale_translations( array $domains, string $locale ): void { + global $l10n; + + if ( ! is_array( $l10n ) ) { + $l10n = []; + } + + foreach ( $domains as $domain => $lang_dir ) { + unload_textdomain( $domain, true ); + + if ( $locale === 'en_US' ) { + // There is no `en_US` language pack since it's the default, no-op the translations. + $l10n[ $domain ] = new \NOOP_Translations(); + } else { + // Load the translations using the wrapper Common function. + $dir = is_string( $lang_dir ) && ! empty( $lang_dir ) ? $lang_dir : false; + \Tribe__Main::instance()->load_text_domain( $domain, $dir ); + } + } + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Abstract_Deactivation.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Abstract_Deactivation.php new file mode 100644 index 000000000..b32c40f8d --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Abstract_Deactivation.php @@ -0,0 +1,71 @@ +network = (bool) $network; + } + + /** + * Tell WordPress to flush rewrite rules. + * Since our post types are already registered, + * we delete the option and let WP regenerate it + * on the next page load. + */ + protected function flush_rewrite_rules() { + delete_option( 'rewrite_rules' ); + } + + /** + * Deactivate the plugin. This should not remove data. + * It's job is to remove run-time traces of the plugin. + * + * @return void + */ + public function deactivate() { + if ( is_multisite() && $this->network ) { + $this->multisite_deactivate(); + } else { + $this->blog_deactivate(); + } + } + + /** + * Run the deactivation script on every blog for a multisite install + * + * @return void + */ + protected function multisite_deactivate() { + /** @var wpdb $wpdb */ + global $wpdb; + $site = get_current_site(); + $blog_ids = $wpdb->get_col( $wpdb->prepare( "SELECT blog_id FROM {$wpdb->blogs} WHERE site_id=%d", $site->id ) ); + $large = wp_is_large_network(); + foreach ( $blog_ids as $blog ) { + tribe_set_time_limit( 30 ); + switch_to_blog( $blog ); + $large ? $this->short_blog_deactivate() : $this->blog_deactivate(); + restore_current_blog(); + } + } + + /** + * The deactivation routine for a single blog + * + * @return void + */ + abstract protected function blog_deactivate(); + + + /** + * An abridged version that is less DB intensive for use on large networks. + * + * @see wp_is_large_network() and the 'wp_is_large_network' filter + * + * @return void + */ + protected function short_blog_deactivate() { + $this->blog_deactivate(); + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Abstract_Plugin_Register.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Abstract_Plugin_Register.php new file mode 100644 index 000000000..d6d791aeb --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Abstract_Plugin_Register.php @@ -0,0 +1,72 @@ + [], + 'co-dependencies' => [], + 'addon-dependencies' => [], + ]; + + /** + * Registers a plugin with dependencies + */ + public function register_plugin() { + tribe_register_plugin( + $this->base_dir, + $this->main_class, + $this->version, + $this->classes_req, + $this->dependencies + ); + } + + /** + * Returns whether or not the dependencies have been met + * + * This is basically an aliased function - register_plugins, upon + * second calling, returns whether or not a plugin should load. + * + * @deprecated since 4.9.17 It is unused by any Tribe plugins and returned void. + * @todo remove in 4.11 + */ + public function has_valid_dependencies() { + _deprecated_function( __METHOD__, '4.9.17' ); + } +} \ No newline at end of file diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Activation_Page.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Activation_Page.php new file mode 100644 index 000000000..c9db44099 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Activation_Page.php @@ -0,0 +1,380 @@ +args = wp_parse_args( $args, [ + 'slug' => '', + 'admin_page' => '', + 'admin_url' => '', + 'activation_transient' => '', + 'version' => '', + 'plugin_path' => '', + 'version_history_slug' => '', + 'update_page_title' => '', + 'update_page_template' => '', + 'welcome_page_title' => '', + 'welcome_page_template' => '', + ] ); + + $this->update_slug .= $this->args['slug']; + $this->welcome_slug .= $this->args['slug']; + + $this->hooks(); + } + + /** + * Determines if we are currently on the Welcome page. + * + * @since 4.12.11 + * + * @return bool + */ + public function is_welcome_page() { + return isset( $_GET[ $this->welcome_slug ] ); + } + + /** + * Determines if we are currently on the update page. + * + * @since 4.12.11 + * + * @return bool + */ + public function is_update_page() { + return isset( $_GET[ $this->update_slug ] ); + } + + /** + * Listen for opportunities to show update and welcome splash pages. + */ + public function hooks() { + // Never show this on the front-end. + if ( ! is_admin() ) { + return; + } + + if ( + tribe_is_truthy( get_option( 'tribe_skip_welcome', false ) ) + || tribe_is_truthy( tribe_get_option( 'skip_welcome', false ) ) + ) { + return; + } + + add_action( 'admin_init', [ $this, 'maybe_redirect' ], 10, 0 ); + add_action( 'admin_menu', [ $this, 'register_page' ], 100, 0 ); // Come in after the default page is registered. + + add_action( 'update_plugin_complete_actions', [ $this, 'update_complete_actions' ], 15, 2 ); + add_action( 'update_bulk_plugins_complete_actions', [ $this, 'update_complete_actions' ], 15, 2 ); + } + + /** + * Filter the Default WordPress actions when updating the plugin to prevent users to be redirected if they have an + * specific intention of going back to the plugins page. + * + * @param array $actions The Array of links (html) + * @param string $plugin Which plugins are been updated + * @return array The filtered Links + */ + public function update_complete_actions( $actions, $plugin ) { + $plugins = []; + + if ( ! empty( $_GET['plugins'] ) ) { + $plugins = explode( ',', esc_attr( $_GET['plugins'] ) ); + } + + if ( ! in_array( $this->args['plugin_path'], $plugins ) ) { + return $actions; + } + + if ( isset( $actions['plugins_page'] ) ) { + $actions['plugins_page'] = '' . esc_html__( 'Return to Plugins page' ) . ''; + + if ( ! current_user_can( 'activate_plugins' ) ) { + unset( $actions['plugins_page'] ); + } + } + + if ( isset( $actions['updates_page'] ) ) { + $actions['updates_page'] = '' . esc_html__( 'Return to WordPress Updates' ) . ''; + } + + return $actions; + } + + /** + * Maybe redirect to the welcome page (or to the update page - though this is + * currently disabled). + */ + public function maybe_redirect() { + if ( ! empty( $_POST ) ) { + return; // Don't interrupt anything the user's trying to do. + } + + if ( ! is_admin() || defined( 'DOING_AJAX' ) ) { + return; + } + + if ( defined( 'IFRAME_REQUEST' ) && IFRAME_REQUEST ) { + return; // Probably the plugin update/install iframe. + } + + if ( isset( $_GET[ $this->welcome_slug ] ) || isset( $_GET[ $this->update_slug ] ) ) { + return; // No infinite redirects. + } + + if ( isset( $_GET['tribe-skip-welcome'] ) ) { + return; // A way to skip these checks and. + } + + if ( + ! $this->showed_update_message_for_current_version() + && ! $this->is_new_install() + ) { + $page = tribe_get_request_var( 'page' ); + if ( empty( $page ) ) { + return; + } + + $match_page = str_replace( 'tribe_events_page_', '', $this->args['admin_page'] ); + + if ( $page !== $match_page ) { + return; + } + + /** + * Filters whether we should disable the update page redirect. + * + * @since 5.0.0 + * + * @param $bypass bool + */ + $bypass_update_page = apply_filters( 'tec_admin_update_page_bypass', false, $this ); + + if ( $bypass_update_page ) { + return; + } + + $this->redirect_to_update_page(); + } + + // Bail if we aren't activating a plugin. + if ( ! get_transient( $this->args['activation_transient'] ) ) { + return; + } + + delete_transient( $this->args['activation_transient'] ); + + if ( ! current_user_can( Tribe__Settings::instance()->requiredCap ) ) { + return; + } + + // the redirect might be intercepted by another plugin, but + // we'll go ahead and mark it as viewed right now, just in case + // we end up in a redirect loop + // see #31088 + $this->log_display_of_message_page(); + + if ( $this->is_new_install() ) { + $this->redirect_to_welcome_page(); + } + } + + /** + * Have we shown the welcome/update message for the current version? + * + * @since 5.0.0 Turned this method public. + * + * @return bool + */ + public function showed_update_message_for_current_version() { + $message_version_displayed = Tribe__Settings_Manager::get_option( 'last-update-message-' . $this->args['slug'] ); + + if ( empty( $message_version_displayed ) ) { + return false; + } + + $current_version = explode( '.', $this->args['version'] ); + $current_version = array_splice( $current_version, 0, 2 ); + $current_version = array_pad( $current_version, 3, 0 ); + $current_version = implode( '.', $current_version ); + + $message_version_displayed = explode( '.', $message_version_displayed ); + $message_version_displayed = array_splice( $message_version_displayed, 0, 2 ); + $message_version_displayed = array_pad( $message_version_displayed, 3, 0 ); + $message_version_displayed = implode( '.', $message_version_displayed ); + + if ( version_compare( $message_version_displayed, $current_version, '<' ) ) { + return false; + } + return true; + } + + /** + * Records the fact that we displayed the update message in relation to a specific + * version of the plugin (so we don't show it again until/unless they update to + * a higher version). + */ + protected function log_display_of_message_page() { + Tribe__Settings_Manager::set_option( 'last-update-message-' . $this->args['slug'], $this->args['version'] ); + } + + /** + * The previous_ecp_versions option will be empty or set to 0 + * if the current version is the first version to be installed. + * + * @return bool + * @see Tribe__Events__Main::maybeSetTECVersion() + */ + protected function is_new_install() { + $previous_versions = Tribe__Settings_Manager::get_option( $this->args['version_history_slug'] ); + return empty( $previous_versions ) || ( end( $previous_versions ) == '0' ); + } + + /** + * Handles taking a user to a post-installation welcome page. + */ + protected function redirect_to_welcome_page() { + $url = $this->get_message_page_url( $this->welcome_slug ); + wp_safe_redirect( $url ); + exit(); + } + + /** + * Handles taking the user to a post-update splash screen. + * + * Disused since TEC PR 88 (targeting Tribe__Events__Activation_Page, + * which this class was derived from). + * + * @see https://github.com/the-events-calendar/the-events-calendar/pull/88 + * + * @todo decide whether to reinstate or remove + */ + protected function redirect_to_update_page() { + $url = $this->get_message_page_url( $this->update_slug ); + wp_safe_redirect( $url ); + exit(); + } + + /** + * Return the URL of the splash page. + * + * @param string $slug + * + * @return string + */ + protected function get_message_page_url( $slug ) { + $settings = Tribe__Settings::instance(); + + $url = ! empty( $this->args['admin_url'] ) ? $this->args['admin_url'] : $settings->get_url(); + + $url = esc_url_raw( add_query_arg( $slug, 1, $url ) ); + + return $url; + } + + /** + * Dynamically registers the splash page when required. + */ + public function register_page() { + if ( isset( $_GET[ $this->welcome_slug ] ) ) { + $this->current_context = 'welcome'; + } elseif ( isset( $_GET[ $this->update_slug ] ) ) { + $this->current_context = 'update'; + } else { + return; + } + + $this->disable_default_settings_page(); + add_filter( 'admin_body_class', [ $this, 'admin_body_class' ] ); + add_action( $this->args['admin_page'], [ $this, 'display_page' ] ); + } + + /** + * Hooked to admin_body_class to add a class for the update or welcome page + * + * @param string $classes a space separated string of classes to be added to body + * + * @return string + */ + public function admin_body_class( $classes ) { + $classes .= ' tribe-' . $this->current_context; + return $classes; + } + + /** + * Deactivates the regular settings screen (the splash screen will display + * in the Events > Settings slot instead, for this request only). + */ + protected function disable_default_settings_page() { + remove_action( $this->args['admin_page'], [ Tribe__Settings::instance(), 'generatePage' ] ); + } + + /** + * Prints the splash screen. + * + * @param string $context + * + * @return string|null + */ + public function display_page() { + if ( empty( $this->args[ $this->current_context . '_page_title' ] ) || empty( $this->args[ $this->current_context . '_page_template'] ) ) { + return null; + } + + do_action( 'tribe_settings_top' ); + + $context = isset( $_GET[ $this->welcome_slug ] ) ? 'welcome': 'update'; + $title = esc_html( $this->args[ $context . '_page_title'] ); + $html = $this->get_view( $this->args[ $context . '_page_template'] ); + + echo " +
+

{$title}

+ {$html} +
+ "; + + do_action( 'tribe_settings_bottom' ); + $this->log_display_of_message_page(); + } + + /** + * Returns the output of the specified template. + * + * @param string $path + * + * @return string + */ + protected function get_view( $path ) { + if ( ! file_exists( $path ) ) { + return ''; + } + + ob_start(); + include $path; + return ob_get_clean(); + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Conditional_Content/Black_Friday.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Conditional_Content/Black_Friday.php new file mode 100644 index 000000000..320d41f7e --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Conditional_Content/Black_Friday.php @@ -0,0 +1,84 @@ +modify( '-3 days' ); + + return $date; + } + + /** + * Replace the opening markup for the general settings info box. + * + * @since 4.14.7 + * @return void + */ + public function add_conditional_content( $fields ) { + // Check if the content should currently be displayed. + if( ! $this->should_display() ) { + return $fields; + } + + // Set up template variables. + $images_dir = \Tribe__Main::instance()->plugin_url . 'src/resources/images/'; + $template_args = [ + 'branding_logo' => $images_dir . 'logo/tec-brand.svg', + 'background_image' => $images_dir . 'marketing/bf-promo.png', + 'button_link' => 'https://evnt.is/1aqi', + ]; + + // Get the Black Friday promo content. + $content = $this->get_template()->template( 'conditional_content/black-friday', $template_args, false ); + + // Replace starting info box markup. + $fields['info-start']['html'] .= $content; + + return $fields; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Conditional_Content/Datetime_Conditional_Abstract.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Conditional_Content/Datetime_Conditional_Abstract.php new file mode 100644 index 000000000..47c93351c --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Conditional_Content/Datetime_Conditional_Abstract.php @@ -0,0 +1,160 @@ +start_date, 'UTC' ); + // If not set, set to midnight. + if ( empty( $this->start_time ) ) { + $this->start_time = 0; + } + + $date = $date->setTime( $this->start_time, 0 ); + + /** + * Allow filtering of the start date for testing. + * + * @since 4.14.7 + * @param \DateTime $date - Unix timestamp for start date + * @param object $this + */ + $date = apply_filters( "tec_admin_conditional_content_{$this->slug}_start_date", $date, $this ); + + return $date; + } + + /** + * Unix datetime for content end. + * + * @since 4.14.7 + * @return \Tribe\Utils\Date_I18n - Date Object + */ + protected function get_end_time() { + $date = Dates::build_date_object( $this->end_date, 'UTC' ); + // If not set, set to midnight. + if ( empty( $this->end_time ) ) { + $this->end_time = 0; + } + + $date = $date->setTime( $this->end_time, 0 ); + + /** + * Allow filtering of the end date for testing. + * + * @since 4.14.7 + * @param \DateTime $date - Unix timestamp for end date + * @param object $this + */ + $date = apply_filters( "tec_admin_conditional_content_{$this->slug}_end_date", $date, $this ); + + return $date; + } + + /** + * Whether the content should display. + * + * @since 4.14.7 + * @return boolean - Whether the content should display + */ + protected function should_display() { + $now = Dates::build_date_object( 'now', 'UTC' ); + $notice_start = $this->get_start_time(); + $notice_end = $this->get_end_time(); + $display = $notice_start <= $now && $now < $notice_end; + + /** + * Allow filtering whether the content should display. + * + * @since 4.14.7 + * @param bool $should_display - whether the content should display + * @param object $this - the conditional content object + */ + $should_display = apply_filters( "tec_admin_conditional_content_{$this->slug}_should_display", $display, $this ); + + return $should_display; + } + + /** + * Gets the template instance used to setup the rendering of the page. + * + * @since 4.14.7 + * + * @return \Tribe__Template + */ + public function get_template() { + if ( empty( $this->template ) ) { + $this->template = new \Tribe__Template(); + $this->template->set_template_origin( \Tribe__Main::instance() ); + $this->template->set_template_folder( 'src/admin-views' ); + $this->template->set_template_context_extract( true ); + $this->template->set_template_folder_lookup( false ); + } + + return $this->template; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Conditional_Content/End_Of_Year_Sale.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Conditional_Content/End_Of_Year_Sale.php new file mode 100644 index 000000000..ffded7f29 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Conditional_Content/End_Of_Year_Sale.php @@ -0,0 +1,85 @@ +should_display() ) { + return $fields; + } + + // Set up template variables. + $images_dir = \Tribe__Main::instance()->plugin_url . 'src/resources/images/'; + $template_args = [ + 'branding_logo' => $images_dir . 'logo/tec-brand.svg', + 'background_image' => $images_dir . 'marketing/eoy-sale-promo.png', + 'button_link' => 'https://evnt.is/1a-x', + ]; + + // Get the promo content. + $content = $this->get_template()->template( 'conditional_content/end-of-year-sale', $template_args, false ); + + // Replace starting info box markup. + $fields['info-start']['html'] .= $content; + + return $fields; + } + + /** + * Unix time for notice end. + * + * @since 4.14.9 + * + * @return \Tribe\Utils\Date_I18n - Date Object + */ + public function get_end_time() { + $date = parent::get_end_time(); + $date = $date->setTime( 23, 59 ); + + return $date; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Conditional_Content/Service_Provider.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Conditional_Content/Service_Provider.php new file mode 100644 index 000000000..f02671b36 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Conditional_Content/Service_Provider.php @@ -0,0 +1,52 @@ +container->singleton( Black_Friday::class, Black_Friday::class, [ 'hook' ] ); + // EOY Sale disabled for 2022 + // $this->container->singleton( End_Of_Year_Sale::class, End_Of_Year_Sale::class, [ 'hook' ] ); + $this->hooks(); + } + + /** + * Set up hooks for classes. + * + * @since 4.14.7 + */ + protected function hooks() { + add_action( 'tribe_plugins_loaded', [ $this, 'plugins_loaded' ] ); + } + + /** + * Setup for things that require plugins loaded first. + * + * @since 4.14.7 + */ + public function plugins_loaded() { + $this->container->make( Black_Friday::class ); + // EOY Sale disabled for 2022 + // $this->container->make( End_Of_Year_Sale::class ); + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Help_Page.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Help_Page.php new file mode 100644 index 000000000..b854f999a --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Help_Page.php @@ -0,0 +1,1188 @@ +is_current_page() ) { + return $classes; + } + + $classes .= ' tribe-help tec-help'; + return $classes; + } + + /** + * Checks if the current page is the Help one + * + * @since 4.5.7 + * + * @return bool + */ + public function is_current_page() { + global $current_screen; + + $help_pages = [ + 'tribe_events_page_tec-events-help', + 'tickets_page_tec-tickets-help', + ]; + + return in_array( $current_screen->id, $help_pages ); + } + + /** + * Register the Admin assets for the help page + * + * @since 4.9.12 + * + * @return void + */ + public function register_assets() { + $plugin = Tribe__Main::instance(); + tribe_asset( + $plugin, + 'tribe-admin-help-page', + 'admin/help-page.js', + [ 'tribe-clipboard', 'tribe-common' ], + 'admin_enqueue_scripts', + [ + 'conditionals' => [ $this, 'is_current_page' ], + 'localize' => [ + 'name' => 'tribe_system_info', + 'data' => [ + 'sysinfo_optin_nonce' => wp_create_nonce( 'sysinfo_optin_nonce' ), + 'clipboard_btn_text' => _x( 'Copy to clipboard', 'Copy to clipboard button text.', 'tribe-common' ), + 'clipboard_copied_text' => _x( 'System info copied', 'Copy to clipboard success message', 'tribe-common' ), + 'clipboard_fail_text' => _x( 'Press "Cmd + C" to copy', 'Copy to clipboard instructions', 'tribe-common' ), + 'sysinfo_error_message_text' => _x( 'Something has gone wrong!', 'Default error message for system info optin', 'tribe-common' ), + 'sysinfo_error_code_text' => _x( 'Code:', 'Error code label for system info optin', 'tribe-common'), + 'sysinfo_error_status_text' => _x( 'Status:', 'Error status label for system info optin', 'tribe-common'), + ], + ], + ] + ); + } + + /** + * Get the list of plugins + * + * @since 4.0 + * + * @param string $plugin_name Should get only one plugin? + * @param boolean $is_active Only get active plugins? + * @return array + */ + public function get_plugins( $plugin_name = null, $is_active = true ) { + $plugins = []; + + $plugins['the-events-calendar'] = [ + 'name' => 'the-events-calendar', + 'title' => esc_html__( 'The Events Calendar', 'tribe-common' ), + 'repo' => 'https://wordpress.org/plugins/the-events-calendar/', + 'forum' => 'https://wordpress.org/support/plugin/the-events-calendar/', + 'stars_url' => 'https://wordpress.org/support/plugin/the-events-calendar/reviews/?filter=5', + 'description' => esc_html__( + 'The Events Calendar is a carefully crafted, extensible plugin that lets you easily share your events.', + 'tribe-common' + ), + 'is_active' => false, + 'version' => null, + ]; + + if ( class_exists( 'Tribe__Events__Main' ) ) { + $plugins['the-events-calendar']['version'] = Tribe__Events__Main::VERSION; + $plugins['the-events-calendar']['is_active'] = true; + } + + $plugins['event-tickets'] = [ + 'name' => 'event-tickets', + 'title' => esc_html__( 'Event Tickets', 'tribe-common' ), + 'repo' => 'https://wordpress.org/plugins/event-tickets/', + 'forum' => 'https://wordpress.org/support/plugin/event-tickets', + 'stars_url' => 'https://wordpress.org/support/plugin/event-tickets/reviews/?filter=5', + 'description' => esc_html__( + 'Events Tickets is a carefully crafted, extensible plugin that lets you easily sell tickets for your events.', + 'tribe-common' + ), + 'is_active' => false, + 'version' => null, + ]; + + if ( class_exists( 'Tribe__Tickets__Main' ) ) { + $plugins['event-tickets']['version'] = Tribe__Tickets__Main::VERSION; + $plugins['event-tickets']['is_active'] = true; + } + + $plugins['advanced-post-manager'] = [ + 'name' => 'advanced-post-manager', + 'title' => esc_html__( 'Advanced Post Manager', 'tribe-common' ), + 'repo' => 'https://wordpress.org/plugins/advanced-post-manager/', + 'forum' => 'https://wordpress.org/support/plugin/advanced-post-manager/', + 'stars_url' => 'https://wordpress.org/support/plugin/advanced-post-manager/reviews/?filter=5', + 'description' => esc_html__( + 'Turbo charge your posts admin for any custom post type with sortable filters and columns, and auto-registration of metaboxes.', + 'tribe-common' + ), + 'is_active' => false, + 'version' => null, + ]; + + if ( class_exists( 'Tribe_APM' ) ) { + $plugins['advanced-post-manager']['version'] = 1; + $plugins['advanced-post-manager']['is_active'] = true; + } + + $plugins = (array) apply_filters( 'tribe_help_plugins', $plugins ); + + // Only active ones? + if ( true === $is_active ) { + foreach ( $plugins as $key => $plugin ) { + if ( true !== $plugin['is_active'] ) { + unset( $plugins[ $key ] ); + } + } + } + + // Do the search + if ( is_string( $plugin_name ) ) { + if ( isset( $plugins[ $plugin_name ] ) ) { + return $plugins[ $plugin_name ]; + } else { + return false; + } + } else { + return $plugins; + } + } + + /** + * Get the formatted links of the possible plugins + * + * @since 4.0 + * + * @param boolean $is_active Filter only active plugins + * @return array + */ + public function get_plugin_forum_links( $is_active = true ) { + $plugins = $this->get_plugins( null, $is_active ); + + $list = []; + foreach ( $plugins as $plugin ) { + $list[] = '' . $plugin['title'] . ''; + } + + return $list; + } + + /** + * Get the formatted text of the possible plugins + * + * @since 4.0 + * + * @param boolean $is_active Filter only active plugins + * @return string + */ + public function get_plugins_text( $is_active = true ) { + $plugins = array_merge( $this->get_plugins( null, $is_active ), $this->get_addons( null, $is_active, true ) ); + + $plugins_text = ''; + $i = 0; + $count = count( $plugins ); + foreach ( $plugins as $plugin ) { + $i++; + if ( ! isset( $plugin['is_active'] ) || $plugin['is_active'] !== $is_active ) { + continue; + } + + $plugins_text .= $plugin['title']; + + if ( $i === $count - 1 ) { + $plugins_text .= esc_html__( ' and ', 'tribe-common' ); + } elseif ( $i !== $count ) { + $plugins_text .= ', '; + } + } + + return $plugins_text; + } + + /** + * Get the Addons + * + * @since 4.0 + * + * @param string $plugin Plugin Name to filter + * @param string $is_active Filter if it's active + * @param string $is_important filter if the plugin is important + * @return array + */ + public function get_addons( $plugin = null, $is_active = null, $is_important = null ) { + $addons = []; + + $addons['events-calendar-pro'] = [ + 'id' => 'events-calendar-pro', + 'title' => esc_html__( 'Events Calendar PRO', 'tribe-common' ), + 'link' => 'http://evnt.is/dr', + 'plugin' => [ 'the-events-calendar' ], + 'is_active' => class_exists( 'Tribe__Events__Pro__Main' ), + 'is_important' => true, + ]; + + $addons['eventbrite-tickets'] = [ + 'id' => 'eventbrite-tickets', + 'title' => esc_html__( 'Eventbrite Tickets', 'tribe-common' ), + 'link' => 'http://evnt.is/ds', + 'plugin' => [ 'the-events-calendar' ], + 'is_active' => class_exists( 'Tribe__Events__Tickets__Eventbrite__Main' ), + ]; + + $addons['community-events'] = [ + 'id' => 'community-events', + 'title' => esc_html__( 'Community Events', 'tribe-common' ), + 'link' => 'http://evnt.is/dt', + 'plugin' => [ 'the-events-calendar' ], + 'is_active' => class_exists( 'Tribe__Events__Community__Main' ), + ]; + + $addons['event-aggregator'] = [ + 'id' => 'event-aggregator', + 'title' => esc_html__( 'Event Aggregator', 'tribe-common' ), + 'link' => 'http://evnt.is/19mk', + 'plugin' => [ 'the-events-calendar' ], + 'is_active' => class_exists( 'Tribe__Events__Aggregator' ) && tribe( + 'events-aggregator.main' + )->is_service_active(), + ]; + + $addons['events-filter-bar'] = [ + 'id' => 'events-filter-bar', + 'title' => esc_html__( 'Filter Bar', 'tribe-common' ), + 'link' => 'http://evnt.is/hu', + 'plugin' => [ 'the-events-calendar' ], + 'is_active' => class_exists( 'Tribe__Events__Filterbar__View' ), + ]; + + $addons['events-virtual'] = [ + 'id' => 'events-virtual', + 'title' => esc_html__( 'Virtual Events', 'tribe-common' ), + 'link' => 'http://evnt.is/1alb', + 'plugin' => [ 'the-events-calendar' ], + 'is_active' => class_exists( '\Tribe\Events\Virtual\Plugin' ), + ]; + + $addons['event-tickets-plus'] = [ + 'id' => 'event-tickets-plus', + 'title' => esc_html__( 'Event Tickets Plus', 'tribe-common' ), + 'link' => 'http://evnt.is/18wa', + 'plugin' => [ 'event-tickets' ], + 'is_active' => class_exists( 'Tribe__Tickets_Plus__Main' ), + 'is_important' => true, + ]; + + $addons['event-community-tickets'] = [ + 'id' => 'event-community-tickets', + 'title' => esc_html__( 'Community Tickets', 'tribe-common' ), + 'link' => 'http://evnt.is/18m2', + 'plugin' => [ 'event-tickets' ], + 'is_active' => class_exists( 'Tribe__Events__Community__Tickets__Main' ), + ]; + + /** + * Filter the array of premium addons upsold on the sidebar of the Settings > Help tab + * + * @param array $addons + */ + $addons = (array) apply_filters( 'tribe_help_addons', $addons ); + + // Should I filter something + if ( is_null( $plugin ) && is_null( $is_active ) && is_null( $is_important ) ) { + return $addons; + } + + // Allow for easily grab the addons for a plugin + $filtered = []; + foreach ( $addons as $id => $addon ) { + if ( ! is_null( $plugin ) && ! in_array( $plugin, (array) $addon['plugin'] ) ) { + continue; + } + + // Filter by is_active + if ( + ! is_null( $is_active ) && + ( ! isset( $addon['is_active'] ) || $is_active !== $addon['is_active'] ) + ) { + continue; + } + + // Filter by is_important + if ( + ! is_null( $is_important ) && + ( ! isset( $addon['is_important'] ) || $is_important !== $addon['is_important'] ) + ) { + continue; + } + + $filtered[ $id ] = $addon; + } + + return $filtered; + } + + public function is_active( $should_be_active ) { + $plugins = $this->get_plugins( null, true ); + $addons = $this->get_addons( null, true ); + + $actives = array_merge( $plugins, $addons ); + $is_active = []; + + foreach ( $actives as $id => $active ) { + if ( in_array( $id, (array) $should_be_active ) ) { + $is_active[] = $id; + } + } + + return count( array_filter( $is_active ) ) === 0 ? false : true; + } + + /** + * From a Given link returns it with a GA arguments + * + * @since 4.0 + * + * @param string $link An absolute or a Relative link + * @param boolean $relative Is the Link absolute or relative? + * @return string Link with the GA arguments + */ + public function get_ga_link( $link = null, $relative = true ) { + $query_args = [ + 'utm_source' => 'helptab', + 'utm_medium' => 'plugin-tec', + 'utm_campaign' => 'in-app', + ]; + + if ( true === $relative ) { + $link = trailingslashit( Tribe__Main::$tec_url . $link ); + } + + return esc_url( add_query_arg( $query_args, $link ) ); + } + + /** + * Gets the Feed items from The Events Calendar's Blog + * + * @since 4.0 + * + * @return array Feed Title and Link + */ + public function get_feed_items() { + $news_rss = fetch_feed( Tribe__Main::FEED_URL ); + $news_feed = []; + + if ( ! is_wp_error( $news_rss ) ) { + /** + * Filter the maximum number of items returned from the tribe news feed + * + * @param int $max_items default 5 + */ + $maxitems = $news_rss->get_item_quantity( apply_filters( 'tribe_help_rss_max_items', 5 ) ); + $rss_items = $news_rss->get_items( 0, $maxitems ); + if ( $maxitems > 0 ) { + foreach ( $rss_items as $item ) { + $item = [ + 'title' => esc_html( $item->get_title() ), + 'link' => esc_url( $item->get_permalink() ), + ]; + $news_feed[] = $item; + } + } + } + + return $news_feed; + } + + /** + * Get the information from the Plugin API data + * + * @since 4.0 + * + * @param object $plugin Plugin Object to be used + * @return object An object with the API data + */ + private function get_plugin_api_data( $plugin = null ) { + if ( is_scalar( $plugin ) ) { + return false; + } + + $plugin = (object) $plugin; + + /** + * Filter the amount of time (seconds) we will keep api data to avoid too many external calls + * @var int + */ + $timeout = apply_filters( 'tribe_help_api_data_timeout', 3 * HOUR_IN_SECONDS ); + $transient = 'tribe_help_api_data-' . $plugin->name; + $data = get_transient( $transient ); + + if ( false === $data ) { + if ( ! function_exists( 'plugins_api' ) ) { + include_once ABSPATH . '/wp-admin/includes/plugin-install.php'; + } + + // Fetch the data + $data = plugins_api( 'plugin_information', [ + 'slug' => $plugin->name, + 'is_ssl' => is_ssl(), + 'fields' => [ + 'banners' => true, + 'reviews' => true, + 'downloaded' => true, + 'active_installs' => true, + ], + ] ); + + if ( ! is_wp_error( $data ) ) { + // Format Downloaded Infomation + $data->downloaded = $data->downloaded ? number_format( $data->downloaded ) : _x( 'n/a', 'not available', 'tribe-common' ); + } else { + // If there was a bug on the Current Request just leave + return false; + } + + set_transient( $transient, $data, $timeout ); + } + $data->up_to_date = ( version_compare( $plugin->version, $data->version, '<' ) ) ? esc_html__( 'You need to upgrade!', 'tribe-common' ) : esc_html__( 'You are up to date!', 'tribe-common' ); + + /** + * Filters the API data that was stored in the Transient option + * + * @var array + * @var object The plugin object, check `$this->get_plugins()` for more info + */ + return (object) apply_filters( 'tribe_help_api_data', $data, $plugin ); + } + + /** + * Parses the help text from an Array to the final HTML. + * + * It is the responsibility of code calling this function to ensure proper escaping + * within any HTML. + * + * @since 4.0 + * + * @param string|array $mixed The mixed value to create the HTML from + * @return string + */ + public function get_content_html( $mixed = '' ) { + // If it's an StdObj or String it will be converted + $mixed = (array) $mixed; + + // Loop to start the HTML + foreach ( $mixed as &$line ) { + // If we have content we use that + if ( isset( $line->content ) ) { + $line = $line->content; + } + + if ( is_string( $line ) ) { + continue; + } elseif ( is_array( $line ) ) { + // Allow the developer to pass some configuration + if ( empty( $line['type'] ) ) { + $line['type'] = 'ul'; + } + + $text = '<' . $line['type'] . '>' . "\n"; + foreach ( $line as $key => $item ) { + // Don't add non-numeric items (a.k.a.: configuration) + if ( ! is_numeric( $key ) ) { + continue; + } + + // Only add List Item if is a UL or OL + if ( in_array( $line['type'], [ 'ul', 'ol' ] ) ) { + $text .= '
  • ' . "\n"; + } + + $text .= $this->get_content_html( $item ); + + if ( in_array( $line['type'], [ 'ul', 'ol' ] ) ) { + $text .= '
  • ' . "\n"; + } + } + $text .= '' . "\n"; + + // Create the list as html instead of array + $line = $text; + } + } + + return wpautop( implode( "\n\n", $mixed ) ); + } + + /** + * A Private storage for sections. + * + * @since 4.0 + * + * @access private + * @var array + */ + private $sections = []; + + /** + * Incremented with each method call, then stored in $section->uid. + * + * Used when sorting two instances whose priorities are equal. + * + * @since 4.0 + * + * @static + * @access protected + * @var int + */ + protected static $section_count = 0; + + /** + * Helper function to compare two objects by priority, ensuring sort stability via uid. + * + * @since 4.0 + * + * @access protected + * @param object $a Object A. + * @param object $b Object B. + * + * @return int + */ + protected function by_priority( $a, $b ) { + if ( ! isset( $a->priority ) || ! isset( $b->priority ) || $a->priority === $b->priority ) { + if ( ! isset( $a->unique_call_order ) || ! isset( $b->unique_call_order ) ) { + return 0; + } else { + return $a->unique_call_order > $b->unique_call_order ? 1 : -1; + } + } else { + return $a->priority > $b->priority ? 1 : -1; + } + } + + /** + * Adds a new section to the Help Page + * + * @since 4.0 + * + * @param string $id HTML like ID + * @param string $title The Title of the section, doesn't allow HTML + * @param integer $priority A Numeric ordering for the Section + * @param string $type by default only 'default' or 'box' + * + * @return object The section added + */ + public function add_section( $id, $title = null, $priority = 10, $type = 'default' ) { + if ( empty( $id ) ) { + return false; + } + + // Everytime you call this we will add this up + self::$section_count++; + + $possible_types = (array) apply_filters( 'tribe_help_available_section_types', [ 'default', 'box' ] ); + + // Set a Default type + if ( empty( $type ) || ! in_array( $type, $possible_types ) ) { + $type = 'default'; + } + + // Create the section and Sanitize the values to avoid having to do it later + $section = (object) [ + 'id' => sanitize_html_class( $id ), + 'title' => esc_html( $title ), + 'priority' => absint( $priority ), + 'type' => sanitize_html_class( $type ), + + // This Method Unique count integer used for ordering with priority + 'unique_call_order' => self::$section_count, + + // Counter for ordering Content + 'content_count' => 0, + + // Setup the Base for the content to come + 'content' => [], + ]; + + $this->sections[ $section->id ] = $section; + + return $section; + } + + /** + * Add a New content Item to a Help page Section + * + * @since 4.0 + * + * @param string $section_id Which section this content should be assigned to + * @param string|array $content Item text or array of items, will be passed to `$this->get_content_html` + * @param integer $priority A Numeric priority + * @param array $arguments If you need arguments for item, they can be passed here + * + * @return object The content item added + */ + public function add_section_content( $section_id, $content, $priority = 10, $arguments = [] ) { + $section_id = sanitize_html_class( $section_id ); + + // Check if the section exists + if ( empty( $this->sections[ $section_id ] ) ) { + return false; + } + + // Make sure we have arguments as Array + if ( ! is_array( $arguments ) ) { + return false; + } + + $section = &$this->sections[ $section_id ]; + + // Increment the content counter + $section->content_count++; + + $item = (object) $arguments; + + // Set the priority + $item->priority = absint( $priority ); + + // Set the uid to help ordering + $item->unique_call_order = $section->content_count; + + // Content is not Safe, will be Sanitized on Output + $item->content = $content; + + $section->content[] = $item; + + return $item; + } + + /** + * Remove a section based on the ID + * This method will remove any sections that are indexed at that ID on the sections array + * And the sections that have a propriety of `id` equals to the given $section_id argument + * + * @param string|int $section_id You can use Numeric or String indexes to search + * @return bool|int Returns `false` when no sections were removed and an `int` with the number of sections removed + */ + public function remove_section( $section_id ) { + if ( + ! isset( $this->sections[ $section_id ] ) && + ! in_array( (object) [ 'id' => $section_id ], $this->sections, true ) + ) { + // There are no sections to remove, so false + return false; + } + + $removed = []; + foreach ( $this->sections as $id => $section ) { + if ( ! is_numeric( $id ) && ! is_numeric( $section_id ) && ! empty( $section->id ) ) { + if ( $section->id === $section_id ) { + unset( $this->sections[ $id ] ); + // Mark that this section was removed + $removed[ $id ] = true; + } + } elseif ( $id === $section_id ) { + unset( $this->sections[ $section_id ] ); + // Mark that this section was removed + $removed[ $id ] = true; + } else { + // Mark that this section was NOT removed + $removed[ $id ] = false; + } + } + + // Count how many were removed + $total = count( array_filter( $removed ) ); + + // if Zero just return false + return $total === 0 ? false : $total; + } + + /** + * Based on an Array of sections it render the Help Page contents + * + * @since 4.0 + * + * @param boolean $print Return or Print the HTML after + * @return void|string + */ + public function get_sections( $print = true ) { + /** + * Allow third-party sections here + * + * @var Tribe__Admin__Help_Page + */ + do_action( 'tribe_help_pre_get_sections', $this ); + + /** + * Allow developers to filter all the sections at once + * NOTE: You should be using `tribe_help_add_sections` to add new sections or content + * + * @var array + */ + $sections = apply_filters( 'tribe_help_sections', $this->sections ); + + if ( ! is_array( $sections ) || empty( $sections ) ) { + return false; + } + + // Sort by Priority + uasort( $sections, [ $this, 'by_priority' ] ); + + $html = []; + + foreach ( $sections as $index => $section ) { + $section = (object) $section; + + // If it has no ID or Content, skip + if ( empty( $section->id ) || empty( $section->content ) ) { + continue; + } + + // Set a Default type + if ( empty( $section->type ) ) { + $section->type = 'default'; + } + + /** + * Creates a way to filter a specific section based on the ID + * + * @var object + */ + $section = apply_filters( 'tribe_help_section_' . $section->id, $section, $this ); + + // Sort by Priority + uasort( $section->content, [ $this, 'by_priority' ] ); + + $html[ $section->id . '-start' ] = '
    '; + + if ( ! empty( $section->title ) ) { + $html[ $section->id . '-title' ] = '

    ' . esc_html__( $section->title ) . '

    '; + } + + $html[ $section->id . '-content' ] = $this->get_content_html( $section->content ); + + $html[ $section->id . '-end' ] = '
    '; + } + + /** + * Creates a way for developers to hook to the final HTML + * @var array $html + * @var array $sections + */ + $html = apply_filters( 'tribe_help_sections_html', $html, $sections ); + + if ( true === $print ) { + echo implode( "\n", $html ); + } else { + return $html; + } + + } + + /** + * Prints the Plugin box for the given plugin + * + * @since 4.0 + * + * @param string $plugin Plugin Name key + * @return void + */ + public function print_plugin_box( $plugin ) { + $plugin = (object) $this->get_plugins( $plugin, false ); + $api_data = $this->get_plugin_api_data( $plugin ); + $addons = $this->get_addons( $plugin->name ); + $plugins = get_plugins(); + + if ( $api_data ) { + if ( ! function_exists( 'install_plugin_install_status' ) ) { + include_once ABSPATH . '/wp-admin/includes/plugin-install.php'; + } + $status = install_plugin_install_status( $api_data ); + $plugin_active = is_plugin_active( $status['file'] ); + $plugin_exists = isset( $plugins[ $status['file'] ] ); + + if ( 'install' !== $status['status'] && ! $plugin_active ) { + $args = [ + 'action' => 'activate', + 'plugin' => $status['file'], + 'plugin_status' => 'all', + 'paged' => 1, + 's' => '', + ]; + $activate_url = wp_nonce_url( add_query_arg( $args, 'plugins.php' ), 'activate-plugin_' . $status['file'] ); + $link = '' . esc_html__( 'Activate Plugin', 'tribe-common' ) . ''; + } elseif ( 'update_available' === $status['status'] ) { + $args = [ + 'action' => 'upgrade-plugin', + 'plugin' => $status['file'], + ]; + $update_url = wp_nonce_url( add_query_arg( $args, 'update.php' ), 'upgrade-plugin_' . $status['file'] ); + + $link = '' . esc_html__( 'Upgrade Plugin', 'tribe-common' ) . ''; + } elseif ( $plugin_exists ) { + $link = '' . esc_html__( 'You are up to date!', 'tribe-common' ) . ''; + } + } + + if ( ! isset( $link ) ) { + if ( $api_data ) { + $args = [ + 'tab' => 'plugin-information', + 'plugin' => $plugin->name, + 'TB_iframe' => true, + 'width' => 772, + 'height' => 600, + ]; + $iframe_url = add_query_arg( $args, admin_url( '/plugin-install.php' ) ); + $link = '' . esc_html__( 'Install Plugin', 'tribe-common' ) . ''; + } else { + $link = null; + } + } + ?> +
    +

    title ); ?>

    + + description ) && ! $plugin->is_active ) { + echo wpautop( $plugin->description ); + } + ?> + + +
    +
    +
    version ); ?>
    + +
    +
    requires ); ?>+
    + +
    +
    active_installs ) ); ?>+
    + +
    +
    + + $api_data->rating, + 'type' => 'percent', + 'number' => $api_data->num_ratings, + ] ); + ?> + +
    +
    + + + ' . $link . '

    ' : ''; + } + ?> + + +

    + + +
    + __( 'Can I have more than one calendar?', 'tribe-common' ), + 'answer' => __( 'No, but you can use event categories or tags to display certain events like having...', 'tribe-common' ), + 'link' => 'https://evnt.is/1arh', + ], + [ + 'question' => __( 'What do I get with Events Calendar Pro?', 'tribe-common' ), + 'answer' => __( 'Events Calendar Pro runs alongside The Events Calendar and enhances...' ), + 'link' => 'https://evnt.is/1arj', + ], + [ + 'question' => __( 'How do I sell tickets to events?', 'tribe-common' ), + 'answer' => __( 'Use our free Event Tickets plugin to get started with tickets and RSVPs.', 'tribe-common' ), + 'link' => 'https://evnt.is/1ark', + ], + [ + 'question' => __( 'Where can I find a list of available shortcodes?', 'tribe-common' ), + 'answer' => __( 'Our plugins include many shortcodes that do everything from embedding the calendar...', 'tribe-common' ), + 'link' => 'https://evnt.is/1arl', + ], + ] ); + + return $faqs; + } + + /** + * Defines calendar extensions and displays them in the UI. + * + * @since 4.14.2 + * + * @return array of extensions which are displayed on the calendar and community tab of the in-app help page. + */ + public function get_calendar_extensions() { + $extensions = apply_filters( 'tec_help_calendar_extensions', [ + [ + 'title' => __( 'Calendar widget areas', 'tribe-common' ), + 'description' => __( 'This extension creates a useful variety of WordPress widget areas (a.k.a. sidebars).', 'tribe-common' ), + 'link' => 'https://evnt.is/1arc', + 'product-slug' => 'the-events-calendar', + ], + [ + 'title' => __( 'Event block patterns', 'tribe-common' ), + 'description' => __( 'This extension adds a set of block patterns for events to the WordPress block editor.', 'tribe-common' ), + 'link' => 'https://evnt.is/1ard', + 'product-slug' => 'the-events-calendar', + ], + [ + 'title' => __( 'Alternative photo view', 'tribe-common' ), + 'description' => __( 'This extension replaces photo view with a tiled grid of cards featuring event images.', 'tribe-common' ), + 'link' => 'https://evnt.is/1are', + 'product-slug' => 'events-calendar-pro', + ], + [ + 'title' => __( 'The Events Calendar Tweaks', 'tribe-common' ), + 'description' => __( 'This extension is a collection of tweaks and snippets for The Events Calendar.', 'tribe-common' ), + 'link' => 'https://evnt.is/1arg', + 'product-slug' => 'the-events-calendar', + ], + ] ); + + return $extensions; + } + + /** + * Defines calendar products. + * + * @since 4.14.2 + * + * @return array of products which are displayed on the calendar tab of the in-app help page. + */ + public function get_calendar_products() { + $calendar_products = apply_filters( 'tec_help_calendar_products', [ + 'events-calendar-pro', + 'tribe-filterbar', + 'event-aggregator', + 'event-automator', + 'events-virtual', + ] ); + + return $calendar_products; + } + + /** + * Defines ticketing frequently asked questions and displays them in the UI. + * + * @since 4.14.2 + * + * @return array of FAQs which are displayed on the ticketing tab of the in-app help page. + */ + public function get_ticketing_faqs() { + $faqs = apply_filters( 'tec_help_ticketing_faqs', [ + [ + 'question' => __( 'How Do I create events with Tickets or RSVP’s?', 'tribe-common' ), + 'answer' => __( 'We’ve put together a video tutorial showing how to create events with Tickets using our plugins. Click on the link in the link in the title to learn more.', 'tribe-common' ), + 'link' => 'https://evnt.is/1art', + ], + [ + 'question' => __( 'How Do I Set Up E-Commerce Plugins for Selling Tickets?', 'tribe-common' ), + 'answer' => __( 'You can sell tickets using our built-in e-commerce option, or upgrade to Event Tickets Plus to use ecommerce plugins such as WooCommerce.', 'tribe-common' ), + 'link' => 'https://evnt.is/1arq', + ], + [ + 'question' => __( 'Can I have a seating chart associated with my tickets?', 'tribe-common' ), + 'answer' => __( 'Yes! You can easily accomplish this task using the stock options and multiple ticket types available with Event Tickets.', 'tribe-common' ), + 'link' => 'https://evnt.is/1arr', + ], + [ + 'question' => __( 'How do I process refunds for tickets?', 'tribe-common' ), + 'answer' => __( 'When it comes to paid tickets, these orders can be refunded through the e-commerce platform in use.', 'tribe-common' ), + 'link' => 'https://evnt.is/1ars', + ], + ] ); + + return $faqs; + } + + /** + * Defines ticketing extensions and displays them in the UI. + * + * @since 4.14.2 + * + * @return array of extensions which are displayed on the ticketing tab of the in-app help page. + */ + public function get_ticketing_extensions() { + $extensions = apply_filters( 'tec_help_ticketing_extensions', [ + [ + 'title' => __( 'Ticket Email Settings', 'tribe-common' ), + 'description' => __( 'Adds a new settings panel in Events > Settings that gives more control over the ticket and rsvp emails that are sent to attendees after registration.', 'tribe-common' ), + 'link' => 'https://evnt.is/1arx', + 'product-slug' => 'event-tickets', + ], + [ + 'title' => __( 'Per Event Check In API', 'tribe-common' ), + 'description' => __( 'This extension shows a meta box with an API key on each Event with Ticket/RSVP.', 'tribe-common' ), + 'link' => 'https://evnt.is/1arw', + 'product-slug' => 'event-tickets', + ], + [ + 'title' => __( 'Add Event & Attendee Info to WooCommerce Order Details', 'tribe-common' ), + 'description' => __( 'Displays the information collected by “attendee meta fields” in the WooCommerce order screens as well.', 'tribe-common' ), + 'link' => 'https://evnt.is/1arv', + 'product-slug' => 'event-tickets', + ], + [ + 'title' => __( 'Organizer Notification Email', 'tribe-common' ), + 'description' => __( 'This extension will send an email to event organizers whenever a user registers for their event.', 'tribe-common' ), + 'link' => 'https://evnt.is/1aru', + 'product-slug' => 'event-tickets', + ], + ] ); + + return $extensions; + } + + /** + * Defines ticketing products. + * + * @since 4.14.2 + * + * @return array of products which are displayed on the ticketing tab of the in-app help page. + */ + public function get_ticketing_products() { + $ticketing_products = apply_filters( 'tec_help_ticketing_products', [ + 'event-tickets', + 'event-tickets-plus', + 'tribe-eventbrite', + 'promoter', + ] ); + + return $ticketing_products; + } + + /** + * Defines community extensions and displays them in the UI. + * + * @since 4.14.2 + * + * @return array of extensions which are displayed on the community tab of the in-app help page. + */ + public function get_community_extensions() { + $extensions = apply_filters( 'tec_help_ticketing_extensions', [ + [ + 'title' => __( 'Add Cost Currency Symbol', 'tribe-common' ), + 'description' => __( 'This extension allows you to set default currency symbols for your users to choose from instead of having a plain text field.', 'tribe-common' ), + 'link' => 'https://evnt.is/1arn', + 'product-slug' => 'community-events', + ], + [ + 'title' => __( 'Add Google Maps Display and Link Options', 'tribe-common' ), + 'description' => __( 'This extension adds the “Show Google Maps” and “Show Google Maps Link” checkboxes when creating a new Venue.', 'tribe-common' ), + 'link' => 'https://evnt.is/1arm', + 'product-slug' => 'community-events', + ], + [ + 'title' => __( 'Hide Others’ Organizers and Venues', 'tribe-common' ), + 'description' => __( 'This extension allows you to hide the Organizers and Venues that a visitor has not created from the Community Events submission form.', 'tribe-common' ), + 'link' => 'https://evnt.is/1aro', + 'product-slug' => 'community-events', + ], + [ + 'title' => __( 'Display Custom HTML', 'tribe-common' ), + 'description' => __( 'This extension allows you to add custom HTML content to the top of the Community Events submission form.', 'tribe-common' ), + 'link' => 'https://evnt.is/1arp', + 'product-slug' => 'community-events', + ], + ] ); + + return $extensions; + } + + /** + * Defines community products. + * + * @since 4.14.2 + * + * @return array of products which are displayed on the community tab of the in-app help page. + */ + public function get_community_products() { + $community_products = apply_filters( 'tec_help_ticketing_products', [ + 'events-community', + 'events-community-tickets', + ] ); + + return $community_products; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Helpers.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Helpers.php new file mode 100644 index 000000000..7f1d832b2 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Helpers.php @@ -0,0 +1,193 @@ +doing_ajax() ) { + return false; + } + + // Avoid Notices by checking the object type of WP_Screen + if ( ! $this->is_wp_screen() ) { + return false; + } + + $defaults = apply_filters( 'tribe_is_post_type_screen_post_types', Tribe__Main::get_post_types() ); + + // Match any Post Type from Tribe + if ( is_null( $post_type ) && in_array( $current_screen->post_type, $defaults ) ) { + return true; + } + + // Match any of the post_types set + if ( ! is_scalar( $post_type ) && in_array( $current_screen->post_type, (array) $post_type ) ) { + return true; + } + + // Match a specific Post Type + if ( $current_screen->post_type === $post_type ) { + return true; + } + + return false; + } + + /** + * Matcher for administration pages that are from Tribe the easier way + * + * @param string|array|null $id What will be checked to see if we return true or false + * + * @return boolean + */ + public function is_screen( $id = null ) { + global $current_screen; + + // Not in the admin we don't even care + if ( ! is_admin() ) { + return false; + } + + // Doing AJAX? bail. + if ( tribe( 'context' )->doing_ajax() ) { + return false; + } + + // Avoid Notices by checking the object type of WP_Screen. + if ( ! $this->is_wp_screen() ) { + return false; + } + + // Match any screen from Tribe. + if ( is_null( $id ) && false !== strpos( $current_screen->id, 'tribe' ) ) { + return true; + } + + // Match any screen from TEC. + if ( is_null( $id ) && false !== strpos( $current_screen->id, 'tec' ) ) { + return true; + } + + // Match any of the pages set + if ( ! is_scalar( $id ) && in_array( $current_screen->id, (array) $id ) ) { + return true; + } + + // Match a specific page. + if ( $current_screen->id === $id ) { + return true; + } + + // Match any post type page in the supported post types. + $defaults = apply_filters( 'tribe_is_post_type_screen_post_types', Tribe__Main::get_post_types() ); + if ( in_array( $current_screen->post_type, $defaults ) ) { + return true; + } + return false; + + } + + /** + * Matcher for administration pages action + * + * @param string|array|null $action What will be checked to see if we return true or false + * + * @return boolean + */ + public function is_action( $action = null ) { + global $current_screen; + + // Not in the admin we don't even care + if ( ! is_admin() ) { + return false; + } + + // Doing AJAX? bail. + if ( tribe( 'context' )->doing_ajax() ) { + return false; + } + + // Avoid Notices by checking the object type of WP_Screen + if ( ! $this->is_wp_screen() ) { + return false; + } + + // Match any of the pages set + if ( ! is_scalar( $action ) && in_array( $current_screen->action, (array) $action ) ) { + return true; + } + + // Match a specific page + if ( $current_screen->action === $action ) { + return true; + } + + return false; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Live_Date_Preview.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Live_Date_Preview.php new file mode 100644 index 000000000..9b53e99bc --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Live_Date_Preview.php @@ -0,0 +1,68 @@ + Settings > Display admin screen. + */ +class Tribe__Admin__Live_Date_Preview { + protected $target_fields = [ + 'dateWithYearFormat', + 'dateWithoutYearFormat', + 'monthAndYearFormat', + 'weekDayFormat', + ]; + + /** + * Static Singleton Holder + * + * @var self + */ + protected static $instance; + + /** + * Static Singleton Factory Method + * + * @return self + */ + public static function instance() { + return self::$instance ? self::$instance : self::$instance = new self; + } + + /** + * Adds live date previews to the display settings tab (nothing is setup unless + * the user is actually on that tab). + */ + public function __construct() { + add_action( 'tribe_settings_after_do_tabs', [ $this, 'listen' ] ); + } + + /** + * If the user looking at the Display settings tab, adds live date preview facilities. + */ + public function listen() { + // We are only interested in the "Display" tab + if ( 'display' !== Tribe__Settings::instance()->currentTab ) { + return; + } + + /** + * Add or remove fields which should have live date/time preview facilities. + * + * @var array $target_fields + */ + $this->target_fields = (array) apply_filters( 'tribe_settings_date_preview_fields', $this->target_fields ); + + add_filter( 'tribe_field_div_end', [ $this, 'setup_date_previews' ], 10, 2 ); + + // We are still before `admin_enqueue_scripts` making it safe to use `tribe_asset` + tribe_asset( Tribe__Main::instance(), 'tribe-date-live-refresh', 'admin-date-preview.js', [ 'jquery' ], 'admin_enqueue_scripts' ); + } + + public function setup_date_previews( $html, $field ) { + // Not one of the fields we're interested in? Return without modification + if ( ! in_array( $field->id, $this->target_fields ) ) { + return $html; + } + + $preview = esc_html( date_i18n( $field->value ) ); + return " $preview $html"; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/Date_Based.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/Date_Based.php new file mode 100644 index 000000000..79780d00c --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/Date_Based.php @@ -0,0 +1,378 @@ +tec_is_active = $tribe_dependency->is_plugin_active( 'Tribe__Events__Main' ); + $this->et_is_active = $tribe_dependency->is_plugin_active( 'Tribe__Tickets__Main' ); + + $now = Dates::build_date_object( 'now', 'UTC' ); + $notice_start = $this->get_start_time(); + $notice_end = $this->get_end_time(); + $extension_date = $this->get_extension_time(); + + // If we have an extension date defined. + if ( ! empty( $this->get_extension_time() ) ) { + // If the sale has started and + if ( + $notice_start <= $now + && $notice_end < $now + && $now < $extension_date + ) { + add_filter( "tribe_{$this->slug}_notice_end_date", function() { + return $this->get_extension_time(); + }); + } + } + + $this->hook(); + } + + /** + * Register the various Marketing notices. + * + * @since 4.14.2 + */ + public function hook() { + $this->hook_notice(); + } + + /** + * Register the notice. + * + * @since 4.14.2 + */ + public function hook_notice() { + tribe_notice( + $this->slug, + [ $this, "display_notice" ], + [ + 'type' => 'tribe-banner', + 'dismiss' => 1, + 'priority' => -1, + 'wrap' => false, + ], + [ $this, "should_display" ] + ); + } + + /** + * HTML for the notice. + * + * @since 4.14.2 + * + * @return string The HTML string to be displayed. + */ + abstract function display_notice(); + + /** + * Function to get and filter the screens the notice is displayed on. + * + * @since 4.15.4 + * + * @return array List of allowed screens. + */ + public function get_screens() { + $screens = $this->screens; + + /** + * Allows filtering of the screens for all date-based notices. + * + * @since 4.15.4 + * + * @param array $screens The current list of allowed screens. + * @param string $slug The slug for the current notice. + * + * @return array $screens The modified list of allowed screens. + */ + $screens = apply_filters( + 'tec_date_based_notice_get_screens', + $screens, + $this->slug + ); + + /** + * Allows filtering of the screens for a specific date-based notice. + * + * @since 4.15.4 + * + * @param array $screens The current list of allowed screens. + * + * @return array $screens The modified list of allowed screens. + */ + $screens = apply_filters( + "tec_date_based_notice_get_screens_{$this->slug}", + $screens + ); + + return $screens; + } + + /** + * Whether the notice should display. + * + * @since 4.14.2 + * + * @return boolean $should_display Whether the notice should display or not. + */ + public function should_display() { + // If upsells have been manually hidden, respect that. + if ( tec_should_hide_upsell() ) { + return false; + } + + $current_screen = get_current_screen(); + + $screens = $this->get_screens(); + + // If not a valid screen, don't display. + if ( empty( $current_screen->id ) || ! in_array( $current_screen->id, $screens, true ) ) { + return false; + } + + $now = Dates::build_date_object( 'now', 'UTC' ); + $notice_start = $this->get_start_time(); + $notice_end = $this->get_end_time(); + + $should_display = $notice_start <= $now && $now < $notice_end; + + + /** + * Allow filtering of whether the notice should display. + * + * @since 4.14.2 + * + * @param boolean $should_display Whether the notice should display. + * @param Tribe__Admin__Notice_Date_Based $notice The notice object. + */ + return apply_filters( "tribe_{$this->slug}_notice_should_display", $should_display, $this ); + } + + /** + * Unix time for notice start. + * + * @since 4.14.2 + * + * @return \Tribe\Utils\Date_I18n - Date Object + */ + public function get_start_time() { + $date = Dates::build_date_object( $this->start_date, 'UTC' ); + if ( $this->start_time !== null ) { + $date = $date->setTime( $this->start_time, 0 ); + } + + /** + * Allow filtering of the start date DateTime object, + * to allow for things like "the day before" ( $date->modify( '-1 day' ) ) and such. + * + * @since 4.14.2 + * + * @param \DateTime $date Date object for the notice start. + */ + $date = apply_filters( "tribe_{$this->slug}_notice_start_date", $date, $this ); + + return $date; + } + + /** + * Unix time for notice end. + * + * @since 4.14.2 + * + * @return \Tribe\Utils\Date_I18n - Date Object + */ + public function get_end_time() { + $date = Dates::build_date_object( $this->end_date, 'UTC' ); + if ( $this->end_time !== null ) { + $date = $date->setTime( $this->end_time, 0 ); + } + + /** + * Allow filtering of the end date DateTime object, + * to allow for things like "the day after" ( $date->modify( '+1 day' ) ) and such. + * + * @since 4.14.2 + * + * @param \DateTime $date Date object for the notice end. + */ + $date = apply_filters( "tribe_{$this->slug}_notice_end_date", $date, $this ); + + return $date; + } + + + + /** + * Unix time for notice extension end. + * + * @since 4.15.4 + * + * @return int $end_time The date & time the notice should stop displaying, as a Unix timestamp. + */ + public function get_extension_time() { + if ( $this->extension_date === null ) { + return null; + } + + $date = Dates::build_date_object( $this->extension_date, 'UTC' ); + + if ( $this->extension_time !== null ) { + $date = $date->setTime( $this->extension_time, 0 ); + } + + /** + * Allow filtering of the extension date DateTime object, + * to allow for things like "the day after" ( $date->modify( '+1 day' ) ) and such. + * + * @since 4.14.2 + * + * @param \DateTime $date Date object for the notice end. + */ + $date = apply_filters( "tribe_{$this->slug}_notice_extension_date", $date, $this ); + + return $date; + } + + /** + * Gets the template instance used to setup the rendering of the page. + * + * @since 4.14.7 + * + * @return \Tribe__Template + */ + public function get_template() { + if ( empty( $this->template ) ) { + $this->template = new \Tribe__Template(); + $this->template->set_template_origin( \Tribe__Main::instance() ); + $this->template->set_template_folder( 'src/admin-views' ); + $this->template->set_template_context_extract( true ); + $this->template->set_template_folder_lookup( false ); + } + + return $this->template; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/Marketing.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/Marketing.php new file mode 100644 index 000000000..add054cb8 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/Marketing.php @@ -0,0 +1,190 @@ +tec_is_active = $tribe_dependency->is_plugin_active( 'Tribe__Events__Main' ); + $this->et_is_active = $tribe_dependency->is_plugin_active( 'Tribe__Tickets__Main' ); + } + + /** + * Register the various Marketing notices. + * + * @since 4.7.23 + */ + public function hook() { + $this->black_friday_hook_notice(); + } + + /** + * Register the Black Friday notice. + * + * @since 4.12.14 + */ + public function black_friday_hook_notice() { + + tribe_notice( + 'black-friday', + [ $this, 'black_friday_display_notice' ], + [ + 'type' => 'tribe-banner', + 'dismiss' => 1, + 'priority' => -1, + 'wrap' => false, + ], + [ $this, 'black_friday_should_display' ] + ); + } + + /** + * Unix time for Monday of Thanksgiving week @ 11am UTC. (11am UTC is 6am EST). + * + * @since 4.12.14 + * + * @return int + */ + public function get_black_friday_start_time() { + $date = Dates::build_date_object( 'fourth Thursday of November ' . date( 'Y' ), 'UTC' ); + $date = $date->modify( '-3 days' ); + $date = $date->setTime( 11, 0 ); + + $start_time = $date->format( 'U' ); + + /** + * Allow filtering of the Black Friday sale start date, mainly for testing purposes. + * + * @since 4.12.14 + * + * @param int $bf_start_date Unix time for the Monday of Thanksgiving week @ 6am UTC. + */ + return apply_filters( 'tribe_black_friday_start_time', $start_time ); + } + + /** + * Unix time for Dec 1 @ 5am UTC. (5am UTC is 12am EST). + * + * @since 4.12.14 + * + * @return int + */ + public function get_black_friday_end_time() { + $date = Dates::build_date_object( 'December 1st', 'UTC' ); + $date = $date->setTime( 5, 0 ); + + $end_time = $date->format( 'U' ); + + /** + * Allow filtering of the Black Friday sale end date, mainly for testing purposes. + * + * @since 4.12.14 + * + * @param int $bf_end_date Unix time for Dec 1 @ 6am UTC. + */ + return apply_filters( 'tribe_black_friday_end_time', $end_time ); + } + /** + * Whether the Black Friday notice should display. + * + * Unix times for Monday of Thanksgiving week @ 6am UTC and Dec 1 2020 @ 6am UTC. + * 6am UTC is midnight for TheEventsCalendar.com, which uses the America/Los_Angeles time zone. + * + * @since 4.12.14 + * + * @return boolean + */ + public function black_friday_should_display() { + // If upsells have been manually hidden, respect that. + if ( tec_should_hide_upsell() ) { + return false; + } + + $now = Dates::build_date_object( 'now', 'UTC' )->format( 'U' ); + $bf_sale_start = $this->get_black_friday_start_time(); + $bf_sale_end = $this->get_black_friday_end_time(); + + $current_screen = get_current_screen(); + + $screens = [ + 'tribe_events_page_tribe-app-shop', // App shop. + 'events_page_tribe-app-shop', // App shop. + 'tribe_events_page_tribe-common', // Settings & Welcome. + 'tribe_events_page_tec-events-settings', // New Settings & Welcome. + 'events_page_tribe-common', // Settings & Welcome. + 'toplevel_page_tribe-common', // Settings & Welcome. + ]; + + // If not a valid screen, don't display. + if ( empty( $current_screen->id ) || ! in_array( $current_screen->id, $screens, true ) ) { + return false; + } + + return $bf_sale_start <= $now && $now < $bf_sale_end; + } + + /** + * HTML for the Black Friday notice. + * + * @since 4.12.14 + * + * @return string + */ + public function black_friday_display_notice() { + Tribe__Assets::instance()->enqueue( [ 'tribe-common-admin' ] ); + + $current_screen = get_current_screen(); + + $icon_url = Tribe__Main::instance()->plugin_url . 'src/resources/images/icons/sale-burst.svg'; + $cta_url = 'https://evnt.is/bf' . date( 'Y' ); + $screens = [ + 'tribe_events_page_tribe-common', + 'tribe_events_page_tec-events-settings', + 'events_page_tribe-common', + 'toplevel_page_tribe-common', + ]; + + // If we are on the settings page or a welcome page, change the Black Friday URL. + if ( + ! empty( $current_screen->id ) + && in_array( $current_screen->id, $screens ) + ) { + if ( isset( $_GET['welcome-message-the-events-calendar'] ) || isset( $_GET['welcome-message-event-tickets' ] ) ) { + $cta_url .= 'welcome'; + } else { + $cta_url .= 'settings'; + } + } + + ob_start(); + + include Tribe__Main::instance()->plugin_path . 'src/admin-views/notices/tribe-bf-general.php'; + + return ob_get_clean(); + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/Marketing/Black_Friday.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/Marketing/Black_Friday.php new file mode 100644 index 000000000..ddcb17f0d --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/Marketing/Black_Friday.php @@ -0,0 +1,72 @@ +enqueue( [ 'tribe-common-admin' ] ); + + // Set up template variables. + $template_args = [ + 'icon_url' => \Tribe__Main::instance()->plugin_url . 'src/resources/images/icons/sale-burst.svg', + 'cta_url' => 'https://evnt.is/1aqi', + 'end_date' => $this->get_end_time()->format_i18n( 'F jS' ), + ]; + + // Get the Black Friday notice content. + $content = $this->get_template()->template( 'notices/tribe-bf-general', $template_args, false ); + + return $content; + } + + /** + * Unix time for notice start. + * + * @since 4.14.2 + * + * @return \Tribe\Utils\Date_I18n - Date Object + */ + public function get_start_time() { + $date = parent::get_start_time(); + $date = $date->modify( '-3 days' ); + + return $date; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/Marketing/End_Of_Year_Sale.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/Marketing/End_Of_Year_Sale.php new file mode 100644 index 000000000..30de2a3c3 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/Marketing/End_Of_Year_Sale.php @@ -0,0 +1,66 @@ + \Tribe__Main::instance()->plugin_url . 'src/resources/images/marketing/eoy-sale-2021.svg', + 'cta_url' => 'https://evnt.is/1a-x', + ]; + + // Get the sale notice content. + $content = $this->get_template()->template( 'notices/end-of-year-sale', $template_args, false ); + + return $content; + } + + /** + * Unix time for notice end. + * + * @since 4.14.9 + * + * @return \Tribe\Utils\Date_I18n - Date Object + */ + public function get_end_time() { + $date = parent::get_end_time(); + $date = $date->setTime( 23, 59 ); + + return $date; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/Marketing/Stellar_Sale.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/Marketing/Stellar_Sale.php new file mode 100644 index 000000000..232e72d60 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/Marketing/Stellar_Sale.php @@ -0,0 +1,82 @@ +enqueue( [ 'tribe-common-admin' ] ); + + // Used in the template. + $cta_url = 'https://evnt.is/1bcv'; + $stellar_url = 'https://evnt.is/1bcu'; + $end_date = $this->get_end_time(); + $template_args = [ + 'cta_url' => 'https://evnt.is/1bcv', + 'stellar_url' => 'https://evnt.is/1bcu', + 'end_date' => $this->get_end_time(), + ]; + $dependency = tribe( \Tribe__Dependency::class ); + + if ( $dependency->has_active_premium_plugin() ) { + return $this->get_template()->template( 'notices/tribe-stellar-sale-premium', $template_args, false ); + } else { + return $this->get_template()->template( 'notices/tribe-stellar-sale', $template_args, false ); + } + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/Php_Version.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/Php_Version.php new file mode 100644 index 000000000..cfb18e4ee --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/Php_Version.php @@ -0,0 +1,106 @@ + 'warning', + 'dismiss' => 1, + 'wrap' => 'p', + ], + [ $this, 'should_display' ] + ); + + } + + /** + * Return the list of the Tribe active plugins + * + * @since 4.7.16 + * + * @return string String of items + */ + public function get_active_plugins() { + + $active_plugins = Tribe__Dependency::instance()->get_active_plugins(); + + foreach ( $active_plugins as $active_plugin ) { + + if ( ! $active_plugin['path'] ) { + continue; + } + + $plugin_data = get_plugin_data( $active_plugin['path'] ); + $plugins[] = $plugin_data['Name']; + + } + + return $this->implode_with_grammar( $plugins ); + + } + + /** + * Implodes a list items using 'and' as the final separator and a comma everywhere else + * + * @param array $items List of items to implode + * @since 4.7.16 + * + * @return string String of items + */ + public function implode_with_grammar( $items ) { + + $separator = _x( ', ', 'separator used in a list of items', 'tribe-common' ); + $conjunction = _x( ' and ', 'the final separator in a list of two or more items', 'tribe-common' ); + $output = $last_item = array_pop( $items ); + + if ( $items ) { + $output = implode( $separator, $items ) . $conjunction . $last_item; + } + + return $output; + } + + /** + * We only want to display notices for users + * who are in PHP < 5.6 + * + * @since 4.7.16 + * + * @return boolean + */ + public function should_display() { + // Bail if the user is not admin or can manage plugins + if ( ! current_user_can( 'activate_plugins' ) ) { + return false; + } + + return version_compare( PHP_VERSION, '7.4.0' ) < 0; + } + + /** + * HTML for the PHP notice + * + * @since 4.7.16 + * + * @return string + */ + public function display_notice() { + /* Translators: %1$s list of plugins, %2$s current PHP version, %3$s open anchor html link for read more, %4$s open anchor html link for read more */ + $text = _x( + 'Starting February 2023, %1$s will require PHP 7.4 or later. Currently, your site is using PHP version %2$s. Please update to a newer version. %3$sRead more%4$s.', + 'Message notifying users they need to upgrade PHP', + 'tribe-common' + ); + + $plugins = $this->get_active_plugins(); + + return sprintf( $text, $plugins, PHP_VERSION, '', '' ); + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/Plugin_Download.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/Plugin_Download.php new file mode 100644 index 000000000..5efd1e234 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/Plugin_Download.php @@ -0,0 +1,164 @@ +plugin_path = $plugin_path; + + tribe_notice( + plugin_basename( $plugin_path ), + [ $this, 'show_inactive_plugins_alert' ] + ); + } + + /** + * Add a required plugin to the notice + * + * @since 4.8.3 Method introduced. + * @since 4.9 Added $version and $addon parameters. + * @since 4.9.12 Add $has_pue_notice param + * @since 4.9.17 Appended "+" to all version numbers to indicate "or any later version". + * + * @param string $name Name of the required plugin + * @param null $thickbox_url Download or purchase URL for plugin from within /wp-admin/ thickbox + * @param bool $is_active Indicates if the plugin is installed and active or not + * @param string $version Optional version number of the required plugin + * @param bool $addon Indicates if the plugin is an add-on for The Events Calendar or Event Tickets + * @param bool $has_pue_notice Indicates that we need to change the messaging due to expired key. + */ + public function add_required_plugin( $name, $thickbox_url = null, $is_active = null, $version = null, $addon = false, $has_pue_notice = false ) { + $this->plugins_required[ $name ] = [ + 'name' => $name, + 'thickbox_url' => $thickbox_url, + 'is_active' => $is_active, + 'version' => $version ? $version . '+' : null, + 'addon' => $addon, + 'has_pue_notice' => $has_pue_notice, + ]; + } + + /** + * Echoes the admin notice, attach to admin_notices + * + * @see \Tribe__Admin__Notice__Plugin_Download::add_required_plugin() + * + * @since 4.9.17 Altered the notice to remove "latest version" verbiage since "+" is now added to the version numbers. + */ + public function show_inactive_plugins_alert() { + if ( ! current_user_can( 'activate_plugins' ) ) { + return; + } + + $plugin_data = get_plugin_data( $this->plugin_path ); + $req_plugins = []; + + if ( empty( $this->plugins_required ) ) { + return; + } + + // Make sure Thickbox is available and consistent appearance regardless of which admin page we're on + add_thickbox(); + + $has_pue_notices = false; + + foreach ( $this->plugins_required as $req_plugin ) { + $item = $req_plugin['name']; + $version = empty( $req_plugin['version'] ) ? '' : ' (' . str_replace( '-dev', '', $req_plugin['version'] ) . ')'; + + if ( ! empty( $req_plugin['thickbox_url'] ) ) { + $item = sprintf( + '%3$s%4$s', + esc_attr( $req_plugin['thickbox_url'] ), + esc_attr( $req_plugin['name'] ), + esc_html( $item ), + esc_html( $version ) + ); + } + + if ( false === $req_plugin['is_active'] ) { + $item = sprintf( + '%1$s', + $item + ); + } + + if ( ! empty( $req_plugin['addon'] ) ) { + $plugin_name[] = $req_plugin['name']; + } + + $req_plugins[] = $item; + + // If any of the items has PUE notice we will warn the user. + if ( $req_plugin['has_pue_notice'] ) { + $has_pue_notices = true; + } + } + + // If empty then add in the default name. + if ( empty( $plugin_name[0] ) ) { + $plugin_name[] = $plugin_data['Name']; + } + + $allowed_html = [ + 'strong' => [], + 'a' => [ 'href' => [] ], + ]; + + $plugin_names_clean_text = wp_kses( $this->implode_with_grammar( $plugin_name ), $allowed_html ); + $req_plugin_names_clean_text = wp_kses( $this->implode_with_grammar( $req_plugins ), $allowed_html ); + + $notice_html_content = '

    ' . esc_html__( 'To begin using %2$s, please install (or upgrade) and activate %3$s.', 'tribe-common' ) . '

    '; + + $read_more_link = '' . esc_html__( 'Read more', 'tribe-common' ) . '.'; + $pue_notice_text = esc_html__( 'There’s a new version of %1$s available, but your license is expired. You’ll need to renew your license to get access to the latest version. If you plan to continue using your current version of the plugin(s), be sure to use a compatible version of The Events Calendar. %2$s', 'tribe-common' ); + $pue_notice_html = '

    ' . sprintf( $pue_notice_text, $plugin_names_clean_text, $read_more_link ) . '

    '; + + printf( + '
    ' + . $notice_html_content + . ( $has_pue_notices ? $pue_notice_html : '' ) + . '
    ', + esc_attr( sanitize_title( $plugin_data['Name'] ) ), + $plugin_names_clean_text, + $req_plugin_names_clean_text + ); + } + + /** + * Implodes a list of items with proper grammar. + * + * If only 1 item, no grammar. If 2 items, just conjunction. If 3+ items, commas with conjunction. + * + * @param array $items List of items to implode + * + * @return string String of items + */ + public function implode_with_grammar( $items ) { + $separator = _x( ', ', 'separator used in a list of items', 'tribe-common' ); + $conjunction = _x( ' and ', 'the final separator in a list of two or more items', 'tribe-common' ); + $output = $last_item = array_pop( $items ); + + if ( $items ) { + $output = implode( $separator, $items ); + + if ( 1 < count( $items ) ) { + $output .= $separator; + } + + $output .= $conjunction . $last_item; + } + + return $output; + } + +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/Plugin_Upgrade_Notice.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/Plugin_Upgrade_Notice.php new file mode 100644 index 000000000..819d85ba5 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/Plugin_Upgrade_Notice.php @@ -0,0 +1,239 @@ +current_version = $current_version; + $this->plugin_path = $plugin_path; + + add_action( "in_plugin_update_message-$plugin_path", [ $this, 'maybe_run' ] ); + } + + /** + * Test if there is a plugin upgrade notice and displays it if so. + * + * Expects to fire during "in_plugin_update_message-{plugin_path}", therefore + * this should only run if WordPress has detected that an upgrade is indeed + * available. + */ + public function maybe_run() { + $this->test_for_upgrade_notice(); + + if ( $this->upgrade_notice ) { + $this->display_message(); + } + } + + /** + * Tests to see if an upgrade notice is available. + */ + protected function test_for_upgrade_notice() { + $cache_key = $this->cache_key(); + $this->upgrade_notice = get_transient( $cache_key ); + + if ( false === $this->upgrade_notice ) { + $this->discover_upgrade_notice(); + } + + set_transient( $cache_key, $this->upgrade_notice, $this->cache_expiration() ); + } + + /** + * Returns a cache key unique to the current plugin path and version, that + * still fits within the 45-char limit of regular WP transient keys. + * + * @return string + */ + protected function cache_key() { + return 'tribe_plugin_upgrade_notice-' . hash( 'crc32b', $this->plugin_path . $this->current_version ); + } + + /** + * Returns the period of time (in seconds) for which to cache plugin upgrade messages. + * + * @return int + */ + protected function cache_expiration() { + /** + * Number of seconds to cache plugin upgrade messages for. + * + * Defaults to one day, which provides a decent balance between efficiency savings + * and allowing for the possibility that some upgrade messages may be changed or + * rescinded. + * + * @var int $cache_expiration + */ + return (int) apply_filters( 'tribe_plugin_upgrade_notice_expiration', DAY_IN_SECONDS, $this->plugin_path ); + } + + /** + * Looks at the current stable plugin readme.txt and parses to try and find the first + * available upgrade notice relating to a plugin version higher than this one. + * + * By default, WP SVN is the source. + */ + protected function discover_upgrade_notice() { + /** + * The URL for the current plugin readme.txt file. + * + * @var string $url + * @var string $plugin_path + */ + $readme_url = apply_filters( 'tribe_plugin_upgrade_readme_url', + $this->form_wp_svn_readme_url(), + $this->plugin_path + ); + + if ( ! empty( $readme_url ) ) { + $response = wp_safe_remote_get( $readme_url ); + } + + if ( ! empty( $response ) && ! is_wp_error( $response ) ) { + $readme = $response['body']; + } + + if ( ! empty( $readme ) ) { + $this->parse_for_upgrade_notice( $readme ); + $this->format_upgrade_notice(); + } + + /** + * The upgrade notice for the current plugin (may be empty). + * + * @var string $upgrade_notice + * @var string $plugin_path + */ + return apply_filters( 'tribe_plugin_upgrade_notice', + $this->upgrade_notice, + $this->plugin_path + ); + } + + /** + * Forms the expected URL to the trunk readme.txt file as it is on WP SVN + * or an empty string if for any reason it cannot be determined. + * + * @return string + */ + protected function form_wp_svn_readme_url() { + $parts = explode( '/', $this->plugin_path ); + $slug = empty( $parts[0] ) ? '' : $parts[0]; + return esc_url( "https://plugins.svn.wordpress.org/$slug/trunk/readme.txt" ); + } + + /** + * Given a standard Markdown-format WP readme.txt file, finds the first upgrade + * notice (if any) for a version higher than $this->current_version. + * + * @param string $readme + * @return string + */ + protected function parse_for_upgrade_notice( $readme ) { + $in_upgrade_notice = false; + $in_version_notice = false; + $readme_lines = explode( "\n", $readme ); + + foreach ( $readme_lines as $line ) { + // Once we leave the Upgrade Notice section (ie, we encounter a new section header), bail + if ( $in_upgrade_notice && 0 === strpos( $line, '==' ) ) { + break; + } + + // Look out for the start of the Upgrade Notice section + if ( ! $in_upgrade_notice && preg_match( '/^==\s*Upgrade\s+Notice\s*==/i', $line ) ) { + $in_upgrade_notice = true; + } + + // Also test to see if we have left the version specific note (ie, we encounter a new sub heading or header) + if ( $in_upgrade_notice && $in_version_notice && 0 === strpos( $line, '=' ) ) { + break; + } + + // Look out for the first applicable version-specific note within the Upgrade Notice section + if ( $in_upgrade_notice && ! $in_version_notice && preg_match( '/^=\s*\[?([0-9\.]{3,})\]?\s*=/', $line, $matches ) ) { + // Is this a higher version than currently installed? + if ( version_compare( $matches[1], $this->current_version, '>' ) ) { + $in_version_notice = true; + } + } + + // Copy the details of the upgrade notice for the first higher version we find + if ( $in_upgrade_notice && $in_version_notice ) { + $this->upgrade_notice .= $line . "\n"; + } + } + } + + /** + * Convert the plugin version header and any links from Markdown to HTML. + */ + protected function format_upgrade_notice() { + // Convert [links](http://...) to tags + $this->upgrade_notice = preg_replace( + '/\[([^\]]*)\]\(([^\)]*)\)/', + '${1}', + $this->upgrade_notice + ); + + // Convert =4.0= headings to

    4.0

    tags + $this->upgrade_notice = preg_replace( + '/=\s*([a-zA-Z0-9\.]{3,})\s*=/', + '

    ${1}

    ', + $this->upgrade_notice + ); + } + + /** + * Render the actual upgrade notice. + * + * Please note if plugin-specific styling is required for the message, you can + * use an ID generated by WordPress for one of the message's parent elements + * which takes the form "{plugin_name}-update". Example: + * + * #the-events-calendar-update .tribe-plugin-update-message { ... } + */ + public function display_message() { + $notice = wp_kses_post( $this->upgrade_notice ); + echo "
    $notice
    "; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/Service_Provider.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/Service_Provider.php new file mode 100644 index 000000000..72ad60246 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/Service_Provider.php @@ -0,0 +1,66 @@ +hooks(); + } + + /** + * Set up hooks for classes. + * + * @since 4.14.2 + */ + private function hooks() { + add_action( 'tribe_plugins_loaded', [ $this, 'plugins_loaded'] ); + } + + /** + * Setup for things that require plugins loaded first. + * + * @since 4.14.2 + */ + public function plugins_loaded() { + tribe( 'pue.notices' ); + tribe( 'admin.notice.php.version' ); + tribe( WP_Version::class ); + + if ( defined( 'TRIBE_HIDE_MARKETING_NOTICES' ) ) { + return; + } + + tribe( Marketing\Stellar_Sale::class ); + tribe( Marketing\Black_Friday::class ); + // EOY Sale disabled for 2022 + // tribe( Marketing\End_Of_Year_Sale::class ); + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/WP_Version.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/WP_Version.php new file mode 100644 index 000000000..8c3137957 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notice/WP_Version.php @@ -0,0 +1,87 @@ + 'warning', + 'dismiss' => 1, + 'priority' => -1, + 'wrap' => 'p', + ], + [ $this, 'wp_version_57_should_display' ] + ); + } + + /** + * Whether the WordPress 5.7 notice should display. + * + * @since 4.12.17 + * + * @return boolean + */ + public function wp_version_57_should_display() { + global $wp_version, $current_screen; + + $screens = [ + 'tribe_events_page_tribe-app-shop', // App shop. + 'events_page_tribe-app-shop', // App shop. + 'tribe_events_page_tribe-common', // Settings & Welcome. + 'tribe_events_page_tec-events-settings', // New Settings & Welcome. + 'events_page_tribe-common', // Settings & Welcome. + 'toplevel_page_tribe-common', // Settings & Welcome. + ]; + + // If not a valid screen, don't display. + if ( empty( $current_screen->id ) || ! in_array( $current_screen->id, $screens, true ) ) { + return false; + } + + $wp_version_min_version_required = '5.8'; + $common_version_required = '4.12.18-dev'; + + return + version_compare( Common::VERSION, $common_version_required, '<' ) + && version_compare( $wp_version, $wp_version_min_version_required, '<' ); + } + + /** + * HTML for the WordPress 5.7 notice. + * + * @since 4.12.17 + * + * @see https://evnt.is/wp5-7 + * + * @return string + */ + public function wp_version_57_display_notice() { + global $wp_version; + $is_wp_57 = version_compare( $wp_version, '5.7-beta', '>=' ); + $html = ''; + + if ( $is_wp_57 ) { + $html .= esc_html__( 'You are using WordPress 5.7 which included a major jQuery update that may cause compatibility issues with past versions of The Events Calendar, Event Tickets and other plugins.', 'tribe-common' ); + } else { + $html .= esc_html__( 'WordPress 5.7 includes a major jQuery update that may cause compatibility issues with past versions of The Events Calendar, Event Tickets and other plugins.', 'tribe-common' ); + } + $html .= ' ' . esc_html__( 'Read more.', 'tribe-common' ) . ''; + + return $html; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notices.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notices.php new file mode 100644 index 000000000..f1a2965b4 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Notices.php @@ -0,0 +1,821 @@ +doing_ajax() ) { + return; + } + + // Hook the actual rendering of notices + add_action( 'current_screen', [ $this, 'hook' ], 20 ); + + // Add our notice dismissal script + tribe_asset( + Tribe__Main::instance(), + 'tribe-notice-dismiss', + 'notice-dismiss.js', + [ 'jquery' ], + null, + [ + 'groups' => 'tec-admin-notices', + ] + ); + } + + /** + * This will happen on the `current_screen` and will hook to the correct actions and display the notices + * + * @since 4.3 + * + * @return void + */ + public function hook() { + $transients = $this->get_transients(); + + foreach ( $transients as $slug => $transient ) { + if ( $this->transient_notice_expired( $slug ) ) { + continue; + } + list( $html, $args, $expire ) = $transients[ $slug ]; + $this->register( $slug, $html, $args ); + } + + foreach ( $this->notices as $notice ) { + if ( ! $this->showing_notice( $notice->slug ) ) { + continue; + } + + add_action( $notice->action, $notice->callback, $notice->priority ); + } + } + + /** + * This will allow the user to Dismiss the Notice using JS. + * + * We will dismiss the notice without checking to see if the slug was already + * registered (via a call to exists()) for the reason that, during dismissal + * ajax request, some valid notices may not have been registered yet. + * + * @since 4.3 + * + * @return void + */ + public function maybe_dismiss() { + if ( empty( $_GET[ self::$meta_key ] ) ) { + wp_send_json( false ); + } + + $slug = sanitize_key( $_GET[ self::$meta_key ] ); + + // Send a JSON answer with the status of dismissal + wp_send_json( $this->dismiss( $slug ) ); + } + + /** + * Allows a Magic to remove the Requirement of creating a callback + * + * @since 4.3 + * + * @param string $name Name of the method used to create the slug of the notice. + * @param array $arguments Which arguments were used, normally empty. + * + * @return string + */ + public function __call( $name, $arguments ) { + // Transform from Method name to Notice number + $slug = preg_replace( '/render_/', '', $name, 1 ); + + if ( ! $this->exists( $slug ) ) { + return false; + } + + $notice = $this->get( $slug ); + + if ( + empty( $notice->active_callback ) + || ( + is_callable( $notice->active_callback ) + && true == call_user_func( $notice->active_callback ) + ) + ) { + $content = $notice->content; + $wrap = isset( $notice->wrap ) ? $notice->wrap : false; + + if ( is_array( $content ) && isset( $content[0] ) && $content[0] instanceof __PHP_Incomplete_Class ) { + // From a class that no longer exists (e.g. the plugin is not active), clean and bail. + $this->remove( $slug ); + $this->remove_transient( $slug ); + + return false; + } + + if ( is_callable( $content ) ) { + $content = call_user_func_array( $content, [ $notice ] ); + } + + if ( empty( $content ) ) { + // There is nothing to render, let's avoid the empty notice frame. + return false; + } + + tribe_asset_enqueue_group( 'tec-admin-notices' ); + + // Return the rendered HTML. + $html = $this->render( $slug, $content, false, $wrap ); + + // Remove the notice and the transient (if any) since it's been rendered. + $this->remove( $slug ); + $this->remove_transient( $slug ); + + return $html; + } + + return false; + } + + /** + * This is a helper to actually print the Message + * + * @since 4.3 + * + * @param string $slug The name of the notice. + * @param string $content The content of the notice. + * @param boolean $return Echo or return the content. + * @param string|bool $wrap An optional HTML tag to wrap the content. + * + * @return bool|string + */ + public function render( $slug, $content = null, $return = true, $wrap = false ) { + if ( ! $this->exists( $slug ) ) { + return false; + } + + // Bail if we already rendered + if ( $this->is_rendered( $slug ) ) { + if ( $this->is_rendered_html( $slug, $content ) && ! $return ) { + echo $content; + } + + return false; + } + + $notice = $this->get( $slug ); + $this->notices[ $slug ]->is_rendered = true; + + $classes = [ 'tribe-dismiss-notice', 'notice' ]; + $classes[] = sanitize_html_class( 'notice-' . $notice->type ); + $classes[] = sanitize_html_class( 'tribe-notice-' . $notice->slug ); + + if ( $notice->dismiss ) { + $classes[] = 'is-dismissible'; + } + + if ( $notice->inline ) { + $classes[] = 'inline'; + } + + // Prevents Empty Notices + if ( empty( $content ) ) { + return false; + } + + if ( is_string( $wrap ) ) { + $content = sprintf( '<%1$s>' . $content . '', $wrap ); + } + + $html = sprintf( '
    %s
    ', implode( ' ', $classes ), $notice->slug, $content ); + tribe_asset_enqueue_group( 'tec-admin-notices' ); + + if ( ! $return ) { + echo $html; + } + + return $html; + } + + /** + * This is a helper to print the message surrounded by `p` tags. + * + * @since 4.3 + * + * @param string $slug The name of the notice. + * @param string $content The content of the notice. + * @param boolean $return Echo or return the content. + * + * @return boolean|string + */ + public function render_paragraph( $slug, $content = null, $return = true ) { + return $this->render( $slug, $content, $return, 'p' ); + } + + /** + * Checks if a given notice is rendered + * + * @since 4.7.10 + * + * @param string $slug Which notice to check. + * + * @return boolean + */ + public function is_rendered( $slug ) { + if ( ! $this->exists( $slug ) ) { + return false; + } + + $notice = $this->get( $slug ); + + return isset( $notice->is_rendered ) ? $notice->is_rendered : false; + } + + /** + * Checks if a given string is a notice rendered + * + * @since 4.7.10 + * + * @param string $slug Which notice to check. + * @param string $html Which html string we are check. + * + * @return boolean + */ + public function is_rendered_html( $slug, $html ) { + if ( ! $this->exists( $slug ) ) { + return false; + } + + $search = sprintf( 'data-ref="%s"', $slug ); + + return false !== strpos( $html, $search ); + } + + /** + * Checks if a given user has dismissed a given notice. + * + * @since 4.13.0 + * + * @param string $slug The name of the notice. + * @param int|null $user_id The user ID. + * + * @return boolean + */ + public function has_user_dismissed( $slug, $user_id = null ) { + + if ( is_null( $user_id ) ) { + $user_id = get_current_user_id(); + } + + $dismissed_notices = get_user_meta( $user_id, self::$meta_key ); + + if ( ! is_array( $dismissed_notices ) ) { + return false; + } + + if ( ! in_array( $slug, $dismissed_notices ) ) { + return false; + } + + $notice = $this->get( $slug ); + if ( + is_object( $notice ) + && $notice->recurring + && $this->should_recurring_notice_show( $slug, $user_id ) + ) { + return false; + } + + return true; + } + + /** + * Checks if a given user has dismissed a given notice. + * + * @since 4.3 + * @deprecated 4.13.0 Deprecated in favor of correcting the typo. + * + * @param string $slug The Name of the Notice + * @param int|null $user_id The user ID + * + * @return boolean + */ + public function has_user_dimissed( $slug, $user_id = null ) { + return $this->has_user_dismissed( $slug, $user_id ); + } + + /** + * Gets the last Dismissal for a given notice slug and user. + * + * @since 4.13.0 + * + * @param string $slug Slug of the notice to look for. + * @param int|null $user_id Which user? If null will default to current user. + * + * @return false|\Tribe\Utils\Date_I18n + */ + public function get_last_dismissal( $slug, $user_id = null ) { + if ( is_null( $user_id ) ) { + $user_id = get_current_user_id(); + } + + $dismissed_time = get_user_meta( $user_id, static::$meta_key_time_prefix . $slug, true ); + + if ( ! is_numeric( $dismissed_time ) ) { + return false; + } + + return Dates::build_date_object( $dismissed_time ); + } + + /** + * Determines if a given notice needs to be re-displayed in case of recurring notice. + * + * @since 4.13.0 + * + * @param string $slug Slug of the notice to look for. + * @param int|null $user_id Which user? If null will default to current user. + * + * @return false|\Tribe\Utils\Date_I18n + */ + public function should_recurring_notice_show( $slug, $user_id = null ) { + $notice = $this->get( $slug ); + if ( ! is_object( $notice ) ) { + return false; + } + + if ( ! $notice->recurring || ! $notice->recurring_interval ) { + return false; + } + + if ( is_null( $user_id ) ) { + $user_id = get_current_user_id(); + } + + $interval = Dates::interval( $notice->recurring_interval ); + $last_dismissal = $this->get_last_dismissal( $slug, $user_id ); + if ( ! $last_dismissal ) { + return false; + } + + $next_dismissal = $last_dismissal->add( $interval ); + $now = Dates::build_date_object( 'now' ); + + if ( $now >= $next_dismissal ) { + delete_user_meta( $user_id, self::$meta_key, $slug ); + + return true; + } + + return false; + } + + /** + * A Method to actually add the Meta value telling that this notice has been dismissed + * + * @since 4.3 + * + * @param string $slug The Name of the Notice + * @param int|null $user_id The user ID + * + * @return boolean + */ + public function dismiss( $slug, $user_id = null ) { + if ( is_null( $user_id ) ) { + $user_id = get_current_user_id(); + } + + // If this user has dismissed we don't care either + if ( $this->has_user_dismissed( $slug, $user_id ) ) { + return true; + } + + update_user_meta( $user_id, static::$meta_key_time_prefix . $slug, time() ); + + return add_user_meta( $user_id, self::$meta_key, $slug, false ); + } + + /** + * Removes the User meta holding if a notice was dismissed + * + * @param string $slug The Name of the Notice + * @param int|null $user_id The user ID + * + * @return boolean + */ + public function undismiss( $slug, $user_id = null ) { + if ( is_null( $user_id ) ) { + $user_id = get_current_user_id(); + } + + // If this user has dismissed we don't care either + if ( ! $this->has_user_dismissed( $slug, $user_id ) ) { + return false; + } + + return delete_user_meta( $user_id, self::$meta_key, $slug ); + } + + /** + * Undismisses the specified notice for all users. + * + * @since 4.3 + * + * @param string $slug + * + * @return int + */ + public function undismiss_for_all( $slug ) { + $user_query = new WP_User_Query( [ + 'meta_key' => self::$meta_key, + 'meta_value' => $slug, + ] ); + + $affected = 0; + + foreach ( $user_query->get_results() as $user ) { + if ( $this->undismiss( $slug, $user->ID ) ) { + $affected ++; + } + } + + return $affected; + } + + /** + * Register a Notice and attach a callback to the required action to display it correctly + * + * @since 4.3 + * + * @param string $slug Slug to save the notice + * @param callable|string $callback A callable Method/Function to actually display the notice + * @param array $arguments Arguments to Setup a notice + * @param callable|null $active_callback An optional callback that should return bool values + * to indicate whether the notice should display or not. + * + * @return stdClass + */ + public function register( $slug, $callback, $arguments = [], $active_callback = null ) { + // Prevent weird stuff here + $slug = sanitize_key( $slug ); + + $defaults = [ + 'callback' => null, + 'content' => null, + 'action' => 'admin_notices', + 'priority' => 10, + 'expire' => false, + 'dismiss' => false, + 'inline' => false, + 'recurring' => false, + 'recurring_interval' => null, + 'type' => 'error', + 'is_rendered' => false, + 'wrap' => false, + ]; + + $defaults['callback'] = [ $this, 'render_' . $slug ]; + $defaults['content'] = $callback; + + if ( is_callable( $active_callback ) ) { + $defaults['active_callback'] = $active_callback; + } + + // Merge Arguments + $notice = (object) wp_parse_args( $arguments, $defaults ); + + // Enforce this one + $notice->slug = $slug; + + // Clean these + $notice->priority = absint( $notice->priority ); + $notice->expire = (bool) $notice->expire; + $notice->recurring = (bool) $notice->recurring; + + if ( ! is_callable( $notice->dismiss ) ) { + $notice->dismiss = (bool) $notice->dismiss; + } + if ( ! is_callable( $notice->inline ) ) { + $notice->inline = (bool) $notice->inline; + } + + // Set the Notice on the array of notices + $this->notices[ $slug ] = $notice; + + // Return the notice Object because it might be modified + return $notice; + } + + /** + * Create a transient Admin Notice easily. + * + * A transient admin notice is a "fire-and-forget" admin notice that will display once registered and + * until dismissed (if dismissible) without need, on the side of the source code, to register it on each request. + * + * @since 4.7.7 + * + * @param string $slug Slug to save the notice + * @param string $html The notice output HTML code + * @param array $arguments Arguments to Setup a notice + * @param int $expire After how much time (in seconds) the notice will stop showing. + * + * @return stdClass Which notice was registered + */ + public function register_transient( $slug, $html, $arguments = [], $expire = null ) { + $notices = $this->get_transients(); + $notices[ $slug ] = [ $html, $arguments, time() + $expire ]; + $this->set_transients( $notices ); + } + + /** + * Removes a transient notice based on its slug. + * + * @since 4.7.7 + * + * @param string $slug + */ + public function remove_transient( $slug ) { + $notices = $this->get_transients(); + unset( $notices[ $slug ] ); + $this->set_transients( $notices ); + } + + /** + * Removes a notice based on its slug. + * + * @since 4.3 + * + * @param string $slug + * + * @return bool + */ + public function remove( $slug ) { + if ( ! $this->exists( $slug ) ) { + return false; + } + + unset( $this->notices[ $slug ] ); + + return true; + } + + /** + * Gets the configuration for the Notices + * + * @since 4.3 + * + * @param string $slug + * + * @return object|array|null + */ + public function get( $slug = null ) { + if ( is_null( $slug ) ) { + return $this->notices; + } + + // Prevent weird stuff here + $slug = sanitize_key( $slug ); + + if ( ! empty( $this->notices[ $slug ] ) ) { + // I want to avoid modifying the registered value. + $notice = $this->notices[ $slug ]; + + if ( is_callable( $notice->inline ) ) { + $notice->inline = call_user_func( $notice->inline, $notice ); + } + + if ( is_callable( $notice->dismiss ) ) { + $notice->dismiss = call_user_func( $notice->dismiss, $notice ); + } + + return $notice; + } + + return null; + } + + /** + * Checks if a given notice exists + * + * @since 4.3 + * + * @param string $slug + * + * @return bool + */ + public function exists( $slug ) { + return is_object( $this->get( $slug ) ) ? true : false; + } + + /** + * Returns an array of registered transient notices. + * + * @since 4.7.7 + * + * @return array An associative array in the shape [ => [ , , ] ] + */ + protected function get_transients() { + $cached = tribe( 'cache' )['transient_admin_notices']; + + if ( false !== $cached ) { + return $cached; + } + + $transient = self::$transient_notices_name; + $notices = get_transient( $transient ); + $notices = is_array( $notices ) ? $notices : []; + + if ( $this->did_prune_transients ) { + $this->did_prune_transients = true; + foreach ( $notices as $key => $notice ) { + list( $html, $args, $expire_at ) = $notice; + + if ( $expire_at < time() ) { + unset( $notices[ $key ] ); + } + } + } + + tribe( 'cache' )['transient_admin_notices'] = $notices; + + return $notices; + } + + /** + * Updates/sets the transient notices transient. + * + * @since 4.7.7 + * + * @param array $notices An associative array in the shape [ => [ , , ] ] + */ + protected function set_transients( $notices ) { + $transient = self::$transient_notices_name; + set_transient( $transient, $notices, MONTH_IN_SECONDS ); + } + + /** + * Checks whether a specific transient admin notices is being shown or not, depending on its expiration and + * dismissible status. + * + * + * @since 4.11.1 + * + * @param string|array $slug The slug, or slugs, of the transient notices to check. This is the same slug used + * to register the transient notice in the `tribe_transient_notice` function or the + * `Tribe__Admin__Notices::register_transient()` method. + * + * @return bool Whether the transient notice is showing or not. + */ + public function showing_transient_notice( $slug ) { + $transient_notices = (array) $this->get_transients(); + + return isset( $transient_notices[ $slug ] ) + && ! $this->has_user_dismissed( $slug ) + && ! $this->transient_notice_expired( $slug ); + } + + /** + * Checks whether a transient notice expired or not. + * + * @since 4.11.1 + * + * @param string|array $slug The slug, or slugs, of the transient notices to check. This is the same slug used + * to register the transient notice in the `tribe_transient_notice` function or the + * `Tribe__Admin__Notices::register_transient()` method. + * + * @return bool Whether the transient notice is expired or not. + */ + protected function transient_notice_expired( $slug ) { + $transients = (array) $this->get_transients(); + + if ( ! isset( $transients[ $slug ] ) ) { + return true; + } + + list( $html, $args, $expire ) = $transients[ $slug ]; + if ( $expire < time() ) { + return true; + } + + return false; + } + + /** + * Checks whether a notice is being shown or not; the result takes the notice callback and dismissible status into + * account. + * + * @since 4.11.1 + * + * @param string|array $slug The slug, or slugs, of the transient notices to check. This is the same slug used + * to register the transient notice in the `tribe_transient_notice` function or the + * `Tribe__Admin__Notices::register_transient()` method. + * + * @return bool Whether the notice is showing or not. + */ + public function showing_notice( $slug ) { + if ( ! isset( $this->notices[ $slug ] ) ) { + return false; + } + + $notice = $this->notices[ $slug ]; + if ( $notice->dismiss && $this->has_user_dismissed( $notice->slug ) ) { + return false; + } + + if ( + ! empty( $notice->active_callback ) + && is_callable( $notice->active_callback ) + && false == call_user_func( $notice->active_callback ) + ) { + return false; + } + + return true; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Pages.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Pages.php new file mode 100644 index 000000000..3a05e4f84 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Pages.php @@ -0,0 +1,309 @@ + + */ + private $pages = []; + + /** + * Get registered pages. + * + * @since 4.15.0 + * + * @return array $pages { + * Array containing the registered pages. + * + * @type array $page_id { + * @type string id Id to reference the page. + * @type array title Page title. Used in menus and breadcrumbs. + * @type string|null parent Parent ID. Null for new top level page. + * @type string path Path for this page, full path in app context; ex /analytics/report + * @type string capability Capability needed to access the page. + * @type string icon Icon. Dashicons helper class, base64-encoded SVG, or 'none'. + * @type int position Menu item position. + * @type int order Navigation item order. + * @type callable callback The function to be called to output the content for the page. + * } + * } + */ + public function get_pages() { + /** + * Filters the list of registered TEC admin pages. + * + * @since 4.15.0 + * + * @param array $pages { + * Array containing the registered pages to be filtered + * + * @type array $page_id { + * @type string id Id to reference the page. + * @type array title Page title. Used in menus and breadcrumbs. + * @type string|null parent Parent ID. Null for new top level page. + * @type string path Path for this page, full path in app context; ex /analytics/report + * @type string capability Capability needed to access the page. + * @type string icon Icon. Dashicons helper class, base64-encoded SVG, or 'none'. + * @type int position Menu item position. + * @type int order Navigation item order. + * @type callable callback The function to be called to output the content for the page. + * } + * } + */ + $pages = apply_filters( 'tec_admin_pages', $this->pages ); + + return $pages; + } + + /** + * Adds a page to `tec-admin`. + * + * @since 4.15.0 + * + * @param array $options { + * Array describing the page. + * + * @type string id Id to reference the page. + * @type string title Page title. Used in menus and breadcrumbs. + * @type string|null parent Parent ID. Null for new top level page. + * @type string path Path for this page, full path in app context; ex /analytics/report + * @type string capability Capability needed to access the page. + * @type string icon Icon. Dashicons helper class, base64-encoded SVG, or 'none'. + * @type int position Menu item position. + * @type int order Navigation item order. + * @type callable callback The function to be called to output the content for the page. + * } + * + * @return string $page The resulting page's hook_suffix. + * + */ + public function register_page( $options = [] ) { + $defaults = [ + 'id' => null, + 'parent' => null, + 'title' => '', + 'capability' => self::get_capability(), + 'path' => '', + 'icon' => '', + 'position' => null, + 'callback' => [ __CLASS__, 'render_page' ], + ]; + + $options = wp_parse_args( $options, $defaults ); + + if ( is_null( $options['parent'] ) ) { + $page = add_menu_page( + $options['title'], + $options['title'], + $options['capability'], + $options['path'], + $options['callback'], + $options['icon'], + $options['position'] + ); + } else { + $page = add_submenu_page( + $options['parent'], + $options['title'], + $options['title'], + $options['capability'], + $options['path'], + $options['callback'] + ); + } + + $this->connect_page( $options ); + + return $page; + } + + /** + * Get the current page. + * + * @since 4.15.0 + * + * @return string|boolean Current page or false if not registered with this controller. + */ + public function get_current_page() { + if ( is_null( $this->current_page ) ) { + $this->determine_current_page(); + } + + return $this->current_page; + } + + /** + * Determine the current page. + * + * @since 4.15.0 + * + * @return string|boolean Current page or false if not registered with this controller. + */ + public function determine_current_page() { + $current_screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; + + if ( is_null( $current_screen ) ) { + $this->current_page = tribe_get_request_var( 'page' ); + return $this->current_page; + } + + $this->current_page = $current_screen->id; + + return $this->current_page; + } + + /** + * Connect an existing page to wp-admin. + * + * @since 4.15.0 + * + * @param array $options { + * Array describing the page. + * + * @type string id Id to reference the page. + * @type string|array title Page title. Used in menus and breadcrumbs. + * @type string|null parent Parent ID. Null for new top level page. + * @type string path Path for this page. E.g. admin.php?page=wc-settings&tab=checkout + * @type string capability Capability needed to access the page. + * @type string icon Icon. Dashicons helper class, base64-encoded SVG, or 'none'. + * @type int position Menu item position. + * } + */ + public function connect_page( $options = [] ) { + if ( ! is_array( $options['title'] ) ) { + $options['title'] = array( $options['title'] ); + } + + /** + * Filter the options when connecting or registering a page. + * + * @param array $options { + * Array describing the page. + * + * @type string id Id to reference the page. + * @type string|array title Page title. Used in menus and breadcrumbs. + * @type string|null parent Parent ID. Null for new top level page. + * @type string screen_id The screen ID that represents the connected page. (Not required for registering). + * @type string path Path for this page. E.g. admin.php?page=wc-settings&tab=checkout + * @type string capability Capability needed to access the page. + * @type string icon Icon. Dashicons helper class, base64-encoded SVG, or 'none'. + * @type int position Menu item position. + * @type boolean js_page If this is a JS-powered page. + * } + */ + $options = apply_filters( 'tec_admin_pages_connect_page_options', $options ); + + $this->pages[ $options['id'] ] = $options; + } + + /** + * Get the capability. + * + * @param string $capability The capability required for a TEC page to be displayed to the user. + * + * @since 4.15.0 + * + * @return string The capability required for a TEC page to be displayed to the user. + */ + public static function get_capability( $capability = 'manage_options' ) { + /** + * Filters the default capability for Tribe admin pages. + * + * @param string $capability The capability required for a TEC page to be displayed to the user. + * + * @todo: We'll need to deprecate this one in favor of the one below. + */ + $capability = apply_filters( 'tribe_common_event_page_capability', $capability ); + + /** + * Filters the default capability for TEC admin pages. + * + * @param string $capability The capability required for a TEC page to be displayed to the user. + * + * @since 4.15.0 + */ + $capability = apply_filters( 'tec_admin_pages_capability', $capability ); + + return $capability; + } + + /** + * Define if is a `tec` admin page (registered). + * + * @since 4.15.0 + * + * @param string $page_id The ID of the page to check if is a `tec` admin page. + * + * @return boolean True if is a `tec` admin page, false otherwise. + */ + public function is_tec_page( $page_id = '' ) { + return in_array( $page_id, array_keys( $this->pages ), true ); + } + + /** + * Get pages with tabs. + * @since 4.15.0 + * + * @param array $pages The list of pages with tabs. + * @return array $pages The list of pages with tabs, filtered. + */ + public function get_pages_with_tabs( $pages = [] ) { + /** + * Filters the pages with tabs. + * + * @param array $pages Pages with tabs. + * + * @since 4.15.0 + */ + return apply_filters( + 'tec_admin_pages_with_tabs', + $pages + ); + } + + /** + * Check if the current page has tabs. + * + * @since 4.15.0 + * + * @param string $page The page slug. + * @return boolean True if the page has tabs, false otherwise. + */ + public function has_tabs( $page = '' ) { + if ( empty( $page ) ) { + $page = $this->get_current_page(); + } + + return in_array( $page, $this->get_pages_with_tabs() ); + } + + /** + * Generic page. + * + * @since 4.15.0 + */ + public static function render_page() { + ?> +
    + should_setup_pages() ) { + return; + } + + $page_title = esc_html__( 'Troubleshooting', 'tribe-common' ); + $menu_title = esc_html__( 'Troubleshooting', 'tribe-common' ); + + $capability = $this->get_required_capability(); + + $where = Tribe__Settings::instance()->get_parent_slug(); + + $this->admin_page = add_submenu_page( + $where, + $page_title, + $menu_title, + $capability, + static::MENU_SLUG, + [ + $this, + 'do_menu_page', + ] + ); + } + + /** + * Gets the required capability for the troubleshooting page. + * + * @since 4.14.2 + * + * @return string Which capability we required for the troubleshooting page. + */ + public function get_required_capability() { + /** + * Allows third party filtering of capability required to see the Troubleshooting page. + * + * @since 4.14.2 + * + * @param string $capability Which capability we are using as the one required for the + * troubleshooting page. + * @param static $troubleshooting The current instance of the class that handles this page. + */ + $capability = apply_filters( 'tec_troubleshooting_capability', 'install_plugins', $this ); + return $capability; + } + + /** + * Hooked to admin_body_class to add a class for troubleshooting page. + * + * @since 4.15.0 + * + * @param string $classes a space separated string of classes to be added to body. + * + * @return string $classes a space separated string of classes to be added to body. + */ + public function admin_body_class( $classes ) { + if ( ! $this->is_current_page() ) { + return $classes; + } + + $classes .= ' tec-troubleshooting'; + return $classes; + } + + /** + * Adds the troubleshooting menu to the the WP admin bar under events. + * + * @since 4.14.2 + * + */ + public function add_toolbar_item() { + $capability = $this->get_required_capability(); + + if ( ! current_user_can( $capability ) ) { + return; + } + + global $wp_admin_bar; + + $wp_admin_bar->add_menu( [ + 'id' => 'tec-troubleshooting', + 'title' => esc_html__( 'Troubleshooting', 'tribe-common' ), + 'href' => Tribe__Settings::instance()->get_url( [ 'page' => static::MENU_SLUG ] ), + 'parent' => 'tribe-events-settings-group', + ] ); + } + + /** + * Checks if the current page is the troubleshooting page. + * + * @since 4.14.2 + * + * @return boolean returns true if the current page is the troubleshooting page. + */ + public function is_current_page() { + if ( ! Tribe__Settings::instance()->should_setup_pages() || ! did_action( 'admin_menu' ) ) { + return false; + } + + if ( is_null( $this->admin_page ) ) { + _doing_it_wrong( + __FUNCTION__, + 'Function was called before it is possible to accurately determine what the current page is.', + '4.5.6' + ); + return false; + } + + global $current_screen; + + $troubleshooting_pages = [ + 'tribe_events_page_tec-troubleshooting', + 'tickets_page_tec-tickets-troubleshooting', + ]; + + return in_array( $current_screen->id, $troubleshooting_pages ); + } + + /** + * Renders the Troubleshooting page. + * + * @since 4.14.2 + * + */ + public function do_menu_page() { + tribe_asset_enqueue( 'tribe-admin-help-page' ); + $main = Tribe__Main::instance(); + include_once Tribe__Main::instance()->plugin_path . 'src/admin-views/troubleshooting.php'; + } + + /** + * This method checks if there are any active issues that need to be flagged. + * + * @since 4.14.2 + * + * @return boolean returns true if there are any active issues. + */ + public function is_any_issue_active() { + $issues = $this->get_issues_found(); + $active_issues = wp_list_pluck( $issues, 'active' ); + return in_array( true, $active_issues ); + } + + /** + * Checks if any active TEC plugins require an update. + * + * @since 4.14.2 + * + * @return boolean returns true is any of the plugins requires an update. + */ + public function is_any_tec_plugin_out_of_date() { + $current = get_site_transient( 'update_plugins' ); + $plugins = []; + if ( defined( 'TRIBE_EVENTS_FILE' ) ) { + $plugins[] = TRIBE_EVENTS_FILE; + } + if ( defined( 'EVENTS_CALENDAR_PRO_FILE' ) ) { + $plugins[] = EVENTS_CALENDAR_PRO_FILE; + } + if ( defined( 'EVENT_TICKETS_PLUS_FILE' ) ) { + $plugins[] = EVENT_TICKETS_PLUS_FILE; + } + if ( defined( 'EVENTS_VIRTUAL_FILE' ) ) { + $plugins[] = EVENTS_VIRTUAL_FILE; + } + if ( defined( 'EVENT_TICKETS_MAIN_PLUGIN_FILE' ) ) { + $plugins[] = EVENT_TICKETS_MAIN_PLUGIN_FILE; + } + if ( defined( 'TRIBE_EVENTS_FILTERBAR_FILE' ) ) { + $plugins[] = TRIBE_EVENTS_FILTERBAR_FILE; + } + if ( defined( 'EVENTS_COMMUNITY_TICKETS_FILE' ) ) { + $plugins[] = EVENTS_COMMUNITY_TICKETS_FILE; + } + if ( defined( 'EVENTS_COMMUNITY_FILE' ) ) { + $plugins[] = EVENTS_COMMUNITY_FILE; + } + if ( defined( 'EVENTBRITE_PLUGIN_FILE' ) ) { + $plugins[] = EVENTBRITE_PLUGIN_FILE; + } + if ( defined( 'TRIBE_APM_FILE' ) ) { + $plugins[] = TRIBE_APM_FILE; + } + if ( defined( 'IMAGE_WIDGET_PLUS_DIR' ) ) { + $plugins[] = IMAGE_WIDGET_PLUS_DIR; + } + $plugins = array_map( static function( $file ) { + $file = \str_replace( WP_PLUGIN_DIR . '/', '', $file ); + return $file; + }, $plugins ); + + foreach ( $plugins as $file ) { + if ( ! isset( $current->response[ $file ] ) ) { + continue; + } + $response = $current->response[ $file ]; + if ( ! empty( $response->new_version ) ) { + return true; + } + } + return false; + } + + /** + * Checks if any of the issues defined are active. + * + * @since 4.14.2 + * + * @param string $slug the slug of active issue. + * + * @return boolean returns a boolean value for each individual issue depending on whether it is active or not. + */ + public function is_active_issue( $slug ) { + if ( 'timezone' === $slug ) { + return Timezones::is_utc_offset( Timezones::wp_timezone_string() ); + } + if ( 'geolocation' === $slug && class_exists( 'Tribe__Events__Google__Maps_API_Key' ) ) { + $key = \tribe_get_option( 'google_maps_js_api_key', false ); + return empty( $key ) || Tribe__Events__Google__Maps_API_Key::$default_api_key === $key ; + } + if ( 'out-of-date' === $slug ) { + return $this->is_any_tec_plugin_out_of_date(); + } + return false; + } + + /** + * Displays issues found in the UI. + * + * @since 4.14.2 + * + * @return array of issues which are displayed on the troubleshooting page. + */ + public function get_issues_found() { + $issues_found = apply_filters( 'tec_help_troubleshooting_issues_found', [ + [ + 'title' => __( 'Site time zone uses UTC', 'tribe-common' ), + 'description' => __( 'When using The Events Calendar, we highly recommend that you use a geographic timezone such as "America/Los_Angeles" and avoid using a UTC timezone offset such as “UTC+9”. Choosing a UTC timezone for your site or individual events may cause problems when importing events or with Daylight Saving Time. Go to your the General WordPress settings to adjust your site timezone.', 'tribe-common' ), + 'more_info' => 'http://evnt.is/1ad3', + 'resolve_text' => __( 'Adjust your timezone', 'tribe-common' ), + 'fix' => '/wp-admin/options-general.php', + 'active' => $this->is_active_issue( 'timezone' ), + ], + [ + 'title' => __( 'Install max has been reached', 'tribe-common' ), + 'description' => __( 'License keys can only be used on a limited number of sites, which varies depending on your license level. You\'ll need to remove the license from one or more other site\'s in order to use it on this one.', 'tribe-common' ), + 'more_info' => 'https://evnt.is/1aqz', + 'resolve_text' => __( 'Manage your licenses', 'tribe-common' ), + 'fix' => 'https://evnt.is/1aq-', + 'active' => $this->is_active_issue( 'install-max' ), + ], + [ + 'title' => __( 'Default Google Maps API key', 'tribe-common' ), + 'description' => __( 'The Events Calendar comes with an API key for basic maps functionality. If you’d like to use more advanced features like custom map pins, dynamic map loads, or Events Calendar Pro\'s Location Search and advanced Map View, you’ll need to get your own Google Maps API key and add it to Events > Settings > Integrations', 'tribe-common' ), + 'more_info' => 'https://evnt.is/1aqx', + 'resolve_text' => __( 'Enter a custom API key', 'tribe-common' ), + 'fix' => '/wp-admin/edit.php?page=tec-events-settings&tab=addons&post_type=tribe_events#tribe-field-google_maps_js_api_key', + 'active' => $this->is_active_issue( 'geolocation' ), + ], + [ + 'title' => __( 'Plugin(s) are out of date', 'tribe-common' ), + 'description' => __( 'It\'s important to use the most recent versions of our plugins so that you have access to the latest features, bug fixes, and security updates. Plugin functionality can be comprimised if your site is running outdated or mis-matched versions.', 'tribe-common' ), + 'more_info' => 'https://evnt.is/1aqy', + 'resolve_text' => __( 'Check for updates', 'tribe-common' ), + 'fix' => '/wp-admin/update-core.php', + 'active' => $this->is_active_issue( 'out-of-date' ), + ], + ] ); + + return $issues_found; + } + + /** + * Defines common troubleshooting issues and displays them in the UI. + * + * @since 4.14.2 + * + * @return array of common issues which are displayed on the troubleshooting page. + */ + public function get_common_issues() { + $common_issues = apply_filters( 'tec_help_troubleshooting_issues', [ + [ + 'issue' => __( 'Common Error Messages', 'tribe-common' ), + 'solution' => __( 'Here’s an overview of %s and what they mean.', 'tribe-common' ), + 'link' => 'https://evnt.is/1as0', + 'link_label' => 'common error messages', + ], + [ + 'issue' => __( 'My calendar doesn’t look right.', 'tribe-common' ), + 'solution' => __( 'This can happen when other plugins try to improve performance. %s.' ), + 'link' => 'https://theeventscalendar.com/knowledgebase/k/troubleshooting-the-most-common-installation-issues/#layout-issue', + 'link_label' => 'More info', + ], + [ + 'issue' => __( 'I installed the calendar and it crashed my site.', 'tribe-common' ), + 'solution' => __( '%s and other common installation issues.', 'tribe-common' ), + 'link' => 'https://theeventscalendar.com/knowledgebase/k/troubleshooting-the-most-common-installation-issues/#fatal-errors', + 'link_label' => 'Find solutions to this', + ], + [ + 'issue' => __( 'I keep getting “Page Not Found” on events.', 'tribe-common' ), + 'solution' => __( 'There are a few %s to resolve and prevent 404 errors.', 'tribe-common' ), + 'link' => 'https://evnt.is/1as2', + 'link_label' => 'things you can do', + ], + ] ); + + return $common_issues; + } + + /** + * Fired to display notices in the admin pages where the method is called. + * + * @since 4.14.2 + * + * @param string $page the page which the action is being applied. + * + */ + public function admin_notice( $page ) { + do_action( 'tec_admin_notice_area', $page ); + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Upsell_Notice/Main.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Upsell_Notice/Main.php new file mode 100644 index 000000000..5f220d0e5 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Upsell_Notice/Main.php @@ -0,0 +1,97 @@ +template = new \Tribe__Template(); + $this->template->set_template_origin( \Tribe__Main::instance() ); + $this->template->set_template_folder( 'src/admin-views/notices/upsell' ); + $this->template->set_template_context_extract( true ); + $this->template->set_template_folder_lookup( false ); + } + + return $this->template; + } + + /** + * Checks if upsell should be rendered. + * + * @since 4.14.17 + * + * @return boolean + */ + private function should_render() { + if ( function_exists( 'tec_should_hide_upsell' ) ) { + return ! tec_should_hide_upsell(); + } + if ( defined( 'TRIBE_HIDE_UPSELL' ) ) { + return ! tribe_is_truthy( TRIBE_HIDE_UPSELL ); + } + return true; + } + + /** + * Render upsell notice. + * + * @since 4.14.17 + * + * @param array $args Array of arguments that will ultimately be sent to the template. + * @param bool $echo Whether or not to echo the HTML. Defaults to true. + * + * @return string HTML of upsell notice. + */ + public function render( $args, $echo = true ) { + // Check if upsell should be rendered. + if ( ! $this->should_render() ) { + return; + } + + // Default args for the container. + $args = wp_parse_args( $args, [ + 'classes' => [], + 'text' => '', + 'link_target' => '_blank', + 'icon_url' => tribe_resource_url( 'images/icons/circle-bolt.svg', false, null, \Tribe__Main::instance() ), + 'link' => [], + ] ); + + // Default args for the link. + $args['link'] = wp_parse_args( $args['link'], [ + 'classes' => [], + 'text' => '', + 'url' => '', + 'target' => '_blank', + 'rel' => 'noopener noreferrer', + ] ); + + $template = $this->get_template(); + return $template->template( 'main', $args, $echo ); + } +} \ No newline at end of file diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Upsell_Notice/README.md b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Upsell_Notice/README.md new file mode 100644 index 000000000..aa0b1012e --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Upsell_Notice/README.md @@ -0,0 +1,65 @@ +# Upsell Notices + +To add an upsell notice, use the following code: + +``` +tribe( \Tribe\Admin\Upsell_Notice\Main::class )->render( [ + 'text' => 'Text to explain what you are promoting.', + 'link' => [ + 'text' => 'Text for the link.', + 'url' => 'https://url.com/to/more/info', + ], +] ); +``` + +## Customizing the notice container + +There are a couple of classes you can add that will display the upsell notice with different styles. + +- `.tec-admin__upsell--rounded-corners` - Adds a rounded-corner, light gray background around the entire notice. +- `.tec-admin__upsell--rounded-corners-text` - Adds a rounded-corner, light gray background around the notice text, only. + +Example: +``` +tribe( \Tribe\Admin\Upsell_Notice\Main::class )->render( [ + 'classes' => [ + 'tec-admin__upsell--rounded-corners' + ], + 'text' => 'Text to explain what you are promoting.', + 'link' => [ + 'text' => 'Text for the link.', + 'url' => 'https://url.com/to/more/info', + ], +] ); +``` + +## Customizing the notice link + +Likewise, you can also add these classes to the link array to change the appearance. + +- `.tec-admin__upsell-link--dark` - Changes the color to a dark color, instead of the default blue. +- `.tec-admin__upsell-link--underlined` - Adds an underline to the link text. + +You can also change the following attributes of the link: + +- `target` - Default is '_blank'. +- `rel` - Default is 'nofollow noreferrer'. + +Example: +``` +tribe( \Tribe\Admin\Upsell_Notice\Main::class )->render( [ + 'classes' => [ + 'tec-admin__upsell--rounded-corners-text' + ], + 'text' => 'Text to explain what you are promoting.', + 'link' => [ + 'classes' => [ + 'tec-admin__upsell-link--dark', + 'tec-admin__upsell-link--underlined', + ], + 'text' => 'Text for the link.', + 'url' => 'https://url.com/to/more/info', + 'target' => '_parent' + ], +] ); +``` \ No newline at end of file diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Wysiwyg.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Wysiwyg.php new file mode 100644 index 000000000..f2395e0cc --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Admin/Wysiwyg.php @@ -0,0 +1,143 @@ +name = $name; + $this->value = $value; + $default_args = [ + 'teeny' => true, + 'wpautop' => true, + 'textarea_name' => $name, + ]; + $this->args = wp_parse_args( $args, $default_args ); + } + + /** + * Filters editor buttons. + * + * @since 5.0.12 + * + * @param array $buttons Array of buttons to include. + * + * @return array Filtered array of buttons. + */ + public function filter_buttons( $buttons ) { + if ( + empty( $this->args ) + || ! isset( $this->args['buttons'] ) + || empty( $this->args['buttons'] ) + ) { + return $buttons; + } + + return $this->args['buttons']; + } + + /** + * Filter 2nd row of buttons. + * + * @since 5.0.12 + * + * @param array $buttons Array of buttons to include. + * + * @return array Filtered array of buttons. + */ + public function maybe_filter_buttons_2( $buttons ) { + if ( + empty( $this->args ) || + ! isset( $this->args['buttons_2'] ) || + empty( $this->args['buttons_2'] ) + ) { + return $buttons; + } + + return $this->args['buttons_2']; + } + + /** + * Get HTML of editor. + * + * @since 5.0.12 + * + * @return string HTML of editor + */ + public function get_html() { + // Add button filters. + add_filter( 'teeny_mce_buttons', [ $this, 'filter_buttons' ] ); + add_filter( 'tiny_mce_buttons', [ $this, 'filter_buttons' ] ); + add_filter( 'mce_buttons', [ $this, 'filter_buttons' ] ); + add_filter( 'mce_buttons_2', [ $this, 'maybe_filter_buttons_2' ] ); + + // Get HTML of editor. + ob_start(); + wp_editor( html_entity_decode( ( $this->value ) ), sanitize_html_class( $this->name ), $this->args ); + $html = ob_get_clean(); + + // Remove button filters. + remove_filter( 'teeny_mce_buttons', [ $this, 'filter_buttons' ] ); + remove_filter( 'tiny_mce_buttons', [ $this, 'filter_buttons' ] ); + remove_filter( 'mce_buttons', [ $this, 'filter_buttons' ] ); + remove_filter( 'mce_buttons_2', [ $this, 'maybe_filter_buttons_2' ] ); + + return $html; + } + + /** + * Renders editor HTML. + * + * @since 5.0.12 + * + * @return void + */ + public function render_html() { + echo $this->get_html(); + } + +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Ajax/Dropdown.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Ajax/Dropdown.php new file mode 100644 index 000000000..071d977cf --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Ajax/Dropdown.php @@ -0,0 +1,340 @@ + $search Search string from Select2 + * @param int $page When we deal with pagination + * @param array $args Which arguments we got from the Template + * @param string $source What source it is + * + * @return array + */ + public function search_terms( $search, $page, $args, $source ) { + $data = []; + + if ( empty( $args['taxonomy'] ) ) { + $this->error( esc_attr__( 'Cannot look for Terms without a taxonomy', 'tribe-common' ) ); + } + + // We always want all the fields so we overwrite it + $args['fields'] = isset( $args['fields'] ) ? $args['fields'] : 'all'; + $args['hide_empty'] = isset( $args['hide_empty'] ) ? $args['hide_empty'] : false; + + if ( ! empty( $search ) ) { + if ( ! is_array( $search ) ) { + // For older pieces that still use Select2 format. + $args['search'] = $search; + } else { + // Newer SelectWoo uses a new search format. + $args['search'] = $search['term']; + } + } + + // On versions older than 4.5 taxonomy goes as an Param + if ( version_compare( $GLOBALS['wp_version'], '4.5', '<' ) ) { + $terms = get_terms( $args['taxonomy'], $args ); + } else { + $terms = get_terms( $args ); + } + + $results = []; + + // Respect the parent/child_of argument if set + $parent = ! empty( $args['child_of'] ) ? (int) $args['child_of'] : 0; + $parent = ! empty( $args['parent'] ) ? (int) $args['parent'] : $parent; + + if ( empty( $args['search'] ) ) { + $this->sort_terms_hierarchically( $terms, $results, $parent ); + $results = $this->convert_children_to_array( $results ); + } else { + foreach ( $terms as $term ) { + // Prep for Select2 + $term->id = $term->term_id; + $term->text = $term->name; + $term->breadcrumbs = []; + + if ( 0 !== (int) $term->parent ) { + $ancestors = get_ancestors( $term->id, $term->taxonomy ); + $ancestors = array_reverse( $ancestors ); + foreach ( $ancestors as $ancestor ) { + $ancestor = get_term( $ancestor ); + $term->breadcrumbs[] = $ancestor->name; + } + } + + $results[] = $term; + } + } + + foreach ( $results as $result ) { + $result->text = wp_specialchars_decode( wp_kses( $result->text, [] ) ); + } + + $data['results'] = $results; + $data['taxonomies'] = get_taxonomies(); + + return $data; + } + + /** + * Search for Posts using Select2 + * + * @since 4.12.17 + * + * @param string|array $search Search string from Select2. + * @param int $page Page we want when we're dealing with pagination. + * @param array $args Arguments to pass to the query. + * @param string|int $selected Selected item ID. + * + * @return array An Array of results. + */ + public function search_posts( $search, $page = 1, $args = [], $selected = null ) { + if ( ! empty( $search ) ) { + if ( is_array( $search ) ) { + // Newer SelectWoo uses a new search format. + $args['s'] = $search['term']; // post? + } else { + // For older pieces that still use Select2 format. + $args['s'] = $search; + } + } + + $args['paged'] = $page; + $args['update_post_meta_cache'] = false; + $args['update_post_term_cache'] = false; + + $results = new WP_Query( $args ); + $has_pagination = $results->post_count < $results->found_posts; + + return $this->format_posts_for_dropdown( $results->posts, $selected, $has_pagination ); + } + + /** + * Formats a given array of posts to be displayed into the Dropdown.js module with SelectWoo. + * + * @since 4.12.17 + * + * @param array $posts + * @param null|int $selected + * @param boolean $pagination + * + * @return array + */ + public function format_posts_for_dropdown( array $posts, $selected = null, $pagination = false ) { + $data = [ + 'posts' => [], + 'pagination' => $pagination, + ]; + + // Skip when we don't have posts + if ( empty( $posts ) ) { + return $data; + } + + foreach ( $posts as $post ) { + if ( ! $post instanceof \WP_Post ) { + $post = get_post( $post ); + } + + // Skip non WP Post Objects. + if ( ! $post instanceof \WP_Post ) { + continue; + } + + // Prep for Select2. + $data['posts'][] = [ + 'id' => $post->ID, + 'text' => ! empty( $post->post_title_formatted ) ? $post->post_title_formatted : $post->post_title, + 'selected' => ! empty( $selected ) && (int) $post->ID === (int) $selected, + ]; + } + + return $data; + } + + /** + * Sorts all the Terms for Select2 hierarchically. + * + * @since 4.6 + * + * @param array &$terms Array of Terms from `get_terms`. + * @param array &$into Variable where we will store the. + * @param integer $parent Used for the recursion. + * + * @return array + */ + public function sort_terms_hierarchically( &$terms, &$into, $parent = 0 ) { + foreach ( $terms as $i => $term ) { + if ( $term->parent === $parent ) { + // Prep for Select2 + $term->id = $term->term_id; + $term->text = $term->name; + + $into[ $term->term_id ] = $term; + unset( $terms[ $i ] ); + } + } + + foreach ( $into as $term ) { + $term->children = []; + $this->sort_terms_hierarchically( $terms, $term->children, $term->term_id ); + } + } + + /** + * Makes sure we have arrays for the JS data for Select2 + * + * @since 4.6 + * + * @param object|array $results The Select2 results + * + * @return array + */ + public function convert_children_to_array( $results ) { + if ( isset( $results->children ) ) { + $results->children = $this->convert_children_to_array( $results->children ); + if ( empty( $results->children ) ) { + unset( $results->children ); + } + } else { + foreach ( $results as $key => $item ) { + $item = $this->convert_children_to_array( $item ); + } + } + + if ( empty( $results ) ) { + return []; + } + + return array_values( (array) $results ); + } + + /** + * Parses the Params coming from Select2 Search box + * + * @since 4.6 + * + * @param array $params Params to overwrite the defaults + * + * @return object + */ + public function parse_params( $params ) { + $defaults = [ + 'search' => null, + 'page' => 0, + 'args' => [], + 'source' => null, + ]; + + $arguments = wp_parse_args( $params, $defaults ); + + // Return Object just for the sake of making it simpler to read + return (object) $arguments; + } + + /** + * The default Method that will route all the AJAX calls from our Dropdown AJAX requests + * It is like a Catch All on `wp_ajax_tribe_dropdown` and `wp_ajax_nopriv_tribe_dropdown` + * + * @since 4.6 + * + * @return void + */ + public function route() { + // Push all POST params into a Default set of data + $args = $this->parse_params( empty( $_POST ) ? [] : $_POST ); + + if ( empty( $args->source ) ) { + $this->error( esc_attr__( 'Missing data source for this dropdown', 'tribe-common' ) ); + } + + // Define a Filter to allow external calls to our Select2 Dropdowns. + $filter = sanitize_key( 'tribe_dropdown_' . $args->source ); + if ( has_filter( $filter ) ) { + $data = apply_filters( $filter, [], $args->search, $args->page, $args->args, $args->source ); + } else { + $data = call_user_func_array( [ $this, $args->source ], array_values( (array) $args ) ); + } + + // If we've got a empty dataset we return an error. + if ( empty( $data ) ) { + $this->error( esc_attr__( 'Empty data set for this dropdown', 'tribe-common' ) ); + } else { + $this->success( $data ); + } + } + + /** + * Prints a success message and ensures that we don't hit bugs on Select2 + * + * @since 4.6 + * + * @param array $data + * + * @return void + */ + private function success( $data ) { + // We need a Results item for Select2 Work + if ( ! isset( $data['results'] ) ) { + $data['results'] = []; + } + + wp_send_json_success( $data ); + } + + /** + * Prints an error message and ensures that we don't hit bugs on Select2 + * + * @since 4.6 + * + * @param string $message + * + * @return void + */ + private function error( $message ) { + $data = [ + 'message' => $message, + 'results' => [], + ]; + + wp_send_json_error( $data ); + } + + /** + * Avoid throwing fatals or notices on sources that are invalid + * + * @since 4.6 + * + * @param string $name + * @param mixed $arguments + * + * @return void + */ + public function __call( $name, $arguments ) { + $message = __( 'The "%s" source is invalid and cannot be reached on "%s" instance.', 'tribe-common' ); + + return $this->error( sprintf( $message, $name, __CLASS__ ) ); + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Ajax/Operations.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Ajax/Operations.php new file mode 100644 index 000000000..09b54ae99 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Ajax/Operations.php @@ -0,0 +1,22 @@ +register_assets(); + } + + /** + * Adds the page to the admin menu + */ + public function add_menu_page() { + if ( ! Tribe__Settings::instance()->should_setup_pages() ) { + return; + } + + $page_title = esc_html__( 'Event Add-Ons', 'tribe-common' ); + $menu_title = esc_html__( 'Event Add-Ons', 'tribe-common' ); + $capability = apply_filters( 'tribe_events_addon_page_capability', 'install_plugins' ); + + $where = Tribe__Settings::instance()->get_parent_slug(); + + $this->admin_page = add_submenu_page( + $where, + $page_title, + $menu_title, + $capability, + self::MENU_SLUG, + [ + $this, + 'do_menu_page', + ] + ); + } + + /** + * Adds a link to the shop app to the WP admin bar + */ + public function add_toolbar_item() { + + $capability = apply_filters( 'tribe_events_addon_page_capability', 'install_plugins' ); + + // prevent users who cannot install plugins from seeing addons link + if ( current_user_can( $capability ) ) { + global $wp_admin_bar; + + $wp_admin_bar->add_menu( [ + 'id' => 'tribe-events-app-shop', + 'title' => esc_html__( 'Event Add-Ons', 'tribe-common' ), + 'href' => Tribe__Settings::instance()->get_url( [ 'page' => self::MENU_SLUG ] ), + 'parent' => 'tribe-events-settings-group', + ] ); + } + } + + /** + * Registers the plugin assets + */ + protected function register_assets() { + tribe_assets( + Tribe__Main::instance(), + [ + [ 'tribe-app-shop-css', 'app-shop.css' ], + [ 'tribe-app-shop-js', 'app-shop.js', [ 'jquery' ] ], + ], + 'admin_enqueue_scripts', + [ + 'conditionals' => [ $this, 'is_current_page' ], + ] + ); + } + + /** + * Checks if the current page is the app shop + * + * @since 4.5.7 + * + * @return bool + */ + public function is_current_page() { + if ( ! Tribe__Settings::instance()->should_setup_pages() || ! did_action( 'admin_menu' ) ) { + return false; + } + + if ( is_null( $this->admin_page ) ) { + _doing_it_wrong( + __FUNCTION__, + 'Function was called before it is possible to accurately determine what the current page is.', + '4.5.6' + ); + return false; + } + + return Tribe__Admin__Helpers::instance()->is_screen( $this->admin_page ); + } + + /** + * Renders the Shop App page + */ + public function do_menu_page() { + $main = Tribe__Main::instance(); + $products = $this->get_all_products(); + $bundles = $this->get_bundles(); + $extensions = $this->get_extensions(); + $stellar_brands = $this->get_stellar_brands(); + include_once Tribe__Main::instance()->plugin_path . 'src/admin-views/app-shop.php'; + } + + /** + * Gets all products from the API + * + * @return array|WP_Error + */ + private function get_all_products() { + $all_products = tribe( 'plugins.api' )->get_products(); + + $products = [ + 'the-events-calendar' => (object) $all_products['the-events-calendar'], + 'events-calendar-pro' => (object) $all_products['events-calendar-pro'], + 'events-virtual' => (object) $all_products['events-virtual'], + 'event-aggregator' => (object) $all_products['event-aggregator'], + 'event-automator' => (object) $all_products['event-automator'], + 'event-tickets' => (object) $all_products['event-tickets'], + 'event-tickets-plus' => (object) $all_products['event-tickets-plus'], + 'promoter' => (object) $all_products['promoter'], + 'tribe-filterbar' => (object) $all_products['tribe-filterbar'], + 'events-community' => (object) $all_products['events-community'], + 'events-community-tickets' => (object) $all_products['events-community-tickets'], + 'tribe-eventbrite' => (object) $all_products['tribe-eventbrite'], + 'image-widget-plus' => (object) $all_products['image-widget-plus'], + ]; + + return $products; + } + + /** + * Gets product bundles + * + * @return array|WP_Error + */ + private function get_bundles() { + $bundles = [ + (object) [ + 'title' => __( 'Events Marketing Bundle', 'tribe-common' ), + 'logo' => 'images/logo/bundle-event-marketing.svg', + 'link' => 'https://evnt.is/1aj3', + 'discount' => __( 'Save over 20%', 'tribe-common' ), + 'description' => __( 'Ticket sales, attendee management, and email marketing for your events', 'tribe-common' ), + 'includes' => [ + 'events-calendar-pro', + 'event-tickets-plus', + 'promoter', + ], + ], + (object) [ + 'title' => __( 'Event Importer Bundle', 'tribe-common' ), + 'logo' => 'images/logo/bundle-event-importer.svg', + 'link' => 'https://evnt.is/1aj2', + 'discount' => __( 'Save over 25%', 'tribe-common' ), + 'description' => __( 'Fill your calendar with events from across the web, including Google Calendar, Meetup, and more.', 'tribe-common' ), + 'includes' => [ + 'events-calendar-pro', + 'tribe-filterbar', + 'event-aggregator' + ], + ], + (object) [ + 'title' => __( 'Virtual Events Marketing Bundle', 'tribe-common' ), + 'logo' => 'images/logo/bundle-virtual-events.svg', + 'link' => 'http://evnt.is/ve-bundle', + 'discount' => __( 'Save over 20%', 'tribe-common' ), + 'description' => __( 'Streamline your online events and increase revenue.', 'tribe-common' ), + 'includes' => [ + 'events-calendar-pro', + 'event-tickets-plus', + 'events-virtual', + 'promoter', + ], + 'features' => [ + __( 'Sell tickets and earn revenue for online events', 'tribe-common' ), + __( 'Zoom integration', 'tribe-common' ), + __( 'Automated emails optimized for virtual events', 'tribe-common' ), + __( 'Add recurring events', 'tribe-common' ), + ], + ], + (object) [ + 'title' => __( 'Community Manager Bundle', 'tribe-common' ), + 'logo' => 'images/logo/bundle-community-manager.svg', + 'link' => 'https://evnt.is/1aj4', + 'discount' => __( 'Save over 20%', 'tribe-common' ), /* code review: fix this */ + 'description' => __( 'Handle event submissions with ticket sales and everything you need to build a robust community.', 'tribe-common' ), + 'includes' => [ + 'event-tickets-plus', + 'events-community', + 'events-community-tickets', + 'tribe-filterbar', + ], + ], + (object) [ + 'title' => __( 'Ultimate Bundle', 'tribe-common' ), + 'logo' => 'images/logo/bundle-ultimate.svg', + 'link' => 'https://evnt.is/1aj5', + 'discount' => __( 'Save over 20%', 'tribe-common' ), /* code review: fix this */ + 'description' => __( 'All of our premium events management plugins at a deep discount.', 'tribe-common' ), + 'includes' => [ + 'events-calendar-pro', + 'event-tickets-plus', + //'events-virtual', // not yet added to the bundle + 'events-community', + 'events-community-tickets', + 'tribe-filterbar', + 'event-aggregator', + 'tribe-eventbrite', + //'promoter', // not yet added to the bundle + ], + ], + + ]; + + return $bundles; + } + + /** + * Gets product extensions + * + * @return array|WP_Error + */ + private function get_extensions() { + $extensions = [ + (object) [ + 'title' => __( 'Website URL CTA', 'tribe-common' ), + 'link' => 'https://evnt.is/1aj6', + 'image' => 'images/shop/extension-web-url-cta.jpg', + 'description' => __( 'Create a strong call-to-action for attendees to "Join Webinar" instead of only sharing a website address.', 'tribe-common' ), + ], + (object) [ + 'title' => __( 'Link Directly to Webinar', 'tribe-common' ), + 'link' => 'https://evnt.is/1aj7', + 'image' => 'images/shop/extension-link-to-webinar.jpg', + 'description' => __( 'When users click on the event title, they’ll be taken right to the source of your event, offering a direct route to join.', 'tribe-common' ), + ], + (object) [ + 'title' => __( 'Events Happening Now', 'tribe-common' ), + 'link' => 'https://evnt.is/1aj8', + 'image' => 'images/shop/extension-events-happening-now.jpg', + 'description' => __( 'Use this shortcode to display events that are currently in progress, like webinars and livestreams.', 'tribe-common' ), + ], + (object) [ + 'title' => __( 'Custom Venue Links', 'tribe-common' ), + 'link' => 'https://evnt.is/1aj9', + 'image' => 'images/shop/extension-custom-venue-links.jpg', + 'description' => __( 'Turn the venue name for your event into a clickable URL — a great way to link directly to a venue’s website or a virtual meeting.', 'tribe-common' ), + ], + (object) [ + 'title' => __( 'Adjust Label', 'tribe-common' ), + 'link' => 'https://evnt.is/1aja', + 'image' => 'images/shop/extension-change-label.jpg', + 'description' => __( 'Change "Events" to "Webinars," or "Venues" to "Livestream," or "Organizers" to "Hosts." Tailor your calendar for virtual events and meetings.', 'tribe-common' ), + ], + (object) [ + 'title' => __( 'Reach Attendees', 'tribe-common' ), + 'link' => 'https://evnt.is/1ajc', + 'image' => 'images/shop/extension-advanced-options.jpg', + 'description' => __( 'From registration to attendance history, view every step of the event lifecycle with this HubSpot integration.', 'tribe-common' ), + ], + ]; + + return $extensions; + } + + /** + * Gets Stellar brands + * + * @return array|WP_Error + */ + private function get_stellar_brands() { + $stellar_brands = [ + (object) [ + 'image' => 'images/shop/stellar-learndash-cta.jpg', + 'logo' => 'images/shop/stellar-learndash-logo.png', + 'title' => __( 'The online course platform created by e-learning experts.', 'tribe-common' ), + 'link' => 'https://evnt.is/learndash', + 'linktext' => __( 'Add Courses', 'tribe-common' ), + 'description' => __( 'Trusted to power learning programs for major universities, startups, entrepreneurs, and bloggers worldwide.', 'tribe-common' ), + ], + (object) [ + 'image' => 'images/shop/stellar-ithemes-cta.jpg', + 'logo' => 'images/shop/stellar-ithemes-logo.png', + 'title' => __( 'Foundational favorites: iThemes Security and Developer Toolkit.', 'tribe-common' ), + 'link' => 'https://evnt.is/ithemes', + 'linktext' => __( 'Add Security', 'tribe-common' ), + 'description' => __( 'iThemes Security, the WordPress security plugin that’s easy to use. Built with performance in mind.', 'tribe-common' ), + ], + (object) [ + 'image' => 'images/shop/stellar-rcp-cta.jpg', + 'logo' => 'images/shop/stellar-rcp-logo.png', + 'title' => __( 'Built with developers in mind.', 'tribe-common' ), + 'link' => 'https://evnt.is/rcp', + 'linktext' => __( 'Add Content Restriction', 'tribe-common' ), + 'description' => __( 'Restrict Content Pro is flexible, easy to extend, and chock full of action hooks and filters, making it easy to modify and tweak to your specific needs.', 'tribe-common' ), + ], + (object) [ + 'image' => 'images/shop/stellar-kadence-cta.jpg', + 'logo' => 'images/shop/stellar-kadence-logo.png', + 'title' => __( 'Build better WordPress websites with Kadence.', 'tribe-common' ), + 'link' => 'https://evnt.is/kadencewp', + 'linktext' => __( 'Add Starter Templates', 'tribe-common' ), + 'description' => __( 'Kadence lets you unlock your creativity in the WordPress Block Editor with expertly designed blocks, a robust theme, and a massive library of starter templates.', 'tribe-common' ), + ], + (object) [ + 'image' => 'images/shop/stellar-iconic-cta.jpg', + 'logo' => 'images/shop/stellar-iconic-logo.png', + 'title' => __( 'Sales-boosting WooCommerce plugins.', 'tribe-common' ), + 'link' => 'https://evnt.is/iconic', + 'linktext' => __( 'Add Commerce Tools', 'tribe-common' ), + 'description' => __( 'Easy-to-use WooCommerce plugins work perfectly together, with any theme. Create a fast and profitable eCommerce store without any technical knowledge. + ', 'tribe-common' ), + ], + (object) [ + 'image' => 'images/shop/stellar-give-cta.jpg', + 'logo' => 'images/shop/stellar-give-logo.png', + 'title' => __( 'The best WordPress donation plugin.', 'tribe-common' ), + 'link' => 'https://evnt.is/givewp', + 'linktext' => __( 'Add Donations', 'tribe-common' ), + 'description' => __( 'GiveWP makes it easy to raise money online with donation forms, donor databases, and fundraising reporting.', 'tribe-common' ), + ], + ]; + + return $stellar_brands; + } + + /** + * Static Singleton Factory Method + * + * @return Tribe__App_Shop + */ + public static function instance() { + if ( ! isset( self::$instance ) ) { + $className = __CLASS__; + self::$instance = new $className; + } + + return self::$instance; + } + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Asset/Data.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Asset/Data.php new file mode 100644 index 000000000..2dd995734 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Asset/Data.php @@ -0,0 +1,67 @@ +objects[ $object_name ] = $data; + } + + /** + * Outputs the + * @internal + */ + public function render_json() { + if ( empty( $this->objects ) ) { + return; + } + + echo ''; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Asset/Factory.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Asset/Factory.php new file mode 100644 index 000000000..fee20c577 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Asset/Factory.php @@ -0,0 +1,56 @@ +get_asset_class_name( $name ); + + // `Jquery_Resize` to `Tribe__Asset__Jquery_Resize` + $full_class_name = $this->get_asset_full_class_name( $class_name ); + + return class_exists( $full_class_name ) ? new $full_class_name() : false; + } + + protected function get_asset_class_name( $name ) { + // `jquery-resize` to `Jquery_Resize` + $class_name = str_replace( ' ', '_', ucwords( str_replace( '-', ' ', $name ) ) ); + + return $class_name; + } + + /** + * @param string $class_name + * + * @return string + */ + private function get_asset_full_class_name( $class_name ) { + // `Jquery_Resize` to `Tribe__Asset__Jquery_Resize` + $full_class_name = $this->get_asset_class_name_prefix() . $class_name; + + return $full_class_name; + } + + /** + * @return string + */ + protected function get_asset_class_name_prefix() { + return 'Tribe__Asset__'; + } + + /** + * @return Tribe__Asset__Factory + */ + public static function instance() { + static $instance; + + if ( ! $instance ) { + $instance = new self; + } + + return $instance; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Assets.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Assets.php new file mode 100644 index 000000000..a9cb962b8 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Assets.php @@ -0,0 +1,949 @@ +get( $handle ) ) { + return (string) $tag; + } + + // Bail when not dealing with JS assets. + if ( 'js' !== $asset->type ) { + return (string) $tag; + } + + // Only go forward if there is any print before or after. + if ( empty( $asset->print_before ) && empty( $asset->print_after ) ) { + return (string) $tag; + } + + $before = ''; + if ( ! empty( $asset->print_before ) ) { + $before = (string) ( is_callable( $asset->print_before ) ? call_user_func( $asset->print_before, $asset ) : $asset->print_before ); + } + + $after = ''; + if ( ! empty( $asset->print_after ) ) { + $after = (string) ( is_callable( $asset->print_after ) ? call_user_func( $asset->print_after, $asset ) : $asset->print_after ); + } + + $tag = $before . (string) $tag . $after; + + return $tag; + } + + /** + * Handles adding localization data, when attached to `script_loader_tag` which allows dependencies to load in their + * localization data as well. + * + * @since 4.13.0 + * + * @param string $tag Tag we are filtering. + * @param string $handle Which is the ID/Handle of the tag we are about to print. + * + * @return string Script tag with the localization variable HTML attached to it. + */ + public function filter_add_localization_data( $tag, $handle ) { + // Only filter for own filters. + if ( ! $asset = $this->get( $handle ) ) { + return $tag; + } + + // Bail when not dealing with JS assets. + if ( 'js' !== $asset->type ) { + return $tag; + } + + // Only localize on JS and if we have data. + if ( empty( $asset->localize ) ) { + return $tag; + } + + global $wp_scripts; + + // Makes sure we have an Array of Localize data. + if ( is_object( $asset->localize ) ) { + $localization = [ $asset->localize ]; + } else { + $localization = (array) $asset->localize; + } + + /** + * Check to ensure we haven't already localized it before. + * + * @since 4.5.8 + */ + foreach ( $localization as $localize ) { + if ( in_array( $localize->name, $this->localized ) ) { + continue; + } + + // If we have a Callable as the Localize data we execute it. + if ( is_callable( $localize->data ) ) { + $localize->data = call_user_func( $localize->data, $asset ); + } + + wp_localize_script( $asset->slug, $localize->name, $localize->data ); + + $this->localized[] = $localize->name; + } + + // Fetch the HTML for all the localized data. + ob_start(); + $wp_scripts->print_extra_script( $asset->slug, true ); + $localization_html = ob_get_clean(); + + // After printing it remove data;| + $wp_scripts->add_data( $asset->slug, 'data', '' ); + + return $localization_html . $tag; + } + + /** + * Filters the Script tags to attach Async and/or Defer based on the rules we set in our Asset class. + * + * @since 4.13.0 + * + * @param string $tag Tag we are filtering. + * @param string $handle Which is the ID/Handle of the tag we are about to print. + * + * @return string Script tag with the defer and/or async attached. + */ + public function filter_tag_async_defer( $tag, $handle ) { + // Only filter for our own filters. + if ( ! $asset = $this->get( $handle ) ) { + return $tag; + } + + // Bail when not dealing with JS assets. + if ( 'js' !== $asset->type ) { + return $tag; + } + + // When async and defer are false we bail with the tag. + if ( ! $asset->defer && ! $asset->async ) { + return $tag; + } + + $tag_has_async = false !== strpos( $tag, ' async ' ); + $tag_has_defer = false !== strpos( $tag, ' defer ' ); + $replacement = '\n" + . $tag + . "\n"; + } + + return $tag; + } + + /** + * After select2 is loaded to the FE we add one scripts after to prevent select2 from breaking. + * + * @since 4.13.2 + * @since 4.14.18 Ensure we don't run this in the admin. + * + * @param string $tag The \n"; + + return $tag; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Autoloader.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Autoloader.php new file mode 100644 index 000000000..90a01e027 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Autoloader.php @@ -0,0 +1,297 @@ +register_prefix( 'Tribe__Admin__', $this_dir . '/src/Tribe/admin' ); + * $autoloader->register_prefix( 'Tribe__Admin__', $this_dir . '/src/Tribe/another-dir' ); + * $autoloader->register_prefix( 'Tribe__Utils__', $this_dir . '/src/Tribe/another-dir' ); + * + * // register a direct class to path + * $autoloader->register_class( 'Tribe__Some_Class', $this_dir . '/some/path/to/Some_Class.php' ); + * + * // register a fallback dir to be searched for the class before giving up + * $autoloader->add_fallback_dir( $this_dir . '/all-the-classes' ); + * + * // calls `spl_autoload_register` + * $autoloader->register_autoloader(); + * + * // class will be searched in the path + * // `/var/www/site/wp-content/plugins/the-events-calendar/src/Tribe/admin/Some_Class.php' + * // and + * // `/var/www/site/wp-content/plugins/the-events-calendar/src/Tribe/another-dir/Some_Class.php' + * $i = new Tribe__Admin__Some_Class(); + * + * // class will be searched in the path + * // `/var/www/site/wp-content/plugins/the-events-calendar/utils/some-dir/Some_Util.php' + * $i = new Tribe__Utils__Some_Util(); + * + * // class will be searched in the path + * // `/var/www/site/wp-content/plugins/the-events-calendar/deprecated/Tribe_DeprecatedClass.php' + * $i = new Tribe_DeprecatedClass(); + */ + class Tribe__Autoloader { + + /** + * @var Tribe__Autoloader + */ + protected static $instance; + + /** + * An arrays of arrays each containing absolute paths. + * + * Paths are stored trimming any trailing `/`. + * E.g. `/var/www/tribe-pro/wp-content/plugins/the-events-calendar/src/Tribe` + * + * @var string[][] + */ + protected $prefixes; + + /** + * An array of registered prefixes with unique slugs. + * + * @var string[] + */ + protected $prefix_slugs; + + /** + * The string acting as a directory separator in a class name. + * + * E.g.: given `__` as `$dir_separator` then `Admin__Metabox__Some_Metabox` + * will map to `/Admin/Metabox/SomeMetabox.php`. + * + * @var string + */ + protected $dir_separator = '__'; + + /** @var string[] */ + protected $fallback_dirs = []; + + /** + * @var array + */ + protected $class_paths = []; + + /** + * Returns the singleton instance of the class. + * + * @return Tribe__Autoloader + */ + public static function instance() { + if ( ! self::$instance instanceof Tribe__Autoloader ) { + self::$instance = new self(); + } + + return self::$instance; + } + + /** + * Registers prefixes and root dirs using an array. + * + * Same as calling `register_prefix` on each one. + * + * @param array $prefixes_to_root_dirs + */ + public function register_prefixes( array $prefixes_to_root_dirs ) { + foreach ( $prefixes_to_root_dirs as $prefix => $root_dir ) { + $this->register_prefix( $prefix, $root_dir ); + } + } + + /** + * Associates a class prefix to an absolute path. + * + * @param string $prefix A class prefix, e.g. `Tribe__Admin__` + * @param string $root_dir The absolute path to the dir containing + * the prefixed classes. + * @param string $slug An optional unique slug to associate to the prefix. + */ + public function register_prefix( $prefix, $root_dir, $slug = '' ) { + $root_dir = $this->normalize_root_dir( $root_dir ); + + // Determine if we need to normalize the $prefix. + $is_namespaced = false !== strpos( $prefix, '\\' ); + + if ( $is_namespaced ) { + // If the prefix is a namespace, then normalize it. + $prefix = trim( $prefix, '\\' ) . '\\'; + } + + if ( ! isset( $this->prefixes[ $prefix ] ) ) { + $this->prefixes[ $prefix ] = []; + } + + $this->prefixes[ $prefix ][] = $root_dir; + + // Let's make sure we're not adding duplicates. + $this->prefixes[ $prefix ] = array_unique( $this->prefixes[ $prefix ] ); + + if ( $slug ) { + $this->prefix_slugs[ $slug ] = $prefix; + } + } + + /** + * Triggers the registration of the autoload method in the SPL + * autoload register. + */ + public function register_autoloader() { + spl_autoload_register( [ $this, 'autoload' ] ); + } + + /** + * Includes the file defining a class. + * + * This is the function that's registered as an autoloader. + * + * @param string $class + */ + public function autoload( $class ) { + $include_path = $this->get_class_path( $class ); + if ( ! empty( $include_path ) ) { + include_once( $include_path ); + } + } + + private function normalize_root_dir( $root_dir ) { + return rtrim( $root_dir, '/' ); + } + + protected function get_prefixed_path( $class ) { + foreach ( $this->prefixes as $prefix => $dirs ) { + $is_namespaced = false !== strpos( $prefix, '\\' ); + + if ( strpos( $class, $prefix ) !== 0 ) { + continue; + } + + $class_name = str_replace( $prefix, '', $class ); + + if ( ! $is_namespaced ) { + $class_path_frag = implode( '/', explode( $this->dir_separator, $class_name ) ) . '.php'; + } else { + $class_path_frag = implode( '/', explode( '\\', $class_name ) ) . '.php'; + } + + foreach ( $dirs as $dir ) { + $path = $dir . '/' . $class_path_frag; + if ( ! file_exists( $path ) ) { + // check if the file exists in lowercase + $class_path_frag = strtolower( $class_path_frag ); + $path = $dir . '/' . $class_path_frag; + } + if ( ! file_exists( $path ) ) { + continue; + } + + return $path; + } + } + return false; + } + + protected function get_fallback_path( $class ) { + foreach ( $this->fallback_dirs as $fallback_dir ) { + $include_path = $fallback_dir . '/' . $class . '.php'; + if ( ! file_exists( $include_path ) ) { + // check if the file exists in lowercase + $class = strtolower( $class ); + $include_path = $fallback_dir . '/' . $class . '.php'; + } + if ( ! file_exists( $include_path ) ) { + continue; + } + + return $include_path; + } + } + + /** + * Gets the absolute path to a class file. + * + * @param string $class The class name + * + * @return string Either the absolute path to the class file or an + * empty string if the file was not found. + */ + public function get_class_path( $class ) { + $prefixed_path = $this->get_prefixed_path( $class ); + if ( $prefixed_path ) { + return $prefixed_path; + } + + $class_path = ! empty( $this->class_paths[ $class ] ) ? $this->class_paths[ $class ] :false; + if ( $class_path ) { + return $class_path; + } + + $fallback_path = $this->get_fallback_path( $class ); + + return $fallback_path ? $fallback_path : ''; + } + + /** + * Get the registered prefix by slug + * + * @param string $slug Unique slug for registered prefix. + * + * @return false|string Either the prefix registered to the + * unique slug or false if not found. + */ + public function get_prefix_by_slug( $slug ) { + $prefix = false; + + if ( isset( $this->prefix_slugs[ $slug ] ) ) { + $prefix = $this->prefix_slugs[ $slug ]; + } + + return $prefix; + } + + /** + * Adds a folder to search for classes that were not found among + * the prefixed ones. + * + * This is the method to use to register a directory of deprecated + * classes. + * + * @param string $dir An absolute path dto a dir. + */ + public function add_fallback_dir( $dir ) { + if ( in_array( $dir, $this->fallback_dirs ) ) { + return; + } + $this->fallback_dirs[] = $this->normalize_root_dir( $dir ); + } + + /** + * @return string + */ + public function get_dir_separator() { + return $this->dir_separator; + } + + /** + * @param string $dir_separator + */ + public function set_dir_separator( $dir_separator ) { + $this->dir_separator = $dir_separator; + } + + public function register_class( $class, $path ) { + $this->class_paths[ $class ] = $path; + } + } + } diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Cache.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Cache.php new file mode 100644 index 000000000..3dbf9e83f --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Cache.php @@ -0,0 +1,646 @@ +get_id( $id, $expiration_trigger ); + + /** + * Filters the expiration for cache objects to provide the ability + * to make non-persistent objects be treated as persistent. + * + * @since 4.8 + * + * @param int $expiration Cache expiration time. + * @param string $id Cache ID. + * @param mixed $value Cache value. + * @param string|array $expiration_trigger Action that triggers automatic expiration. + * @param string $key Unique cache key based on Cache ID and expiration trigger last run time. + */ + $expiration = apply_filters( 'tribe_cache_expiration', $expiration, $id, $value, $expiration_trigger, $key ); + + if ( self::NON_PERSISTENT === $expiration ) { + $group = 'tribe-events-non-persistent'; + $expiration = 1; + + // Add so we know what group to use in the future. + $this->non_persistent_keys[ $id ] = $id; + } else { + $group = 'tribe-events'; + } + + return wp_cache_set( $key, $value, $group, $expiration ); + } + + /** + * @param $id + * @param $value + * @param int $expiration + * @param string|array $expiration_trigger + * + * @return bool + */ + public function set_transient( $id, $value, $expiration = 0, $expiration_trigger = '' ) { + if ( $this->data_size_over_packet_size( $value ) ) { + return false; + } + + return set_transient( $this->get_id( $id, $expiration_trigger ), $value, $expiration ); + } + + /** + * Get cached data. Optionally set data if not previously set. + * + * Note: When a default value or callback is specified, this value gets set in the cache. + * + * @param string $id The key for the cached value. + * @param string|array $expiration_trigger Optional. Hook to trigger cache invalidation. + * @param mixed $default Optional. A default value or callback that returns a default value. + * @param int $expiration Optional. When the default value expires, if it gets set. + * @param mixed $args Optional. Args passed to callback. + * + * @return mixed + */ + public function get( $id, $expiration_trigger = '', $default = false, $expiration = 0, $args = [] ) { + $group = isset( $this->non_persistent_keys[ $id ] ) ? 'tribe-events-non-persistent' : 'tribe-events'; + $value = wp_cache_get( $this->get_id( $id, $expiration_trigger ), $group ); + + // Value found. + if ( false !== $value ) { + return $value; + } + + if ( is_callable( $default ) ) { + // A callback has been specified. + $value = $default( ...$args ); + } else { + // Default is a value. + $value = $default; + } + + // No need to set a cache value to false since non-existent values return false. + if ( false !== $value ) { + $this->set( $id, $value, $expiration, $expiration_trigger ); + } + + return $value; + } + + /** + * @param string $id + * @param string|array $expiration_trigger + * + * @return mixed + */ + public function get_transient( $id, $expiration_trigger = '' ) { + return get_transient( $this->get_id( $id, $expiration_trigger ) ); + } + + /** + * @param string $id + * @param string|array $expiration_trigger + * + * @return bool + */ + public function delete( $id, $expiration_trigger = '' ) { + $group = isset( $this->non_persistent_keys[ $id ] ) ? 'tribe-events-non-persistent' : 'tribe-events'; + + // Delete from non-persistent keys list. + if ( 'tribe-events-non-persistent' === $group ) { + unset( $this->non_persistent_keys[ $id ] ); + } + + return wp_cache_delete( $this->get_id( $id, $expiration_trigger ), $group ); + } + + /** + * @param string $id + * @param string|array $expiration_trigger + * + * @return bool + */ + public function delete_transient( $id, $expiration_trigger = '' ) { + return delete_transient( $this->get_id( $id, $expiration_trigger ) ); + } + + /** + * Purge all expired tribe_ transients. + * + * This uses a modification of the the query from https://core.trac.wordpress.org/ticket/20316 + * + * @since 4.11.0 + * + * @return void Just execute the database SQL no return required. + */ + public function delete_expired_transients() { + if ( tribe_get_var( 'has_deleted_expired_transients', false ) ) { + return; + } + + global $wpdb; + + $time = time(); + + $sql = " + DELETE + a, + b + FROM + {$wpdb->options} a + INNER JOIN {$wpdb->options} b + ON b.option_name = CONCAT( '_transient_timeout_', SUBSTRING( a.option_name, 12 ) ) + AND b.option_value < {$time} + WHERE + a.option_name LIKE '\_transient\_tribe\_%' + AND a.option_name NOT LIKE '\_transient\_timeout\_tribe\_%' + "; + + /** + * Allow third party filtering of the SQL used for deleting expired transients. + * + * @since 4.11.5 + * + * @param string $sql The SQL we execute to delete all the expired transients. + * @param int $time Time we are using to determine what is expired. + */ + $sql = apply_filters( 'tribe_cache_delete_expired_transients_sql', $sql, $time ); + + if ( empty( $sql ) ) { + return; + } + + $wpdb->query( $sql ); + + // Set the variable to prevent this call from running twice. + tribe_set_var( 'has_deleted_expired_transients', true ); + } + + /** + * Flag if we should delete + * + * @since 4.11.5 + * + * @param boolean $value If we should delete transients or not on shutdown. + * + * @return void No return for setting the flag. + */ + public function flag_required_delete_transients( $value = true ) { + tribe_set_var( 'should_delete_expired_transients', $value ); + } + + /** + * Runs on hook `shutdown` and will delete transients on the end of the request. + * + * @since 4.11.5 + * + * @return void No return for action hook method. + */ + public function maybe_delete_expired_transients() { + if ( ! tribe_get_var( 'should_delete_expired_transients', false ) ) { + return; + } + + $this->delete_expired_transients(); + } + + /** + * @param string $key + * @param string|array $expiration_trigger + * + * @return string + */ + public function get_id( $key, $expiration_trigger = '' ) { + if ( is_array( $expiration_trigger ) ) { + $triggers = $expiration_trigger; + } elseif ( 'tribe-events-non-persistent' !== $expiration_trigger && 'tribe-events' !== $expiration_trigger ) { + $triggers = array_filter( explode( '|', $expiration_trigger ) ); + } + + $last = 0; + foreach ( $triggers as $trigger ) { + // Bail on empty trigger otherwise it creates a `tribe_last_` opt on the DB. + if ( empty( $trigger ) ) { + continue; + } + + $occurrence = $this->get_last_occurrence( $trigger ); + + if ( $occurrence > $last ) { + $last = $occurrence; + } + } + + $last = empty( $last ) ? '' : $last; + $id = $key . $last; + if ( strlen( $id ) > 80 ) { + $id = 'tribe_' . md5( $id ); + } + + return $id; + } + + /** + * Returns the time of an action last occurrence. + * + * @since 4.9.14 Changed the return value type from `int` to `float`. + * @since 5.0.17 No longer memoizes the first triggered timestamp. + * + * @param string $action The action to return the time for. + * + * @return float The time (microtime) an action last occurred, or the current microtime if it never occurred. + */ + public function get_last_occurrence( $action ) { + $last_action = (float) get_option( 'tribe_last_' . $action, null ); + + if ( ! $last_action ) { + $last_action = microtime( true ); + $this->set_last_occurrence( $action, $last_action ); + } + + return $last_action; + } + + /** + * Sets the time (microtime) for an action last occurrence. + * + * @since 4.9.14 Changed the type of the time stored from an `int` to a `float`. + * + * @param string $action The action to record the last occurrence of. + * @param int|float $timestamp The timestamp to assign to the action last occurrence or the current time (microtime). + * + * @return boolean IF we were able to set the last occurrence or not. + */ + public function set_last_occurrence( $action, $timestamp = 0 ) { + if ( empty( $timestamp ) ) { + $timestamp = microtime( true ); + } + $updated = update_option( 'tribe_last_' . $action, (float) $timestamp ); + + // For performance reasons we will only expire cache once per request, when needed. + if ( $updated ) { + $this->flag_required_delete_transients( true ); + } + + return $updated; + } + + /** + * Builds a key from an array of components and an optional prefix. + * + * @param mixed $components Either a single component of the key or an array of key components. + * @param string $prefix + * @param bool $sort Whether component arrays should be sorted or not to generate the key; defaults to + * `true`. + * + * @return string The resulting key. + */ + public function make_key( $components, $prefix = '', $sort = true ) { + $key = ''; + $components = is_array( $components ) ? $components : [ $components ]; + foreach ( $components as $component ) { + if ( $sort && is_array( $component ) ) { + $is_associative = count( array_filter( array_keys( $component ), 'is_numeric' ) ) < count( array_keys( $component ) ); + if ( $is_associative ) { + ksort( $component ); + } else { + sort( $component ); + } + } + $key .= maybe_serialize( $component ); + } + + return $this->get_id( $prefix . md5( $key ) ); + } + + /** + * Whether a offset exists. + * + * @since 4.11.0 + * @since 5.0.13 Will check against cache expiration. Previously would give false positive + * if expiration had passed but was cached recently. Will now consider null not set. + * + * @param mixed $offset An offset to check for. + * + * @return boolean Whether the offset exists in the cache. + *@link http://php.net/manual/en/arrayaccess.offsetexists.php + * + */ + #[\ReturnTypeWillChange] + public function offsetExists( $offset ): bool { + $value = $this->get( $offset ); + + return $value !== false && $value !== null; + } + + /** + * Offset to retrieve. + * + * @link http://php.net/manual/en/arrayaccess.offsetget.php + * + * @since 4.11.0 + * + * @param mixed $offset The offset to retrieve. + * + * @return mixed Can return all value types. + */ + #[\ReturnTypeWillChange] + public function offsetGet( $offset ) { + return $this->get( $offset ); + } + + /** + * Offset to set. + * + * @since 4.11.0 + * + * @link http://php.net/manual/en/arrayaccess.offsetset.php + * + * @param mixed $offset The offset to assign the value to. + * @param mixed $value The value to set. + * + * @return void + */ + #[\ReturnTypeWillChange] + public function offsetSet( $offset, $value ): void { + $this->set( $offset, $value, self::NON_PERSISTENT ); + } + + /** + * Offset to unset. + * + * @since 4.11.0 + * + * @link http://php.net/manual/en/arrayaccess.offsetunset.php + * + * @param mixed $offset The offset to unset. + * + * @return void + */ + #[\ReturnTypeWillChange] + public function offsetUnset( $offset ): void { + $this->delete( $offset ); + } + + /** + * Removes a group of the cache, for now only `non_persistent` is supported. + * + * @since 4.14.13 + * + * @return bool + */ + public function reset( $group = 'non_persistent' ) { + if ( 'non_persistent' !== $group ) { + return false; + } + $this->non_persistent_keys = []; + return true; + } + + /** + * Warms up the caches for a collection of posts. + * + * @since 4.10.2 + * + * @param array|int $post_ids A post ID, or a collection of post IDs. + * @param bool $update_post_meta_cache Whether to warm-up the post meta cache for the posts or not. + */ + public function warmup_post_caches( $post_ids, $update_post_meta_cache = false ) { + if ( empty( $post_ids ) ) { + return; + } + + $post_ids = (array) $post_ids; + + global $wpdb; + + $already_cached_ids = []; + foreach ( $post_ids as $post_id ) { + if ( wp_cache_get( $post_id, 'posts' ) instanceof \WP_Post ) { + $already_cached_ids[] = $post_id; + } + } + + $required = array_diff( $post_ids, $already_cached_ids ); + + if ( empty( $required ) ) { + return; + } + + /** @var Tribe__Feature_Detection $feature_detection */ + $feature_detection = tribe( 'feature-detection' ); + $limit = $feature_detection->mysql_limit_for_example( 'post_result' ); + + /** + * Filters the LIMIT that should be used to warm-up post caches and postmeta caches (if the + * `$update_post_meta_cache` parameter is `true`). + * + * Lower this value on less powerful hosts. Return `0` to disable the warm-up completely, and `-1` to remove the + * limit (not recommended). + * + * @since 4.10.2 + * + * @param int $limit The number of posts whose caches will be warmed up, per query. + */ + $limit = (int) apply_filters( 'tribe_cache_warmup_post_cache_limit', min( $limit, count( $post_ids ) ) ); + + if ( 0 === $limit ) { + // Warmup disabled. + return; + } + + $buffer = $post_ids; + $page = 0; + + do { + $limit_clause = $limit < 0 ? sprintf( 'LIMIT %d,%d', $limit * $page, $limit ) : ''; + $page++; + $these_ids = array_splice( $buffer, 0, $limit ); + $interval = implode( ',', array_map( 'absint', $these_ids ) ); + $posts_query = "SELECT * FROM {$wpdb->posts} WHERE ID IN ({$interval}) {$limit_clause}"; + $post_objects = $wpdb->get_results( $posts_query ); + if ( is_array( $post_objects ) && ! empty( $post_objects ) ) { + foreach ( $post_objects as $post_object ) { + $post = new \WP_Post( $post_object ); + wp_cache_set( $post_object->ID, $post, 'posts' ); + } + + if ( $update_post_meta_cache ) { + update_meta_cache( 'post', $these_ids ); + } + } + } while ( ! empty( $post_objects ) && is_array( $post_objects ) && count( $post_objects ) < count( $post_ids ) ); + } + + /** + * If NOT using an external object caching system, then check if the size, in bytes, of the data + * to write to the database would fit into the `max_allowed_packet` setting or not. + * + * @since 4.12.14 + * + * @param string|array|object $value The value to check. + * + * @return bool Whether the data, in its serialized form, would fit into the current database `max_allowed_packet` + * setting or not. + */ + public function data_size_over_packet_size( $value ) { + if ( wp_using_ext_object_cache() ) { + // We cannot know and that is a concern of the external caching system. + return false; + } + + try { + $serialized_value = maybe_serialize( $value ); + $size = strlen( $serialized_value ); + } catch ( Exception $e ) { + // The underlying function would run into the same issue, bail and do not set the transient. + return true; + } + + /** @var Tribe__Feature_Detection $feature_detection */ + $feature_detection = tribe( 'feature-detection' ); + + // If the size of the string is above 90% of the database `max_allowed_packet` setting, then it should not be written to the db. + return $size > ( $feature_detection->get_mysql_max_packet_size() * .9 ); + } + + /** + * Returns a transient that might have been stored, due ot its size, in chunks. + * + * @since 4.13.3 + * + * @param string $id The name of the transients to return. + * @param string|array $expiration_trigger The transient expiration trigger(s). + * + * @return false|mixed Either the transient value, joined back into one, or `false` to indicate + * the transient was not found or was malformed. + */ + public function get_chunkable_transient( $id, $expiration_trigger = '' ) { + $transient = $this->get_id( $id, $expiration_trigger ); + + if ( wp_using_ext_object_cache() ) { + return get_transient( $transient ); + } + + $chunks = []; + $i = 0; + do { + $chunk_transient = $transient . '_' . $i++; + $chunk = get_transient( $chunk_transient ); + $chunks[ $chunk_transient ] = (string) $chunk; + } while ( ! empty( $chunk ) ); + + // Remove any piece of data that was added but is not relevant. + $chunks = array_filter( $chunks ); + + if ( empty( $chunks ) ) { + return false; + } + + try { + $data = implode( '', $chunks ); + $is_serialized = preg_match( '/^[aO]:\\d+:/', $data ); + $unserialized = maybe_unserialize( implode( '', $chunks ) ); + + if ( is_string( $unserialized ) && $unserialized === $data && $is_serialized ) { + // Something was messed up. + return false; + } + + return $unserialized; + } catch ( Exception $e ) { + return false; + } + } + + /** + * Sets a transient in the database with the knowledge that, if too large to be stored in one + * DB row, it will be chunked. + * + * The method will redirect to the `set_transient` function if the site is using object caching. + * + * + * @since 4.13.3 + * + * @param string $id The transient ID. + * @param mixed $value The value to store, that could be chunked. + * @param int $expiration The transient expiration, in seconds. + * @param string|array $expiration_trigger The transient expiration trigger(s). + * + * @return bool Whether the transient, or the transient chunks, have been stored correctly or not. + */ + public function set_chunkable_transient( $id, $value, $expiration = 0, $expiration_trigger = '' ) { + $transient = $this->get_id( $id, $expiration_trigger ); + + if ( wp_using_ext_object_cache() ) { + return $this->set_transient( $transient, $value, $expiration ); + } + + $inserted = []; + $serialized_value = maybe_serialize( $value ); + $chunk_size = tribe( 'feature-detection' )->get_mysql_max_packet_size() * 0.9; + $chunks = str_split( $serialized_value, $chunk_size ); + foreach ( $chunks as $i => $chunk ) { + $chunk_transient = $transient . '_' . $i; + + $set = set_transient( $chunk_transient, $chunk, $expiration ); + + if ( ! $set ) { + foreach ( $inserted as $transient_to_delete ) { + delete_transient( $transient_to_delete ); + } + + return false; + } + + $inserted[] = $chunk_transient; + } + + return true; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Cache_Listener.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Cache_Listener.php new file mode 100644 index 000000000..bc428398a --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Cache_Listener.php @@ -0,0 +1,215 @@ +cache = new Tribe__Cache(); + } + + /** + * Run the init functionality (like add_hooks). + * + * @return void + */ + public function init() { + $this->add_hooks(); + } + + /** + * Add the hooks necessary. + * + * @return void + */ + private function add_hooks() { + add_action( 'save_post', [ $this, 'save_post' ], 0, 2 ); + add_action( 'updated_option', [ $this, 'update_last_updated_option' ], 10, 3 ); + add_action( 'updated_option', [ $this, 'update_last_save_post' ], 10, 3 ); + add_action( 'generate_rewrite_rules', [ $this, 'generate_rewrite_rules' ] ); + add_action( 'clean_post_cache', [ $this, 'save_post' ], 0, 2 ); + } + + /** + * Run the caching functionality that is executed on save post. + * + * @param int $post_id The post_id. + * @param WP_Post $post The current post object being saved.w + */ + public function save_post( $post_id, $post ) { + if ( in_array( $post->post_type, Tribe__Main::get_post_types() ) ) { + $this->cache->set_last_occurrence( self::TRIGGER_SAVE_POST ); + } + } + + /** + * Run the caching functionality that is executed on saving tribe calendar options. + * + * @see 'updated_option' + * + * @param string $option_name Name of the updated option. + * @param mixed $old_value The old option value. + * @param mixed $value The new option value. + */ + public function update_last_save_post( $option_name, $old_value, $value ) { + $triggers = [ + 'tribe_events_calendar_options' => true, + 'permalink_structure' => true, + 'rewrite_rules' => true, + 'start_of_week' => true, + ]; + + $triggers = $this->filter_action_last_occurrence_triggers( $triggers, static::TRIGGER_SAVE_POST, func_get_args() ); + + if ( ! empty( $triggers[ $option_name ] ) ) { + $this->cache->set_last_occurrence( self::TRIGGER_SAVE_POST ); + } + } + + /** + * Run the caching functionality that is executed on saving tribe calendar options. + * + * @see 'updated_option' + * + * @since 4.11.0 + * + * @param string $option_name Name of the updated option. + * @param mixed $old_value The old option value. + * @param mixed $value The new option value. + */ + public function update_last_updated_option( $option_name, $old_value, $value ) { + $triggers = [ + 'active_plugins' => true, + 'tribe_events_calendar_options' => true, + 'permalink_structure' => true, + 'rewrite_rules' => true, + 'start_of_week' => true, + 'sidebars_widgets' => true, + 'stylesheet' => true, + 'template' => true, + 'WPLANG' => true, + ]; + + $triggers = $this->filter_action_last_occurrence_triggers( $triggers, static::TRIGGER_UPDATED_OPTION, func_get_args() ); + + if ( ! empty( $triggers[ $option_name ] ) ) { + $this->cache->set_last_occurrence( self::TRIGGER_UPDATED_OPTION ); + } + } + + /** + * Filtering for last occurrence triggers. + * + * @since 4.13.2 + * + * @param array $triggers Which options will trigger this given action last occurrence. + * @param string $action Which action this trigger will set. + * @param array $args Which arguments from the updated option method. + * + * @return array + */ + public function filter_action_last_occurrence_triggers( array $triggers = [], $action = '', array $args = [] ) { + + /** + * Filters the contents of which options will trigger expiring a given actions cache. + * + * @since 4.13.2 + * + * @param array $triggers Which options will trigger this given action last occurrence. + * @param string $action Which action this trigger will set. + * @param array $args Which arguments from the updated option method. + */ + $triggers = apply_filters( 'tribe_cache_last_occurrence_option_triggers', $triggers, $action, $args ); + + /** + * Filters the contents of which options will trigger expiring a given actions cache. + * Allows filtering a specific action. + * + * @since 4.13.2 + * + * @param array $triggers Which options will trigger this given action last occurrence. + * @param string $action Which action this trigger will set. + * @param array $args Which arguments from the updated option method. + */ + return (array) apply_filters( "tribe_cache_last_occurrence_option_triggers:{$action}", $triggers, $action, $args ); + } + + /** + * For any hook that doesn't need any additional filtering + * + * @param $method + * @param $args + */ + public function __call( $method, $args ) { + $this->cache->set_last_occurrence( $method ); + } + + /** + * Instance method of the cache listener. + * + * @return Tribe__Cache_Listener + */ + public static function instance() { + if ( empty( self::$instance ) ) { + self::$instance = self::create_listener(); + } + + return self::$instance; + } + + /** + * Create a cache listener. + * + * @return Tribe__Cache_Listener + */ + private static function create_listener() { + $listener = new self(); + $listener->init(); + + return $listener; + } + + /** + * Run the caching functionality that is executed when rewrite rules are generated. + * + * @since 4.9.11 + */ + public function generate_rewrite_rules() { + $this->cache->set_last_occurrence( self::TRIGGER_GENERATE_REWRITE_RULES ); + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Changelog_Reader.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Changelog_Reader.php new file mode 100644 index 000000000..43041367e --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Changelog_Reader.php @@ -0,0 +1,53 @@ +version_count = (int) $version_count; + $this->readme_file = empty( $readme_file ) ? $this->default_readme_file() : $readme_file; + } + + protected function default_readme_file() { + return dirname( Tribe__Main::instance()->plugin_path ) . '/readme.txt'; + } + + public function get_changelog() { + $contents = $this->extract_changelog_section(); + $lines = explode( "\n", $contents ); + + $sections = []; + $current_section = ''; + foreach ( $lines as $line ) { + $line = trim( $line ); + if ( substr( $line, 0, 1 ) == '=' ) { + if ( count( $sections ) >= $this->version_count ) { + break; + } + $header = trim( $line, '= ' ); + $current_section = esc_html( $header ); + $sections[ $current_section ] = []; + } elseif ( strlen( $line ) > 0 ) { + $message = trim( $line, '* ' ); + $sections[ $current_section ][] = esc_html( $message ); + } + } + return $sections; + } + + protected function extract_changelog_section() { + $contents = $this->get_readme_file_contents(); + $start = strpos( $contents, '== Changelog ==' ); + if ( $start === false ) { + return ''; + } + $start += 16; // account for the length of the header + $end = strpos( $contents, '==', $start ); + return trim( substr( $contents, $start, $end - $start ) ); + } + + protected function get_readme_file_contents() { + return file_get_contents( $this->readme_file ); + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Container.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Container.php new file mode 100644 index 000000000..56ba83353 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Container.php @@ -0,0 +1,358 @@ +doSomething(); + * + * Need the class built immediately? Build it and register it: + * + * tribe_singleton( 'tec.admin.class', new Tribe__Admin__Class() ); + * + * // some code later... + * + * tribe( 'tec.admin.class' )->doSomething(); + * + * Need a very custom way to build the class? Register a callback: + * + * tribe_singleton( 'tec.admin.class', array( Tribe__Admin__Class__Factory, 'make' ) ); + * + * // some code later... + * + * tribe( 'tec.admin.class' )->doSomething(); + * + * Or register the methods that should be called on the object after its construction: + * + * tribe_singleton( 'tec.admin.class', 'Tribe__Admin__Class', array( 'hook', 'register' ) ); + * + * // some code later... + * + * // the `hook` and `register` methods will be called on the built instance. + * tribe( 'tec.admin.class' )->doSomething(); + * + * The class will be built only once (if passing the class name or a callback function), stored + * and the same instance will be returned from that moment on. + * + * @param string $slug The human-readable and catchy name of the class. + * @param string|object|callable $class The full class name or an instance of the class + * or a callback that will return the instance of the class. + * @param array $after_build_methods An array of methods that should be called on + * the built object after the `__construct` method; the methods + * will be called only once after the singleton instance + * construction. + */ + function tribe_singleton( $slug, $class, array $after_build_methods = null ) { + Tribe__Container::init()->singleton( $slug, $class, $after_build_methods ); + } +} + +if ( ! function_exists( 'tribe_register' ) ) { + /** + * Registers a class. + * + * Each call to obtain an instance of this class made using the `tribe( $slug )` function + * will return a new instance; the instances are built just in time (if not passing an + * object instance, in that case it will work as a singleton) and on the first request. + * The container will call the class `__construct` method on the class (if not passing an object + * or a callback function) and will try to automagically resolve dependencies. + * + * Example use: + * + * tribe_register( 'tec.some', 'Tribe__Some' ); + * + * // some code later... + * + * // class is built here + * $some_one = tribe( 'tec.some' )->doSomething(); + * + * // $some_two !== $some_one + * $some_two = tribe( 'tec.some' )->doSomething(); + * + * Need the class built immediately? Build it and register it: + * + * tribe_register( 'tec.admin.class', new Tribe__Admin__Class() ); + * + * // some code later... + * + * // $some_two === $some_one + * // acts like a singleton + * $some_one = tribe( 'tec.some' )->doSomething(); + * $some_two = tribe( 'tec.some' )->doSomething(); + * + * Need a very custom way to build the class? Register a callback: + * + * tribe_register( 'tec.some', array( Tribe__Some__Factory, 'make' ) ); + * + * // some code later... + * + * // $some_two !== $some_one + * $some_one = tribe( 'tec.some' )->doSomething(); + * $some_two = tribe( 'tec.some' )->doSomething(); + * + * Or register the methods that should be called on the object after its construction: + * + * tribe_singleton( 'tec.admin.class', 'Tribe__Admin__Class', array( 'hook', 'register' ) ); + * + * // some code later... + * + * // the `hook` and `register` methods will be called on the built instance. + * tribe( 'tec.admin.class' )->doSomething(); + * + * @param string $slug The human-readable and catchy name of the class. + * @param string|object|callable $class The full class name or an instance of the class + * or a callback that will return the instance of the class. + * @param array $after_build_methods An array of methods that should be called on + * the built object after the `__construct` method; the methods + * will be called each time after the instance construction. + */ + function tribe_register( $slug, $class, array $after_build_methods = null ) { + Tribe__Container::init()->bind( $slug, $class, $after_build_methods ); + } +} + +if ( ! function_exists( 'tribe' ) ) { + /** + * Returns a ready to use instance of the requested class. + * + * Example use: + * + * tribe_singleton( 'common.main', 'Tribe__Main'); + * + * // some code later... + * + * tribe( 'common.main' )->do_something(); + * + * @param string|null $slug_or_class Either the slug of a binding previously registered using `tribe_singleton` or + * `tribe_register` or the full class name that should be automagically created or + * `null` to get the container instance itself. + * + * @return mixed|object|Tribe__Container The instance of the requested class. Please note that the cardinality of + * the class is controlled registering it as a singleton using `tribe_singleton` + * or `tribe_register`; if the `$slug_or_class` parameter is null then the + * container itself will be returned. + */ + function tribe( $slug_or_class = null ) { + $container = Tribe__Container::init(); + + return null === $slug_or_class ? $container : $container->make( $slug_or_class ); + } +} + +if ( ! function_exists( 'tribe_set_var' ) ) { + /** + * Registers a value under a slug in the container. + * + * Example use: + * + * tribe_set_var( 'tec.url', 'http://example.com' ); + * + * @param string $slug The human-readable and catchy name of the var. + * @param mixed $value The variable value. + */ + function tribe_set_var( $slug, $value ) { + $container = Tribe__Container::init(); + $container->setVar( $slug, $value ); + } +} + +if ( ! function_exists( 'tribe_get_var' ) ) { + /** + * Returns the value of a registered variable. + * + * Example use: + * + * tribe_set_var( 'tec.url', 'http://example.com' ); + * + * $url = tribe_get_var( 'tec.url' ); + * + * @param string $slug The slug of the variable registered using `tribe_set_var`. + * @param null $default The value that should be returned if the variable slug + * is not a registered one. + * + * @return mixed Either the registered value or the default value if the variable + * is not registered. + */ + function tribe_get_var( $slug, $default = null ) { + $container = Tribe__Container::init(); + + try { + $var = $container->getVar( $slug ); + } catch ( InvalidArgumentException $e ) { + return $default; + } + + return $var; + } +} + +if ( ! function_exists( 'tribe_unset_var' ) ) { + /** + * Returns the value of a registered variable. + * + * Example use: + * + * tribe_set_var( 'tec.url', 'http://example.com' ); + * + * tribe_unset_var( 'tec.url' ); + * + * @since 4.11.0 + * + * @param string $slug The slug of the variable registered using `tribe_unset_var`. + * + * @return void + */ + function tribe_unset_var( $slug ) { + $container = Tribe__Container::init(); + try { + $container->offsetUnset( $slug ); + } catch ( Exception $e ) {} + } +} + +if ( ! function_exists( 'tribe_isset_var' ) ) { + /** + * Returns the value of a registered variable. + * + * Example use: + * + * tribe_set_var( 'tec.url', 'http://example.com' ); + * + * tribe_isset_var( 'tec.url' ); + * + * @since 4.11.0 + * + * @param string $slug The slug of the variable checked using `tribe_isset_var`. + * + * @return boolean Either a the given slug exists. + */ + function tribe_isset_var( $slug ) { + $container = Tribe__Container::init(); + return $container->offsetExists( $slug ); + } +} + +if ( ! function_exists( 'tribe_register_provider' ) ) { + /** + * Registers a service provider in the container. + * + * Service providers must implement the `use ServiceProviderInterface` interface or extend + * the `ServiceProvider` class. + * + * @see ServiceProvider + * @see ServiceProviderInterface + * + * @param string $provider_class + */ + function tribe_register_provider( $provider_class ) { + $container = Tribe__Container::init(); + + if ( $provider_class === 'Tribe\Tickets\Admin\Home\Service_Provider' ) { + /** + * Prevent binding a poorly located service provider registration in ET pre 5.6.0 + * and places it after ET Main::bind_implementations(). + * + * @todo: Remove this after TEC 7.5 after enough time has passed. + */ + add_action( + 'tribe_tickets_plugin_loaded', + static function() use ( $container ) { + $container->register( Tribe\Tickets\Admin\Home\Service_Provider::class ); + } + ); + } else { + $container->register( $provider_class ); + } + } + + if ( ! function_exists( 'tribe_callback' ) ) { + /** + * Returns a lambda function suitable to use as a callback; when called the function will build the implementation + * bound to `$classOrInterface` and return the value of a call to `$method` method with the call arguments. + * + * @since 4.7 + * @since 4.6.2 Included the $argsN params + * + * @param string $slug A class or interface fully qualified name or a string slug. + * @param string $method The method that should be called on the resolved implementation with the + * specified array arguments. + * @param mixed [$argsN] (optional) Any number of arguments that will be passed down to the Callback + * + * @return callable A PHP Callable based on the Slug and Methods passed + */ + function tribe_callback( $slug, $method ) { + $container = Tribe__Container::init(); + $arguments = func_get_args(); + $is_empty = 2 === count( $arguments ); + + if ( $is_empty ) { + $callable = $container->callback( $slug, $method ); + } else { + $callback = $container->callback( 'callback', 'get' ); + $callable = call_user_func_array( $callback, $arguments ); + } + + return $callable; + } + } + + if ( ! function_exists( 'tribe_callback_return' ) ) { + /** + * Returns a tribe_callback for a very simple Return value method + * + * Example of Usage: + * + * add_filter( 'admin_title', tribe_callback_return( __( 'Ready to work.' ) ) ); + * + * @since 4.6.2 + * + * @param mixed $value The value to be returned + * + * @return callable A PHP Callable based on the Slug and Methods passed + */ + function tribe_callback_return( $value ) { + return tribe_callback( 'callback', 'return_value', $value ); + } + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Context.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Context.php new file mode 100644 index 000000000..04f753a20 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Context.php @@ -0,0 +1,1655 @@ + => [ 'read' => , 'write' => ] ]. + * The key is used to identify the property that will be accessible with the `get` and + * 'dangerously_set_global_context' method, e.g. `$context->get( 'event_display', 'list' );`. + * The locations is a list of locations the context will search, top to bottom, left to right, to find a value that's + * not empty or the default one, here's a list of supported lookup locations: + * + * request_var - look into $_GET, $_POST, $_PUT, $_DELETE, $_REQUEST. + * query_var - get the value from the main WP_Query object query vars. + * query_prop - get the value from a property of the main WP_Query object. + * tribe_option - get the value from a Tribe option. + * option - get the value from a database option. + * transient - get the value from a transient. + * constant - get the value from a constant, can also be a class constant with ::. + * global_var - get the value from a global variable + * static_prop - get the value from a class static property, format: `array( $class, $prop )`. + * prop - get the value from a tribe() container binding, format `array( $binding, $prop )`. + * static_method - get the value from a class static method. + * method - get the value calling a method on a tribe() container binding. + * func - get the value from a function or a closure. + * filter - get the value by applying a filter. + * location_func - get the value by applying a callback to the value of a location. + * + * For each location additional arguments can be specified: + * orm_arg - if `false` then the location will never produce an ORM argument, if provided the ORM arg produced bye the + * location will have this name. + * orm_transform - if provided the value of the location will be obtained by passing it as an argument to a callable. + * + * As the Context locations increase in number it would be impractical to define them inline here. + * The locations will be loaded by the `Tribe__Context::populate_locations` method from the `Context/locations.php` + * file. + * + * @var array + */ + protected static $locations = []; + + /** + * A utility static property keeping track of write locations that + * will be defined as associative arrays. + * + * @var array + */ + protected static $associative_locations = [ + self::TRANSIENT, + self::METHOD, + self::STATIC_METHOD, + self::PROP, + self::STATIC_PROP, + ]; + + /** + * Whether the static dynamic locations were set or not. + * + * @var bool + */ + protected static $did_populate_locations = false; + + /** + * A list of override locations to read and write from. + * + * This list has the same format and options as the static `$locations` property + * but allows a context instance to override, or add, read and write locations. + * + * @var array + */ + protected $override_locations = []; + + /** + * Whether the context of the current HTTP request is an AJAX one or not. + * + * @var bool + */ + protected $doing_ajax; + + /** + * Whether the context of the current HTTP request is a Cron one or not. + * + * @var bool + */ + protected $doing_cron; + + /** + * A request-based array cache to store the values fetched by the context. + * + * @var array + */ + protected $request_cache = []; + + /** + * Whether this context should use the default locations or not. + * This flag property is set to `false` when a context is obtained using + * the `set_locations` method; it will otherwise be set to `true`. + * + * @var bool + */ + protected $use_default_locations = true; + + /** + * An instance of the post state handler. + * + * @since 5.0.13 + * + * @var Post_Request_Type + */ + protected Post_Request_Type $post_state; + + /** + * Tribe__Context constructor. + * + * since 5.0.13 + * + * @param Post_Request_Type|null $post_state An instance of the post state handler. + */ + public function __construct( Post_Request_Type $post_state = null ) { + $this->post_state = $post_state ?: tribe( Post_Request_Type::class ); + } + + /** + * Whether we are currently creating a new post, a post of post type(s) or not. + * + * @since 4.7.7 + * @since 5.0.13 Extracted the logic to the `TEC\Common\Context\Post_Request_Type` class. + * + * @param null $post_type The optional post type to check. + * + * @return bool Whether we are currently creating a new post, a post of post type(s) or not. + */ + public function is_new_post( $post_type = null ) { + return $this->post_state->is_new_post( $post_type ); + } + + /** + * Whether we are currently editing a post(s), post type(s) or not. + * + * @since 4.7.7 + * @since 5.0.13 Extracted the logic to the `TEC\Common\Context\Post_Request_Type` class. + * + * @param null|array|string|int $post_or_type A post ID, post type, an array of post types or post IDs, `null` + * to just make sure we are currently editing a post. + * + * @return bool + */ + public function is_editing_post( $post_or_type = null ): bool { + return $this->post_state->is_editing_post( $post_or_type ); + } + + /** + * Helper function to indicate whether the current execution context is AJAX. + * + * This method exists to allow us test code that behaves differently depending on the execution + * context. + * + * @since 4.7.12 + * @since 4.9.5 Removed the $doing_ajax parameter. + * + * @return boolean + */ + public function doing_ajax() { + return function_exists( 'wp_doing_ajax' ) + ? wp_doing_ajax() + : defined( 'DOING_AJAX' ) && DOING_AJAX; + } + + /** + * Checks whether the context of the current HTTP request is a Cron one or not. + * + * @since 4.7.23 + * @since 4.9.5 Removed the $doing_cron parameter. + * + * @return bool Whether the context of the current HTTP request is a Cron one or not. + */ + public function doing_cron() { + return function_exists( 'wp_doing_cron' ) + ? wp_doing_cron() + : defined( 'DOING_CRON' ) && DOING_CRON; + } + + /** + * Gets a value reading it from the location(s) defined in the `Tribe__Context::$props + * + * @since 4.9.5 + * + * @param string $key The key of the variable to fetch. + * @param mixed|null $default The default value to return if not found. + * @param bool $force Whether to force the re-fetch of the value from the context or + * not; defaults to `false`. + * + * @return mixed The value from the first location that can provide it or the default + * value if not found. + */ + public function get( $key, $default = null, $force = false ) { + /** + * Filters the value of a context variable skipping all of its logic. + * + * @since 4.9.5 + * + * @param mixed $value The value for the key before it's fetched from the context. + * @param string $key The key of the value to fetch from the context. + * @param mixed $default The default value that should be returned if the value is + * not set in the context. + * @param bool $force Whether to force the re-fetch of the value from the context or + * not; defaults to `false`. + */ + $value = apply_filters( "tribe_context_pre_{$key}", null, $key, $default, $force ); + if ( null !== $value ) { + return $value; + } + + $value = $default; + $locations = $this->get_locations(); + $found = false; + + if ( ! $force && isset( $this->request_cache[ $key ] ) ) { + $value = $this->request_cache[ $key ]; + } elseif ( ! empty( $locations[ $key ]['read'] ) ) { + foreach ( $locations[ $key ]['read'] as $location => $keys ) { + $the_value = $this->$location( (array) $keys, $default ); + + if ( $default !== $the_value && static::NOT_FOUND !== $the_value ) { + $found = true; + $value = $the_value; + break; + } + } + } + + /** + * Filters the value fetched from the context for a key. + * + * Useful for testing and local override. + * + * @since 4.9.5 + * + * @param mixed $value The value as fetched from the context. + */ + $value = apply_filters( "tribe_context_{$key}", $value ); + + // Only cache if the value was found. + if ( $found ) { + $this->request_cache[ $key ] = $value; + } + + return $value; + } + + /** + * Alters the context. + * + * Due to its immutable nature setting values on the context will NOT modify the + * context but return a modified clone. + * If you need to modify the global context update the location(s) it should read from + * and call the `refresh` method. + * Example: `$widget_context = tribe_context()->alter( $widget_args );`. + * + * @since 4.9.5 + * + * @param array $values An associative array of key-value pairs to modify the context. + * + * @return \Tribe__Context A clone, with modified, values, of the context the method was called on. + */ + public function alter( array $values ) { + $clone = clone $this; + + $clone->request_cache = array_merge( $clone->request_cache, $values ); + + return $clone; + } + + /** + * Clears the context cache forcing a re-fetch of the variables from the context. + * + * @since 4.9.5 + * + * @param string $key An optional specific key to refresh, if passed only this key + * will be refreshed. + */ + public function refresh( $key = null ) { + if ( null !== $key ) { + unset( $this->request_cache[ $key ] ); + } else { + $this->request_cache = []; + } + } + + /** + * Returns the read and write locations set on the context. + * + * @since 4.9.5 + * + * @return array An array of read and write location in the shape of the `Tribe__Context::$locations` one, + * `[ => [ 'read' => , 'write' => ] ]`. + */ + public function get_locations() { + $this->populate_locations(); + + $locations = $this->use_default_locations + ? array_merge( self::$locations, $this->override_locations ) + : $this->override_locations; + + if ( $this->use_default_locations ) { + /** + * Filters the locations registered in the Context. + * + * @since 4.10.2 + * + * @param $locations array An array of read and write location in the shape of the `Tribe__Context::$locations` one, + * `[ => [ 'read' => , 'write' => ] ]`. + * @param $context Tribe__Context Current instance of the context. + */ + $locations = apply_filters( 'tribe_context_locations', $locations, $this ); + } + + return $locations; + } + + /** + * Reads the value from one or more $_REQUEST vars. + * + * @since 4.9.5 + * + * @param array $request_vars The list of request vars to lookup, in order. + * @param mixed $default The default value to return. + * + * @return mixed The first valid value found or the default value. + */ + protected function request_var( array $request_vars, $default ) { + $value = $default; + + foreach ( $request_vars as $request_var ) { + $the_value = tribe_get_request_var( $request_var, self::NOT_FOUND ); + if ( $the_value !== self::NOT_FOUND ) { + $value = $the_value; + break; + } + } + + return $value; + } + + /** + * Reads the value from one or more global WP_Query object query variables. + * + * @since 4.9.5 + * + * @param array $query_vars The list of query vars to look up, in order. + * @param mixed $default The default value to return. + * + * @return mixed The first valid value found or the default value. + */ + protected function query_var( array $query_vars, $default ) { + $value = $default; + + global $wp_query; + + if ( ! $wp_query instanceof \WP_Query ) { + return $value; + } + + foreach ( $query_vars as $query_var ) { + $the_value = $wp_query->get( $query_var, self::NOT_FOUND ); + if ( $the_value !== self::NOT_FOUND ) { + $value = $the_value; + break; + } + } + + return $value; + } + + /** + * Reads the value from one or more global WP_Query object properties. + * + * @since 4.9.5 + * + * @param array $query_props The list of properties to look up, in order. + * @param mixed $default The default value to return. + * + * @return mixed The first valid value found or the default value. + */ + protected function query_prop( array $query_props, $default ) { + $value = $default; + + global $wp_query; + foreach ( $query_props as $query_prop ) { + $the_value = isset( $wp_query->{$query_prop} ) ? $wp_query->{$query_prop} : self::NOT_FOUND; + if ( $the_value !== self::NOT_FOUND ) { + $value = $the_value; + break; + } + } + + return $value; + } + + /** + * Reads the value from one more more `tribe_option`s. + * + * @since 4.9.5 + * + * @param array $tribe_options The list of `tribe_option`s to lookup, in order. + * @param mixed $default The default value to return. + * + * @return mixed The first valid value found or the default value. + */ + protected function tribe_option( array $tribe_options, $default ) { + $value = $default; + + foreach ( $tribe_options as $option_name ) { + $the_value = tribe_get_option( $option_name, self::NOT_FOUND ); + if ( $the_value !== self::NOT_FOUND ) { + $value = $the_value; + break; + } + } + + return $value; + } + + /** + * Reads the value from one or more options. + * + * @since 4.9.5 + * + * @param array $options The list of options to lookup, in order. + * @param mixed $default The default value to return. + * + * @return mixed The first valid value found or the default value. + */ + protected function option( array $options, $default ) { + $value = $default; + + foreach ( $options as $option_name ) { + $the_value = get_option( $option_name, self::NOT_FOUND ); + if ( $the_value !== self::NOT_FOUND ) { + $value = $the_value; + break; + } + } + + return $value; + } + + /** + * Reads the value from one or more transients. + * + * @since 4.9.5 + * + * @param array $transients The list of transients to lookup, in order. + * @param mixed $default The default value to return. + * + * @return mixed The first valid value found or the default value. + */ + protected function transient( array $transients, $default ) { + $value = $default; + + foreach ( $transients as $transient ) { + $the_value = get_transient( $transient ); + if ( false !== $the_value ) { + $value = $the_value; + /* + * This will fail when the value is actually `false`. + */ + break; + } + } + + return $value; + } + + /** + * Reads the value from one or more constants. + * + * @since 4.9.5 + * + * @param array $constants The list of constants to lookup, in order. + * @param mixed $default The default value to return. + * + * @return mixed The first valid value found or the default value. + */ + protected function constant( array $constants, $default ) { + $value = $default; + + foreach ( $constants as $constant ) { + $the_value = defined( $constant ) ? constant( $constant ) : self::NOT_FOUND; + if ( $the_value !== self::NOT_FOUND ) { + $value = $the_value; + break; + } + } + + return $value; + } + + /** + * Reads the value from one or more global variable. + * + * @since 4.9.5 + * + * @param array $global_vars The list of global variables to look up, in order. + * @param mixed $default The default value to return. + * + * @return mixed The first valid value found or the default value. + */ + protected function global_var( array $global_vars, $default ) { + $value = $default; + + foreach ( $global_vars as $var ) { + $the_value = isset( $GLOBALS[ $var ] ) ? $GLOBALS[ $var ] : self::NOT_FOUND; + if ( $the_value !== self::NOT_FOUND ) { + $value = $the_value; + break; + } + } + + return $value; + } + + /** + * Reads the value from one or more class static properties. + * + * @since 4.9.5 + * + * @param array $classes_and_props An associative array in the shape [ => ]. + * @param mixed $default The default value to return. + * + * @return mixed The first valid value found or the default value. + */ + protected function static_prop( array $classes_and_props, $default ) { + $value = $default; + + foreach ( $classes_and_props as $class => $prop ) { + if ( class_exists( $class ) ) { + // PHP 5.2 compat, on PHP 5.3+ $class::$$prop + $vars = get_class_vars( $class ); + $the_value = isset( $vars[ $prop ] ) ? $vars[ $prop ] : self::NOT_FOUND; + + if ( $the_value !== self::NOT_FOUND ) { + $value = $the_value; + break; + } + } + } + + return $value; + } + + /** + * Reads the value from one or more properties of implementations bound in the `tribe()` container. + * + * @since 4.9.5 + * + * @param array $bindings_and_props An associative array in the shape [ => ]. + * @param mixed $default The default value to return. + * + * @return mixed The first valid value found or the default value. + */ + protected function prop( array $bindings_and_props, $default ) { + $value = $default; + + foreach ( $bindings_and_props as $binding => $prop ) { + $the_value = tribe()->offsetExists( $binding ) && property_exists( tribe( $binding ), $prop ) + ? tribe( $binding )->{$prop} + : self::NOT_FOUND; + + if ( $the_value !== self::NOT_FOUND ) { + $value = $the_value; + break; + } + } + + return $value; + } + + /** + * Reads the values from one or more static class methods. + * + * @since 4.9.5 + * + * @param array $classes_and_methods An associative array in the shape [ => ]. + * @param mixed $default The default value to return. + * + * @return mixed The first value that's not equal to the default one, the default value + * otherwise. + */ + protected function static_method( array $classes_and_methods, $default ) { + $value = $default; + + foreach ( $classes_and_methods as $class => $method ) { + $the_value = class_exists( $class ) && method_exists( $class, $method ) + ? call_user_func( [ $class, $method ] ) + : self::NOT_FOUND; + + if ( $the_value !== self::NOT_FOUND ) { + $value = $the_value; + break; + } + } + + return $value; + } + + /** + * Reads the value from one or more methods called on implementations bound in the `tribe()` container. + * + * @since 4.9.5 + * + * @param array $bindings_and_methods An associative array in the shape [ => ]. + * @param mixed $default The default value to return. + * + * @return mixed The first value that's not equal to the default one, the default value + * otherwise. + */ + protected function method( array $bindings_and_methods, $default ) { + $value = $default; + $the_value = self::NOT_FOUND; + + foreach ( $bindings_and_methods as $binding => $method ) { + if ( tribe()->offsetExists( $binding ) ) { + $implementation = tribe( $binding ); + if ( method_exists( $implementation, $method ) ) { + $the_value = $implementation->$method(); + } + } + + if ( $the_value !== self::NOT_FOUND ) { + $value = $the_value; + break; + } + } + + return $value; + } + + /** + * Reads the value from one or more functions until one returns a value that's not the default one. + * + * @since 4.9.5 + * + * @param array $functions An array of functions to call, in order. + * @param mixed $default The default value to return. + * + * @return mixed The first value that's not equal to the default one, the default value + * otherwise. + */ + protected function func( array $functions, $default ) { + $value = $default; + $the_value = self::NOT_FOUND; + + foreach ( $functions as $function ) { + if ( is_callable( $function ) || function_exists( $function ) ) { + $the_value = $function(); + } + + if ( $the_value !== self::NOT_FOUND ) { + $value = $the_value; + break; + } + } + + return $value; + } + + /** + * Modifies the global context using the defined write locations to persist the altered values. + * + * Please keep in mind this will set the the global context for the whole request and, when the + * write location is an option, to the database. + * With great power comes great responsibility: think a lot before using this. + * + * @param array|null $fields An optional whitelist or blacklist of fields to write + * depending on the value of the `$whitelist` parameter; + * defaults to writing all available fields. + * @param bool $whitelist Whether the list of fields provided in the `$fields` + * parameter should be treated as a whitelist (`true`) or + * blacklist (`false`). + * + * @since 4.9.5 + */ + public function dangerously_set_global_context( array $fields = null, $whitelist = true ) { + $locations = $this->get_locations(); + + if ( null !== $fields ) { + $locations = $whitelist + ? array_intersect_key( $locations, array_combine( $fields, $fields ) ) + : array_diff_key( $locations, array_combine( $fields, $fields ) ); + } + + /** + * Here we intersect with the request cache to only write values we've actually read + * or modified. If none of the two happened then there's no need to write anything. + */ + foreach ( array_intersect_key( $this->request_cache, $locations ) as $key => $value ) { + if ( ! isset( $locations[ $key ]['write'] ) ) { + continue; + } + + foreach ( (array) $locations[ $key ]['write'] as $location => $targets ) { + $targets = (array) $targets; + $write_func = 'write_' . $location; + + foreach ( $targets as $arg_1 => $arg_2 ) { + if ( self::FUNC === $location && is_array( $arg_2 ) && is_callable( $arg_2 ) ) { + // Handles write functions specified as an array. + $location_args = [ $arg_2 ]; + } else { + $location_args = in_array( $location, self::$associative_locations, true ) + ? [ $arg_1, $arg_2 ] + : (array) $arg_2; + } + + $args = array_merge( $location_args, [ $value ] ); + + call_user_func_array( [ $this, $write_func ], $args ); + } + } + } + } + + /** + * Writes an altered context value to a request var. + * + * @since 4.9.5 + * + * @param string $request_var The request var to write. + * @param mixed $value The value to set on the request var. + */ + protected function write_request_var( $request_var, $value ) { + if ( isset( $_REQUEST ) ) { + $_REQUEST[ $request_var ] = $value; + } + if ( isset( $_GET ) ) { + $_GET[ $request_var ] = $value; + } + if ( isset( $_POST ) ) { + $_POST[ $request_var ] = $value; + } + } + + /** + * Writes an altered context value to a global WP_Query object properties. + * + * @since 4.9.5 + * + * @param string $query_prop The global WP_Query object property to write. + * @param mixed $value The value to set on the query property. + */ + protected function write_query_prop( $query_prop, $value ) { + global $wp_query; + + if ( ! $wp_query instanceof WP_Query ) { + return; + } + + $wp_query->{$query_prop} = $value; + } + + /** + * Writes an altered context value to a global WP_Query object query var. + * + * @since 4.9.5 + * + * @param string $query_var The global WP_Query query var to write. + * @param mixed $value The value to set on the query var. + */ + protected function write_query_var( $query_var, $value ) { + global $wp_query; + + if ( ! $wp_query instanceof WP_Query ) { + return; + } + + $wp_query->set( $query_var, $value ); + } + + /** + * Writes an altered context value to a `tribe_option`. + * + * @since 4.9.5 + * + * @param string $tribe_option The `tribe_option` to write. + * @param mixed $value The value to set on the `tribe_option`. + */ + protected function write_tribe_option( $tribe_option, $value ) { + tribe_update_option( $tribe_option, $value ); + } + + /** + * Writes an altered context value to an option. + * + * @since 4.9.5 + * + * @param string $option_name The option to write. + * @param mixed $value The value to set on the option. + */ + protected function write_option( $option_name, $value ) { + update_option( $option_name, $value ); + } + + /** + * Writes an altered context value to a transient. + * + * @since 4.9.5 + * + * @param string $transient The transient to write. + * @param int $expiration The transient expiration time, in seconds. + * @param mixed $value The value to set on the transient. + */ + protected function write_transient( $transient, $expiration, $value ) { + set_transient( $transient, $value, $expiration ); + } + + /** + * Writes an altered context value to a constant. + * + * @since 4.9.5 + * + * @param string $constant The constant to define. + * @param mixed $value The value to set on the constant. + */ + protected function write_constant( $constant, $value ) { + if ( defined( $constant ) ) { + return; + } + define( $constant, $value ); + } + + /** + * Writes an altered context value to a global var. + * + * @since 4.9.5 + * + * @param string $global_var The global var to set. + * @param mixed $value The value to set on the global_var. + */ + protected function write_global_var( $global_var, $value ) { + $GLOBALS[ $global_var ] = $value; + } + + /** + * Writes an altered context value setting a public static property on a class. + * + * @since 4.9.5 + * + * @param string $class The class to set the static public property on. + * @param string $prop The static public property to set. + * @param mixed $value The value to set on the property. + */ + protected function write_static_prop( $class, $prop, $value ) { + if ( ! ( class_exists( $class ) && property_exists( $class, $prop ) ) ) { + return; + } + + $class::$$prop = $value; + } + + /** + * Writes an altered context value setting a public property on a `tribe()` binding. + * + * @since 4.9.5 + * + * @param string $binding The container binding to set the public property on. + * @param string $prop The public property to set. + * @param mixed $value The value to set on the property. + */ + protected function write_prop( $binding, $prop, $value ) { + if ( ! tribe()->offsetExists( $binding ) ) { + return; + } + + $implementation = tribe( $binding ); + + if ( ! property_exists( $implementation, $prop ) ) { + return; + } + + $implementation->{$prop} = $value; + } + + /** + * Writes an altered context value calling a public static method on a class. + * + * @since 4.9.5 + * + * @param string $class The class to call the public static method on. + * @param string $method The static method to call. + * @param mixed $value The value to pass to the public static method. + */ + protected function write_static_method( $class, $method, $value ) { + if ( ! class_exists( $class ) ) { + return; + } + call_user_func( [ $class, $method ], $value ); + } + + /** + * Writes an altered context value calling a public method on a `tribe()` binding. + * + * @since 4.9.5 + * + * @param string $binding The `tribe()` container binding to call the public method on. + * @param string $method The method to call. + * @param mixed $value The value to pass to the public method. + */ + protected function write_method( $binding, $method, $value ) { + if ( ! tribe()->offsetExists( $binding ) ) { + return; + } + call_user_func( [ tribe( $binding ), $method ], $value ); + } + + /** + * Writes an altered context value calling a function or closure. + * + * @since 4.9.5 + * + * @param callable $func function, closure or callable to call. + * @param mixed $value The value to pass to the callable. + */ + protected function write_func( $func, $value ) { + if ( ! is_callable( $func ) ) { + return; + } + call_user_func( $func, $value ); + } + + /** + * Adds/replaces read and write locations to a context. + * + * Locations are merged with an `array_merge` call. To refine the locations get them first with the + * `get_locations` method. + * + * @since 4.9.5 + * + * @param array $locations An array of read and write locations to add to the context. + * The array should have the same shape as the static `$locations` + * one: `[ => [ 'read' => , 'write' => ] ]`. + * + * + * @return \Tribe__Context A clone of the current context with the additional read and + * write locations added. + */ + public function add_locations( array $locations ) { + $clone = clone $this; + $clone->override_locations = array_merge( $clone->override_locations, $locations ); + + return $clone; + } + + /** + * Sets, replacing them, the locations used by this context. + * + * + * @since 4.9.5 + * + * @param array $locations An array of locations to replace the current ones. + * @param bool $use_default_locations Whether the context should use the default + * locations defined in the static `$locations` + * property or not. + * + * @return \Tribe__Context A clone of the current context with modified locations. + */ + public function set_locations( array $locations, $use_default_locations = true ) { + $clone = clone $this; + $clone->override_locations = $locations; + $clone->use_default_locations = (bool) $use_default_locations; + + return $clone; + } + + /** + * Returns an array representation of the context. + * + * @since 4.9.5 + * + * @return array An associative array of the context keys and values. + */ + public function to_array( ) { + $locations = array_keys( array_merge( $this->get_locations(), $this->request_cache ) ); + $dump = []; + + foreach ( $locations as $location ) { + $the_value = $this->get( $location, self::NOT_FOUND ); + + if ( self::NOT_FOUND === $the_value ) { + continue; + } + + $dump[ $location ] = $the_value; + } + + return $dump; + } + + /** + * Returns the current context state in a format suitable to hydrate a Redux-like + * store on the front-end. + * + * This method is a filtered wrapper around the the `Tribe__Context::to_array` method to allow the + * customization of the format when producing a store-compatible state. + * + * @param array|null $fields An optional whitelist or blacklist of fields to include + * depending on the value of the `$whitelist` parameter; + * defaults to returning all available fields. + * @param bool $whitelist Whether the list of fields provided in the `$fields` + * parameter should be treated as a whitelist (`true`) or + * blacklist (`false`). + * + * @since 4.9.5 + * + * @return array + */ + public function get_state( array $fields = null, $whitelist = true ) { + $state = $this->to_array(); + $is_global_context = tribe_context() === $this; + + if ( null !== $fields ) { + $state = $whitelist + ? array_intersect_key( $state, array_combine( $fields, $fields ) ) + : array_diff_key( $state, array_combine( $fields, $fields ) ); + } + + /** + * Filters the Redux store compatible state produced from the current context. + * + * @since 4.9.5 + * + * @param array $state The Redux store compatible state produced from the current context. + * @param bool $is_global_context Whether the context producing the state is the global one + * or a modified clone of it. + * @param Tribe__Context The context object producing the state. + */ + $state = apply_filters( 'tribe_context_state', $state, $is_global_context, $this ); + + if ( $is_global_context ) { + /** + * Filters the Redux store compatible state produced from the global context. + * + * While the `tribe_context_state` filter will apply to all contexts producing a + * state this filter will only apply to the global context. + * + * @since 4.9.5 + * + * @param array $state The Redux store compatible state produced from the global context. + * @param Tribe__Context The global context object producing the state. + */ + $state = apply_filters( 'tribe_global_context_state', $state, $this ); + } + + return $state; + } + + /** + * Returns an array of ORM arguments generated from the current context values. + * + * @since 4.9.5 + * + * @param array|null $fields An optional whitelist or blacklist of fields to include + * depending on the value of the `$whitelist` parameter; + * defaults to returning all available fields. + * @param bool $whitelist Whether the list of fields provided in the `$fields` + * parameter should be treated as a whitelist (`true`) or + * blacklist (`false`). + * + * @return array A map of ORM fields produced from the context current values. + */ + public function get_orm_args( array $fields = null, $whitelist = true ) { + $locations = $this->get_locations(); + $dump = $this->to_array(); + $orm_args = []; + $is_global_context = tribe_context() === $this; + + foreach ( $dump as $key => $value ) { + $alias = isset( $locations[ $key ]['orm_arg'] ) + ? $locations[ $key ]['orm_arg'] + : $key; + + if ( false === $alias ) { + // Do not provide the variable as an ORM arg. + continue; + } + + if ( isset( $locations[ $key ]['orm_transform'] ) ) { + $value = call_user_func( $locations[ $key ]['orm_transform'], $value ); + } + + $orm_args[ $alias ] = $value; + } + + if ( null !== $fields ) { + /* + * Only keep wanted fields, the filtering is done on the resolved aliases, + * from the perspective of the client code that might ignore the source keys. + */ + $orm_args = $whitelist + ? array_intersect_key( $orm_args, array_combine( $fields, $fields ) ) + : array_diff_key( $orm_args, array_combine( $fields, $fields ) ); + } + + /** + * Filters the ORM arguments produced from the current context. + * + * @since 4.9.5 + * + * @param array $orm_args The ORM args produced from the current context. + * @param bool $is_global_context Whether the context producing the ORM args is the global one + * or a modified clone of it. + * @param Tribe__Context The context object producing the ORM args. + */ + $orm_args = apply_filters( 'tribe_context_orm_args', $orm_args, $is_global_context, $this ); + + if ( $is_global_context ) { + /** + * Filters the ORM arguments produced from the global context. + * + * While the `tribe_context_orm_args` filter will apply to all contexts producing ORM + * args this filter will only apply to the global context. + * + * @since 4.9.5 + * + * @param array $orm_args The ORM args produced from the global context. + * @param Tribe__Context The global context object producing the ORM args. + */ + $orm_args = apply_filters( 'tribe_global_context_orm_args', $orm_args, $this ); + } + + return $orm_args; + } + + /** + * Sets some locations that can only be set at runtime. + * + * Using a flag locations are added only once per request. + * + * @since 4.9.8 + */ + protected function populate_locations() { + if ( static::$did_populate_locations ) { + return; + } + + // To improve the class readability, and as a small optimization, locations are loaded from a file. + static::$locations = include __DIR__ . '/Context/locations.php'; + + /** + * Filters the locations registered in the Context. + * + * @since 4.9.8 + * + * @param array $locations An array of locations registered on the Context object. + */ + static::$locations = apply_filters( 'tribe_context_locations', static::$locations, $this ); + + static::$did_populate_locations = true; + } + + /** + * Just dont... + * Unless you very specifically know what you are doing **DO NOT USE THIS METHOD**! + * + * Please keep in mind this will set force the context to repopulate all locations for the whole request, expensive + * and very dangerous overall since it could affect all this things we hold dear in the request. + * + * With great power comes great responsibility: think a lot before using this. + * + * @since 4.13.0 + */ + public function dangerously_repopulate_locations() { + static::$did_populate_locations = false; + $this->populate_locations(); + } + + /** + * Reads (gets) the value applying one or more filters. + * + * @since 4.9.8 + * + * @param array $filters The list of filters to apply, in order. + * @param mixed $default The default value to return. + * + * @return mixed The first valid value found or the default value. + */ + public function filter( array $filters, $default ) { + foreach ( $filters as $filter ) { + $the_value = apply_filters( $filter, $default ); + if ( $the_value !== $default ) { + return $the_value; + } + } + + return $default; + } + + /** + * Reads (gets) the value reading it from a query var parsed from the global `$wp` object. + * + * @since 4.9.8 + * + * @param array $vars The list of variables to read, in order. + * @param mixed $default The default value to return if no variable was parsed. + * + * @return mixed The first valid value found or the default value. + */ + public function wp_parsed( array $vars, $default ) { + /** @var WP $wp */ + global $wp; + + if ( ! $wp instanceof WP || empty($wp->query_vars) ) { + return $default; + } + + return Arr::get_first_set( (array) $wp->query_vars, $vars, $default ); + } + + /** + * Reads (gets) the value reading it from a query var parsed from the query matched by the global `$wp` object. + * + * @since 4.9.8 + * + * @param array $vars The list of variables to read, in order. + * @param mixed $default The default value to return if no variable was parsed. + * + * @return mixed The first valid value found or the default value. + */ + public function wp_matched_query( array $vars, $default ) { + /** @var WP $wp */ + global $wp; + + if ( ! $wp instanceof WP || empty( $wp->matched_query ) ) { + return $default; + } + + parse_str( $wp->matched_query, $query_vars ); + + return Arr::get_first_set( (array) $query_vars, $vars, $default ); + } + + /** + * Maps an input array to the corresponding read locations. + * + * The resulting array can be used as input for the `alter_values` method. + * The main use of this method is to leverage the Context knowledge of the read locations, and their types, to + * "translate" an array of values to an array of valid read sources. As an example this is useful to "translate" + * the locations to an array of query vars: + * $input = [ 'event_display' => 'some-view', 'event_date' => '2018-01-03' ]; + * $query_args = tribe_context()->map_to_read( $input, Tribe__Context::REQUEST_VAR ); + * $url = add_query_arg( $query_args, home_url() ); + * + * @since 4.9.11 + * + * @param array $input An associative array of values in the shape `[ => ]`; + * where `location` is the name of the location registered in the Context + * locations. + * @param string|array|null $types A white-list of read location types to include in the mapped output; + * `null` + * means all types are allowed. + * @param bool $passthru Whether to pass unknown locations in the output or not; if `false` then + * any input key that's not a context location will not appear in the output; + * defaults to `false` to remove unknown locations from the output. + * + * @return array An associative array in the shape `[ => ]`. Since some read + * locations could have multiple sources the number of elements in this array will likely NOT be the + * same as the number of elements in the input array. When a read location as more than 1 source then + * the value will be duplicated, in the output array, to both sources. + */ + public function map_to_read( array $input, $types = null, $passthru = false ) { + $mapped = []; + $processed = []; + $types = null !== $types ? (array) $types : null; + + $locations = $this->get_locations(); + + // Take the current read locations + foreach ( $locations as $key => $location ) { + if ( ! isset( $location['read'], $input[ $key ] ) ) { + continue; + } + + $processed[] = $key; + + foreach ( $location['read'] as $type => $name ) { + if ( null !== $types && ! in_array( $type, $types, true ) ) { + continue; + } + + foreach ( (array) $name as $destination ) { + $mapped[ $destination ] = $input[ $key ]; + } + } + } + + if ( $passthru ) { + $mapped = array_merge( + $mapped, + array_diff_key( $input, array_keys( $locations ), array_combine( $processed, $processed ) ) + ); + } + + ksort( $mapped ); + + return $mapped; + } + + /** + * Translates sub-locations to their respective location key. + * + * This method leverages the inherent knowledge of aliases stored in the Context locations to "translate" a + * sub-location to its location key. + * E.g. assume the `car` location is `read` from the [ 'carriage', 'vehicle', 'transport_mean' ] query var; calling + * `$context->populate_aliases( [ 'vehicle' => 'hyunday' ], 'read', Context::QUERY_VAR )` would yield + * `[ 'car' => 'hyunday' ]`. + * + * @since 4.9.12 + * + * @param array $values An associative array of value to use as "masters" to populate the aliases. + * @param string $type The type of Context location to use, e.g. `Tribe__Context::QUERY_VAR`. + * @param string $direction The direction to use for the location, one of `read` or `write`. + * + * @return array The original array, merged with the populated values. + */ + public function translate_sub_locations( array $values, $type, $direction = 'read' ) { + if ( ! in_array( $direction, [ 'read', 'write' ], true ) ) { + throw new \InvalidArgumentException( + "Direction must be one of `read` or `write`; `{$direction}` is not valid." + ); + } + + $filled = []; + $locations = $this->get_locations(); + $matching_locations = array_filter( $locations, static function ( $location ) use ( $type, $direction ) { + return isset( $location[ $direction ][ $type ] ); + } ); + + foreach ( $matching_locations as $key => $location ) { + $entry = (array)$location[ $direction ][ $type ]; + $found = array_intersect( array_keys( $values ), array_merge( $entry, [ $key ] ) ); + if ( $found ) { + $filled[ $key ] = $values[ reset( $found ) ]; + } + } + + return $filled; + } + + /** + * Convenience method to get and check if a location has a truthy value or not. + * + * @since 4.9.18 + * + * @param string $flag_key The location to check. + * @param bool $default The default value to return if the location is not set. + * + * @return bool Whether the location has a truthy value or not. + */ + public function is( $flag_key, $default = false ) { + $val = $this->get( $flag_key, $default ); + + return ! empty( $val ) || tribe_is_truthy( $val ); + } + + /** + * Reads the value from one callback, passing it the value of another Context location. + * + * @since 4.9.18 + * + * @param array $location_and_callback An array of two elements: the location key and the callback to call on the + * location value. The callback will receive the location value as argument. + * + * @return mixed The return value of the callback, called on the location value. + */ + public function location_func( array $location_and_callback ) { + list( $location, $callback ) = $location_and_callback; + + return $callback( $this->get( $location ) ); + } + + /** + * Checks whether the current request is a REST API one or not. + * + * @since 4.9.20 + * + * @return bool Whether the current request is a REST API one or not. + */ + public function doing_rest() { + return defined( 'REST_REQUEST' ) && REST_REQUEST; + } + + /** + * Reads the value from one or more global WP_Query object methods. + * + * @since 4.9.20 + * + * @param array $query_vars The list of query methods to call, in order. + * @param mixed $default The default value to return if no method was defined on the global `WP_Query` object. + * + * @return mixed The first valid value found or the default value. + */ + public function query_method( $methods, $default ) { + global $wp_query; + $found = $default; + + foreach ( $methods as $method ) { + $this_value = $wp_query instanceof WP_Query && method_exists( $wp_query, $method ) + ? call_user_func( [ $wp_query, $method ] ) + : static::NOT_FOUND; + + if ( static::NOT_FOUND !== $this_value ) { + return $this_value; + } + } + + return $found; + } + + /** + * Whether the current request is for a PHP-rendered initial state or not. + * + * This method is a shortcut to make sure we're not doing an AJAX, REST or Cron request. + * + * @since 4.9.20 + * + * @return bool Whether the current request is for a PHP-rendered initial state or not. + */ + public function doing_php_initial_state() { + return ! $this->doing_rest() && ! $this->doing_ajax() && ! $this->doing_cron(); + } + + /** + * Returns the first key, if there are many, that will be used to read a location. + * + * The type ar + * + * @since 4.9.20 + * + * @param string $location The location to get the read key for. + * @param string|null $type The type of read location to return the key for; default to `static::REQUEST_VAR`. + * + * @return string Either the first key for the type of read location, or the input location if not found. + */ + public function get_read_key_for( $location, $type = null ) { + $type = $type ?: static::REQUEST_VAR; + $locations = $this->get_locations(); + if ( isset( $locations[ $location ]['read'][ $type ] ) ) { + $keys = (array) $locations[ $location ]['read'][ $type ]; + return reset( $keys ); + } + + return $location; + } + + /** + * Safely set the value of a group of locations. + * + * This method can only augment the context, without altering it; it can only add new values. + * + * @since 4.10.2 + * + * @param array|string $values The values to set, if not already set or the key of the value to set, requires + * the `$value` to be passed. + * @param mixed|null $value The value to set for the key, this parameter will be ignored if the `$values_or_key` + * parameter is not a string. + */ + public function safe_set( $values_or_key, $value = null ) { + $values = func_num_args() === 2 + ? [ $values_or_key => $value ] + : $values_or_key; + + foreach ( $values as $key => $val ) { + if ( static::NOT_FOUND !== $this->get( $key, static::NOT_FOUND ) ) { + continue; + } + $this->request_cache[ $key ] = $val; + } + } + + /** + * Whether the current request is one to edit a list of the specified post types or not. + * + * The admin edit screen for a post type is the one that lists all the posts of that typ, + * it has the URL `/wp-admin/edit.php?post_type=`. + * + * @since 5.0.13 + * @since 5.0.13 Extracted the logic to the `TEC\Common\Context\Post_Request_Type` class. + * + * @param string|array $post_type The post type or post types to check. + * + * @return bool Whether the current request is one to edit a list of the specified post types or not. + */ + public function is_editing_posts_list( $post_type ): bool { + return $this->post_state->is_editing_post_list( $post_type ); + } + + /** + * Whether the current request is one to quick edit a single post of the specified post type or not. + * + * @since 5.0.13 + * + * @param string|array $post_type The post type or post types to check. + * + * @return bool Whether the current request is one to quick edit a single post of the specified post type or not. + */ + public function is_inline_editing_post( $post_type ): bool { + return $this->post_state->is_inline_editing_post( $post_type ); + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Context/locations.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Context/locations.php new file mode 100644 index 000000000..6a433dd97 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Context/locations.php @@ -0,0 +1,163 @@ + [ + 'read' => [ + Tribe__Context::FUNC => static function () { + return get_the_ID(); + } + ], + ], + 'permalink_structure' => [ + 'read' => [ + Tribe__Context::OPTION => [ 'permalink_structure' ], + ], + ], + 'plain_permalink' => [ + 'read' => [ + Tribe__Context::LOCATION_FUNC => [ + 'permalink_structure', + static function( $struct ){ + return empty( $struct ); + }, + ], + ], + ], + 'posts_per_page' => [ + 'read' => [ + Tribe__Context::REQUEST_VAR => 'posts_per_page', + Tribe__Context::OPTION => 'posts_per_page', + Tribe__Context::TRIBE_OPTION => [ 'posts_per_page', 'postsPerPage' ], + ], + 'write' => [ + Tribe__Context::REQUEST_VAR => 'posts_per_page', + ], + ], + 'is_main_query' => [ + 'read' => [ + Tribe__Context::FUNC => static function () { + global $wp_query; + + if ( empty( $wp_query ) ) { + return false; + } + + if ( ! $wp_query instanceof WP_Query ) { + return false; + } + + return $wp_query->is_main_query(); + }, + ], + 'write' => [ + Tribe__Context::FUNC => static function () { + global $wp_query, $wp_the_query; + $wp_the_query = $wp_query; + }, + ], + ], + 'paged' => [ + 'read' => [ + Tribe__Context::REQUEST_VAR => [ 'paged', 'page' ], + Tribe__Context::QUERY_VAR => [ 'paged', 'page' ], + ], + 'write' => [ + Tribe__Context::REQUEST_VAR => 'paged', + Tribe__Context::QUERY_VAR => 'paged', + ], + ], + 'page' => [ + 'read' => [ + Tribe__Context::REQUEST_VAR => [ 'page', 'paged' ], + Tribe__Context::QUERY_VAR => [ 'page', 'paged' ], + ], + 'write' => [ + Tribe__Context::REQUEST_VAR => 'page', + Tribe__Context::QUERY_VAR => 'page', + ], + ], + 'name' => [ + 'read' => [ + Tribe__Context::REQUEST_VAR => [ 'name', 'post_name' ], + Tribe__Context::WP_PARSED => [ 'name', 'post_name' ], + Tribe__Context::QUERY_VAR => [ 'name', 'post_name' ], + ], + 'write' => [ + Tribe__Context::REQUEST_VAR => [ 'name', 'post_name' ], + Tribe__Context::QUERY_VAR => [ 'name', 'post_name' ], + ], + ], + 'post_type' => [ + 'read' => [ + Tribe__Context::FUNC => static function() { + $post_type_objs = get_post_types( + [ + 'public' => true, + '_builtin' => false, + ], + 'objects' + ); + + foreach ( $post_type_objs as $post_type ) { + if ( empty( $post_type->query_var ) ) { + continue; + } + + $url_value = tribe_get_request_var( $post_type->query_var, false ); + if ( empty( $url_value ) ) { + continue; + } + + return $post_type->name; + } + + return Tribe__Context::NOT_FOUND; + }, + Tribe__Context::QUERY_PROP => 'post_type', + Tribe__Context::QUERY_VAR => 'post_type', + Tribe__Context::REQUEST_VAR => 'post_type', + ], + ], + 'single' => [ + 'read' => [ Tribe__Context::QUERY_METHOD => 'is_single' ] + ], + 'taxonomy' => [ + 'read' => [ + Tribe__Context::QUERY_PROP => [ 'taxonomy' ], + Tribe__Context::QUERY_VAR => [ 'taxonomy' ], + Tribe__Context::REQUEST_VAR => [ 'taxonomy' ], + ], + ], + 'post_tag' => [ + 'read' => [ + Tribe__Context::QUERY_PROP => [ 'post_tag', 'tag' ], + Tribe__Context::QUERY_VAR => [ 'post_tag', 'tag' ], + Tribe__Context::REQUEST_VAR => [ 'post_tag', 'tag' ], + ], + ], + 'bulk_edit' => [ + 'read' => [ + Tribe__Context::REQUEST_VAR => [ 'bulk_edit' ], + ], + ], + 'inline_save' => [ + 'read' => [ + Tribe__Context::FUNC => [ + static function () { + return tribe_get_request_var( 'action', false ) === 'inline-save' + ? true + : Tribe__Context::NOT_FOUND; + } + ], + ], + ], +]; diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Cost_Utils.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Cost_Utils.php new file mode 100644 index 000000000..42a112a19 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Cost_Utils.php @@ -0,0 +1,542 @@ +get_cost_regex(), trim( $cost ) ); + } + + /** + * Returns the regular expression that shold be used to identify a valid + * cost string. + * + * @return string + */ + public function get_cost_regex() { + $separators = '[\\' . implode( '\\', $this->get_separators() ) . ']?'; + $cost_regex = '(' . $separators . '([\d]+)' . $separators . '([\d]*))'; + + /** + * Filters the regular expression that will be used to identify a valid cost + * string. + * + * @param string $cost_regex + * + * @deprecated 4.3 Use `tribe_cost_regex` instead + */ + $cost_regex = apply_filters( + 'tribe_events_cost_regex', $cost_regex + ); + + /** + * Filters the regular expression that will be used to identify a valid cost + * string. + * + * @param string $cost_regex + */ + $cost_regex = apply_filters( 'tribe_cost_regex', $cost_regex ); + + return $cost_regex; + } + + /** + * Fetch the possible separators + * + * @return array + */ + public function get_separators() { + $separators = [ ',', '.' ]; + + /** + * Filters the cost string possible separators, those must be only 1 char. + * + * @param array $separators Defaults to comma (",") and period (".") + */ + return apply_filters( 'tribe_events_cost_separators', $separators ); + } + + /** + * If the cost is "0", call it "Free" + * + * @param int|float|string $cost Cost to analyze + * + * @return int|float|string + */ + public function maybe_replace_cost_with_free( $cost ) { + + $cost_with_period = $this->convert_decimal_separator( $cost ); + + if ( + is_numeric( $cost_with_period ) + && '0.00' === number_format( $cost_with_period, 2, '.', ',' ) + ) { + return esc_html__( 'Free', 'tribe-common' ); + } + + return $cost; + } + + /** + * Formats a cost with a currency symbol + * + * @param int|float|string $cost Cost to format + * + * return string + * @param int|WP_Post $event An event post ID or post object. + * @param string $currency_symbol + * @param string $currency_position Either "prefix" or "posfix" + * + * @return float|int|string + */ + public function maybe_format_with_currency( $cost, $event = null, $currency_symbol = null, $currency_position = null ) { + // check if the currency symbol is desired, and it's just a number in the field + // be sure to account for european formats in decimals, and thousands separators + if ( is_numeric( str_replace( $this->get_separators(), '', $cost ) ) ) { + $reverse_position = null; + // currency_position often gets passed as null or an empty string. + if ( ! empty( $currency_position ) ) { + $reverse_position = 'prefix' === $currency_position ? false : true; + } + + $cost = tribe_format_currency( $cost, $event, $currency_symbol, $reverse_position ); + } + + return $cost; + } + + /** + * @param string $original_string_cost A string cost with or without currency symbol, + * e.g. `10 - 20`, `Free` or `2$ - 4$`. + * @param array|string $merging_cost A single string cost representation to merge or an array of + * string cost representations to merge, e.g. ['Free', 10, 20, + * 'Donation'] or `Donation`. + * @param bool $with_currency_symbol Whether the output should prepend the currency symbol to the + * numeric costs or not. + * @param array $sorted_mins An array of non numeric price minimums sorted smaller to larger, + * e.g. `['Really free', 'Somewhat free', 'Free with 3 friends']`. + * @param array $sorted_maxs An array of non numeric price maximums sorted smaller to larger, + * e.g. `['Donation min $10', 'Donation min $20', 'Donation min + * $100']`. + * + * @return string|array The merged cost range. + */ + public function merge_cost_ranges( $original_string_cost, $merging_cost, $with_currency_symbol, $sorted_mins = [], $sorted_maxs = [] ) { + if ( empty( $merging_cost ) || $original_string_cost === $merging_cost ) { + return $original_string_cost; + } + + $_merging_cost = array_map( + [ $this, 'convert_decimal_separator' ], (array) $merging_cost + ); + $_merging_cost = array_map( [ $this, 'numerize_numbers' ], $_merging_cost ); + $numeric_merging_cost_costs = array_filter( $_merging_cost, 'is_numeric' ); + + $matches = []; + preg_match_all( + '!\d+(?:([' . preg_quote( $this->_supported_decimal_separators ) . '])\d+)?!', $original_string_cost, + $matches + ); + $this->_current_original_cost_separator = empty( $matches[1][0] ) ? '.' : $matches[1][0]; + $matches[0] = empty( $matches[0] ) + ? $matches[0] + : array_map( + [ + $this, + 'convert_decimal_separator', + ], + $matches[0] + ); + + $numeric_orignal_costs = empty( $matches[0] ) ? $matches[0] : array_map( + 'floatval', $matches[0] + ); + + $all_numeric_costs = array_filter( array_merge( $numeric_merging_cost_costs, $numeric_orignal_costs ) ); + $cost_min = $cost_max = false; + + $merging_mins = array_intersect( $sorted_mins, (array) $merging_cost ); + $merging_has_min = array_search( reset( $merging_mins ), $sorted_mins ); + $original_has_min = array_search( $original_string_cost, $sorted_mins ); + $merging_has_min = false === $merging_has_min ? 999 : $merging_has_min; + $original_has_min = false === $original_has_min ? 999 : $original_has_min; + $string_min_key = min( $merging_has_min, $original_has_min ); + if ( array_key_exists( $string_min_key, $sorted_mins ) ) { + $cost_min = $sorted_mins[ $string_min_key ]; + } else { + $cost_min = empty( $all_numeric_costs ) ? '' : min( $all_numeric_costs ); + } + + $merging_maxs = array_intersect( $sorted_maxs, (array) $merging_cost ); + $merging_has_max = array_search( end( $merging_maxs ), $sorted_maxs ); + $original_has_max = array_search( $original_string_cost, $sorted_maxs ); + $merging_has_max = false === $merging_has_max ? - 1 : $merging_has_max; + $original_has_max = false === $original_has_max ? - 1 : $original_has_max; + $string_max_key = max( $merging_has_max, $original_has_max ); + if ( array_key_exists( $string_max_key, $sorted_maxs ) ) { + $cost_max = $sorted_maxs[ $string_max_key ]; + } else { + $cost_max = empty( $all_numeric_costs ) ? '' : max( $all_numeric_costs ); + } + + $cost = array_filter( [ $cost_min, $cost_max ] ); + + if ( $with_currency_symbol ) { + $formatted_cost = []; + foreach ( $cost as $c ) { + $formatted_cost[] = is_numeric( $c ) ? tribe_format_currency( $c ) : $c; + } + $cost = $formatted_cost; + } + + return empty( $cost ) ? $original_string_cost : array_map( + [ $this, 'restore_original_decimal_separator' ], + $cost + ); + } + + /** + * Returns a maximum cost in a list of costs. If an array of costs is not passed in, the array of costs is fetched + * via query. + * + * @param $costs mixed Cost(s) to review for max value + * + * @return float + */ + public function get_maximum_cost( $costs = null ) { + return $this->get_cost_by_func( $costs, 'max' ); + } + + /** + * Returns a particular cost within an array of costs + * + * @param $costs mixed Cost(s) to review for max value + * @param $function string Function to use to determine which cost to return from range. Valid values: max, min + * + * @return float + */ + protected function get_cost_by_func( $costs = null, $function = 'max' ) { + if ( null === $costs ) { + $costs = $this->get_all_costs(); + } else { + $costs = (array) $costs; + } + + $costs = $this->parse_cost_range( $costs ); + + // if there's only one item, we're looking at a single event. If the cost is non-numeric, let's + // return the non-numeric cost so that value is preserved + if ( 1 === count( $costs ) && ! is_numeric( current( $costs ) ) ) { + return current( $costs ); + } + + // make sure we are only trying to get numeric min/max values + $costs = array_filter( $costs, 'is_numeric' ); + + if ( empty( $costs ) ) { + return 0; + } + + switch ( $function ) { + case 'min': + $cost = $costs[ min( array_keys( $costs ) ) ]; + break; + case 'max': + default: + $cost = $costs[ max( array_keys( $costs ) ) ]; + break; + } + + // If there isn't anything on the cost just return 0 + if ( empty( $cost ) ) { + return 0; + } + + return $cost; + } + + /** + * Parses a cost into an array of ranges. + * + * If a range isn't provided, the resulting array will hold a single + * value. + * + * @param string|array $costs A cost string or an array of cost strings. + * @param null $max_decimals The maximum number of decimal values that should be returned in the range. + * @param bool $sort Whether the returned values should be sorted. + * + * @return array An associative array of parsed costs in [ => ] format. + */ + public function parse_cost_range( $costs, $max_decimals = null, $sort = true ) { + if ( ! is_array( $costs ) && ! is_string( $costs ) ) { + return []; + } + + // make sure costs is an array + $costs = (array) $costs; + + // If there aren't any costs, return a blank array + if ( 0 === count( $costs ) ) { + return []; + } + + // Build the regular expression + $price_regex = $this->get_cost_regex(); + $max = 0; + + foreach ( $costs as &$cost ) { + // Get the required parts + if ( preg_match_all( '/' . $price_regex . '/', $cost, $matches ) ) { + $cost = reset( $matches ); + } else { + $cost = [ $cost ]; + continue; + } + + // Get the max number of decimals for the range + if ( count( $matches ) === 4 ) { + $decimals = max( array_map( 'strlen', end( $matches ) ) ); + $max = max( $max, $decimals ); + } + } + + // If we passed max decimals + if ( ! is_null( $max_decimals ) ) { + $max = max( $max_decimals, $max ); + } + + $output_costs = []; + $costs = call_user_func_array( 'array_merge', array_values( $costs ) ); + + foreach ( $costs as $cost ) { + $numeric_cost = str_replace( $this->get_separators(), '.', $cost ); + + if ( is_numeric( $numeric_cost ) ) { + // Creates a Well Balanced Index that will perform good on a Key Sorting method + $index = str_replace( [ '.', ',' ], '', number_format( $numeric_cost, $max ) ); + } else { + // Makes sure that we have "index-safe" string + $index = sanitize_title( $numeric_cost ); + } + + // Keep the Costs in a organizable array by keys with the "numeric" value + $output_costs[ $index ] = $cost; + } + + // Filter keeping the Keys + if ( $sort ) { + ksort( $output_costs ); + } + + return (array) $output_costs; + } + + /** + * Returns a minimum cost in a list of costs. If an array of costs is not passed in, the array of costs is fetched + * via query. + * + * @param $costs mixed Cost(s) to review for min value + * + * @return float + */ + public function get_minimum_cost( $costs = null ) { + return $this->get_cost_by_func( $costs, 'min' ); + } + + /** + * Converts the original decimal separator to ".". + * + * @param string|int $value + * + * @return string + */ + protected function convert_decimal_separator( $value ) { + return preg_replace( '/[' . preg_quote( $this->_supported_decimal_separators ) . ']/', '.', $value ); + } + + /** + * Restores the decimal separator to its original symbol. + * + * @param string $value + * + * @return string + */ + protected function restore_original_decimal_separator( $value ) { + return str_replace( '.', $this->_current_original_cost_separator, $value ); + } + + /** + * Extracts int and floats from a numeric "dirty" string like strings that might contain other symbols. + * + * E.g. "$10" will yield "10"; "23.55$" will yield "23.55". + * + * @param string|int $value + * + * @return int|float + */ + protected function numerize_numbers( $value ) { + $matches = []; + + $pattern = '/(\\d{1,}([' . $this->_supported_decimal_separators . ']\\d{1,}))/'; + + return preg_match( $pattern, $value, $matches ) ? $matches[1] : $value; + } + + /** + * Parses the currency symbol part of a cost string. + * + * @param string|array $cost A string cost, a comma separated array of string costs or an array of costs. + * + * @return false|string Either the inferred currency symbol or `false` if the currency symbol is missing or not consistent. + */ + public function parse_currency_symbol( $cost ) { + if ( empty( $cost ) ) { + return false; + } + + $original_costs = is_array( $cost ) ? $cost : preg_split( '/\\s*,\\s*/', $cost ); + $costs = $this->parse_cost_range( $original_costs, null, false ); + + if ( empty( $costs ) ) { + return false; + } + + $currency_symbols = []; + $i = 0; + foreach ( $costs as $string => $value ) { + if ( is_numeric( $string ) ) { + $currency_symbols[] = trim( str_replace( $value, '', $original_costs[ $i ] ) ); + if ( end( $currency_symbols ) !== reset( $currency_symbols ) ) { + return false; + } + } + + $i ++; + } + + return ! empty( $currency_symbols ) ? reset( $currency_symbols ) : false; + } + + /** + * Parses the currency symbol position part of a cost string. + * + * @param string|array $cost A string cost, a comma separated array of string costs or an array of costs. + * + * @return false|string Either the inferred currency symbol position or `false` if not present or not consistent. + */ + public function parse_currency_position( $cost ) { + if ( empty( $cost ) ) { + return false; + } + + $original_costs = is_array( $cost ) ? $cost : preg_split( '/\\s*,\\s*/', $cost ); + $currency_symbol = $this->parse_currency_symbol( $original_costs ); + + if ( empty( $currency_symbol ) ) { + return false; + } + + $currency_positions = []; + foreach ( $original_costs as $original_cost ) { + $currency_symbol_position = strpos( trim( $original_cost ), $currency_symbol ); + if ( false === $currency_symbol_position ) { + continue; + } + + $currency_positions[] = 0 === $currency_symbol_position ? 'prefix' : 'postfix'; + if ( end( $currency_positions ) !== reset( $currency_positions ) ) { + return false; + } + } + + return ! empty( $currency_positions ) ? reset( $currency_positions ) : false; + } + + /** + * Parses the cost value and current locale to infer decimal and thousands separators. + * + * The cost values stored in the meta table might not use the same decimal and thousands separator as the current + * locale. + * To work around this we parse the value assuming the decimal separator will be the last non-numeric symbol, + * if any. + * + * @since 4.9.12 + * + * @param string|int|float $value The cost value to parse. + * + * @return array An array containing the parsed decimal and thousands separator symbols. + */ + public function parse_separators( $value ) { + global $wp_locale; + $locale_decimal_point = $wp_locale->number_format['decimal_point']; + $locale_thousands_sep = $wp_locale->number_format['thousands_sep']; + $decimal_sep = $locale_decimal_point; + $thousands_sep = $locale_thousands_sep; + + preg_match_all( '/[\\.,]+/', $value, $matches ); + + if ( ! empty( $matches[0] ) ) { + $matched_separators = $matches[0]; + if ( count( array_unique( $matched_separators ) ) > 1 ) { + // We have both, the decimal separator will be the last non-numeric symbol. + $decimal_sep = end( $matched_separators ); + $thousands_sep = reset( $matched_separators ); + } else { + /* + * We only have one, we can assume it's the decimal separator if it comes before a number of numeric + * symbols that is not exactly 3. If there are exactly 3 number after the symbols we fall back on the + * locale; we did our best and cannot guess any further. + */ + $frags = explode( end( $matched_separators ), $value ); + if ( strlen( end( $frags ) ) !== 3 ) { + $decimal_sep = end( $matched_separators ); + $thousands_sep = $decimal_sep === $locale_decimal_point ? + $locale_thousands_sep + : $locale_decimal_point; + } + } + } + + return [ $decimal_sep, $thousands_sep ]; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Credits.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Credits.php new file mode 100644 index 000000000..c87bc1f51 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Credits.php @@ -0,0 +1,116 @@ +hook(); + } + + /** + * Hook the functionality of this class into the world + */ + public function hook() { + add_filter( 'tribe_events_after_html', [ $this, 'html_comment_credit' ] ); + add_filter( 'admin_footer_text', [ $this, 'rating_nudge' ], 1, 2 ); + } + + /** + * Add credit in HTML page source + * + * @return void + **/ + public function html_comment_credit( $after_html ) { + + if ( ! class_exists( 'Tribe__Events__Main' ) ) { + return $after_html; + } + + $html_credit = "\n\n"; + $after_html .= apply_filters( 'tribe_html_credit', $html_credit ); + return $after_html; + } + + /** + * Add ratings nudge in admin footer + * + * @param $footer_text + * + * @return string + */ + public function rating_nudge( $footer_text ) { + $admin_helpers = Tribe__Admin__Helpers::instance(); + + add_filter( 'tribe_tickets_post_types', [ $this, 'tmp_return_tribe_events' ], 99 ); + + $review_text_tec = esc_html__( 'Rate %1$sThe Events Calendar%2$s %3$s', 'tribe-common' ); + $review_url_tec = 'https://wordpress.org/support/plugin/the-events-calendar/reviews/?filter=5'; + + $review_text_et = esc_html__( 'If you like %1$sEvent Tickets%2$s please leave us a %3$s. It takes a minute and it helps a lot.', 'tribe-common' ); + $review_url_et = 'https://wordpress.org/support/plugin/event-tickets/reviews/?filter=5'; + + // Only display custom text on Tribe Admin Pages. + if ( $admin_helpers->is_screen() || $admin_helpers->is_post_type_screen() ) { + + if ( class_exists( 'Tribe__Events__Main' ) ) { + // If we have TEC and ET, split the impressions 50/50. + if ( class_exists( 'Tribe__Tickets__Main' ) && wp_rand( 0,1 ) ) { + $review_text = $review_text_et; + $review_url = $review_url_et; + } else { + $review_text = $review_text_tec; + $review_url = $review_url_tec; + } + } else { + $review_text = $review_text_et; + $review_url = $review_url_et; + } + + $footer_text = sprintf( + $review_text, + '', + '', + '★★★★★' + ); + } + + remove_filter( 'tribe_tickets_post_types', [ $this, 'tmp_return_tribe_events' ], 99 ); + + /** + * Filters the admin footer text. + * + * @since 4.15.0 + * + * @param $footer_text The admin footer text. + */ + return apply_filters( 'tec_admin_footer_text', $footer_text ); + } + + /** + * temporary function to filter event types down to only tribe-specific types + * + * This will limit the request for ratings to only those post type pages + */ + public function tmp_return_tribe_events( $unused_post_types ) { + return [ 'tribe_events' ]; + } + + /** + * @var $instance + */ + private static $instance = null; + + /** + * @return self + */ + public static function instance() { + if ( empty( self::$instance ) ) { + self::$instance = new self(); + } + + return self::$instance; + } + +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Customizer.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Customizer.php new file mode 100644 index 000000000..a153ded51 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Customizer.php @@ -0,0 +1,1073 @@ +is_active() ) { + return; + } + + /** + * Filters the Panel ID, which is also the `wp_option` name for the Customizer settings + * + * @since 4.4 + * + * @param string $ID + * @param self $customizer + */ + $this->ID = apply_filters( 'tribe_customizer_panel_id', 'tribe_customizer', $this ); + + // Hook the Registering methods + add_action( 'customize_register', [ $this, 'register' ], 15 ); + + add_action( 'wp_print_footer_scripts', [ $this, 'print_css_template' ], 15 ); + add_action( 'customize_controls_print_footer_scripts', [ $this, 'customize_controls_print_footer_scripts' ], 15 ); + + // front end styles from customizer + add_action( 'tribe_events_pro_widget_render', [ $this, 'inline_style' ], 101 ); + add_action( 'wp_print_footer_scripts', [ $this, 'shortcode_inline_style' ], 10 ); + add_action( 'wp_print_footer_scripts', [ $this, 'widget_inline_style' ], 10 ); + + /** + * Allows filtering the action that will be used to trigger the printing of inline scripts. + * + * By default inline scripts will be printed on the `wp_enqueue_scripts` action, but other + * plugins or later iterations might require inline styles to be printed on other actions. + * + * @since 4.12.15 + * + * @param string $inline_script_action_handle The handle of the action that will be used to try + * and attempt to print inline scripts. + */ + $print_styles_action = apply_filters( 'tribe_customizer_print_styles_action', 'wp_enqueue_scripts' ); + + add_action( $print_styles_action, [ $this, 'inline_style' ], 15 ); + + add_filter( "default_option_{$this->ID}", [ $this, 'maybe_fallback_get_option' ] ); + } + + /** + * Backwards compatibility for the old Customizer Option Save + * + * @since 4.4 + * + * @param mixed $sections + * + * @return mixed + */ + public function maybe_fallback_get_option( $sections ) { + // Return if there is something there + if ( ! empty( $sections ) ) { + return $sections; + } + + return get_option( 'tribe_events_pro_customizer', [] ); + } + + /** + * Loads a Section to the Customizer on The Events Calendar's Panel + * + * @since 4.4 + * + * @param object $section An Object that extends the Abstract `Tribe__Customizer__Section` + * + * @return bool + */ + public function load_section( $section ) { + // You can only add a section if it extends the abstract Section + if ( ! is_object( $section ) || ! in_array( 'Tribe__Customizer__Section', class_parents( $section ) ) ) { + return false; + } + + // Add the Section + // Enforces the usage of `$instance->ID` + $this->sections_class[ $section->ID ] = $section; + + return true; + } + + /** + * Fetches all Section Classes + * + * @since 4.4 + * + * @return array + */ + public function get_loaded_sections() { + /** + * Allow developers to filter Classes from Customizer Sections + * + * @deprecated + * @since 4.0 + * + * @param array $sections_class + * @param self $customizer + */ + $this->sections_class = apply_filters( 'tribe_events_pro_customizer_sections_class', $this->sections_class, $this ); + + /** + * Allow developers to filter Classes from Customizer Sections + * + * @since 4.4 + * + * @param array $sections_class + * @param self $customizer + */ + $this->sections_class = apply_filters( 'tribe_customizer_sections_class', $this->sections_class, $this ); + + return $this->sections_class; + } + + /** + * Returns the section requested by ID. + * + * @since 4.13.3 + * + * @param string $id The ID of the desired section. + * + * @return boolean|Tribe__Customizer__Section The requested section or boolean false if not found. + */ + public function get_section( $id ) { + $sections = $this->get_loaded_sections(); + + if ( empty( $sections[ $id ] ) ) { + return false; + } + + return $sections[ $id ]; + } + + /** + * A easy way to check if customize is active + * + * @since 4.2.2 + * + * @return boolean + */ + public function is_active() { + /** + * Allows Developers to completely deactivate Events Calendar Customizer + * + * @deprecated + * + * @param boolean $is_active + */ + $is_active = apply_filters( 'tribe_events_pro_customizer_is_active', true ); + + /** + * Allows Developers to completely deactivate Events Calendar Customizer + * + * @param boolean $is_active + */ + return apply_filters( 'tribe_customizer_is_active', true ); + } + + /** + * A method to easily search on an array + * + * @since 4.0 + * + * @param array $variable Variable to be searched + * @param array $indexes The index that the method will try to retrieve + * @param mixed $default If the variable doesn't exist, what is the default + * + * @return mixed Return the variable based on the index + */ + public static function search_var( $variable = null, $indexes = [], $default = null ) { + if ( is_object( $variable ) ) { + $variable = (array) $variable; + } + + if ( ! is_array( $variable ) ) { + return $variable; + } + + foreach ( (array) $indexes as $index ) { + if ( ! is_array( $variable ) || ! isset( $variable[ $index ] ) ) { + $variable = $default; + break; + } + + $variable = $variable[ $index ]; + } + + return $variable; + } + + /** + * Get an option from the database, using index search you can retrieve the full panel, a section or even a setting. + * + * @since 4.4 + * + * @param array $search Index search, array( 'section_name', 'setting_name' ). + * @param mixed $default The default, if the requested variable doesn't exits. + * + * @return mixed The requested option or the default. + */ + public function get_option( $search = null, $default = null ) { + $sections = get_option( $this->ID, $default ); + + foreach ( $this->get_loaded_sections() as $section ) { + /** + * Allow filtering the defaults for each settings to be filtered before the Ghost options to be set + * + * @deprecated + * @since 4.0 + * + * @param array $defaults + */ + $defaults[ $section->ID ] = apply_filters( "tribe_events_pro_customizer_section_{$section->ID}_defaults", [] ); + + /** + * Allow filtering the defaults for each settings to be filtered before the Ghost options to be set + * + * @since 4.4 + * + * @param array $defaults + */ + $settings = isset( $sections[ $section->ID ] ) ? $sections[ $section->ID ] : []; + $defaults[ $section->ID ] = apply_filters( "tribe_customizer_section_{$section->ID}_defaults", $settings ); + $sections[ $section->ID ] = wp_parse_args( $settings, $defaults[ $section->ID ] ); + } + + /** + * Allows Ghost Options to be inserted + * + * @deprecated + * @since 4.0 + * + * @param array $sections + * @param array $search + */ + $sections = apply_filters( 'tribe_events_pro_customizer_pre_get_option', $sections, $search ); + + /** + * Allows Ghost Options to be inserted + * + * @since 4.4 + * + * @param array $sections + * @param array $search + */ + $sections = apply_filters( 'tribe_customizer_pre_get_option', $sections, $search ); + + // Search on the Array + if ( ! is_null( $search ) ) { + $option = self::search_var( $sections, $search, $default ); + } else { + $option = $sections; + } + + /** + * Apply Filters After finding the variable + * + * @since 4.4 + * + * @param mixed $option + * @param array $search + * @param array $sections + */ + $option = apply_filters( 'tribe_customizer_get_option', $option, $search, $sections ); + + return $option; + } + + /** + * Check if the option exists, this method is used allow only sections that were saved to be applied. + * + * @param strings Using the following structure: self::has_option( 'section_name', 'setting_name' ); + * + * @return boolean Whether the option exists in the database + */ + public function has_option() { + $search = func_get_args(); + $option = self::get_option(); + $real_option = get_option( $this->ID, [] ); + + // Get section and Settings based on keys + $section = reset( $search ); + $setting = end( $search ); + + if ( empty( $real_option ) || empty( $real_option[ $section ] ) ) { + return false; + } + + // Search on the Array + if ( ! is_null( $search ) ) { + $option = self::search_var( $option, $search, null ); + } + + return ! empty( $option ); + } + + /** + * Add an action for some backwards compatibility. + * + * @since 4.14.2 + * + * @return void + */ + public function customize_controls_print_footer_scripts() { + /** + * Allows plugins to hook in and add any scripts they need at the right time. + * + * @param Tribe__Customizer $customizer The current instance of Tribe__Customizer. + */ + do_action( 'tribe_enqueue_customizer_scripts', $this ); + } + + /** + * Print the CSS for the customizer on `wp_print_footer_scripts` + * + * @since 4.12.6 Moved the template building code to the `get_styles_scripts` method. + * + * @return void + */ + public function print_css_template() { + + //Only load in Customizer + if ( ! is_customize_preview() ) { + return false; + } + + echo $this->get_styles_scripts(); + } + + /** + * Print the CSS for the customizer for shortcodes. + * + * @since 4.12.6 + */ + public function shortcode_inline_style() { + /** + * Whether customizer styles should print for shortcodes or not. + * + * @since 4.12.6 + * + * @param boolean $should_print Whether the inline styles should be printed on screen. + */ + $should_print = apply_filters( 'tribe_customizer_should_print_shortcode_customizer_styles', false ); + + if ( empty( $should_print ) ) { + return; + } + + $this->inline_style(); + } + + /** + * Print the CSS for the customizer for widgets. + * + * @since 4.12.14 + */ + public function widget_inline_style() { + /** + * Whether customizer styles should print for widgets or not. + * + * @since 4.12.14 + * + * @param boolean $should_print Whether the inline styles should be printed on screen. + */ + $should_print = apply_filters( 'tribe_customizer_should_print_widget_customizer_styles', false ); + + if ( empty( $should_print ) ) { + return; + } + + $this->inline_style(); + } + + /** + * Print the CSS for the customizer using wp_add_inline_style + * + * @since 4.12.15 Added the `$force` parameter to force the print of the style inline. + * + * @param bool $force Whether to ignore the context to try and print the style inline, or not. + */ + public function inline_style( $force = false ) { + // Only load once on front-end. + if ( ! $force && ( is_customize_preview() || is_admin() || $this->inline_style ) ) { + return false; + } + + /** + * Use this filter to add more CSS, using Underscore Template style + * + * @since 4.4 + * + * @link http://underscorejs.org/#template + * + * @param string $template + */ + $css_template = trim( apply_filters( 'tribe_customizer_css_template', '' ) ); + + // If we don't have anything on the customizer don't print empty styles + if ( empty( $css_template ) ) { + return false; + } + + $sheets = [ + 'tribe-common-full-style', + ]; + + /** + * Allow plugins to add themselves to this list. + * + * @since 4.12.1 + * + * @param array $sheets An array of sheets to search for. + * @param string $css_template String containing the inline css to add. + */ + $sheets = apply_filters( 'tribe_customizer_inline_stylesheets', $sheets, $css_template ); + + if ( empty( $sheets ) ) { + return false; + } + + // Add customizer styles inline with the latest stylesheet that is enqueued. + foreach ( array_reverse( $sheets ) as $sheet ) { + if ( wp_style_is( $sheet ) ) { + $inline_style = wp_strip_all_tags( $this->parse_css_template( $css_template ) ); + + /** + * Fires before a style is, possibly, printed inline depending on the stylesheet. + * + * @since 4.12.15 + * + * @param string $sheet The handle of the stylesheet the style will be printed inline for. + * @param string $inline_style The inline style contents, as they will be printed on the page. + */ + do_action( 'tribe_customizer_before_inline_style', $sheet, $inline_style ); + + // Just print styles if doing 'wp_print_footer_scripts' action. + $just_print = (bool) doing_action( 'wp_print_footer_scripts' ); + + if ( $just_print ) { + printf( + "\n", + esc_attr( $sheet ), + $inline_style + ); + } else { + wp_add_inline_style( $sheet, $inline_style ); + } + + $this->inline_style = true; + + break; + } + } + } + + /** + * Replaces the Settings using the Underscore templating strings + * + * @param string $template The template variable, that we will look to replace the variables + * @return string A Valid css after replacing the variables + */ + private function parse_css_template( $template ) { + $css = $template; + $sections = $this->get_option(); + + $search = []; + $replace = []; + + foreach ( $sections as $section => $settings ) { + if ( ! is_array( $settings ) ) { + continue; + } + foreach ( $settings as $setting => $value ) { + $index = [ $section, $setting ]; + + // Add search based on Underscore template + $search[] = '<%= ' . implode( '.', $index ) . ' %>'; + + // Get the Replace value + $replace[] = $value; + } + } + + // Finally Str replace + return str_replace( $search, $replace, $css ); + } + + /** + * Method to start setting up the Customizer Section and Fields + * + * @since 4.0 + * + * @param WP_Customize_Manager $customizer WordPress Customizer variable + * @return void + */ + public function register( WP_Customize_Manager $customizer ) { + // Set the Customizer on a class variable + $this->manager = $customizer; + + /** + * Allow users to filter the Panel + * + * @since 4.4 + * + * @param WP_Customize_Panel $panel + * @param Tribe__Customizer $customizer + */ + $this->panel = apply_filters( 'tribe_customizer_panel', $this->register_panel(), $this ); + + /** + * Filter the Sections within our Panel before they are added to the Customize Manager + * + * @since 4.4 + * + * @param array $sections + * @param Tribe__Customizer $customizer + */ + $this->sections = apply_filters( 'tribe_customizer_pre_sections', $this->sections, $this ); + + foreach ( $this->sections as $id => $section ) { + $this->sections[ $id ] = $this->register_section( $id, $section ); + + /** + * Allows people to Register and de-register the method to register more Fields + * + * @since 4.4 + * @since 4.12.15 Add Customizer instance as a parameter. + * + * @param array $section + * @param WP_Customize_Manager $manager + * @param Tribe__Customizer $customizer The current customizer instance. + */ + do_action( "tribe_customizer_register_{$id}_settings", $this->sections[ $id ], $this->manager, $this ); + } + + /** + * Filter the Sections within our Panel, now using the actual WP_Customize_Section + * + * @since 4.4 + * + * @param array $sections + * @param Tribe__Customizer $customizer + */ + $this->sections = apply_filters( 'tribe_customizer_sections', $this->sections, $this ); + + // After everything is done, try to add Selective refresh + $this->maybe_selective_refresh(); + } + + /** + * Register the base Panel for Events Calendar Sections to be attached to + * + * @since 4.0 + * + * @return WP_Customize_Panel + */ + private function register_panel() { + $panel = $this->manager->get_panel( $this->ID ); + + // If the Panel already exists we leave returning it's instance + if ( ! empty( $panel ) ) { + return $panel; + } + + $panel_args = [ + 'title' => esc_html__( 'The Events Calendar', 'tribe-common' ), + 'description' => esc_html__( 'Use the following panel of your customizer to change the styling of your Calendar and Event pages.', 'tribe-common' ), + + // After `static_front_page` + 'priority' => 125, + ]; + + /** + * Filter the Panel Arguments for WP Customize + * + * @since 4.4 + * + * @param array $args + * @param string $ID + * @param Tribe__Customizer $customizer + */ + $panel_args = apply_filters( 'tribe_customizer_panel_args', $panel_args, $this->ID, $this ); + + // Actually Register the Panel + $this->manager->add_panel( $this->ID, $panel_args ); + + // Return the Panel instance + return $this->manager->get_panel( $this->ID ); + } + + /** + * Returns a URL to the TEC Customizer panel. + * + * @since 4.14.0 + * + * @return string The URL to the TEC Customizer panel. + */ + public function get_panel_url() { + $query['autofocus[panel]'] = 'tribe_customizer'; + return add_query_arg( $query, admin_url( 'customize.php' ) ); + } + + /** + * Returns an HTML link directly to the (opened) TEC Customizer panel + * + * @since 4.14.0 + * + * @param string $link_text The (pre)translated text for the link. + * + * @return string The HTML anchor element, linking to the TEC Customizer panel. + * An empty string is returned if missing a parameter. + */ + public function get_panel_link( $link_text ) { + if ( empty( $link_text ) || ! is_string( $link_text ) ) { + return ''; + } + + $panel_url = $this->get_panel_url(); + + return sprintf( + '%2$s', + esc_url( $panel_url ), + esc_html( $link_text ) + ); + } + + /** + * Use a "alias" method to register sections to allow users to filter args and the ID + * + * @since 4.0 + * + * @param string $id The Unique section ID + * @param array $args Arguments to register the section + * + * @link https://codex.wordpress.org/Class_Reference/WP_Customize_Manager/add_section + * + * @return WP_Customize_Section + */ + public function register_section( $id, $args ) { + /** + * Filter the Section ID + * + * @since 4.4 + * + * @param string $section_id + * @param Tribe__Customizer $customizer + */ + $section_id = apply_filters( 'tribe_customizer_section_id', $id, $this ); + + // Tries to fetch the section + $section = $this->manager->get_section( $section_id ); + + // If the Panel already exists we leave returning it's instance + if ( ! empty( $section ) ) { + return $section; + } + + /** + * Filter the Section arguments, so that developers can filter arguments based on $section_id + * + * @since 4.4 + * + * @param array $args + * @param string $section_id + * @param Tribe__Customizer $customizer + */ + $section_args = apply_filters( 'tribe_customizer_section_args', $args, $section_id, $this ); + + // Don't allow sections outside of our panel + $section_args['panel'] = $this->panel->id; + + // Actually Register the Section + $this->manager->add_section( $section_id, $section_args ); + + // Return the Section instance + return $this->manager->get_section( $section_id ); + } + + /** + * Returns a URL to the a specific TEC Customizer section. + * + * @since 4.14.0 + * + * @param string $section The slug for the desired section. + * + * @return string The URL to the TEC Customizer section. + */ + public function get_section_url( $section ) { + if ( empty( $section ) ) { + return ''; + } + + $query['autofocus[section]'] = $section; + return add_query_arg( $query, admin_url( 'customize.php' ) ); + } + + /** + * Gets the HTML link to a section in the TEC Customizer. + * + * @since 4.14.0 + * + * @param string $section The section "slug" to link to. + * @param string $link_text The text for the link. + * + * @return string The HTML anchor element, linking to the TEC Customizer section. + * An empty string is returned if missing a parameter. + */ + public function get_section_link( $section, $link_text = '' ) { + if ( empty( $section ) || empty( $link_text ) || ! is_string($link_text ) ) { + return ''; + } + + + $panel_url = $this->get_section_url( $section ); + if ( empty( $panel_url ) ) { + return ''; + } + + return sprintf( + '%2$s', + esc_url( $panel_url ), + esc_html( $link_text ) + ); + } + + /** + * Build the Setting name using the HTML format for Arrays + * + * @since 4.0 + * + * @param string $slug The actual Setting name + * @param string|WP_Customize_Section $section The section the setting lives in. + * + * @return string HTML name Attribute name of the setting. + */ + public function get_setting_name( $slug, $section = null ) { + $name = ! empty( $this->panel->id ) ? $this->panel->id : ''; + + // If there is a section set append it + if ( $section instanceof WP_Customize_Section ) { + $name .= '[' . $section->id . ']'; + } elseif ( is_string( $section ) ) { + $name .= '[' . $section . ']'; + } + + // Set the actual setting slug + $name .= '[' . esc_attr( $slug ) . ']'; + + return $name; + } + + /** + * Adds a setting field name to the Array of Possible Selective refresh fields + * + * @since 4.2 + * + * @param string $name The actual Setting name + * + * @return array The list of existing Settings, the new one included + */ + public function add_setting_name( $name ) { + $this->settings[] = $name; + return $this->settings; + } + + /** + * Gets the URL to a specific control/setting in the TEC Customizer. + * + * @since 4.14.0 + * + * @param string $section The section "slug" to link into. + * @param string $setting The setting "slug" to link to. + * + * @return string The URL to the setting. + * An empty string is returned if a parameter is missing or the setting control cannot be found. + */ + public function get_setting_url( $section, $setting ) { + // Bail if something is missing. + if ( empty( $setting ) || empty( $section ) ) { + return ''; + } + + $control = $this->get_setting_name( $setting, $section ); + + if ( empty( $control ) ) { + return ''; + } + + $query['autofocus[control]'] = $control; + + return add_query_arg( $query, admin_url( 'customize.php' ) ); + } + + /** + * Gets the link to the a specific control/setting in the TEC Customizer. + * + * @since 4.14.0 + * + * @param string $section The section "slug" to link into. + * @param string $setting The setting "slug" to link to. + * @param string $link_text The translated text for the link. + * + * @return string The HTML anchor element, linking to the TEC Customizer setting. + * An empty string is returned if missing a parameter or the setting control cannot be found. + */ + public function get_setting_link( $section, $setting, $link_text ) { + // Bail if something is missing. + if ( empty( $setting ) || empty( $section ) || empty( $link_text ) ) { + return ''; + } + + $control_url = $this->get_setting_url( $section, $setting ); + + if ( empty( $control_url ) ) { + return ''; + } + + return sprintf( + '%2$s', + esc_url( $control_url ), + esc_html( $link_text ) + ); + } + + /** + * Using the Previously created CSS element, we not just re-create it every setting change + * + * @since 4.2 + * + * @return void + */ + public function maybe_selective_refresh() { + // Only try to apply selective refresh if it's active + if ( ! isset( $this->manager->selective_refresh ) ) { + return; + } + + foreach ( $this->settings as $name ) { + $setting = $this->manager->get_setting( $name ); + + // Skip if we don't have that setting then skip it + if ( is_null( $setting ) ) { + continue; + } + + // Skip if we already have that + if ( ! is_null( $this->manager->selective_refresh->get_partial( $name ) ) ) { + continue; + } + + // Remove the Setting + // We need this because settings are protected on the WP_Customize_Manager + $this->manager->remove_setting( $name ); + + // Change the Transport + $setting->transport = 'postMessage'; + + // Re-add the setting + // We need this because settings are protected on the WP_Customize_Manager + $this->manager->add_setting( $setting ); + + // Add the Partial + $this->manager->selective_refresh->add_partial( + $name, + [ + 'selector' => '#' . esc_attr( $this->ID . '_css' ), + 'render_callback' => [ $this, 'print_css_template' ], + ] + ); + } + } + + /** + * Builds and returns the Customizer CSS template contents. + * + * The method DOES NOT check if the current context is the one where the Customizer template should + * be printed or not; that care is left to the code calling this method. + * + * @since 4.12.6 Extracted this method from the `print_css_template` one. + * + * @return string The CSS template contents. + */ + public function get_styles_scripts() { + /** + * Use this filter to add more CSS, using Underscore Template style. + * + * @since 4.4 + * + * @param string $template The Customizer template. + * + * @link http://underscorejs.org/#template + */ + $css_template = trim( apply_filters( 'tribe_customizer_css_template', '' ) ); + + // If we don't have anything on the Customizer, then don't print empty styles. + if ( empty( $css_template ) ) { + return ''; + } + + // Prepare the customizer scripts. + $result = ''; + + // Prepare the customizer styles. + $result .= ''; + + return $result; + } + + /** + * Inserts link to TEC Customizer section for FSE themes in admin (left) menu. + * + * @since 4.14.8 + */ + public function add_fse_customizer_link() { + _deprecated_function( __METHOD__, '4.14.18', 'No replacement. Customizer menu item is preserved as long as we activate it.'); + // Exit early if the current theme is not a FSE theme. + if ( ! tec_is_full_site_editor() ) { + return; + } + + // Add a link to the TEC panel in the Customizer. + add_submenu_page( + 'themes.php', + _x( 'Customize The Events Calendar', 'Page title for the TEC Customizer section.', 'tribe-common' ), + _x( 'Customize The Events Calendar', 'Menu item text for the TEC Customizer section link.', 'tribe-common' ), + 'edit_theme_options', + esc_url( add_query_arg( 'autofocus[panel]', 'tribe_customizer' , admin_url( 'customize.php' ) ) ) + ); + } + + /** + * Inserts link to TEC Customizer section for FSE themes in Events > Settings > Display. + * + * @since 4.14.8 + * + * @param array $settings The existing settings array. + * + * @return array $settings The modified settings array. + */ + public function add_fse_customizer_link_to_display_tab( $settings ) { + _deprecated_function( __METHOD__, '4.14.18', 'No replacement. Customizer link is preserved as long as we activate it.'); + // Exit early if the current theme is not a FSE theme. + if ( ! tec_is_full_site_editor() ) { + return $settings; + } + + $new_settings = [ + 'tribe-customizer-section-title' => [ + 'type' => 'html', + 'html' => '

    ' . __( 'Customizer', 'tribe-common' ) . '

    ', + ], + 'tribe-customizer-link-description' => [ + 'type' => 'html', + 'html' => '

    ' . __( 'Adjust colors, fonts, and more with the WordPress Customizer.', 'tribe-common' ) . '

    ', + ], + 'tribe-customizer-link' => [ + 'type' => 'html', + 'html' => sprintf( + /* translators: %1$s: opening anchor tag; %2$s: closing anchor tag */ + esc_html_x( '%1$sCustomize The Events Calendar%2$s', 'Link text added to the TEC->Settings->Display tab.', 'tribe-common' ), + '

    ', + '

    ' + ), + ], + ]; + + $settings = Tribe__Main::array_insert_after_key( 'tribe-form-content-start', $settings, $new_settings ); + + return $settings; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Customizer/Control.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Customizer/Control.php new file mode 100644 index 000000000..2a4dacf8e --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Customizer/Control.php @@ -0,0 +1,20 @@ + + */ + public $settings = []; + + /** + * Render the control's content + * + * @since 4.12.14 + */ + public function render_content() { ?> + +

    + label ); ?> +

    + + description ) ) : ?> + +

    + description ); ?> +

    + + + * so that it matches our other controls stylistically (spacing, etc). + * + * @since 4.12.13 + * + * @package Tribe\Customizer\Controls + */ + +namespace Tribe\Customizer\Controls; + +use Tribe\Customizer\Control; + +/** + * Class Number + * + * @since 4.12.13 + * + * @package Tribe\Customizer\Controls + */ +class Number extends Control { + + /** + * Control's Type. + * + * @since 4.12.13 + * + * @var string + */ + public $type = 'number'; + + /** + * Anyone able to set theme options will be able to see the input. + * + * @since 4.12.13 + * + * @var string + */ + public $capability = 'edit_theme_options'; + + /** + * Render the control's content + * + * @since 4.12.13 + */ + public function render_content() { + $input_id = '_customize-input-' . $this->id; + $description_id = '_customize-description-' . $this->id; + $describedby_attr = ( ! empty( $this->description ) ) ? ' aria-describedby="' . esc_attr( $description_id ) . '" ' : ''; + $name = '_customize-number-' . $this->id; + + if ( ! empty( $this->label ) ) : ?> + + + description ) ) : ?> + + description ); ?> + + +
    + + input_attrs(); ?> + input_attrs['value'] ) ) : ?> + value="value() ); ?>" + + link(); ?> + /> +
    + choices ) ) { + return; + } + + $input_id = '_customize-input-' . $this->id; + $description_id = '_customize-description-' . $this->id; + $describedby_attr = ( ! empty( $this->description ) ) ? ' aria-describedby="' . esc_attr( $description_id ) . '" ' : ''; + $name = '_customize-radio-' . $this->id; + ?> + label ) ) : ?> + label ); ?> + + description ) ) : ?> + + description ); ?> + + + + choices as $value => $label ) : ?> + + + value="" + name="" + link(); ?> + value(), $value ); ?> + /> + + + + id; + $description_id = '_customize-description-' . $this->id; + $describedby_attr = ( ! empty( $this->description ) ) ? ' aria-describedby="' . esc_attr( $description_id ) . '" ' : ''; + ?> + label ) ) : ?> + label ); ?> + + description ) ) : ?> + + description ); ?> + + + + + + + name="id ); ?>" + input_attrs(); ?> + link(); ?> + choices ) ) : ?> + list="" + + /> + choices ) ) : ?> +
    + choices as $label => $value ) : ?> +
    +
    + +
    + +
    + + */ + public $settings = []; + + /** + * Render the control's content + * + * @since 4.13.3 + */ + public function render_content() { + ?> +

    +


    +

    + id; + $description_id = '_customize-description-' . $this->id; + $describedby_attr = ( ! empty( $this->description ) ) ? ' aria-describedby="' . esc_attr( $description_id ) . '" ' : ''; + $name = '_customize-toggle-' . $this->id; + + ?> + label ) ) : ?> + label ); ?> + + description ) ) : ?> + + description ); ?> + + + + + + + 10, + 'capability' => 'edit_theme_options', + 'title' => null, + 'description' => null, + ]; + + /** + * Allows sections to be loaded in order for overrides. + * + * @var integer + */ + public $queue_priority = 15; + + /** + * Private variable holding the class Instance. + * + * @since 4.0 + * + * @access private + * @var Tribe__Events__Pro__Customizer__Section + */ + private static $instances; + + /** + * Contains the arguments for the section headings. + * + * @since 4.14.2 + * + * @var array + */ + protected $content_headings = []; + + /** + * Contains the arguments for the section settings. + * + * @since 4.14.2 + * + * @var array + */ + protected $content_settings = []; + + /** + * Contains the arguments for the section controls. + * + * @since 4.14.2 + * + * @var array + */ + protected $content_controls = []; + + /** + * Setup and Load hooks for this Section. + * + * @since 4.0 + * + * @return Tribe__Customizer__Section + */ + final public function __construct() { + // If for weird reason we don't have the Section name + if ( ! is_string( $this->ID ) ){ + $this->ID = self::get_section_slug( get_class( $this ) ); + } + + // Allow child classes to setup the section. + $this->setup(); + + // Hook the Register methods + add_action( "tribe_customizer_register_{$this->ID}_settings", [ $this, 'register_settings' ], 10, 2 ); + add_filter( 'tribe_customizer_pre_sections', [ $this, 'register' ], 10, 2 ); + + // Append this section CSS template + add_filter( 'tribe_customizer_css_template', [ $this, 'setup_css_template' ], $this->queue_priority ); + add_filter( "tribe_customizer_section_{$this->ID}_defaults", [ $this, 'get_defaults' ], 10 ); + + // Create the Ghost Options + add_filter( 'tribe_customizer_pre_get_option', [ $this, 'filter_settings' ], 10, 2 ); + + // By Default Invoking a new Section will load, unless `load` is set to false + if ( true === (bool) $this->load ) { + tribe( 'customizer' )->load_section( $this ); + } + } + + /** + * This method will be executed when the Class is Initialized. + * Overwrite this method to be able to setup the arguments of your section. + * + * @return void + */ + public function setup() { + $this->arguments = $this->get_arguments(); + $this->setup_content_arguments(); + } + + /** + * Register this Section. + * + * @param array $sections Array of Sections. + * @param Tribe__Customizer $customizer Our internal Cutomizer Class Instance. + * + * @return array Return the modified version of the Section array. + */ + public function register( $sections, Tribe__Customizer $customizer ) { + $sections[ $this->ID ] = $this->arguments; + + return $sections; + } + + /** + * Overwrite this method to create the Fields/Settings for this section. + * + * @param WP_Customize_Section $section The WordPress section instance. + * @param WP_Customize_Manager $manager The WordPress Customizer Manager. + * + * @return void + */ + public function register_settings( WP_Customize_Section $section, WP_Customize_Manager $manager ) { + $customizer = tribe( 'customizer' ); + + $settings = $this->get_content_settings(); + + if ( ! empty( $settings ) ) { + foreach ( $settings as $name => $args ) { + $this->add_setting( + $manager, + $customizer->get_setting_name( $name, $section ), + $name, + $args + ); + } + } + + $headings = $this->get_content_headings(); + + if ( ! empty( $headings ) ) { + foreach ( $headings as $name => $args ) { + $this->add_heading( + $section, + $manager, + $customizer->get_setting_name( $name, $section ), + $args + ); + } + } + + $controls = $this->get_content_controls(); + + if ( ! empty( $controls ) ) { + foreach ( $controls as $name => $args ) { + $this->add_control( + $section, + $manager, + $customizer->get_setting_name( $name, $section ), + $args + ); + } + } + } + + /** + * Function that encapsulates the logic for if a setting should be added to the Customizer style template. + * Note: this depends on a default value being set - + * if the setting value is empty OR set to the default value, it's not displayed. + * + * @since 4.13.3 + * + * @param string $setting The setting slug, like 'grid_lines_color'. + * @param int $section_id The ID for the section - defaults to the current one if not set. + * + * @return boolean If the setting should be added to the style template. + */ + public function should_include_setting_css( $setting, $section_id = null ) { + if ( empty( $setting ) || ! is_string( $setting ) ) { + return false; + } + + if ( empty( $section_id ) ) { + $section_id = $this->ID; + } + + $setting_value = tribe( 'customizer' )->get_option( [ $section_id, $setting ] ); + $section = tribe( 'customizer' )->get_section( $section_id ); + + // Something has gone wrong and we can't get the section. + if ( false === $section ) { + return; + } + + return ! empty( $setting_value ) && $section->get_default( $setting ) !== $setting_value; + } + + /** + * Function to simplify getting an option value. + * + * @since 4.13.3 + * + * @param string $setting The setting slug, like 'grid_lines_color'. + * + * @return string The setting value; + */ + public function get_option( $setting ) { + if ( empty( $setting ) ) { + return ''; + } + + return tribe( 'customizer' )->get_option( [ $this->ID, $setting ] ); + } + + /** + * Overwrite this method to be able to create dynamic settings. + * + * @param array $settings The actual options on the database. + * + * @return array $settings The modified settings. + */ + public function create_ghost_settings( $settings = [] ) { + return $settings; + } + + /** + * Get the section slug based on the Class name. + * + * @param string $class_name The name of this Class. + * @return string $slug The slug for this Class. + */ + final public static function get_section_slug( $class_name ) { + $abstract_name = __CLASS__; + $reflection = new ReflectionClass( $class_name ); + + // Get the Slug without the Base name. + $slug = str_replace( $abstract_name . '_', '', $reflection->getShortName() ); + + if ( false !== strpos( $slug, '__Customizer__' ) ) { + $slug = explode( '__Customizer__', $slug ); + $slug = end( $slug ); + } + + return strtolower( $slug ); + } + + /** + * Hooks to the `tribe_customizer_pre_get_option`. This applies the `$this->create_ghost_settings()` method + * to the settings on the correct section. + * + * @param array $settings Values from the Database from Customizer actions. + * @param array $search Indexed search @see Tribe__Customizer::search_var(). + * + * @return array + */ + public function filter_settings( $settings, $search ) { + // Exit early. + if ( null === $search ) { + return $settings; + } + + // Only Apply if getting the full options or Section. + if ( is_array( $search ) && count( $search ) > 1 ) { + return $settings; + } + + if ( is_array( $search ) && count( $search ) === 1 ) { + $settings = $this->create_ghost_settings( $settings ); + } else { + $settings[ $this->ID ] = $this->create_ghost_settings( $settings[ $this->ID ] ); + } + + return $settings; + } + + /* Arguments */ + + /** + * Set up section arguments. + * + * @since 4.13.3 + * + * @return void + */ + public function setup_arguments() {} + + /** + * Filter section arguments. + * + * @since 4.14.0 + * + * @return void + */ + public function filter_arguments( $arguments ) { + /** + * Applies a filter to the argument map for settings. + * + * @since 4.13.3 + * + * @param array $arguments Current set of callbacks for arguments. + * @param static $instance The section instance we are dealing with. + */ + $arguments = apply_filters( 'tribe_customizer_section_arguments', $arguments, $this ); + + $section_slug = static::get_section_slug( get_class( $this ) ); + + /** + * Applies a filter to the argument map for settings for a specific section. Based on the section slug. + * + * @since 4.13.3 + * + * @param array $arguments Current set of callbacks for arguments. + * @param static $instance The section instance we are dealing with. + */ + return apply_filters( "tribe_customizer_section_{$section_slug}_arguments", $arguments, $this ); + } + + /** + * Retrieve section arguments. + * + * @since 4.14.0 + * + * @return void + */ + public function get_arguments() { + return $this->filter_arguments( $this->setup_arguments() ); + } + + /** + * Sets up the Customizer section content. + * + * @since 4.13.3 + */ + public function setup_content_arguments(){ + $this->defaults = $this->setup_defaults(); + $this->content_settings = $this->setup_content_settings(); + $this->content_headings = $this->setup_content_headings(); + $this->content_controls = $this->setup_content_controls(); + } + + /* Default Values */ + + /** + * Set up default values. + * + * @since 4.13.3 + */ + public function setup_defaults() {} + + /** + * Get the (filtered) default settings. + * + * @return array The filtered defaults. + */ + public function get_defaults( $settings = [] ) { + // Create Ghost Options + $settings = $this->create_ghost_settings( wp_parse_args( $settings, $this->setup_defaults() ) ); + + return $this->filter_defaults( $settings ); + } + + public function filter_defaults( $settings ) { + + /** + * Allows filtering the default values for all sections. + * + * @since 4.13.3 + * + * @param array $settings The default settings + * @param Tribe__Customizer__Section $section The section object. + */ + $settings = apply_filters( 'tribe_customizer_section_default_settings', $settings, $this ); + + $section_slug = static::get_section_slug( get_class( $this ) ); + + /** + * Allows filtering the default values for a specific section. + * + * @since 4.13.3 + * + * @param array $settings The default settings + * @param Tribe__Customizer__Section $section The section object. + */ + $settings = apply_filters( "tribe_customizer_section_{$section_slug}_default_settings", $settings, $this ); + + return $settings; + } + + /** + * Get a single Default Value by key. + * + * @param string $key The key for the requested value. + * + * @return mixed The requested value. + */ + public function get_default( $key ) { + $defaults = $this->get_defaults(); + + if ( ! isset( $defaults[ $key ] ) ) { + return null; + } + + return $defaults[ $key ]; + } + + /* Utility Functions */ + + /** + * Sugar function that returns the results of Tribe__Customizer->get_section_url() for the current section. + * + * @since 4.14.0 + * + * @return string The URL to the TEC Customizer section. + */ + public function get_section_url() { + return tribe( 'customizer' )->get_section_url( $this->ID ); + } + + /** + * Sugar function that returns the results of Tribe__Customizer->get_section_link() for the current section. + * Gets the HTML link to the current section in the TEC Customizer. + * + * @since 4.14.0 + * + * @param string $link_text The text for the link. + * + * @return string The HTML anchor element, linking to the TEC Customizer section. + * An empty string is returned if missing a parameter. + */ + public function get_section_link( $link_text ) { + return tribe( 'customizer' )->get_section_link( $this->ID, $link_text ); + } + + /** + * Sugar function that returns the results of Tribe__Customizer->get_settings_url() + * for the specified setting in the _current section_. + * + * @since 4.14.0 + * + * @param string $setting The setting "slug" to link to. + * + * @return string The URL to the setting. + */ + public function get_setting_url( $setting ) { + return tribe( 'customizer' )->get_setting_url( $this->ID, $setting ); + } + + /** + * Sugar function that returns the results of Tribe__Customizer->get_settings_url() + * for the specified setting in the _current section_. + * + * @since 4.14.0 + * + * @param string $setting The setting "slug" to link to. + * @param string $link_text The translated text for the link. + * + * @return string The HTML anchor element, linking to the TEC Customizer setting. + */ + public function get_setting_link( $setting, $link_text ) { + return tribe( 'customizer' )->get_setting_link( $this->ID, $setting, $link_text ); + } + + /* Settings */ + + /** + * Sets up the Customizer settings arguments. + * + * @since 4.13.3 + */ + public function setup_content_settings() {} + + /** + * Get the (filtered) content setting arguments. + * @see filter_content_settings() + * + * @since 4.13.3 + * + * @return array The filtered arguments. + */ + public function get_content_settings() { + return $this->filter_content_settings( $this->setup_content_settings() ); + } + + /** + * Filter the content settings arguments + * + * @since 4.13.3 + * + * @param array $arguments The list of arguments for settings. + * + * @return array $arguments The filtered array of arguments. + */ + public function filter_content_settings( $arguments ) { + /** + * Applies a filter to the validation map for settings. + * + * @since 4.13.3 + * + * @param array $arguments Current set of callbacks for arguments. + * @param static $instance The section instance we are dealing with. + */ + $arguments = apply_filters( 'tribe_customizer_section_content_settings', $arguments, $this ); + + $section_slug = static::get_section_slug( get_class( $this ) ); + + /** + * Applies a filter to the validation map for settings for a specific section. Based on the section slug. + * Ex: tribe_customizer_section_tec_events_bar_default_settings + * + * @since 4.13.3 + * + * @param array $arguments Current set of callbacks for arguments. + * @param static $instance The section instance we are dealing with. + */ + $arguments = apply_filters( "tribe_customizer_section_{$section_slug}_content_settings", $arguments, $this ); + + return $arguments; + } + + /** + * Sugar syntax to add a setting to the customizer content. + * + * @since 4.13.3 + * + * @param WP_Customize_Manager $manager The instance of the Customizer Manager. + * @param string $setting_name HTML name Attribute name of the setting. + * @param string $key The key for the default value. + * @param array $arguments The control arguments. + */ + protected function add_setting( $manager, $setting_name, $key, $args ) { + // Get the default values. + $defaults = [ + 'default' => $this->get_default( $key ), + 'type' => 'option', + ]; + + // Add a setting. + $manager->add_setting( + $setting_name, + array_merge( $defaults, $args ) + ); + } + + /* Headings */ + + /** + * Sets up the Customizer section Header and Separator arguments. + * + * @since 4.13.3 + */ + public function setup_content_headings() {} + + /** + * Get the (filtered) content headings and separator arguments. + * @see filter_content_headings() + * + * @since 4.13.3 + * + * @return array The filtered arguments. + */ + public function get_content_headings() { + return $this->filter_content_headings( $this->setup_content_headings() ); + } + + /** + * Filter the content headings arguments + * + * @since 4.13.3 + * + * @param array $arguments The list of arguments for headings and separators. + * + * @return array $arguments The filtered array of arguments. + */ + public function filter_content_headings( $arguments ) { + /** + * Applies a filter to the validation map for headings. + * + * @since 4.13.3 + * + * @param array $arguments Current set of callbacks for arguments. + * @param static $instance The section instance we are dealing with. + */ + $arguments = apply_filters( 'tribe_customizer_section_content_headings', $arguments, $this ); + + $section_slug = static::get_section_slug( get_class( $this ) ); + + /** + * Applies a filter to the validation map for headings for a specific section. Based on the section slug. + * + * @since 4.13.3 + * + * @param array $arguments Current set of callbacks for arguments. + * @param static $instance The section instance we are dealing with. + */ + return apply_filters( "tribe_customizer_section_{$section_slug}_content_headings", $arguments, $this ); + } + + /** + * Sugar syntax to add heading and separator sections to the customizer content. + * These are controls only in name: they do not actually control or save any setting. + * + * @since 4.13.3 + * + * @param WP_Customize_Manager $manager The instance of the Customizer Manager. + * @param string $name HTML name Attribute name of the setting. + * @param array $arguments The control arguments. + * + */ + protected function add_heading( $section, $manager, $name, $args ) { + $this->add_control( $section, $manager, $name, $args ); + } + + /* Controls */ + + /** + * Sets up the Customizer controls arguments. + * + * @since 4.13.3 + */ + public function setup_content_controls() {} + + /** + * Get a list (array) of accepted control types. + * In the format slug => control class name. + * + * @since 4.13.3 + * + * @return array The array of control types and their associated classes. + */ + public function get_accepted_control_types() { + $accepted_control_types = [ + 'checkbox' => WP_Customize_Control::class, + 'color' => WP_Customize_Color_Control::class, + 'default' => WP_Customize_Control::class, + 'dropdown-pages' => WP_Customize_Control::class, + 'heading' => Heading::class, + 'image' => WP_Customize_Image_Control::class, + 'radio' => Radio::class, + 'select' => WP_Customize_Control::class, + 'separator' => Separator::class, + 'text' => WP_Customize_Control::class, + 'textarea' => WP_Customize_Control::class, + 'number' => Number::class, + 'range-slider' => Range_Slider::class, + 'toggle' => Toggle::class, + ]; + + /** + * Allows filtering the accepted control types. + * + * @since 4.13.3 + * + * @param array $control_types The map of keys to WP Control classes. + */ + return apply_filters( 'tribe_customizer_accepted_control_types', $accepted_control_types, $this ); + } + + /** + * Determine if a control type is in our list of accepted ones. + * + * @since 4.13.3 + * + * @param string $type The "slug" of the control type. + * + * @return boolean If a control type is in our list of accepted ones. + */ + public function is_control_type_accepted( $type ) { + $types = $this->get_accepted_control_types(); + + if ( empty( $type ) ) { + return false; + } + + if ( empty( $types[ $type ] ) ) { + return false; + } + + if ( ! class_exists( $types[ $type ] ) ) { + return false; + } + + return true; + } + + /** + * Gets the class object associated with a control type. + * + * @since 4.13.3 + * + * @param string $type The "slug" of the control type. + * + * @return object|false The control type class or false if type not found. + */ + public function get_control_type( $type ) { + $types = $this->get_accepted_control_types(); + + if ( empty( $type ) ) { + return $types[ 'default' ]; + } + + if ( empty( $types[ $type ] ) ) { + return false; + } + + return $types[ $type ]; + } + + /** + * Get the (filtered) content control arguments. + * @see filter_content_controls() + * + * @since 4.13.3 + * + * @return array The filtered arguments. + */ + public function get_content_controls() { + return $this->filter_content_controls( $this->setup_content_controls() ); + } + + /** + * Filter the content control arguments + * + * @since 4.13.3 + * + * @param array $arguments The list of arguments for controls. + * + * @return array $arguments The filtered array of arguments. + */ + public function filter_content_controls( $arguments ) { + /** + * Applies a filter to the validation map for controls. + * + * @since 4.13.3 + * + * @param array $arguments Current set of callbacks for arguments. + * @param static $instance The section instance we are dealing with. + */ + $arguments = apply_filters( 'tribe_customizer_section_content_controls', $arguments, $this ); + + $section_slug = static::get_section_slug( get_class( $this ) ); + + /** + * Applies a filter to the validation map for controls for a specific section. Based on the section slug. + * + * @since 4.13.3 + * + * @param array $arguments Current set of callbacks for arguments. + * @param static $instance The section instance we are dealing with. + */ + return apply_filters( "tribe_customizer_section_{$section_slug}_content_controls", $arguments, $this ); + } + + /** + * Sugar syntax to add a control to the customizer content. + * + * @since 4.13.3 + * + * @param WP_Customize_Manager $manager The instance of the Customizer Manager. + * @param string $setting_name HTML name Attribute name of the setting. + * @param array $arguments The control arguments. + */ + protected function add_control( $section, $manager, $setting_name, $args ) { + // Validate our control choice. + if ( ! isset( $args['type'] ) ) { + return; + } + + $type = (string) $args['type']; + + if ( ! $this->is_control_type_accepted( $type ) ) { + return; + } + + $type = $this->get_control_type( $type ); + + if ( $section instanceof WP_Customize_Section ) { + $section = (string) $section->id; + } + + if ( ! is_string( $section ) ) { + return; + } + + // Get the default values. + $defaults = [ + 'section' => $section, + ]; + + $args = array_merge( $defaults, $args ); + + $manager->add_control( + new $type( + $manager, + $setting_name, + $args + ) + ); + } + + /* CSS Output Functions */ + + public function setup_css_template( $template ) { + $template = $this->filter_css_template( $this->get_css_template( $template ) ); + + return $template; + } + + /** + * Overwrite this method to be able to implement the CSS template related to this section. + * + * @return string The CSS template. + */ + public function get_css_template( $template ) { + return $template; + } + + /** + * Filter the content headings arguments + * + * @since 4.13.3 + * + * @param array $arguments The list of arguments for headings and separators. + * + * @return array $arguments The filtered array of arguments. + */ + public function filter_css_template( $template ) { + /** + * Applies a filter to the css output. + * Note this is appended to the output - so it's not inside any selectors! + * + * @since 4.13.3 + * + * @param array $template Current set of callbacks for css output. + * @param static $instance The section instance we are dealing with. + */ + $template = apply_filters( 'tribe_customizer_section_css_template', $template, $this ); + + $section_slug = static::get_section_slug( get_class( $this ) ); + + /** + * Applies a filter to the css output for a specific section. Based on the section slug. + * Note this is appended to the output - so it's not inside any selectors! + * + * @since 4.13.3 + * + * @param array $template Current set of callbacks for css output. + * @param static $instance The section instance we are dealing with. + */ + $template = apply_filters( "tribe_customizer_section_{$section_slug}_css_template", $template, $this ); + + return $template; + } + + /** + * Utility function for when we need a color in RGB format, + * since the Customizer always works with hex. Keepin' it DRY. + * + * @since 4.14.2 + * + * @param string $option The option slug, like "grid-lines-color" + * @param string $section The optional section slug, like 'global_elements' + * + * @return string $color_rgb The hex color expressed as an rgb string, like "255,255,255" + */ + public function get_rgb_color( $option, $section = null ) { + $color = is_null( $section ) + ? tribe( 'customizer' )->get_option( [ $this->ID, $option ] ) + : tribe( 'customizer' )->get_option( [ $section, $option ] ); + + $color_obj = new Tribe__Utils__Color( $color ); + $color_arr = $color_obj->getRgb(); + $color_rgb = $color_arr['R'] . ',' . $color_arr['G'] . ',' . $color_arr['B']; + return $color_rgb; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/DB_Lock.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/DB_Lock.php new file mode 100644 index 000000000..cb9e1955b --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/DB_Lock.php @@ -0,0 +1,352 @@ + + */ + protected static $held_db_locks = []; + + /** + * Prunes the stale locks stored in the options table. + * + * @since 4.12.6 + * + * @return int|false The number of pruned locks, or `false` to indicate the query to prune the locks generated + * an error (logged). + */ + public static function prune_stale_db_locks() { + global $wpdb; + $prefix = static::$db_lock_option_prefix; + $affected_rows = $wpdb->query( + "DELETE FROM {$wpdb->options} + WHERE option_name LIKE '{$prefix}%' + AND option_value < ( UNIX_TIMESTAMP() - 86400 )" + ); + + if ( false === $affected_rows ) { + $log_data = [ + 'message' => 'Error while trying to prune stale db locks.', + 'error' => $wpdb->last_error + ]; + do_action( 'tribe_log', 'error', __CLASS__, $log_data ); + + return false; + } + + return (int) $affected_rows; + } + + /** + * Acquires a db lock. + * + * To ensure back-compatibility with MySQL 5.6, the lock will hash the lock key using SHA1. + * + * @since 4.12.6 + * + * @param string $lock_key The name of the db lock key to acquire. + * + * @return bool Whether the lock acquisition was successful or not. + */ + public function acquire_db_lock( $lock_key ) { + /** + * Filters the timeout, in seconds, of the database lock acquisition attempts. + * + * The timeout will not be used when locks are managed using queries in place of + * MySQL functions. + * + * @since 4.12.6 + * + * @param int $timeout The timeout, in seconds, of the lock acquisition attempt. + * @param string $lock_key The lock key the target of the acquisition attempt. + * @param static $this The object that's trying to acquire the lock by means of the trait. + */ + $timeout = apply_filters( 'tribe_db_lock_timeout', 3, $lock_key, $this ); + + if ( $this->manage_db_lock_w_mysql_functions() ) { + return $this->acquire_db_lock_w_mysql_functions( $lock_key, $timeout ); + } + + return $this->acquire_db_lock_w_queries( $lock_key ); + } + + /** + * Returns whether the traits should try to acquire and release locks using MySQL `GET_LOCK` and `RELEASE_LOCK` + * functions or not. + * + * If not, then the trait will manage the locks by means of direct SQL queries on the options table. + * + * @since 4.12.6 + * + * @return bool Whether the trait should use MySQL functions to manage the locks, or not. + */ + protected function manage_db_lock_w_mysql_functions() { + /** + * Filters whether the database lock should be acquired using the `GET_LOCK` and `RELEASE_LOCK` + * MySQL functions or not. + * + * If the filter returns a falsy value, then the trait will attempt to manage locks using `SELECT` + * and `UPDATE` queries on the options table. + * + * @since 4.12.6 + */ + return tribe_is_truthy( apply_filters( 'tribe_db_lock_use_msyql_functions', true ) ); + } + + /** + * Tries to acquire the database lock using MySQL functions (`GET_LOCK` and `IS_FREE_LOCK`). + * + * @since 4.12.6 + * + * @param string $lock_key The lock key to try and acquire the lock for. + * @param int $timeout The timeout, in seconds, to try and acquire the lock. + * + * @return bool Whether the lock was acquired or not. + */ + protected function acquire_db_lock_w_mysql_functions( $lock_key, $timeout ) { + /* + * On MySQL 5.6 if a session (a db connection) fires two requests of `GET_LOCK`, the lock is + * implicitly released and re-acquired. + * While this will not cause issues in the context of different db sessions (e.g. two diff. PHP + * processes competing for a lock), it would cause issues when the lock acquisition is attempted + * in the context of the same PHP process. + * To avoid a read-what-you-write issue in the context of the same request, we check if the lock is + * free, using `IS_FREE_LOCK` first. + */ + + global $wpdb; + + $free = $wpdb->get_var( + $wpdb->prepare( 'SELECT IS_FREE_LOCK( SHA1( %s ) )', $lock_key ) + ); + + if ( ! $free ) { + return false; + } + + $acquired = $wpdb->get_var( + $wpdb->prepare( 'SELECT GET_LOCK( SHA1( %s ),%d )', $lock_key, $timeout ) + + ); + + if ( false === $acquired ) { + // Only log errors, a failure to acquire lock is not an error. + $log_data = [ + 'message' => 'Error while trying to acquire lock.', + 'key' => $lock_key, + 'error' => $wpdb->last_error + ]; + do_action( 'tribe_log', 'error', __CLASS__, $log_data ); + + return false; + } + + return true; + } + + /** + * Tries to acquire the lock using SQL queries. + * + * This kind of lock does not support timeout to avoid sieging the MySQL server during processes + * that are most likely already stressing it. Either the lock is available the moment it's required or not. + * The method leverages `INSERT IGNORE` that it's available on MySQL 5.6 and is atomic provided one of the values + * we're trying to insert is UNIQUE or PRIMARY: `option_name` is UNIQUE in the `options` table. + * + * @since 4.12.6 + * + * @param string $lock_key The lock key to try and acquire the lock for. + * + * @return bool Whether the lock was acquired or not. + */ + protected function acquire_db_lock_w_queries( $lock_key ) { + global $wpdb; + $option_name = $this->get_db_lock_option_name( $lock_key ); + $lock_time = microtime( true ); + + //phpcs:disable + $rows_affected = $wpdb->query( + $wpdb->prepare( "INSERT IGNORE INTO {$wpdb->options} + (option_name, option_value, autoload) + VALUES + (%s, %s, 'no')", + $option_name, + $lock_time + ) + ); + //phpcs:enable + + if ( false === $rows_affected ) { + $log_data = [ + 'message' => 'Error while trying to acquire lock with database.', + 'key' => $lock_key, + 'option_name' => $option_name, + 'error' => $wpdb->last_error, + ]; + do_action( 'tribe_log', 'error', __CLASS__, $log_data ); + + return false; + } + + /* + * The `wpdb::query()` method will return the number of affected rows when using `INSERT`. + * 1 row affected means we could INSERT and have the lock, 0 rows affected means we could not INSERT + * and have not the lock. + */ + + if ( $rows_affected ) { + self::$held_db_locks[ $lock_key ] = $lock_time; + } + + return (bool) $rows_affected; + } + + /** + * Returns the option name used to manage the lock for a key in the options table. + * + * @since 4.12.6 + * + * @param string $lock_key The lock key to build the option name for. + * + * @return string The name of the option that will be used to manage the lock for the specified key in the + * options table. + */ + public function get_db_lock_option_name( $lock_key ) { + return self::$db_lock_option_prefix . $lock_key; + } + + /** + * Releases the database lock of the record. + * + * Release a not held db lock will return `null`, not `false`. + * + * @since 4.12.6 + * + * @param string $lock_key The name of the lock to release. + * + * @return bool Whether the lock was correctly released or not. + */ + public function release_db_lock( $lock_key ) { + if ( $this->manage_db_lock_w_mysql_functions() ) { + return $this->release_db_lock_w_mysql_functions( $lock_key ); + } + + return $this->release_db_lock_w_queries( $lock_key ); + } + + /** + * Releases a DB lock held by the current database session (`$wpdb` instance) by + * using the MySQL `RELEASE_LOCK` function. + * + * @since 4.12.6 + * + * @param string $lock_key The lock key to release the lock for. + * + * @return bool Whether the lock was correctly released or not. + */ + protected function release_db_lock_w_mysql_functions( $lock_key ) { + global $wpdb; + + $released = $wpdb->query( + $wpdb->prepare( "SELECT RELEASE_LOCK( SHA1( %s ) )", $lock_key ) + ); + + if ( false === $released ) { + $log_data = [ + 'message' => 'Error while trying to release lock.', + 'key' => $lock_key, + 'error' => $wpdb->last_error + ]; + do_action( 'tribe_log', 'error', __CLASS__, $log_data ); + + return false; + } + + return true; + } + + /** + * Releases a lock using SQL queries. + * + * Note: differently from the `release_db_lock_w_mysql_functions`, this method will release the lock + * even if the current session is not the one holding the lock. + * To protect from this the trait uses a map of registered locks and when the locks where registered. + * + * @since 4.12.6 + * + * @param string $lock_key The lock key to release the lock for. + * + * @return bool Whether the lock was released or not, errors will be logged, a `false` value is returned if + * the lock was not held to begin with. + */ + protected function release_db_lock_w_queries( $lock_key ) { + if ( ! isset( self::$held_db_locks[ $lock_key ] ) ) { + // Avoid sessions that do nothold the lock to release it. + return false; + } + + global $wpdb; + $option_name = $this->get_db_lock_option_name( $lock_key ); + //phpcs:disable + $rows_affected = $wpdb->delete( + $wpdb->options, + [ 'option_name' => $option_name ], + [ '%s' ] + ); + //phpcs:enable + + if ( false === $rows_affected ) { + $log_data = [ + 'message' => 'Error while trying to release lock with database.', + 'key' => $lock_key, + 'option_name' => $option_name, + 'error' => $wpdb->last_error, + ]; + do_action( 'tribe_log', 'error', __CLASS__, $log_data ); + + return false; + } + + if ( $rows_affected ) { + // Lock successfully released. + unset( self::$held_db_locks[ $lock_key ] ); + } + + return (bool) $rows_affected; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Data.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Data.php new file mode 100644 index 000000000..7679fa943 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Data.php @@ -0,0 +1,235 @@ + 'dolor' ); + * + * // by default return 'nope' when a value is not set in the data + * $data = new Tribe__Data( $my_data, 'nope' ); + * + * // set some values in the data + * $data['foo'] = 'bar'; + * $data['bar'] = 23; + * + * // fetch some values + * $var_1 = $data['foo']; // "bar" + * $var_2 = $data['bar']; // 23 + * $var_3 = $data['lorem']; // "dolor" + * $var_4 = $data['woo']; // "nope" + * + * $data->set_default( 'not found' ); + * + * $var_4 = $data['woo']; // "not found" + * + */ +class Tribe__Data implements ArrayAccess, Iterator { + /** + * @var int + */ + protected $index = 0; + + /** + * @var array The data managed by this object. + */ + protected $data; + + /** + * @var mixed The default value that will be returned when trying to get the value + * of a non set key. + */ + protected $default; + + /** + * Tribe__Data constructor. + * + * @param array|object $data An array or object of data. + * @param mixed $default The default value that should be returned if a key is not set + */ + public function __construct( $data = [], $default = false ) { + $this->data = (array) $data; + $this->default = $default; + } + + /** + * Whether a offset exists + * + * @link http://php.net/manual/en/arrayaccess.offsetexists.php + * @param mixed $offset

    + * An offset to check for. + *

    + * @return boolean true on success or false on failure. + *

    + *

    + * The return value will be casted to boolean if non-boolean was returned. + * @since 4.11.0 + */ + public function offsetExists( $offset ) { + return isset( $this->data[ $offset ] ); + } + + /** + * Offset to retrieve + * + * @link http://php.net/manual/en/arrayaccess.offsetget.php + * @param mixed $offset

    + * The offset to retrieve. + *

    + * @return mixed Can return all value types. + * @since 4.11.0 + */ + public function offsetGet( $offset ) { + return isset( $this->data[ $offset ] ) + ? $this->data[ $offset ] + : $this->default; + } + + /** + * Offset to set + * + * @link http://php.net/manual/en/arrayaccess.offsetset.php + * @param mixed $offset

    + * The offset to assign the value to. + *

    + * @param mixed $value

    + * The value to set. + *

    + * @return void + * @since 4.11.0 + */ + public function offsetSet( $offset, $value ) { + $this->data[ $offset ] = $value; + } + + /** + * Offset to unset + * + * @link http://php.net/manual/en/arrayaccess.offsetunset.php + * @param mixed $offset

    + * The offset to unset. + *

    + * @return void + * @since 4.11.0 + */ + public function offsetUnset( $offset ) { + unset( $this->data[ $offset ] ); + } + + /** + * Gets the data this object manages. + * + * @return array + */ + public function get_data() { + return $this->data; + } + + /** + * Sets the data this object will manage. + * + * @param array $data + */ + public function set_data( array $data ) { + $this->data = $data; + } + + /** + * Gets the default value that will be returned when a key is not set. + * + * @return mixed + */ + public function get_default() { + return $this->default; + } + + /** + * Sets the default value that should be returned when a key is not set. + * + * @param mixed $default + */ + public function set_default( $default ) { + $this->default = $default; + } + + /** + * Return the current element + * + * @link http://php.net/manual/en/iterator.current.php + * @return mixed Can return any type. + * @since 4.11.0 + */ + public function current() { + $keys = array_keys( $this->data ); + + return $this->data[ $keys[ $this->index ] ]; + } + + /** + * Move forward to next element + * + * @link http://php.net/manual/en/iterator.next.php + * @return void Any returned value is ignored. + * @since 4.11.0 + */ + public function next() { + $keys = array_keys( $this->data ); + + if ( isset( $keys[ ++ $this->index ] ) ) { + return $this->data[ $keys[ $this->index ] ]; + } + + return false; + } + + /** + * Return the key of the current element + * + * @link http://php.net/manual/en/iterator.key.php + * @return mixed scalar on success, or null on failure. + * @since 4.11.0 + */ + public function key() { + $keys = array_keys( $this->data ); + + return $keys[ $this->index ]; + } + + /** + * Checks if current position is valid + * + * @link http://php.net/manual/en/iterator.valid.php + * @return boolean The return value will be casted to boolean and then evaluated. + * Returns true on success or false on failure. + * @since 4.11.0 + */ + public function valid() { + $keys = array_keys( $this->data ); + + return isset( $keys[ $this->index ] ); + } + + /** + * Rewind the Iterator to the first element + * + * @link http://php.net/manual/en/iterator.rewind.php + * @return void Any returned value is ignored. + * @since 4.11.0 + */ + public function rewind() { + $this->index = 0; + } + + /** + * Converts the data object in an array. + * + * @return array + * + * @since 4.6 + */ + public function to_array() { + return $this->get_data(); + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Date_Utils.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Date_Utils.php new file mode 100644 index 000000000..d521915b1 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Date_Utils.php @@ -0,0 +1,1678 @@ + 'Y-m-d', + 1 => 'n/j/Y', + 2 => 'm/d/Y', + 3 => 'j/n/Y', + 4 => 'd/m/Y', + 5 => 'n-j-Y', + 6 => 'm-d-Y', + 7 => 'j-n-Y', + 8 => 'd-m-Y', + 9 => 'Y.m.d', + 10 => 'm.d.Y', + 11 => 'd.m.Y', + 'm0' => 'Y-m', + 'm1' => 'n/Y', + 'm2' => 'm/Y', + 'm3' => 'n/Y', + 'm4' => 'm/Y', + 'm5' => 'n-Y', + 'm6' => 'm-Y', + 'm7' => 'n-Y', + 'm8' => 'm-Y', + 'm9' => 'Y.m', + 'm10' => 'm.Y', + 'm11' => 'm.Y', + ]; + + if ( is_null( $translate ) ) { + return $formats; + } + + return isset( $formats[ $translate ] ) ? $formats[ $translate ] : $formats[ static::get_datepicker_format_index() ]; + } + + /** + * As PHP 5.2 doesn't have a good version of `date_parse_from_format`, this is how we deal with + * possible weird datepicker formats not working + * + * @param string $format The weird format you are using + * @param string $date The date string to parse + * + * @return string A DB formated Date, includes time if possible + */ + public static function datetime_from_format( $format, $date ) { + // Reverse engineer the relevant date formats + $keys = [ + // Year with 4 Digits + 'Y' => [ 'year', '\d{4}' ], + + // Year with 2 Digits + 'y' => [ 'year', '\d{2}' ], + + // Month with leading 0 + 'm' => [ 'month', '\d{2}' ], + + // Month without the leading 0 + 'n' => [ 'month', '\d{1,2}' ], + + // Month ABBR 3 letters + 'M' => [ 'month', '[A-Z][a-z]{2}' ], + + // Month Name + 'F' => [ 'month', '[A-Z][a-z]{2,8}' ], + + // Day with leading 0 + 'd' => [ 'day', '\d{2}' ], + + // Day without leading 0 + 'j' => [ 'day', '\d{1,2}' ], + + // Day ABBR 3 Letters + 'D' => [ 'day', '[A-Z][a-z]{2}' ], + + // Day Name + 'l' => [ 'day', '[A-Z][a-z]{5,8}' ], + + // Hour 12h formatted, with leading 0 + 'h' => [ 'hour', '\d{2}' ], + + // Hour 24h formatted, with leading 0 + 'H' => [ 'hour', '\d{2}' ], + + // Hour 12h formatted, without leading 0 + 'g' => [ 'hour', '\d{1,2}' ], + + // Hour 24h formatted, without leading 0 + 'G' => [ 'hour', '\d{1,2}' ], + + // Minutes with leading 0 + 'i' => [ 'minute', '\d{2}' ], + + // Seconds with leading 0 + 's' => [ 'second', '\d{2}' ], + ]; + + $date_regex = "/{$keys['Y'][1]}-{$keys['m'][1]}-{$keys['d'][1]}( {$keys['H'][1]}:{$keys['i'][1]}:{$keys['s'][1]})?$/"; + + // if the date is already in Y-m-d or Y-m-d H:i:s, just return it + if ( preg_match( $date_regex, $date ) ) { + return $date; + } + + + // Convert format string to regex + $regex = ''; + $chars = str_split( $format ); + foreach ( $chars as $n => $char ) { + $last_char = isset( $chars[ $n - 1 ] ) ? $chars[ $n - 1 ] : ''; + $skip_current = '\\' == $last_char; + if ( ! $skip_current && isset( $keys[ $char ] ) ) { + $regex .= '(?P<' . $keys[ $char ][0] . '>' . $keys[ $char ][1] . ')'; + } elseif ( '\\' == $char ) { + $regex .= $char; + } else { + $regex .= preg_quote( $char ); + } + } + + $dt = []; + + // Now try to match it + if ( preg_match( '#^' . $regex . '$#', $date, $dt ) ) { + // Remove unwanted Indexes + foreach ( $dt as $k => $v ) { + if ( is_int( $k ) ) { + unset( $dt[ $k ] ); + } + } + + // We need at least Month + Day + Year to work with + if ( ! checkdate( $dt['month'], $dt['day'], $dt['year'] ) ) { + return false; + } + } else { + return false; + } + + $dt['month'] = str_pad( $dt['month'], 2, '0', STR_PAD_LEFT ); + $dt['day'] = str_pad( $dt['day'], 2, '0', STR_PAD_LEFT ); + + $formatted = '{year}-{month}-{day}' . ( isset( $dt['hour'], $dt['minute'], $dt['second'] ) ? ' {hour}:{minute}:{second}' : '' ); + foreach ( $dt as $key => $value ) { + $formatted = str_replace( '{' . $key . '}', $value, $formatted ); + } + + return $formatted; + } + + /** + * Returns the date only. + * + * @param int|string $date The date (timestamp or string). + * @param bool $isTimestamp Is $date in timestamp format? + * @param string|null $format The format used + * + * @return string The date only in DB format. + */ + public static function date_only( $date, $isTimestamp = false, $format = null ) { + $date = $isTimestamp ? $date : strtotime( $date ); + + if ( is_null( $format ) ) { + $format = self::DBDATEFORMAT; + } + + return date( $format, $date ); + } + + /** + * Returns as string the nearest half a hour for a given valid string datetime. + * + * @since 4.10.2 + * + * @param string $date Valid DateTime string. + * + * @return string Rounded datetime string + */ + public static function round_nearest_half_hour( $date ) { + $date_object = static::build_date_object( $date ); + $rounded_minutes = floor( $date_object->format( 'i' ) / 30 ) * 30; + + return $date_object->format( 'Y-m-d H:' ) . $rounded_minutes . ':00'; + } + + /** + * Returns the time only. + * + * @param string $date The date. + * + * @return string The time only in DB format. + */ + public static function time_only( $date ) { + $date = is_numeric( $date ) ? $date : strtotime( $date ); + return date( self::DBTIMEFORMAT, $date ); + } + + /** + * Returns the hour only. + * + * @param string $date The date. + * + * @return string The hour only. + */ + public static function hour_only( $date ) { + $date = is_numeric( $date ) ? $date : strtotime( $date ); + return date( self::HOURFORMAT, $date ); + } + + /** + * Returns the minute only. + * + * @param string $date The date. + * + * @return string The minute only. + */ + public static function minutes_only( $date ) { + $date = is_numeric( $date ) ? $date : strtotime( $date ); + return date( self::MINUTEFORMAT, $date ); + } + + /** + * Returns the meridian (am or pm) only. + * + * @param string $date The date. + * + * @return string The meridian only in DB format. + */ + public static function meridian_only( $date ) { + $date = is_numeric( $date ) ? $date : strtotime( $date ); + return date( self::MERIDIANFORMAT, $date ); + } + + /** + * Returns the number of seconds (absolute value) between two dates/times. + * + * @param string $date1 The first date. + * @param string $date2 The second date. + * + * @return int The number of seconds between the dates. + */ + public static function time_between( $date1, $date2 ) { + return abs( strtotime( $date1 ) - strtotime( $date2 ) ); + } + + /** + * The number of days between two arbitrary dates. + * + * @param string $date1 The first date. + * @param string $date2 The second date. + * + * @return int The number of days between two dates. + */ + public static function date_diff( $date1, $date2 ) { + // Get number of days between by finding seconds between and dividing by # of seconds in a day + $days = self::time_between( $date1, $date2 ) / ( 60 * 60 * 24 ); + + return $days; + } + + /** + * Returns the last day of the month given a php date. + * + * @param int $timestamp THe timestamp. + * + * @return string The last day of the month. + */ + public static function get_last_day_of_month( $timestamp ) { + $curmonth = date( 'n', $timestamp ); + $curYear = date( 'Y', $timestamp ); + $nextmonth = mktime( 0, 0, 0, $curmonth + 1, 1, $curYear ); + $lastDay = strtotime( date( self::DBDATETIMEFORMAT, $nextmonth ) . ' - 1 day' ); + + return date( 'j', $lastDay ); + } + + /** + * Returns true if the timestamp is a weekday. + * + * @param int $curDate A timestamp. + * + * @return bool If the timestamp is a weekday. + */ + public static function is_weekday( $curdate ) { + return in_array( date( 'N', $curdate ), [ 1, 2, 3, 4, 5 ] ); + } + + /** + * Returns true if the timestamp is a weekend. + * + * @param int $curDate A timestamp. + * + * @return bool If the timestamp is a weekend. + */ + public static function is_weekend( $curdate ) { + return in_array( date( 'N', $curdate ), [ 6, 7 ] ); + } + + /** + * Gets the last day of the week in a month (ie the last Tuesday). Passing in -1 gives you the last day in the month. + * + * @param int $curdate A timestamp. + * @param int $day_of_week The index of the day of the week. + * + * @return int The timestamp of the date that fits the qualifications. + */ + public static function get_last_day_of_week_in_month( $curdate, $day_of_week ) { + $nextdate = mktime( date( 'H', $curdate ), date( 'i', $curdate ), date( 's', $curdate ), date( 'n', $curdate ), self::get_last_day_of_month( $curdate ), date( 'Y', $curdate ) );; + + while ( date( 'N', $nextdate ) != $day_of_week && $day_of_week != - 1 ) { + $nextdate = strtotime( date( self::DBDATETIMEFORMAT, $nextdate ) . ' - 1 day' ); + } + + return $nextdate; + } + + /** + * Gets the first day of the week in a month (ie the first Tuesday). + * + * @param int $curdate A timestamp. + * @param int $day_of_week The index of the day of the week. + * + * @return int The timestamp of the date that fits the qualifications. + */ + public static function get_first_day_of_week_in_month( $curdate, $day_of_week ) { + $nextdate = mktime( 0, 0, 0, date( 'n', $curdate ), 1, date( 'Y', $curdate ) ); + + while ( ! ( $day_of_week > 0 && date( 'N', $nextdate ) == $day_of_week ) && + ! ( $day_of_week == - 1 && self::is_weekday( $nextdate ) ) && + ! ( $day_of_week == - 2 && self::is_weekend( $nextdate ) ) ) { + $nextdate = strtotime( date( self::DBDATETIMEFORMAT, $nextdate ) . ' + 1 day' ); + } + + return $nextdate; + } + + /** + * From http://php.net/manual/en/function.date.php + * + * @param int $number A number. + * + * @return string The ordinal for that number. + */ + public static function number_to_ordinal( $number ) { + $output = $number . ( ( ( strlen( $number ) > 1 ) && ( substr( $number, - 2, 1 ) == '1' ) ) ? + 'th' : date( 'S', mktime( 0, 0, 0, 0, substr( $number, - 1 ), 0 ) ) ); + + return apply_filters( 'tribe_events_number_to_ordinal', $output, $number ); + } + + /** + * check if a given string is a timestamp + * + * @param $timestamp + * + * @return bool + */ + public static function is_timestamp( $timestamp ) { + if ( is_numeric( $timestamp ) && (int) $timestamp == $timestamp && date( 'U', $timestamp ) == $timestamp ) { + return true; + } + + return false; + } + + /** + * Accepts a string representing a date/time and attempts to convert it to + * the specified format, returning an empty string if this is not possible. + * + * @param $dt_string + * @param $new_format + * + * @return string + */ + public static function reformat( $dt_string, $new_format ) { + $timestamp = self::is_timestamp( $dt_string ) ? $dt_string : strtotime( $dt_string ); + $revised = date( $new_format, $timestamp ); + + return $revised ? $revised : ''; + } + + /** + * Accepts a numeric offset (such as "4" or "-6" as stored in the gmt_offset + * option) and converts it to a strtotime() style modifier that can be used + * to adjust a DateTime object, etc. + * + * @param $offset + * + * @return string + */ + public static function get_modifier_from_offset( $offset ) { + $modifier = ''; + $offset = (float) $offset; + + // Separate out hours, minutes, polarity + $hours = (int) $offset; + $minutes = (int) ( ( $offset - $hours ) * 60 ); + $polarity = ( $offset >= 0 ) ? '+' : '-'; + + // Correct hours and minutes to positive values + if ( $hours < 0 ) $hours *= -1; + if ( $minutes < 0 ) $minutes *= -1; + + // Form the modifier string + if ( $hours >= 0 ) $modifier = "$polarity $hours hours "; + if ( $minutes > 0 ) $modifier .= "$minutes minutes"; + + return $modifier; + } + + /** + * Returns the weekday of the 1st day of the month in + * "w" format (ie, Sunday is 0 and Saturday is 6) or + * false if this cannot be established. + * + * @param mixed $month + * @return int|bool + */ + public static function first_day_in_month( $month ) { + try { + $date = new DateTime( $month ); + $day_1 = new DateTime( $date->format( 'Y-m-01 ' ) ); + return $day_1->format( 'w' ); + } + catch ( Exception $e ) { + return false; + } + } + + /** + * Returns the weekday of the last day of the month in + * "w" format (ie, Sunday is 0 and Saturday is 6) or + * false if this cannot be established. + * + * @param mixed $month + * @return int|bool + */ + public static function last_day_in_month( $month ) { + try { + $date = new DateTime( $month ); + $day_1 = new DateTime( $date->format( 'Y-m-t' ) ); + return $day_1->format( 'w' ); + } + catch ( Exception $e ) { + return false; + } + } + + /** + * Returns the day of the week the week ends on, expressed as a "w" value + * (ie, Sunday is 0 and Saturday is 6). + * + * @param int $week_starts_on + * + * @return int + */ + public static function week_ends_on( $week_starts_on ) { + if ( --$week_starts_on < 0 ) $week_starts_on = 6; + return $week_starts_on; + } + + /** + * Helper method to convert EventAllDay values to a boolean + * + * @param mixed $all_day_value Value to check for "all day" status. All day values: (true, 'true', 'TRUE', 'yes') + * + * @return boolean Is value considered "All Day"? + */ + public static function is_all_day( $all_day_value ) { + $all_day_value = trim( $all_day_value ); + + return ( + 'true' === strtolower( $all_day_value ) + || 'yes' === strtolower( $all_day_value ) + || true === $all_day_value + || 1 == $all_day_value + ); + } + + /** + * Determine if "now" is between two dates. + * + * @since 5.0.2 + * + * @param string|DateTime|int $start_date A `strtotime` parsable string, a DateTime object or a timestamp. + * @param string|DateTime|int $end_date A `strtotime` parsable string, a DateTime object or a timestamp. + * @param string|DateTime|int $now A `strtotime` parsable string, a DateTime object or a timestamp. Defaults to 'now'. + * + * @return boolean Whether the current datetime (or passed "now") is between the passed start and end dates. + */ + public static function is_now( $start_date, $end_date, $now = 'now' ) : bool { + $now = self::build_date_object( $now ); + $start_date = self::build_date_object( $start_date ); + $end_date = self::build_date_object( $end_date ); + + // If the dates are identical, bail early. + if ( $start_date === $end_date ) { + return false; + } + + // Handle dates passed out of chronological order. + [ $start_date, $end_date ] = self::sort( [ $start_date, $end_date ] ); + + // If span starts after now, return false. + if ( $start_date > $now ) { + return false; + } + + // If span ends on or before now, return false. + if ( $end_date <= $now ) { + return false; + } + + return true; + } + + /** + * Sort an array of dates. + * + * @since 5.0.2 + * + * @param mixed $dates A single array of dates, or dates passed as individual params. + * Individual dates can be a `strtotime` parsable string, a DateTime object or a timestamp. + * @param string $direction 'ASC' or 'DESC' for ascending/descending sorting. Defaults to 'ASC'. + * + * @return array A sorted array of DateTime objects. + */ + public static function sort( array $dates, string $direction = 'ASC' ) :array { + // If we get passed a single array, break it out of the containing array. + if ( is_array( $dates[0] ) ) { + $dates = $dates[0]; + } + + // Ensure we're always dealing with date objects here. + $dates = array_map( + function( $date ) { + return self::build_date_object( $date ); + }, + $dates + ); + + // If anything other than 'DESC' gets passed (or nothing) we sort ascending. + if ( 'DESC' === $direction ) { + rsort( $dates ); + } else { + sort( $dates ); + } + + return $dates; + } + + /** + * Given 2 datetime ranges, return whether the 2nd one occurs during the 1st one + * Note: all params should be unix timestamps + * + * @param integer $range_1_start timestamp for start of the first range + * @param integer $range_1_end timestamp for end of the first range + * @param integer $range_2_start timestamp for start of the second range + * @param integer $range_2_end timestamp for end of the second range + * + * @return bool + */ + public static function range_coincides( $range_1_start, $range_1_end, $range_2_start, $range_2_end ) { + + // Initialize the return value + $range_coincides = false; + + /** + * conditions: + * range 2 starts during range 1 (range 2 start time is between start and end of range 1 ) + * range 2 ends during range 1 (range 2 end time is between start and end of range 1 ) + * range 2 encloses range 1 (range 2 starts before range 1 and ends after range 1) + */ + + $range_2_starts_during_range_1 = $range_2_start >= $range_1_start && $range_2_start < $range_1_end; + $range_2_ends_during_range_1 = $range_2_end > $range_1_start && $range_2_end <= $range_1_end; + $range_2_encloses_range_1 = $range_2_start < $range_1_start && $range_2_end > $range_1_end; + + if ( $range_2_starts_during_range_1 || $range_2_ends_during_range_1 || $range_2_encloses_range_1 ) { + $range_coincides = true; + } + + return $range_coincides; + + } + + /** + * Converts a locally-formatted date to a unix timestamp. This is a drop-in + * replacement for `strtotime()`, except that where strtotime assumes GMT, this + * assumes local time (as described below). If a timezone is specified, this + * function defers to strtotime(). + * + * If there is a timezone_string available, the date is assumed to be in that + * timezone, otherwise it simply subtracts the value of the 'gmt_offset' + * option. + * + * @see strtotime() + * @uses get_option() to retrieve the value of 'gmt_offset' + * + * @param string $string A date/time string. See `strtotime` for valid formats + * + * @return int UNIX timestamp. + */ + public static function wp_strtotime( $string ) { + // If there's a timezone specified, we shouldn't convert it + try { + $test_date = new DateTime( $string ); + if ( 'UTC' != $test_date->getTimezone()->getName() ) { + return strtotime( $string ); + } + } catch ( Exception $e ) { + return strtotime( $string ); + } + + $cache = tribe( 'cache' ); + if ( ! isset( $cache['option_timezone_string'] ) ) { + $cache['option_timezone_string'] = get_option( 'timezone_string' ); + } + if ( ! isset( $cache['option_gmt_offset'] ) ) { + $cache['option_gmt_offset'] = get_option( 'gmt_offset' ); + } + + $tz = $cache['option_timezone_string']; + if ( ! empty( $tz ) ) { + $date = date_create( $string, new DateTimeZone( $tz ) ); + if ( ! $date ) { + return strtotime( $string ); + } + $date->setTimezone( new DateTimeZone( 'UTC' ) ); + return $date->format( 'U' ); + } else { + $offset = (float) $cache['option_gmt_offset']; + $seconds = intval( $offset * HOUR_IN_SECONDS ); + $timestamp = strtotime( $string ) - $seconds; + return $timestamp; + } + } + + /** + * Returns an array of localized full month names. + * + * @return array + */ + public static function get_localized_months_full() { + global $wp_locale; + + if ( empty( self::$localized_months ) ) { + self::build_localized_months(); + } + + if ( empty( self::$localized_months_full ) ) { + self::$localized_months_full = [ + 'January' => self::$localized_months['full']['01'], + 'February' => self::$localized_months['full']['02'], + 'March' => self::$localized_months['full']['03'], + 'April' => self::$localized_months['full']['04'], + 'May' => self::$localized_months['full']['05'], + 'June' => self::$localized_months['full']['06'], + 'July' => self::$localized_months['full']['07'], + 'August' => self::$localized_months['full']['08'], + 'September' => self::$localized_months['full']['09'], + 'October' => self::$localized_months['full']['10'], + 'November' => self::$localized_months['full']['11'], + 'December' => self::$localized_months['full']['12'], + ]; + } + + return self::$localized_months_full; + } + + /** + * Returns an array of localized short month names. + * + * @return array + */ + public static function get_localized_months_short() { + global $wp_locale; + + if ( empty( self::$localized_months ) ) { + self::build_localized_months(); + } + + if ( empty( self::$localized_months_short ) ) { + self::$localized_months_short = [ + 'Jan' => self::$localized_months['short']['01'], + 'Feb' => self::$localized_months['short']['02'], + 'Mar' => self::$localized_months['short']['03'], + 'Apr' => self::$localized_months['short']['04'], + 'May' => self::$localized_months['short']['05'], + 'Jun' => self::$localized_months['short']['06'], + 'Jul' => self::$localized_months['short']['07'], + 'Aug' => self::$localized_months['short']['08'], + 'Sep' => self::$localized_months['short']['09'], + 'Oct' => self::$localized_months['short']['10'], + 'Nov' => self::$localized_months['short']['11'], + 'Dec' => self::$localized_months['short']['12'], + ]; + } + + return self::$localized_months_short; + } + + /** + * Returns an array of localized full week day names. + * + * @return array + */ + public static function get_localized_weekdays_full() { + if ( empty( self::$localized_weekdays ) ) { + self::build_localized_weekdays(); + } + + return self::$localized_weekdays['full']; + } + + /** + * Returns an array of localized short week day names. + * + * @return array + */ + public static function get_localized_weekdays_short() { + if ( empty( self::$localized_weekdays ) ) { + self::build_localized_weekdays(); + } + + return self::$localized_weekdays['short']; + } + + /** + * Returns an array of localized week day initials. + * + * @return array + */ + public static function get_localized_weekdays_initial() { + if ( empty( self::$localized_weekdays ) ) { + self::build_localized_weekdays(); + } + + return self::$localized_weekdays['initial']; + } + + /** + * Builds arrays of localized full, short and initialized weekdays. + */ + private static function build_localized_weekdays() { + global $wp_locale; + + for ( $i = 0; $i <= 6; $i++ ) { + $day = $wp_locale->get_weekday( $i ); + self::$localized_weekdays['full'][ $i ] = $day; + self::$localized_weekdays['short'][ $i ] = $wp_locale->get_weekday_abbrev( $day ); + self::$localized_weekdays['initial'][ $i ] = $wp_locale->get_weekday_initial( $day ); + } + } + + /** + * Builds arrays of localized full and short months. + * + * @since 4.4.3 + */ + private static function build_localized_months() { + global $wp_locale; + + for ( $i = 1; $i <= 12; $i++ ) { + $month_number = str_pad( $i, 2, '0', STR_PAD_LEFT ); + $month = $wp_locale->get_month( $month_number ); + self::$localized_months['full'][ $month_number ] = $month; + self::$localized_months['short'][ $month_number ] = $wp_locale->get_month_abbrev( $month ); + } + } + + /** + * Return a WP Locale weekday in the specified format + * + * @since 4.4.3 + * + * @param int|string $weekday Day of week + * @param string $format Weekday format: full, weekday, initial, abbreviation, abbrev, abbr, short + * + * @return string + */ + public static function wp_locale_weekday( $weekday, $format = 'weekday' ) { + $weekday = trim( $weekday ); + + $valid_formats = [ + 'full', + 'weekday', + 'initial', + 'abbreviation', + 'abbrev', + 'abbr', + 'short', + ]; + + // if there isn't a valid format, bail without providing a localized string + if ( ! in_array( $format, $valid_formats ) ) { + return $weekday; + } + + if ( empty( self::$localized_weekdays ) ) { + self::build_localized_weekdays(); + } + + // if the weekday isn't numeric, we need to convert to numeric in order to + // leverage self::localized_weekdays + if ( ! is_numeric( $weekday ) ) { + $days_of_week = [ + 'Sun', + 'Mon', + 'Tue', + 'Wed', + 'Thu', + 'Fri', + 'Sat', + ]; + + $day_index = array_search( ucwords( substr( $weekday, 0, 3 ) ), $days_of_week ); + + if ( false === $day_index ) { + return $weekday; + } + + $weekday = $day_index; + } + + switch ( $format ) { + case 'initial': + $type = 'initial'; + break; + case 'abbreviation': + case 'abbrev': + case 'abbr': + case 'short': + $type = 'short'; + break; + case 'weekday': + case 'full': + default: + $type = 'full'; + break; + } + + return self::$localized_weekdays[ $type ][ $weekday ]; + } + + /** + * Return a WP Locale month in the specified format + * + * @since 4.4.3 + * + * @param int|string $month Month of year + * @param string $format Month format: full, month, abbreviation, abbrev, abbr, short + * + * @return string + */ + public static function wp_locale_month( $month, $format = 'month' ) { + $month = trim( $month ); + + $valid_formats = [ + 'full', + 'month', + 'abbreviation', + 'abbrev', + 'abbr', + 'short', + ]; + + // if there isn't a valid format, bail without providing a localized string + if ( ! in_array( $format, $valid_formats ) ) { + return $month; + } + + if ( empty( self::$localized_months ) ) { + self::build_localized_months(); + } + + // make sure numeric months are valid + if ( is_numeric( $month ) ) { + $month_num = (int) $month; + + // if the month num falls out of range, bail without localizing + if ( 0 > $month_num || 12 < $month_num ) { + return $month; + } + } else { + $months = [ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'May', + 'Jun', + 'Jul', + 'Aug', + 'Sep', + 'Oct', + 'Nov', + 'Dec', + ]; + + // convert the provided month to a 3-character month and find it in the months array so we + // can build an appropriate month number + $month_num = array_search( ucwords( substr( $month, 0, 3 ) ), $months ); + + // if we can't find the provided month in our month list, bail without localizing + if ( false === $month_num ) { + return $month; + } + + // let's increment the num because months start at 01 rather than 00 + $month_num++; + } + + $month_num = str_pad( $month_num, 2, '0', STR_PAD_LEFT ); + + $type = ( 'full' === $format || 'month' === $format ) ? 'full' : 'short'; + + return self::$localized_months[ $type ][ $month_num ]; + } + + // DEPRECATED METHODS + // @codingStandardsIgnoreStart + /** + * Deprecated camelCase version of self::date_only + * + * @param int|string $date The date (timestamp or string). + * @param bool $isTimestamp Is $date in timestamp format? + * + * @return string The date only in DB format. + */ + public static function dateOnly( $date, $isTimestamp = false ) { + _deprecated_function( __METHOD__, '3.11', __CLASS__ . '::date_only' ); + return self::date_only( $date, $isTimestamp ); + } + + /** + * Deprecated camelCase version of self::time_only + * + * @param string $date The date. + * + * @return string The time only in DB format. + */ + public static function timeOnly( $date ) { + _deprecated_function( __METHOD__, '3.11', __CLASS__ . '::time_only' ); + return self::time_only( $date ); + } + + /** + * Deprecated camelCase version of self::hour_only + * + * @param string $date The date. + * + * @return string The hour only. + */ + public static function hourOnly( $date ) { + _deprecated_function( __METHOD__, '3.11', __CLASS__ . '::hour_only' ); + return self::hour_only( $date ); + } + + /** + * Deprecated camelCase version of self::minutes_only + * + * @param string $date The date. + * + * @return string The minute only. + */ + public static function minutesOnly( $date ) { + _deprecated_function( __METHOD__, '3.11', __CLASS__ . '::minutes_only' ); + return self::minutes_only( $date ); + } + + /** + * Deprecated camelCase version of self::meridian_only + * + * @param string $date The date. + * + * @return string The meridian only in DB format. + */ + public static function meridianOnly( $date ) { + _deprecated_function( __METHOD__, '3.11', __CLASS__ . '::meridian_only' ); + return self::meridian_only( $date ); + } + + /** + * Returns the end of a given day. + * + * @deprecated since 3.10 - use tribe_event_end_of_day() + * @todo remove in 4.1 + * + * @param int|string $date The date (timestamp or string). + * @param bool $isTimestamp Is $date in timestamp format? + * + * @return string The date and time of the end of a given day + */ + public static function endOfDay( $date, $isTimestamp = false ) { + _deprecated_function( __METHOD__, '3.10', 'tribe_event_end_of_day' ); + + if ( $isTimestamp ) { + $date = date( self::DBDATEFORMAT, $date ); + } + + return tribe_event_end_of_day( $date, self::DBDATETIMEFORMAT ); + } + + /** + * Returns the beginning of a given day. + * + * @deprecated since 3.10 + * @todo remove in 4.1 + * + * @param int|string $date The date (timestamp or string). + * @param bool $isTimestamp Is $date in timestamp format? + * + * @return string The date and time of the beginning of a given day. + */ + public static function beginningOfDay( $date, $isTimestamp = false ) { + _deprecated_function( __METHOD__, '3.10', 'tribe_event_beginning_of_day' ); + + if ( $isTimestamp ) { + $date = date( self::DBDATEFORMAT, $date ); + } + + return tribe_event_beginning_of_day( $date, self::DBDATETIMEFORMAT ); + } + + /** + * Deprecated camelCase version of self::time_between + * + * @param string $date1 The first date. + * @param string $date2 The second date. + * + * @return int The number of seconds between the dates. + */ + public static function timeBetween( $date1, $date2 ) { + _deprecated_function( __METHOD__, '3.11', __CLASS__ . '::time_between' ); + return self::time_between( $date1, $date2 ); + } + + /** + * Deprecated camelCase version of self::date_diff + * + * @param string $date1 The first date. + * @param string $date2 The second date. + * + * @return int The number of days between two dates. + */ + public static function dateDiff( $date1, $date2 ) { + _deprecated_function( __METHOD__, '3.11', __CLASS__ . '::date_diff' ); + return self::date_diff( $date1, $date2 ); + } + + /** + * Deprecated camelCase version of self::get_last_day_of_month + * + * @param int $timestamp THe timestamp. + * + * @return string The last day of the month. + */ + public static function getLastDayOfMonth( $timestamp ) { + _deprecated_function( __METHOD__, '3.11', __CLASS__ . '::get_last_day_of_month' ); + return self::get_last_day_of_month( $timestamp ); + } + + /** + * Deprecated camelCase version of self::is_weekday + * + * @param int $curDate A timestamp. + * + * @return bool If the timestamp is a weekday. + */ + public static function isWeekday( $curdate ) { + _deprecated_function( __METHOD__, '3.11', __CLASS__ . '::is_weekday' ); + return self::is_weekday( $curdate ); + } + + /** + * Deprecated camelCase version of self::is_weekend + * + * @param int $curDate A timestamp. + * + * @return bool If the timestamp is a weekend. + */ + public static function isWeekend( $curdate ) { + _deprecated_function( __METHOD__, '3.11', __CLASS__ . '::is_weekend' ); + return self::is_weekend( $curdate ); + } + + /** + * Deprecated camelCase version of self::get_last_day_of_week_in_month + * + * @param int $curdate A timestamp. + * @param int $day_of_week The index of the day of the week. + * + * @return int The timestamp of the date that fits the qualifications. + */ + public static function getLastDayOfWeekInMonth( $curdate, $day_of_week ) { + _deprecated_function( __METHOD__, '3.11', __CLASS__ . '::get_last_day_of_week_in_month' ); + return self::get_last_day_of_week_in_month( $curdate, $day_of_week ); + } + + /** + * Deprecated camelCase version of self::get_first_day_of_week_in_month + * + * @param int $curdate A timestamp. + * @param int $day_of_week The index of the day of the week. + * + * @return int The timestamp of the date that fits the qualifications. + */ + public static function getFirstDayOfWeekInMonth( $curdate, $day_of_week ) { + _deprecated_function( __METHOD__, '3.11', __CLASS__ . '::get_fist_day_of_week_in_month' ); + return self::get_first_day_of_week_in_month( $curdate, $day_of_week ); + } + + /** + * Deprecated camelCase version of self::number_to_ordinal + * + * @param int $number A number. + * + * @return string The ordinal for that number. + */ + public static function numberToOrdinal( $number ) { + _deprecated_function( __METHOD__, '3.11', __CLASS__ . '::number_to_ordinal' ); + return self::number_to_ordinal( $number ); + } + + /** + * Deprecated camelCase version of self::is_timestamp + * + * @param $timestamp + * + * @return bool + */ + public static function isTimestamp( $timestamp ) { + _deprecated_function( __METHOD__, '3.11', __CLASS__ . '::is_timestamp' ); + return self::is_timestamp( $timestamp ); + } + + /** + * Gets the timestamp of a day in week, month and year context. + * + * Kudos to [icedwater StackOverflow user](http://stackoverflow.com/users/1091386/icedwater) in + * [his answer](http://stackoverflow.com/questions/924246/get-the-first-or-last-friday-in-a-month). + * + * Usage examples: + * "The second Wednesday of March 2015" - `get_day_timestamp( 3, 2, 3, 2015, 1)` + * "The last Friday of December 2015" - `get_day_timestamp( 5, 1, 12, 2015, -1)` + * "The first Monday of April 2016 - `get_day_timestamp( 1, 1, 4, 2016, 1)` + * "The penultimate Thursday of January 2012" - `get_day_timestamp( 4, 2, 1, 2012, -1)` + * + * @param int $day_of_week The day representing the number in the week, Monday is `1`, Tuesday is `2`, Sunday is `7` + * @param int $week_in_month The week number in the month; first week is `1`, second week is `2`; when direction is reverse + * then `1` is last week of the month, `2` is penultimate week of the month and so on. + * @param int $month The month number in the year, January is `1` + * @param int $year The year number, e.g. "2015" + * @param int $week_direction Either `1` or `-1`; the direction for the search referring to the week, defaults to `1` + * to specify weeks in natural order so: + * $week_direction `1` and $week_in_month `1` means "first week of the month" + * $week_direction `1` and $week_in_month `3` means "third week of the month" + * $week_direction `-1` and $week_in_month `1` means "last week of the month" + * $week_direction `-1` and $week_in_month `2` means "penultimmate week of the month" + * + * @return int The day timestamp + */ + public static function get_weekday_timestamp( $day_of_week, $week_in_month, $month, $year, $week_direction = 1 ) { + if ( + ! ( + is_numeric( $day_of_week ) + && is_numeric( $week_in_month ) + && is_numeric( $month ) + && is_numeric( $year ) + && is_numeric( $week_direction ) + && in_array( $week_direction, [ -1, 1 ] ) + ) + ) { + return false; + } + + if ( $week_direction > 0 ) { + $startday = 1; + } else { + $startday = date( 't', mktime( 0, 0, 0, $month, 1, $year ) ); + } + + $start = mktime( 0, 0, 0, $month, $startday, $year ); + $weekday = date( 'N', $start ); + + if ( $week_direction * $day_of_week >= $week_direction * $weekday ) { + $offset = - $week_direction * 7; + } else { + $offset = 0; + } + + $offset += $week_direction * ( $week_in_month * 7 ) + ( $day_of_week - $weekday ); + + return mktime( 0, 0, 0, $month, $startday + $offset, $year ); + } + + /** + * Unescapes date format strings to be used in functions like `date`. + * + * Double escaping happens when storing a date format in the database. + * + * @param mixed $date_format A date format string. + * + * @return mixed Either the original input or an unescaped date format string. + */ + public static function unescape_date_format( $date_format ) { + if ( ! is_string( $date_format ) ) { + return $date_format; + } + + // Why so simple? Let's handle other cases as those come up. We have tests in place! + return str_replace( '\\\\', '\\', $date_format ); + } + + /** + * Builds a date object from a given datetime and timezone. + * + * @since 4.9.5 + * + * @param string|DateTime|int $datetime A `strtotime` parsable string, a DateTime object or + * a timestamp; defaults to `now`. + * @param string|DateTimeZone|null $timezone A timezone string, UTC offset or DateTimeZone object; + * defaults to the site timezone; this parameter is ignored + * if the `$datetime` parameter is a DatTime object. + * @param bool $with_fallback Whether to return a DateTime object even when the date data is + * invalid or not; defaults to `true`. + * + * @return DateTime|false A DateTime object built using the specified date, time and timezone; if `$with_fallback` + * is set to `false` then `false` will be returned if a DateTime object could not be built. + */ + public static function build_date_object( $datetime = 'now', $timezone = null, $with_fallback = true ) { + if ( $datetime instanceof DateTime ) { + return clone $datetime; + } + + if ( class_exists( 'DateTimeImmutable' ) && $datetime instanceof DateTimeImmutable ) { + // Return the mutable version of the date. + return Date_I18n::createFromImmutable( $datetime ); + } + + $timezone_object = null; + $datetime = empty( $datetime ) ? 'now' : $datetime; + + try { + // PHP 5.2 will not throw an exception but will generate an error. + $utc = new DateTimeZone( 'UTC' ); + $timezone_object = Tribe__Timezones::build_timezone_object( $timezone ); + + if ( self::is_timestamp( $datetime ) ) { + $timestamp_timezone = $timezone ? $timezone_object : $utc; + + return new Date_I18n( '@' . $datetime, $timestamp_timezone ); + } + + set_error_handler( 'tribe_catch_and_throw' ); + $date = new Date_I18n( $datetime, $timezone_object ); + restore_error_handler(); + } catch ( Exception $e ) { + // If we encounter an error, we need to restore after catching. + restore_error_handler(); + + if ( $timezone_object === null ) { + $timezone_object = Tribe__Timezones::build_timezone_object( $timezone ); + } + + return $with_fallback + ? new Date_I18n( 'now', $timezone_object ) + : false; + } + + return $date; + } + + /** + * Validates a date string to make sure it can be used to build DateTime objects. + * + * @since 4.9.5 + * + * @param string $date The date string that should validated. + * + * @return bool Whether the date string can be used to build DateTime objects, and is thus parsable by functions + * like `strtotime`, or not. + */ + public static function is_valid_date( $date ) { + static $cache_var_name = __FUNCTION__; + + $cache_date_check = tribe_get_var( $cache_var_name, [] ); + + if ( isset( $cache_date_check[ $date ] ) ) { + return $cache_date_check[ $date ]; + } + + $cache_date_check[ $date ] = self::build_date_object( $date, null, false ) instanceof DateTimeInterface; + + tribe_set_var( $cache_var_name, $cache_date_check ); + + return $cache_date_check[ $date ]; + } + + /** + * Returns the DateTime object representing the start of the week for a date. + * + * @since 4.9.21 + * + * @throws Exception + * + * @param string|int|\DateTime $date The date string, timestamp or object. + * @param int|null $start_of_week The number representing the start of week day as handled by + * WordPress: `0` (for Sunday) through `6` (for Saturday). + * + * @return array An array of objects representing the week start and end days, or `false` if the + * supplied date is invalid. The timezone of the returned object is set to the site one. + * The week start has its time set to `00:00:00`, the week end will have its time set + * `23:59:59`. + */ + public static function get_week_start_end( $date, $start_of_week = null ) { + static $cache_var_name = __FUNCTION__; + + $cache_week_start_end = tribe_get_var( $cache_var_name, [] ); + + $date_obj = static::build_date_object( $date ); + $date_obj->setTime( 0, 0, 0 ); + + $date_string = $date_obj->format( static::DBDATEFORMAT ); + + // `0` (for Sunday) through `6` (for Saturday), the way WP handles the `start_of_week` option. + $week_start_day = null !== $start_of_week + ? (int) $start_of_week + : (int) get_option( 'start_of_week', 0 ); + + $memory_cache_key = "{$date_string}:{$week_start_day}"; + + if ( isset( $cache_week_start_end[ $memory_cache_key ] ) ) { + return $cache_week_start_end[ $memory_cache_key ]; + } + + $cache_key = md5( + __METHOD__ . serialize( [ $date_obj->format( static::DBDATEFORMAT ), $week_start_day ] ) + ); + $cache = tribe( 'cache' ); + + if ( false !== $cached = $cache[ $cache_key ] ) { + return $cached; + } + + // `0` (for Sunday) through `6` (for Saturday), the way WP handles the `start_of_week` option. + $date_day = (int) $date_obj->format( 'w' ); + + $week_offset = 0; + if ( 0 === $date_day && 0 !== $week_start_day ) { + $week_offset = 0; + } elseif ( $date_day < $week_start_day ) { + // If the current date of the week is before the start of the week, move back a week. + $week_offset = -1; + } elseif ( 0 === $date_day ) { + // When start of the week is on a sunday we add a week. + $week_offset = 1; + } + + $week_start = clone $date_obj; + + /* + * From the PHP docs, the `W` format stands for: + * - ISO-8601 week number of year, weeks starting on Monday + */ + $week_start->setISODate( + (int) $week_start->format( 'o' ), + (int) $week_start->format( 'W' ) + $week_offset, + $week_start_day + ); + + $week_end = clone $week_start; + // Add 6 days, then move at the end of the day. + $week_end->add( new DateInterval( 'P6D' ) ); + $week_end->setTime( 23, 59, 59 ); + + $week_start = static::immutable( $week_start ); + $week_end = static::immutable( $week_end ); + + $cache[ $cache_key ] = [ $week_start, $week_end ]; + $cache_week_start_end[ $memory_cache_key ] = [ $week_start, $week_end ]; + + tribe_set_var( $cache_var_name, $cache_week_start_end ); + + return [ $week_start, $week_end ]; + } + + /** + * Given a specific DateTime we determine the end of that day based on our Internal End of Day Cut-off. + * + * @since 4.11.2 + * + * @param string|DateTimeInterface $date Date that we are getting the end of day from. + * @param null|string $cutoff Which cutoff to use. + * + * @return DateTimeInterface|false Returns a DateTimeInterface when a valid date is given or false. + */ + public static function get_shifted_end_of_day( $date, $cutoff = null ) { + $date_obj = static::build_date_object( $date ); + + if ( ! $date_obj ) { + return false; + } + + $start_of_day = clone $date_obj; + $end_of_day = clone $date_obj; + + if ( empty( $cutoff ) || ! is_string( $cutoff ) || false === strpos( $cutoff, ':' ) ) { + $cutoff = tribe_get_option( 'multiDayCutoff', '00:00' ); + } + + list( $hours_to_add, $minutes_to_add ) = array_map( 'absint', explode( ':', $cutoff ) ); + + $seconds_to_add = ( $hours_to_add * HOUR_IN_SECONDS ) + ( $minutes_to_add * MINUTE_IN_SECONDS ); + if ( 0 !== $seconds_to_add ) { + $interval = static::interval( "PT{$seconds_to_add}S" ); + } + + $start_of_day->setTime( '0', '0', '0' ); + $end_of_day->setTime( '23', '59', '59' ); + + if ( 0 !== $seconds_to_add ) { + $start_of_day->add( $interval ); + $end_of_day->add( $interval ); + } + + if ( $end_of_day >= $date_obj && $date_obj >= $start_of_day ) { + return $end_of_day; + } + + $start_of_day->sub( static::interval( 'P1D' ) ); + + if ( $start_of_day < $date_obj ) { + $end_of_day->sub( static::interval( 'P1D' ) ); + } + + return $end_of_day; + } + + /** + * Given a specific DateTime we determine the start of that day based on our Internal End of Day Cut-off. + * + * @since 4.11.2 + * + * @param string|DateTimeInterface $date Date that we are getting the start of day from. + * @param null|string $cutoff Which cutoff to use. + * + * @return DateTimeInterface|false Returns a DateTimeInterface when a valid date is given or false. + */ + public static function get_shifted_start_of_day( $date, $cutoff = null ) { + $date_obj = static::build_date_object( $date ); + + if ( ! $date_obj ) { + return false; + } + + $start_of_day = clone $date_obj; + $end_of_day = clone $date_obj; + + if ( empty( $cutoff ) || ! is_string( $cutoff ) || false === strpos( $cutoff, ':' ) ) { + $cutoff = tribe_get_option( 'multiDayCutoff', '00:00' ); + } + + list( $hours_to_add, $minutes_to_add ) = array_map( 'absint', explode( ':', $cutoff ) ); + + $seconds_to_add = ( $hours_to_add * HOUR_IN_SECONDS ) + ( $minutes_to_add * MINUTE_IN_SECONDS ); + if ( 0 !== $seconds_to_add ) { + $interval = static::interval( "PT{$seconds_to_add}S" ); + } + + $start_of_day->setTime( '0', '0', '0' ); + $end_of_day->setTime( '23', '59', '59' ); + + if ( 0 !== $seconds_to_add ) { + $start_of_day->add( $interval ); + $end_of_day->add( $interval ); + } + + if ( $end_of_day <= $date_obj && $date_obj >= $start_of_day ) { + return $start_of_day; + } + + $end_of_day->sub( static::interval( 'P1D' ) ); + + if ( $end_of_day > $date_obj ) { + $start_of_day->sub( static::interval( 'P1D' ) ); + } + + return $start_of_day; + } + + /** + * Builds and returns a `DateInterval` object from the interval specification. + * + * For performance purposes the use of `DateInterval` specifications is preferred, so `P1D` is better than + * `1 day`. + * + * @since 4.10.2 + * + * @return DateInterval The built date interval object. + */ + public static function interval( $interval_spec ) { + try { + $interval = new \DateInterval( $interval_spec ); + } catch ( \Exception $e ) { + $interval = DateInterval::createFromDateString( $interval_spec ); + } + + return $interval; + } + + /** + * Builds the immutable version of a date from a string, integer (timestamp) or \DateTime object. + * + * It's the immutable version of the `Tribe__Date_Utils::build_date_object` method. + * + * @since 4.10.2 + * + * @param string|DateTime|int $datetime A `strtotime` parsable string, a DateTime object or + * a timestamp; defaults to `now`. + * @param string|DateTimeZone|null $timezone A timezone string, UTC offset or DateTimeZone object; + * defaults to the site timezone; this parameter is ignored + * if the `$datetime` parameter is a DatTime object. + * @param bool $with_fallback Whether to return a DateTime object even when the date data is + * invalid or not; defaults to `true`. + * + * @return DateTimeImmutable|false A DateTime object built using the specified date, time and timezone; if + * `$with_fallback` is set to `false` then `false` will be returned if a + * DateTime object could not be built. + */ + static function immutable( $datetime = 'now', $timezone = null, $with_fallback = true ) { + if ( $datetime instanceof DateTimeImmutable ) { + return $datetime; + } + + if ( $datetime instanceof DateTime ) { + return Date_I18n_Immutable::createFromMutable( $datetime ); + } + + $mutable = static::build_date_object( $datetime, $timezone, $with_fallback ); + + if ( false === $mutable ) { + return false; + } + + $cache_key = md5( ( __METHOD__ . $mutable->getTimezone()->getName() . $mutable->getTimestamp() ) ); + $cache = tribe( 'cache' ); + + if ( false !== $cached = $cache[ $cache_key ] ) { + return $cached; + } + + $immutable = Date_I18n_Immutable::createFromMutable( $mutable ); + + $cache[ $cache_key ] = $immutable; + + return $immutable; + } + + /** + * Builds a date object from a given datetime and timezone. + * + * An alias of the `Tribe__Date_Utils::build_date_object` function. + * + * @since 4.10.2 + * + * @param string|DateTime|int $datetime A `strtotime` parsable string, a DateTime object or + * a timestamp; defaults to `now`. + * @param string|DateTimeZone|null $timezone A timezone string, UTC offset or DateTimeZone object; + * defaults to the site timezone; this parameter is ignored + * if the `$datetime` parameter is a DatTime object. + * @param bool $with_fallback Whether to return a DateTime object even when the date data is + * invalid or not; defaults to `true`. + * + * @return DateTime|false A DateTime object built using the specified date, time and timezone; if `$with_fallback` + * is set to `false` then `false` will be returned if a DateTime object could not be built. + */ + public static function mutable( $datetime = 'now', $timezone = null, $with_fallback = true ) { + return static::build_date_object( $datetime, $timezone, $with_fallback ); + } + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Db.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Db.php new file mode 100644 index 000000000..de92149ff --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Db.php @@ -0,0 +1,38 @@ +get_results( "SHOW VARIABLES LIKE 'max_allowed_packet';", ARRAY_A ); + // default the size to 1MB + $max_size = ! empty( $max_size[0]['Value'] ) ? $max_size[0]['Value'] : 1048576; + + /** + * Filters the size of the `max_allowed_packet` setting in bytes. + * + * @since 4.7.12 + * + * @param int $max_size By default the `max_allowed_packet` from the database. + */ + $max_size = apply_filters( 'tribe_db_max_allowed_packet', $max_size ); + + return $max_size; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Debug.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Debug.php new file mode 100644 index 000000000..65d557947 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Debug.php @@ -0,0 +1,59 @@ +get_option( 'debugEvents' ) ) { + $plugin = basename( dirname( Tribe__Main::instance()->plugin_path ) ); + error_log( "$plugin/common - $format: $title" ); + if ( $data && $data != '' ) { + error_log( "$plugin/common - $format: " . print_r( $data, true ) ); + } + } + } + + /** + * Static Singleton Factory Method + * + * @return Tribe__Debug + */ + public static function instance() { + static $instance; + + if ( ! $instance ) { + $class_name = __CLASS__; + $instance = new $class_name; + } + + return $instance; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Debug_Bar/Panels/Context.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Debug_Bar/Panels/Context.php new file mode 100644 index 000000000..162aa1740 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Debug_Bar/Panels/Context.php @@ -0,0 +1,99 @@ + + #mt-debug-bar .mt-debug-bar-title { + margin-bottom: 1em; + } + #mt-debug-bar .mt-debug-bar-context-table { + width: 100%; + font-size: 120%; + } + #mt-debug-bar .mt-debug-bar-context-table td { + padding: .5em .5em .5em 1em; + border: black solid 1px; + } + #mt-debug-bar .mt-debug-bar-context-table th { + padding: 1em; + border: black solid 1px; + } + '; + $html .= '
    '; + + $html .= '

    ' . esc_html__( 'The Events Calendar Context', 'tribe-common' ) . '

    '; + + $html .= '
    '; + $html .= '

    ' . esc_html__( 'PHP Render Context', 'tribe-common' ) . '

    '; + $html .= ''; + + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + + $locations = tribe_context()->get_locations(); + $context = tribe_context()->to_array(); + $orm_args = tribe_context()->get_orm_args(); + + foreach ( $locations as $key => $rw_data ) { + $orm_arg_key = Arr::get( $locations, [ $key, 'orm_arg' ], $key ); + $orm_arg_value = Arr::get( $orm_args, $orm_arg_key, null ); + + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + $html .= ''; + } + + $html .= '
    ' . __( 'Key', 'tribe-common' ) . '' . __( 'Value', 'tribe-common' ) . '' . __( 'ORM arg', 'tribe-common' ) . '' . __( 'Reads', 'tribe-common' ) . '' . __( 'Writes', 'tribe-common' ) . '
    ' . $key . '' . ( isset( $context[ $key ] ) ? print_r( $context[ $key ], true ) : 'undefined' ) . '' . ( false !== $orm_arg_key ? '' . print_r( $orm_arg_key, true ) . ' => ' . print_r( $orm_arg_value, true ) . '' : '' ) . '' . ( isset( $locations[ $key ]['read'] ) ? 'yes' : 'no' ) . '' . ( isset( $locations[ $key ]['write'] ) ? 'yes' : 'no' ) . '
    '; + $html .= '
    '; + $html .= '
    '; + + $state = tribe_context()->get_state(); + $html .= '

    ' . esc_attr__( 'State', + 'tribe-common' ) . '

    '; + $html .= '' . json_encode( $state, JSON_PRETTY_PRINT ) . '
    '; + + $html .= '
    '; + + echo $html; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Debug_Bar/Panels/Json_Ld.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Debug_Bar/Panels/Json_Ld.php new file mode 100644 index 000000000..5886ec1c6 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Debug_Bar/Panels/Json_Ld.php @@ -0,0 +1,84 @@ + + #mt-debug-bar .mt-debug-bar-title { + margin-bottom: 1em; + } + #mt-debug-bar .mt-debug-bar-section { + padding: .5em .5em .5em 1em; + } + '; + $html .= '
    '; + + $html .= '

    ' . esc_html__( 'The Events Calendar JSON-LD Data', + 'tribe-common' ) . '

    '; + + + $json_ld_data = array_filter( (array) tribe_cache()['json-ld-data'] ); + + if ( ! empty( $json_ld_data ) ) { + $html .= '
    '; + $html .= sprintf( + '
    The request produced %d JSON-LD data %s.

    ', + count( $json_ld_data ), + count( $json_ld_data ) > 1 ? 'scripts' : 'script' + ); + + $html .= '

    Copy the code below and paste it into ' . + '' . + 'Google\'s Structured Data Testing Tool' . + '' . + ' to test it using the Code Snippet option.


    '; + + foreach ( $json_ld_data as $full_entry ) { + preg_match( + '/(?^\\s*]*?>\\s*)(?.*)(?\\s<\\/script>)$/uism', + $full_entry, + $frags + ); + + if ( isset( $frags['open'], $frags['json'], $frags['close'] ) ) { + // Let's try and format it if we've got all the pieces. + $full_entry = $frags['open'] + . json_encode( json_decode( $frags['json'], true ), JSON_PRETTY_PRINT ) + . $frags['close']; + } + + $html .= sprintf( '
    %s
    ', esc_html( $full_entry ) ); + } + + $html .= '
    '; + } + + $html .= '
    '; + + echo $html; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Dependency.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Dependency.php new file mode 100644 index 000000000..cd5342a9c --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Dependency.php @@ -0,0 +1,616 @@ + 'main class name', + * 'version' => 'version num', (optional) + * 'path' => 'Path to the main plugin/bootstrap file' (optional) + * ] + */ + protected $active_plugins = []; + + /** + * A multidimensional array of active tribe plugins in the following format + * + * [ + * 'class' => 'main class name', + * 'path' => 'Path to the main plugin/bootstrap file' + * 'version' => 'version num', (optional) + * 'dependencies' => 'A multidimensional of dependencies' (optional) + * ] + */ + protected $registered_plugins = []; + + /** + * An array of class Tribe__Admin__Notice__Plugin_Download per plugin + * + * @since 4.9 + * + */ + protected $admin_messages = []; + + private $extensions = [ + 'Tribe__AdminDashboard', + ]; + + /** + * Adds a plugin to the active list + * + * @since 4.9 + * + * @param string $main_class Main/base class for this plugin + * @param null|string $version Version number of plugin + * @param null|string $path Path to the main plugin/bootstrap file + * @param array $dependencies An array of dependencies for a plugin + */ + public function add_registered_plugin( $main_class, $version = null, $path = null, $dependencies = [] ) { + $plugin = [ + 'class' => $main_class, + 'version' => $version, + 'path' => $path, + 'dependencies' => $dependencies, + ]; + + $this->registered_plugins[ $main_class ] = $plugin; + + if ( $path ) { + $this->admin_messages[ $main_class ] = new Tribe__Admin__Notice__Plugin_Download( $path ); + } + } + + /** + * Retrieves registered plugin array + * + * @since 4.9 + * + * @return array + */ + public function get_registered_plugins() { + return $this->registered_plugins; + } + + /** + * Adds a plugin to the active list + * + * @param string $main_class Main/base class for this plugin + * @param string $version Version number of plugin + * @param string $path Path to the main plugin/bootstrap file + */ + public function add_active_plugin( $main_class, $version = null, $path = null ) { + $plugin = [ + 'class' => $main_class, + 'version' => $version, + 'path' => $path, + ]; + + $this->active_plugins[ $main_class ] = $plugin; + } + + /** + * Retrieves active plugin array + * + * @return array + */ + public function get_active_plugins() { + return $this->active_plugins; + } + + /** + * Searches the plugin list for key/value pair and return the full details for that plugin + * + * @param string $search_key The array key this value will appear in + * @param string $search_val The value itself + * + * @return array|null + */ + public function get_plugin_by_key( $search_key, $search_val ) { + foreach ( $this->get_active_plugins() as $plugin ) { + if ( isset( $plugin[ $search_key ] ) && $plugin[ $search_key ] === $search_val ) { + return $plugin; + } + } + + return null; + } + + /** + * Retrieves the plugins details by class name + * + * @param string $main_class Main/base class for this plugin + * + * @return array|null + */ + public function get_plugin_by_class( $main_class ) { + return $this->get_plugin_by_key( 'class', $main_class ); + } + + /** + * Retrieves the version of the plugin + * + * @param string $main_class Main/base class for this plugin + * + * @return string|null Version + */ + public function get_plugin_version( $main_class ) { + $plugin = $this->get_plugin_by_class( $main_class ); + + return ( isset( $plugin['version'] ) ? $plugin['version'] : null ); + } + + /** + * Checks if the plugin is active + * + * @param string $main_class Main/base class for this plugin + * + * @return bool + */ + public function is_plugin_active( $main_class ) { + return ( $this->get_plugin_by_class( $main_class ) !== null ); + } + + /** + * Searches the registered plugin list for key/value pair and return the full details for that plugin + * + * @since 4.9 + * + * @param string $search_key The array key this value will appear in + * @param string $search_val The value itself + * + * @return array|null + */ + public function get_registered_plugin_by_key( $search_key, $search_val ) { + foreach ( $this->get_registered_plugins() as $plugin ) { + if ( isset( $plugin[ $search_key ] ) && $plugin[ $search_key ] === $search_val ) { + return $plugin; + } + } + + return null; + } + + /** + * Retrieves the registered plugins details by class name + * + * @since 4.9 + * + * @param string $main_class Main/base class for this plugin + * + * @return array|null + */ + public function get_registered_plugin_by_class( $main_class ) { + return $this->get_registered_plugin_by_key( 'class', $main_class ); + } + + /** + * Retrieves the version of the registered plugin + * + * @since 4.9 + * + * @param string $main_class Main/base class for this plugin + * + * @return string|null Version + */ + public function get_registered_plugin_version( $main_class ) { + $plugin = $this->get_registered_plugin_by_class( $main_class ); + + return ( isset( $plugin['version'] ) ? $plugin['version'] : null ); + } + + /** + * Checks if the plugin is active + * + * @since 4.9 + * + * @param string $main_class Main/base class for this plugin + * + * @return bool + */ + public function is_plugin_registered( $main_class ) { + return ( $this->get_registered_plugin_by_class( $main_class ) !== null ); + } + + + /** + * Checks if a plugin is active and has the specified version + * + * @since 4.9 + * + * @param string $main_class Main/base class for this plugin + * @param string $version Version to do a compare against + * @param string $compare Version compare string, defaults to >= + * + * @return bool + */ + public function is_plugin_version( $main_class, $version, $compare = '>=' ) { + //active plugin check to see if the correct version is active + if ( ! $this->is_plugin_active( $main_class ) ) { + return false; + } elseif ( version_compare( $this->get_plugin_version( $main_class ), $version, $compare ) ) { + return true; + } + + return false; + } + + /** + * Is the plugin registered with at least the minimum version + * + * @since 4.9 + * + * @param string $main_class Main/base class for this plugin + * @param string $version Version to do a compare against + * @param string $compare Version compare string, defaults to >= + * + * @return bool + */ + public function is_plugin_version_registered( $main_class, $version, $compare = '>=' ) { + //registered plugin check if addon as it tests if it might load + if ( ! $this->is_plugin_registered( $main_class ) ) { + return false; + } elseif ( version_compare( $this->get_registered_plugin_version( $main_class ), $version, $compare ) ) { + return true; + } + + return false; + } + + /** + * Checks if each plugin is active and exceeds the specified version number + * + * @param array $plugins_required Each item is a 'class_name' => 'min version' pair. Min ver can be null. + * + * @return bool + */ + public function has_requisite_plugins( $plugins_required = [] ) { + foreach ( $plugins_required as $class => $version ) { + // Return false if the plugin is not set or is a lesser version + if ( ! $this->is_plugin_active( $class ) ) { + return false; + } + + if ( null !== $version && ! $this->is_plugin_version( $class, $version ) ) { + return false; + } + } + + return true; + } + + /** + * Retrieves Registered Plugin by Class Name from Array + * + * @since 4.9 + * + * @return array|boolean + */ + public function get_registered_plugin( $class ) { + $plugins = $this->registered_plugins; + + return isset( $plugins[ $class ] ) ? $plugins[ $class ] : false; + } + + /** + * Gets all dependencies or single class requirements if parent, co, add does not exist use array as is if they + * do exist check each one in turn. + * + * @since 4.9 + * + * @param array $plugin An array of data for given registered plugin., + * @param array $dependencies An array of dependencies for a plugin. + * @param bool $addon Indicates if the plugin is an add-on for The Events Calendar or Event Tickets. + * + * @return true|int The number of failed dependency checks; `true` or `0` to indicate no checks failed. + */ + public function has_valid_dependencies( $plugin, $dependencies = [], $addon = false ) { + if ( empty( $dependencies ) ) { + return true; + } + + $failed_dependency = 0; + + $tribe_plugins = new Tribe__Plugins(); + + foreach ( $dependencies as $class => $version ) { + + // if no class for add-on + $checked_plugin = $this->get_registered_plugin( $class ); + if ( $addon && empty( $checked_plugin ) ) { + continue; + } + + $is_registered = $this->is_plugin_version_registered( $class, $version ); + if ( ! empty( $is_registered ) ) { + continue; + } + + $dependent_plugin = $tribe_plugins->get_plugin_by_class( $class ); + + $pue = $this->get_pue_from_class( $dependent_plugin['class'] ); + $has_pue_notice = $pue ? tribe( 'pue.notices' )->has_notice( $pue->pue_install_key ) : false; + + $this->admin_messages[ $plugin['class'] ]->add_required_plugin( + $dependent_plugin['short_name'], + $dependent_plugin['thickbox_url'], + $is_registered, + $version, + $addon, + $has_pue_notice + ); + $failed_dependency++; + } + + return $failed_dependency; + } + + /** + * Gets the Tribe__PUE__Checker instance of a given plugin based on the class. + * + * @since 4.9.12 + * + * @param string $class Which plugin main class we are looking for. + * + * @return Tribe__PUE__Checker + */ + public function get_pue_from_class( $class ) { + if ( ! is_string( $class ) ) { + return false; + } + + // If class doesn't exist the plugin doesn't exist. + if ( ! class_exists( $class ) ) { + return false; + } + + /** + * These callbacks are only required to prevent fatals. + * Only happen for plugin that use PUE. + */ + $callback_map = [ + 'Tribe__Events__Pro__Main' => function() { + $pue_reflection = new ReflectionClass( Tribe__Events__Pro__PUE::class ); + $values = $pue_reflection->getStaticProperties(); + $values['plugin_file'] = EVENTS_CALENDAR_PRO_FILE; + return $values; + }, + 'Tribe__Events__Filterbar__View' => function() { + $pue_reflection = new ReflectionClass( Tribe__Events__Filterbar__PUE::class ); + $values = $pue_reflection->getStaticProperties(); + $values['plugin_file'] = TRIBE_EVENTS_FILTERBAR_FILE; + return $values; + }, + 'Tribe__Events__Tickets__Eventbrite__Main' => function() { + $pue_reflection = new ReflectionClass( Tribe__Events__Tickets__Eventbrite__PUE::class ); + $values = $pue_reflection->getStaticProperties(); + $values['plugin_file'] = EVENTBRITE_PLUGIN_FILE; + return $values; + }, + ]; + + // Bail when class is not mapped. + if ( ! isset( $callback_map[ $class ] ) ) { + return false; + } + + // Use the callback to get the returns without fatals + $values = $callback_map[ $class ](); + $pue_instance = new Tribe__PUE__Checker( $values['update_url'], $values['pue_slug'], [], plugin_basename( $values['plugin_file'] ) ); + + return $pue_instance; + } + + /** + * Register a Plugin + * + * @since 4.9 + * + * @param string $file_path Full file path to the base plugin file. + * @param string $main_class The Main/base class for this plugin. + * @param string $version The plugin version. + * @param array $classes_req Any Main class files/tribe plugins required for this to run. + * @param array $dependencies an array of dependencies to check. + */ + public function register_plugin( $file_path, $main_class, $version, $classes_req = [], $dependencies = [] ) { + /** + * Filters the version string for a plugin. + * + * @since 4.9 + * + * @param string $version The plugin version number, e.g. "4.0.4". + * @param array $dependencies An array of dependencies for the plugins. These can include parent, add-on and other dependencies. + * @param string $file_path The absolute path to the plugin main file. + * @param array $classes_req Any Main class files/tribe plugins required for this to run. + */ + $version = apply_filters( "tribe_register_{$main_class}_plugin_version", $version, $dependencies, $file_path, $classes_req ); + /** + * Filters the dependencies array for a plugin. + * + * @since 4.9 + * + * @param array $dependencies An array of dependencies for the plugins. These can include parent, add-on and other dependencies. + * @param string $version The plugin version number, e.g. "4.0.4". + * @param string $file_path The absolute path to the plugin main file. + * @param array $classes_req Any Main class files/tribe plugins required for this to run. + */ + $dependencies = apply_filters( "tribe_register_{$main_class}_plugin_dependencies", $dependencies, $version, $file_path, $classes_req ); + + //add all plugins to registered_plugins + $this->add_registered_plugin( $main_class, $version, $file_path, $dependencies ); + + // Checks to see if the plugins are active for extensions + if ( ! empty( $classes_req ) && ! $this->has_requisite_plugins( $classes_req ) ) { + $tribe_plugins = new Tribe__Plugins(); + foreach ( $classes_req as $class => $plugin_version ) { + $plugin = $tribe_plugins->get_plugin_by_class( $class ); + + $is_active = $this->is_plugin_version( $class, $plugin_version ); + $pue = $this->get_pue_from_class( $plugin['class'] ); + $has_pue_notice = $pue ? tribe( 'pue.notices' )->has_notice( $pue->pue_install_key ) : false; + + $this->admin_messages[ $main_class ]->add_required_plugin( + $plugin['short_name'], + $plugin['thickbox_url'], + $is_active, + $plugin_version, + false, + $has_pue_notice + ); + } + } + + // only set The Events Calendar and Event Tickets to Active when registering + if ( 'Tribe__Events__Main' === $main_class || 'Tribe__Tickets__Main' === $main_class ) { + $this->add_active_plugin( $main_class, $version, $file_path ); + } + + } + + /** + * Checks if this plugin has permission to run, if not it notifies the admin + * + * @since 4.9 + * + * @param string $main_class The Main/base class for this plugin + * + * @return bool Indicates if plugin should continue initialization + */ + public function check_plugin( $main_class ) { + + $parent_dependencies = $co_dependencies = $addon_dependencies = 0; + + // Check if plugin is registered, if not return false. + $plugin = $this->get_registered_plugin( $main_class ); + if ( empty( $plugin ) ) { + return false; + } + + // Check parent dependencies in add-on. + if ( ! empty( $plugin['dependencies']['parent-dependencies'] ) ) { + $parent_dependencies = $this->has_valid_dependencies( $plugin, $plugin['dependencies']['parent-dependencies'] ); + } + // Check co-dependencies in add-on. + if ( ! empty( $plugin['dependencies']['co-dependencies'] ) ) { + $co_dependencies = $this->has_valid_dependencies( $plugin, $plugin['dependencies']['co-dependencies'] ); + } + + // Check add-on dependencies from parent. + $addon_dependencies = $this->check_addon_dependencies( $main_class ); + + // If good then we set as active plugin and continue to load. + if ( ! $parent_dependencies && ! $co_dependencies && ! $addon_dependencies ) { + $this->add_active_plugin( $main_class, $plugin['version'], $plugin['path'] ); + + return true; + } + + return false; + } + + /** + * Check an add-on dependencies for its parent + * + * @since 4.9 + * + * @param string $main_class A string of the main class for the plugin being checked. + * + * @return bool Returns false if any dependency is invalid. + */ + protected function check_addon_dependencies( $main_class ) { + foreach ( $this->registered_plugins as $registered ) { + if ( empty( $registered['dependencies']['addon-dependencies'][ $main_class ] ) ) { + continue; + } + + $dependencies = [ $main_class => $registered['dependencies']['addon-dependencies'][ $main_class ] ]; + $check = $this->has_valid_dependencies( $registered, $dependencies, true ); + + // A value of `true` or `0` indicates there are no failing checks. So here we check for ints gt 0. + if ( is_int( $check ) && $check > 0 ) { + return true; + } + } + + return false; + } + + /** + * Static Singleton Factory Method + * + * @deprecated 4.9.12 We shouldn't be handling singletons internally. + * + * @return self + */ + public static function instance() { + return tribe( self::class ); + } + + /** + * Test if plugin is an extension. + * This is cludgy as none of our extensions flag themselves as such. + * THus, we have to do some text searching. + * + * @since 5.1.3 + * + * @param string $class - the classname to use to look up the plugin. + * + * @return boolean + */ + public function is_extension( $class ): bool { + $plugin = $this->get_registered_plugin_by_class( $class ); + $bad_words = [ + 'extension', + 'labs', + 'hubspot', + 'mt_support', + 'support_team', + 'idea_garden', + ]; + + foreach ( $bad_words as $bad_word ) { + if ( stripos( $plugin['class'], $bad_word ) !== false ) { + return true; + } + } + + // For extensions that break the rules. + if ( in_array( $plugin['class'], $this->extensions, true) ) { + return true; + } + + return false; + } + + /** + * Return if we have a premium plugin active. + * Results may be unreliable if called before the tribe_plugins_loaded hook. + * + * @since 5.1.3 + * + * @return bool + */ + public function has_active_premium_plugin(): bool { + $active_plugins = $this->get_active_plugins(); + foreach ( $active_plugins as $plugin ) { + if ( + 'Tribe__Events__Main' !== $plugin['class'] + && 'Tribe__Tickets__Main' !== $plugin['class'] + && ! $this->is_extension( $plugin['class'] ) // ensure extensions don't trigger the "premium" flag. + ) { + return true; + } + } + + return false; + } + } + +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Deprecation.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Deprecation.php new file mode 100644 index 000000000..04ad0a255 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Deprecation.php @@ -0,0 +1,194 @@ + => array(, )`. + * e.g. `'tribe_current' => array ('4.3', 'tribe_deprecated')` + * + * For performance reasons this array is manually set and **not** + * dynamically populated. + * + * @var array + */ + protected $deprecated_filters = [ + 'tribe_cost_regex' => [ '4.3', 'tribe_events_cost_regex' ], + 'tribe_rewrite_prepared_slug' => [ '4.3', 'tribe_events_rewrite_prepared_slug' ], + ]; + + /** + * An array specifying the tag, version and optional replacements + * for deprecated actions. + * + * Use the format ` => array(, )`. + * e.g. `'tribe_current' => array ('4.3', 'tribe_deprecated')` + * + * For performance reasons this array is manually set and **not** + * dynamically populated. + * + * @var array + */ + protected $deprecated_actions = [ + 'tribe_pre_rewrite' => [ '4.3', 'tribe_events_pre_rewrite' ], + ]; + + /** + * @return Tribe__Deprecation + */ + public static function instance() { + if ( empty( self::$instance ) ) { + $instance = new self(); + + $instance->deprecate_actions(); + $instance->deprecate_filters(); + + self::$instance = $instance; + } + + return self::$instance; + } + + /** + * Hooks the deprecation notices for actions. + * + * @internal + */ + public function deprecate_actions() { + foreach ( array_keys( $this->deprecated_actions ) as $new_action_tag ) { + add_action( $new_action_tag, [ $this, 'deprecated_action_message' ] ); + add_filter( + $this->deprecated_actions[ $new_action_tag ][1], [ $this, 'deprecated_action_message' ] + ); + } + } + + /** + * Hooks the deprecation notices for filters. + * + * @internal + */ + public function deprecate_filters() { + foreach ( array_keys( $this->deprecated_filters ) as $new_filter_tag ) { + add_filter( $new_filter_tag, [ $this, 'deprecated_filter_message' ] ); + add_filter( + $this->deprecated_filters[ $new_filter_tag ][1], [ $this, 'deprecated_filter_message' ] + ); + } + } + + /** + * Triggers a deprecation notice if there is any callback hooked on a deprecated action. + */ + public function deprecated_action_message() { + $action = current_action(); + if ( isset( $this->deprecated_actions[ $action ] ) ) { + $deprecated_tag = $this->deprecated_actions[ $action ][1]; + } else { + $deprecated_tag = $action; + $action = $this->get_action_for_deprecated_tag( $action ); + } + + remove_action( $deprecated_tag, [ $this, 'deprecated_action_message' ] ); + + if ( doing_action( $deprecated_tag ) || has_filter( $deprecated_tag ) ) { + _deprecated_function( + 'The ' . $deprecated_tag . ' action', $this->deprecated_actions[ $action ][0], $action + ); + } + + add_action( $deprecated_tag, [ $this, 'deprecated_action_message' ] ); + } + + /** + * Triggers a deprecation notice if there is any callback hooked on a deprecated filter. + * + * @since 4.5.13 the filtered value is passed through unchanged + * + * @param mixed $value + * + * @return mixed + */ + public function deprecated_filter_message( $value = null ) { + $filter = current_filter(); + if ( isset( $this->deprecated_filters[ $filter ] ) ) { + $deprecated_tag = $this->deprecated_filters[ $filter ][1]; + } else { + $deprecated_tag = $filter; + $filter = $this->get_filter_for_deprecated_tag( $filter ); + } + + remove_filter( $deprecated_tag, [ $this, 'deprecated_filter_message' ] ); + + if ( has_filter( $deprecated_tag ) || doing_filter( $deprecated_tag ) ) { + $version = Tribe__Utils__Array::get( $this->deprecated_filters, [ $filter, 0 ], null ); + + _deprecated_function( + 'The ' . $deprecated_tag . ' filter', $version, $filter + ); + } + + add_filter( $deprecated_tag, [ $this, 'deprecated_filter_message' ] ); + + return $value; + } + + /** + * @param array $deprecated_filters + * + * @internal + */ + public function set_deprecated_filters( $deprecated_filters ) { + $this->deprecated_filters = $deprecated_filters; + } + + /** + * @param array $deprecated_actions + * + * @internal + */ + public function set_deprecated_actions( $deprecated_actions ) { + $this->deprecated_actions = $deprecated_actions; + } + + /** + * @param string $deprecated_tag + * + * @return int|string + */ + protected function get_action_for_deprecated_tag( $deprecated_tag ) { + foreach ( $this->deprecated_actions as $new_tag => $args ) { + if ( $args[1] === $deprecated_tag ) { + return $new_tag; + } + } + } + + /** + * @param string $deprecated_tag + * + * @return int|string + */ + protected function get_filter_for_deprecated_tag( $deprecated_tag ) { + foreach ( $this->deprecated_filters as $new_tag => $args ) { + if ( $args[1] === $deprecated_tag ) { + return $new_tag; + } + } + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Dialog/View.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Dialog/View.php new file mode 100644 index 000000000..3e560ccc4 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Dialog/View.php @@ -0,0 +1,617 @@ +set_template_origin( \Tribe__Main::instance() ); + $this->set_template_folder( 'src/views/dialog' ); + + // Configures this templating class to extract variables. + $this->set_template_context_extract( true ); + + // Uses the public folders. + $this->set_template_folder_lookup( true ); + } + + /** + * Public wrapper for build method. + * Contains all the logic/validation checks. + * + * @since 4.10.0 + * + * @param string $content Content as an HTML string. + * @param array $args { + * List of arguments to override dialog template. + * + * @type string $button_id The ID for the trigger button (optional). + * @type array $button_classes Any desired classes for the trigger button (optional). + * @type array $button_attributes Any desired attributes for the trigger button (optional). + * @type boolean $button_disabled Should the button be disabled (optional). + * @type string $button_text The text for the dialog trigger button ("Open the dialog window"). + * @type string $button_type The type for the trigger button (optional). + * @type string $button_value The value for the trigger button (optional). + * @type boolean $button_display If the dialog button should be displayed or not (optional). + * @type string $close_event The dialog close event hook name (`tribe_dialog_close_dialog`). + * @type string $content_classes The dialog content classes ("tribe-dialog__content"). + * @type array $context Any additional context data you need to expose to this file (optional). + * @type string $id The unique ID for this dialog (`uniqid()`). + * @type string $show_event The dialog event show hook name (`tribe_dialog_show_dialog`). + * @type string $template The dialog template name (dialog). + * @type string $title The dialog title (optional). + * @type string $trigger_classes Classes for the dialog trigger ("tribe_dialog_trigger"). + * + * Dialog script option overrides. + * + * @type string $append_target The dialog will be inserted after the button, you could supply a selector string here to override (optional). + * @type boolean $body_lock Whether to lock the body while dialog open (false). + * @type string $close_button_aria_label Aria label for the close button ("Close this dialog window"). + * @type string $close_button_classes Classes for the close button ("tribe-dialog__close-button"). + * @type string $content_wrapper_classes Dialog content wrapper classes. This wrapper includes the close button ("tribe-dialog__wrapper"). + * @type string $effect CSS effect on open. none or fade (optional). + * @type string $effect_easing A css easing string to apply ("ease-in-out"). + * @type int $effect_speed CSS effect speed in milliseconds (optional). + * @type string $overlay_classes The dialog overlay classes ("tribe-dialog__overlay"). + * @type boolean $overlay_click_closes If clicking the overlay closes the dialog (false). + * @type string $wrapper_classes The wrapper class for the dialog ("tribe-dialog"). + * } + * @param string $id The unique ID for this dialog. Gets prepended to the data attributes. Generated if not passed (`uniqid()`). + * @param boolean $echo Whether to echo the script or to return it (default: true). + * + * @return string An HTML string of the dialog. + */ + public function render_dialog( $content, $args = [], $id = null, $echo = true ) { + // Check for content to be passed. + if ( empty( $content ) ) { + return ''; + } + + // Generate an ID if we weren't passed one. + if ( is_null( $id ) ) { + $id = \uniqid(); + } + + /** @var \Tribe__Assets $assets */ + $assets = tribe( 'assets' ); + $assets->enqueue_group( 'tribe-dialog' ); + + $html = $this->build_dialog( $content, $id, $args ); + + if ( ! $echo ) { + return $html; + } + + echo $html; + } + + /** + * Syntactic sugar for `render_dialog()` to make creating modals easier. + * Adds sensible defaults for modals. + * + * @since 4.10.0 + * + * @param string $content Content as an HTML string. + * @param array $args { + * List of arguments to override dialog template. + * + * @type string $button_id The ID for the trigger button (optional). + * @type array $button_classes Any desired classes for the trigger button (optional). + * @type array $button_attributes Any desired attributes for the trigger button (optional). + * @type boolean $button_disabled Should the button be disabled (optional). + * @type string $button_text The text for the dialog trigger button ("Open the modal window"). + * @type string $button_type The type for the trigger button (optional). + * @type string $button_value The value for the trigger button (optional). + * @type boolean $button_display If the dialog button should be displayed or not (optional). + * @type string $close_event The dialog close event hook name (`tribe_dialog_close_modal`). + * @type string $content_classes The dialog content classes ("tribe-dialog__content tribe-modal__content"). + * @type string $title_classes The dialog title classes ("tribe-dialog__title tribe-modal__title"). + * @type array $context Any additional context data you need to expose to this file (optional). + * @type string $id The unique ID for this dialog (`uniqid()`). + * @type string $show_event The dialog event hook name (`tribe_dialog_show_modal`). + * @type string $template The dialog template name (modal). + * @type string $title The dialog title (optional). + * @type string $trigger_classes Classes for the dialog trigger ("tribe_dialog_trigger"). + * + * Dialog script option overrides. + * + * @type string $append_target The dialog will be inserted after the button, you could supply a selector string here to override ("body"). + * @type boolean $body_lock Whether to lock the body while dialog open (true). + * @type string $close_button_aria_label Aria label for the close button ("Close this modal window"). + * @type string $close_button_classes Classes for the close button ("tribe-dialog__close-button tribe-modal__close-button"). + * @type string $content_wrapper_classes Dialog content wrapper classes. This wrapper includes the close button ("tribe-dialog__wrapper tribe-modal__wrapper"). + * @type string $effect CSS effect on open. none or fade ("fade"). + * @type string $effect_easing A css easing string to apply ("ease-in-out"). + * @type int $effect_speed CSS effect speed in milliseconds (300). + * @type string $overlay_classes The dialog overlay classes ("tribe-dialog__overlay tribe-modal__overlay"). + * @type boolean $overlay_click_closes If clicking the overlay closes the dialog (true). + * @type string $wrapper_classes The wrapper class for the dialog ("tribe-dialog"). + * } + * @param string $id The unique ID for this dialog. Gets prepended to the data attributes. Generated if not passed (`uniqid()`). + * @param boolean $echo Whether to echo the script or to return it (default: true). + * + * @return string An HTML string of the dialog. + */ + public function render_modal( $content, $args = [], $id = null, $echo = true ) { + $default_args = [ + 'append_target' => '', + 'body_lock' => true, + 'button_text' => __( 'Open the modal window', 'tribe-common' ), + 'close_button_aria_label' => __( 'Close this modal window', 'tribe-common' ), + 'close_button_classes' => 'tribe-dialog__close-button tribe-modal__close-button', + 'close_event' => 'tribe_dialog_close_modal', + 'content_classes' => 'tribe-dialog__content tribe-modal__content', + 'content_wrapper_classes' => 'tribe-dialog__wrapper tribe-modal__wrapper', + 'effect' => 'fade', + 'effect_speed' => 300, + 'overlay_classes' => 'tribe-dialog__overlay tribe-modal__overlay', + 'overlay_click_closes' => true, + 'show_event' => 'tribe_dialog_show_modal', + 'template' => 'modal', + 'title_classes' => [ 'tribe-dialog__title', 'tribe-modal__title' ], + ]; + + $args = wp_parse_args( $args, $default_args ); + + return $this->render_dialog( $content, $args, $id, $echo ); + } + + /** + * Syntactic sugar for `render_dialog()` to make creating custom confirmation dialogs easier. + * Adds sensible defaults for confirmation dialogs. + * + * @since 4.10.0 + * + * @param string $content Content as an HTML string. + * @param array $args { + * List of arguments to override dialog template. + * + * @type string $button_id The ID for the trigger button (optional). + * @type array $button_classes Any desired classes for the trigger button (optional). + * @type array $button_attributes Any desired attributes for the trigger button (optional). + * @type boolean $button_disabled Should the button be disabled (optional). + * @type string $button_text The text for the dialog trigger button ("Open the dialog window"). + * @type string $button_type The type for the trigger button (optional). + * @type string $button_value The value for the trigger button (optional). + * @type boolean $button_display If the dialog button should be displayed or not (optional). + * @type string $cancel_button_text Text for the "Cancel" button ("Cancel"). + * @type string $content_classes The dialog content classes ("tribe-dialog__content tribe-confirm__content"). + * @type string $continue_button_text Text for the "Continue" button ("Confirm"). + * @type array $context Any additional context data you need to expose to this file (optional). + * @type string $id The unique ID for this dialog (`uniqid()`). + * @type string $template The dialog template name (confirm). + * @type string $title The dialog title (optional). + * @type string $trigger_classes Classes for the dialog trigger ("tribe_dialog_trigger"). + * + * Dialog script option overrides. + * + * @type string $append_target The dialog will be inserted after the button, you could supply a selector string here to override (optional). + * @type boolean $body_lock Whether to lock the body while dialog open (true). + * @type string $close_button_aria_label Aria label for the close button (optional). + * @type string $close_button_classes Classes for the close button ("tribe-dialog__close-button--hidden"). + * @type string $close_event The dialog close event hook name (`tribe_dialog_close_confirm`). + * @type string $content_wrapper_classes Dialog content wrapper classes. This wrapper includes the close button ("tribe-dialog__wrapper tribe-confirm__wrapper"). + * @type string $effect CSS effect on open. none or fade (optional). + * @type string $effect_easing A css easing string to apply ("ease-in-out"). + * @type int $effect_speed CSS effect speed in milliseconds (optional). + * @type string $overlay_classes The dialog overlay classes ("tribe-dialog__overlay tribe-confirm__overlay"). + * @type boolean $overlay_click_closes If clicking the overlay closes the dialog (false). + * @type string $show_event The dialog event hook name (`tribe_dialog_show_confirm`). + * @type string $wrapper_classes The wrapper class for the dialog ("tribe-dialog"). + * } + * @param string $id The unique ID for this dialog. Gets prepended to the data attributes. Generated if not passed (`uniqid()`). + * @param boolean $echo Whether to echo the script or to return it (default: true). + * + * @return string An HTML string of the dialog. + */ + public function render_confirm( $content, $args = [], $id = null, $echo = true ) { + $default_args = [ + 'body_lock' => true, + 'cancel_button_text' => __( 'Cancel', 'tribe-common' ), + 'continue_button_text' => __( 'Confirm', 'tribe-common' ), + 'close_button_aria_label' => '', + 'close_button_classes' => 'tribe-dialog__close-button--hidden', + 'close_event' => 'tribe_dialog_close_confirm', + 'content_classes' => 'tribe-dialog__content tribe-confirm__content', + 'content_wrapper_classes' => 'tribe-dialog__wrapper tribe-confirm__wrapper', + 'overlay_classes' => 'tribe-dialog__overlay tribe-confirm__overlay', + 'show_event' => 'tribe_dialog_show_confirm', + 'template' => 'confirm', + 'title_classes' => [ 'tribe-dialog__title', 'tribe-confirm__title' ], + ]; + + $args = wp_parse_args( $args, $default_args ); + + return $this->render_dialog( $content, $args, $id, $echo ); + } + + /** + * Syntactic sugar for `render_dialog()` to make creating custom confirmation dialogs easier. + * Adds sensible defaults for warning dialogs. + * + * @since 4.12.13 + * + * @param string $content Content as an HTML string. + * @param array $args { + * List of arguments to override dialog template. + * + * @type string $button_id The ID for the trigger button (optional). + * @type array $button_classes Any desired classes for the trigger button (optional). + * @type array $button_attributes Any desired attributes for the trigger button (optional). + * @type boolean $button_disabled Should the button be disabled (optional). + * @type string $button_text The text for the dialog trigger button ("Open the dialog window"). + * @type string $button_type The type for the trigger button (optional). + * @type string $button_value The value for the trigger button (optional). + * @type boolean $button_display If the dialog button should be displayed or not (optional). + * @type string $cancel_button_text Text for the "Cancel" button ("Cancel"). + * @type string $cancel_button_classes Any desired classes for the cancel button (optional). + * @type string $content_classes The dialog content classes ("tribe-dialog__content tribe-confirm__content"). + * @type string $continue_button_text Text for the "Continue" button ("Confirm"). + * @type string $continue_button_classes Any desired classes for the continue button (optional). + * @type array $context Any additional context data you need to expose to this file (optional). + * @type string $id The unique ID for this dialog (`uniqid()`). + * @type string $template The dialog template name (confirm). + * @type string $title The dialog title (optional). + * @type string $trigger_classes Classes for the dialog trigger ("tribe_dialog_trigger"). + * + * Dialog script option overrides. + * + * @type string $append_target The dialog will be inserted after the button, you could supply a selector string here to override (optional). + * @type boolean $body_lock Whether to lock the body while dialog open (true). + * @type string $close_button_aria_label Aria label for the close button (optional). + * @type string $close_button_classes Classes for the close button ("tribe-dialog__close-button--hidden"). + * @type string $close_event The dialog close event hook name (`tribe_dialog_close_confirm`). + * @type string $content_wrapper_classes Dialog content wrapper classes. This wrapper includes the close button ("tribe-dialog__wrapper tribe-confirm__wrapper"). + * @type string $effect CSS effect on open. none or fade (optional). + * @type string $effect_easing A css easing string to apply ("ease-in-out"). + * @type int $effect_speed CSS effect speed in milliseconds (optional). + * @type string $overlay_classes The dialog overlay classes ("tribe-dialog__overlay tribe-confirm__overlay"). + * @type boolean $overlay_click_closes If clicking the overlay closes the dialog (false). + * @type string $show_event The dialog event hook name (`tribe_dialog_show_confirm`). + * @type string $wrapper_classes The wrapper class for the dialog ("tribe-dialog"). + * } + * @param string $id The unique ID for this dialog. Gets prepended to the data attributes. Generated if not passed (`uniqid()`). + * @param boolean $echo Whether to echo the script or to return it (default: true). + * + * @return string An HTML string of the dialog. + */ + public function render_warning( $content, $args = [], $id = null, $echo = true ) { + $default_args = [ + 'body_lock' => true, + 'button_display' => false, + 'cancel_button_text' => __( 'Cancel', 'tribe-common' ), + 'cancel_button_classes' => 'tribe-dialog__button tribe-dialog__button-cancel tribe-common-c-btn tribe-common-c-btn-border', + 'continue_button_text' => __( 'OK', 'tribe-common' ), + 'continue_button_classes' => 'tribe-dialog__button tribe-dialog__button-continue tribe-common-c-btn-border tribe-common-c-btn-border--alt', + 'close_button_aria_label' => '', + 'close_button_classes' => 'tribe-dialog__close-button--hidden', + 'close_event' => 'tribe_dialog_close_confirm', + 'content_classes' => 'tribe-dialog__content tribe-confirm__content', + 'content_wrapper_classes' => 'tribe-dialog__wrapper tribe-confirm__wrapper', + 'overlay_classes' => 'tribe-dialog__overlay tribe-modal__overlay tribe-warning__overlay', + 'overlay_click_closes' => false, + 'show_event' => 'tribe_dialog_show_confirm', + 'template' => 'warning', + 'title_classes' => [ 'tribe-dialog__title', 'tribe-confirm__title' ], + ]; + + $args = wp_parse_args( $args, $default_args ); + + return $this->render_dialog( $content, $args, $id, $echo ); + } + + /** + * Syntactic sugar for `render_dialog()` to make creating custom alerts easier. + * Adds sensible defaults for alerts. + * + * @since 4.10.0 + * + * @param string $content Content as an HTML string. + * @param array $args { + * List of arguments to override dialog template. + * + * @type string $alert_button_text Text for the "OK" button ("OK"). + * @type string $button_id The ID for the trigger button (optional). + * @type array $button_classes Any desired classes for the trigger button (optional). + * @type array $button_attributes Any desired attributes for the trigger button (optional). + * @type boolean $button_disabled Should the button be disabled (optional). + * @type string $button_text The text for the dialog trigger button ("Open the dialog window"). + * @type string $button_type The type for the trigger button (optional). + * @type string $button_value The value for the trigger button (optional). + * @type boolean $button_display If the dialog button should be displayed or not (optional). + * @type string $content_classes The dialog content classes ("tribe-dialog__content tribe-alert__content"). + * @type string $title_classes The dialog title classes ("tribe-dialog__title tribe-alert__title"). + * @type array $context Any additional context data you need to expose to this file (optional). + * @type string $id The unique ID for this dialog (`uniqid()`). + * @type string $template The dialog template name (alert). + * @type string $title The dialog title (optional). + * @type string $trigger_classes Classes for the dialog trigger ("tribe_dialog_trigger"). + * + * Dialog script option overrides. + * + * @type string $append_target The dialog will be inserted after the button, you could supply a selector string here to override (optional). + * @type boolean $body_lock Whether to lock the body while dialog open (true). + * @type string $close_button_aria_label Aria label for the close button (optional). + * @type string $close_button_classes Classes for the close button ("tribe-dialog__close-button--hidden"). + * @type string $close_event The dialog close event hook name (`tribe_dialog_close_alert`). + * @type string $content_wrapper_classes Dialog content wrapper classes. This wrapper includes the close button ("tribe-dialog__wrapper tribe-alert__wrapper"). + * @type string $effect CSS effect on open. none or fade (optional). + * @type string $effect_easing A css easing string to apply ("ease-in-out"). + * @type int $effect_speed CSS effect speed in milliseconds (optional). + * @type string $overlay_classes The dialog overlay classes ("tribe-dialog__overlay tribe-alert__overlay"). + * @type boolean $overlay_click_closes If clicking the overlay closes the dialog (false). + * @type string $show_event The dialog event hook name (`tribe_dialog_show_alert`). + * @type string $wrapper_classes The wrapper class for the dialog ("tribe-dialog"). + * } + * @param string $id The unique ID for this dialog. Gets prepended to the data attributes. Generated if not passed (`uniqid()`). + * @param boolean $echo Whether to echo the script or to return it (default: true). + * + * @return string An HTML string of the dialog. + */ + public function render_alert( $content, $args = [], $id = null, $echo = true ) { + $default_args = [ + 'alert_button_text' => __( 'OK', 'tribe-common' ), + 'body_lock' => true, + 'close_button_aria_label' => '', + 'close_button_classes' => 'tribe-dialog__close-button--hidden', + 'close_event' => 'tribe_dialog_close_alert', + 'content_classes' => 'tribe-dialog__content tribe-alert__content', + 'content_wrapper_classes' => 'tribe-dialog__wrapper tribe-alert__wrapper', + 'overlay_classes' => 'tribe-dialog__overlay tribe-alert__overlay', + 'show_event' => 'tribe_dialog_show_alert', + 'template' => 'alert', + 'title_classes' => [ 'tribe-dialog__title', 'tribe-alert__title' ], + ]; + + $args = wp_parse_args( $args, $default_args ); + + return $this->render_dialog( $content, $args, $id, $echo ); + } + + /** + * Factory method for dialog HTML + * + * @since 4.10.0 + * + * @param string $content HTML dialog content. + * @param string $id The unique ID for this dialog (`uniqid()`) Gets prepended to the data attributes. + * @param array $args { + * List of arguments to override dialog template. + * + * @type string $button_id The ID for the trigger button (optional). + * @type array $button_classes Any desired classes for the trigger button (optional). + * @type array $button_attributes Any desired attributes for the trigger button (optional). + * @type boolean $button_disabled Should the button be disabled (optional). + * @type string $button_text The text for the dialog trigger button ("Open the dialog window"). + * @type string $button_type The type for the trigger button (optional). + * @type string $button_value The value for the trigger button (optional). + * @type boolean $button_display If the dialog button should be displayed or not (optional). + * @type string $cancel_button_classes Any desired classes for the cancel button (optional). + * @type string $close_event The dialog event hook name (`tribe_dialog_close_dialog`). + * @type string $content_classes The dialog content classes ("tribe-dialog__content"). + * @type string $continue_button_classes Any desired classes for the continue button (optional). + * @type string $title_classes The dialog title classes ("tribe-dialog__title"). + * @type array $context Any additional context data you need to expose to this file (optional). + * @type string $id The unique ID for this dialog (`uniqid()`). + * @type string $show_event The dialog event hook name (`tribe_dialog_show_dialog`). + * @type string $template The dialog template name (dialog). + * @type string $title The dialog title (optional). + * @type string $trigger_classes Classes for the dialog trigger ("tribe_dialog_trigger"). + * + * Dialog script option overrides. + * + * @type string $append_target The dialog will be inserted after the button, you could supply a selector string here to override (optional). + * @type boolean $body_lock Whether to lock the body while dialog open (false). + * @type string $close_button_aria_label Aria label for the close button ("Close this dialog window"). + * @type string $close_button_classes Classes for the close button ("tribe-dialog__close-button"). + * @type string $content_wrapper_classes Dialog content wrapper classes. This wrapper includes the close button ("tribe-dialog__wrapper"). + * @type string $effect CSS effect on open. none or fade (optional). + * @type string $effect_easing A css easing string to apply ("ease-in-out"). + * @type int $effect_speed CSS effect speed in milliseconds (optional). + * @type string $overlay_classes The dialog overlay classes ("tribe-dialog__overlay"). + * @type boolean $overlay_click_closes If clicking the overlay closes the dialog (false). + * @type string $wrapper_classes The wrapper class for the dialog ("tribe-dialog"). + * } + * + * @return string An HTML string of the dialog. + */ + private function build_dialog( $content, $id, $args ) { + $default_args = [ + 'button_classes' => '', + 'button_attributes' => [], + 'button_disabled' => false, + 'button_id' => '', + 'button_name' => '', + 'button_text' => __( 'Open the dialog window', 'tribe-common' ), + 'button_type' => '', + 'button_value' => '', + 'button_display' => true, + 'cancel_button_classes' => 'tribe-dialog__button tribe-dialog__button-cancel tribe-common-c-btn-border tribe-common-c-btn-border--alt', + 'continue_button_classes' => 'tribe-dialog__button tribe-dialog__button-continue tribe-common-c-btn tribe-common-c-btn-border', + 'close_event' => 'tribe_dialog_close_dialog', + 'content_classes' => 'tribe-dialog__content', + 'context' => '', + 'show_event' => 'tribe_dialog_show_dialog', + 'template' => 'dialog', + 'title_classes' => 'tribe-dialog__title', + 'title' => '', + 'trigger_classes' => 'tribe_dialog_trigger', + // Dialog script options. + 'append_target' => '', // The dialog will be inserted after the button, you could supply a selector string here to override. + 'body_lock' => false, // Lock the body while dialog open? + 'close_button_aria_label' => __( 'Close this dialog window', 'tribe-common' ), // Aria label for close button. + 'close_button_classes' => 'tribe-dialog__close-button', // Classes for close button. + 'content_wrapper_classes' => 'tribe-dialog__wrapper', // Dialog content classes. + 'effect' => 'none', // None or fade (for now). + 'effect_speed' => 0, // Effect speed in milliseconds. + 'effect_easing' => 'ease-in-out', // A css easing string. + 'overlay_classes' => 'tribe-dialog__overlay', // Overlay classes. + 'overlay_click_closes' => false, // Clicking overlay closes dialog. + 'wrapper_classes' => 'tribe-dialog', // The wrapper class for the dialog. + ]; + + $args = wp_parse_args( $args, $default_args ); + + $args[ 'content' ] = $content; + $args[ 'id' ] = $id; + + /** + * Allow us to filter the dialog arguments. + * + * @since 4.10.0 + * + * @param array $args The dialog arguments. + * @param string $content HTML content string. + */ + $args = apply_filters( 'tribe_dialog_args', $args, $content ); + + $template = $args[ 'template' ]; + /** + * Allow us to filter the dialog template name. + * + * @since 4.10.0 + * + * @param string $template The dialog template name. + * @param array $args The dialog arguments. + */ + $template_name = apply_filters( 'tribe_dialog_template', $template, $args ); + + ob_start(); + + $this->template( $template_name, $args, true ); + + $this->get_dialog_script( $args ); + + $html = ob_get_clean(); + + /** + * Allow us to filter the dialog output (HTML string). + * + * @since 4.10.0 + * + * @param string $html The dialog HTML string. + * @param array $args The dialog arguments. + */ + return apply_filters( 'tribe_dialog_html', $html, $args ); + } + + /** + * Get dialog + build_dialog(). + */ + $html = apply_filters( 'tribe_dialog_script_html', $html ); + + if ( $echo ) { + echo $html; + return; + } + + return $html; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Documentation/Swagger/Builder_Interface.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Documentation/Swagger/Builder_Interface.php new file mode 100644 index 000000000..1b757f220 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Documentation/Swagger/Builder_Interface.php @@ -0,0 +1,29 @@ + 'object', + 'properties' => [ + 'currency_symbol' => [ + 'type' => 'string', + 'description' => __( 'The cost currency symbol', 'tribe-common' ), + ], + 'currency_position ' => [ + 'type' => 'string', + 'description' => __( 'The position of the currency symbol in the cost string', 'tribe-common' ), + 'enum' => [ 'prefix', 'postfix' ], + ], + 'values' => [ + 'type' => 'array', + 'items' => [ 'type' => 'integer' ], + 'description' => __( 'A sorted array of all the numeric values for the cost', 'tribe-common' ), + ], + ], + ]; + + /** + * Filters the Swagger documentation generated for a cost details in the TEC REST API. + * + * @param array $documentation An associative PHP array in the format supported by Swagger. + * + * @link http://swagger.io/ + */ + $documentation = apply_filters( 'tribe_rest_swagger_cost_details_documentation', $documentation ); + + return $documentation; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Documentation/Swagger/Date_Details_Definition_Provider.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Documentation/Swagger/Date_Details_Definition_Provider.php new file mode 100644 index 000000000..c76f15db7 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Documentation/Swagger/Date_Details_Definition_Provider.php @@ -0,0 +1,60 @@ + 'object', + 'properties' => [ + 'year' => [ + 'type' => 'integer', + 'description' => __( 'The date year', 'tribe-common' ), + ], + 'month' => [ + 'type' => 'integer', + 'description' => __( 'The date month', 'tribe-common' ), + ], + 'day' => [ + 'type' => 'integer', + 'description' => __( 'The date day', 'tribe-common' ), + ], + 'hour' => [ + 'type' => 'integer', + 'description' => __( 'The date hour', 'tribe-common' ), + ], + 'minutes' => [ + 'type' => 'integer', + 'description' => __( 'The date minutes', 'tribe-common' ), + ], + 'seconds' => [ + 'type' => 'integer', + 'description' => __( 'The date seconds', 'tribe-common' ), + ], + ], + ]; + + /** + * Filters the Swagger documentation generated for an date details in the TEC REST API. + * + * @param array $documentation An associative PHP array in the format supported by Swagger. + * + * @link http://swagger.io/ + */ + $documentation = apply_filters( 'tribe_rest_swagger_date_details_documentation', $documentation ); + + return $documentation; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Documentation/Swagger/Image_Definition_Provider.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Documentation/Swagger/Image_Definition_Provider.php new file mode 100644 index 000000000..3633af9d8 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Documentation/Swagger/Image_Definition_Provider.php @@ -0,0 +1,64 @@ + 'object', + 'properties' => [ + 'url' => [ + 'type' => 'string', + 'format' => 'uri', + 'description' => __( 'The URL to the full size version of the image', 'tribe-common' ), + ], + 'id' => [ + 'type' => 'integer', + 'description' => __( 'The image WordPress post ID', 'tribe-common' ), + ], + 'extension' => [ + 'type' => 'string', + 'description' => __( 'The image file extension', 'tribe-common' ), + ], + 'width' => [ + 'type' => 'integer', + 'description' => __( 'The image natural width in pixels', 'tribe-common' ), + ], + 'height' => [ + 'type' => 'integer', + 'description' => __( 'The image natural height in pixels', 'tribe-common' ), + ], + 'sizes' => [ + 'type' => 'array', + 'description' => __( 'The details about each size available for the image', 'tribe-common' ), + 'items' => [ + '$ref' => '#/components/schemas/ImageSize', + ], + ], + ], + ]; + + /** + * Filters the Swagger documentation generated for an image deatails in the TEC REST API. + * + * @param array $documentation An associative PHP array in the format supported by Swagger. + * + * @link http://swagger.io/ + */ + $documentation = apply_filters( 'tribe_rest_swagger_image_details_documentation', $documentation ); + + return $documentation; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Documentation/Swagger/Image_Size_Definition_Provider.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Documentation/Swagger/Image_Size_Definition_Provider.php new file mode 100644 index 000000000..91921b57b --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Documentation/Swagger/Image_Size_Definition_Provider.php @@ -0,0 +1,53 @@ + 'object', + 'properties' => [ + 'width' => [ + 'type' => 'integer', + 'description' => __( 'The image width in pixels in the specified size', 'tribe-common' ), + ], + 'height' => [ + 'type' => 'integer', + 'description' => __( 'The image height in pixels in the specified size', 'tribe-common' ), + ], + 'mime-type' => [ + 'type' => 'string', + 'description' => __( 'The image mime-type', 'tribe-common' ), + ], + 'url' => [ + 'type' => 'string', + 'format' => 'uri', + 'description' => __( 'The link to the image in the specified size on the site', 'tribe-common' ), + ], + ], + ]; + + /** + * Filters the Swagger documentation generated for an image size in the TEC REST API. + * + * @param array $documentation An associative PHP array in the format supported by Swagger. + * + * @link http://swagger.io/ + */ + $documentation = apply_filters( 'tribe_rest_swagger_image_size_documentation', $documentation ); + + return $documentation; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Documentation/Swagger/Provider_Interface.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Documentation/Swagger/Provider_Interface.php new file mode 100644 index 000000000..c57448f87 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Documentation/Swagger/Provider_Interface.php @@ -0,0 +1,17 @@ + 'object', + 'properties' => [ + 'id' => [ + 'type' => 'integer', + 'description' => __( 'The WordPress term ID', 'tribe-common' ), + ], + 'name' => [ + 'type' => 'string', + 'description' => __( 'The term name', 'tribe-common' ), + ], + 'slug' => [ + 'type' => 'string', + 'description' => __( 'The term slug', 'tribe-common' ), + ], + 'taxonomy' => [ + 'type' => 'string', + 'description' => __( 'The taxonomy the term belongs to', 'tribe-common' ), + ], + 'description' => [ + 'type' => 'string', + 'description' => __( 'The term description', 'tribe-common' ), + ], + 'parent' => [ + 'type' => 'integer', + 'description' => __( 'The term parent term if any', 'tribe-common' ), + ], + 'count' => [ + 'type' => 'integer', + 'description' => __( 'The number of posts associated with the term', 'tribe-common' ), + ], + 'url' => [ + 'type' => 'string', + 'description' => __( 'The URL to the term archive page', 'tribe-common' ), + ], + 'urls' => [ + 'type' => 'array', + 'items' => [ 'type' => 'string' ], + 'description' => __( 'A list of links to the term own, archive and parent REST URL', 'tribe-common' ), + ], + ], + ]; + + /** + * Filters the Swagger documentation generated for an term in the TEC REST API. + * + * @param array $documentation An associative PHP array in the format supported by Swagger. + * + * @link http://swagger.io/ + */ + $documentation = apply_filters( 'tribe_rest_swagger_term_documentation', $documentation ); + + return $documentation; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Duplicate/Post.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Duplicate/Post.php new file mode 100644 index 000000000..ab1a489c0 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Duplicate/Post.php @@ -0,0 +1,443 @@ +factory = null !== $factory ? $factory : tribe( 'post-duplicate.strategy-factory' ); + } + + /** + * Sets the post fields that should be used to find a duplicate in the database. + * + * Each entry should be in the [ => [ 'match' => ]] format. + * If not the strategy will be set to the default one. + * + * @param array $post_fields + * + * @since 4.6 + */ + public function use_post_fields( array $post_fields ) { + if ( empty( $post_fields ) ) { + $this->post_fields = []; + + return; + } + + $cast = $this->cast_to_strategy( $post_fields ); + $this->post_fields = array_intersect_key( $cast, array_combine( self::$post_table_columns, self::$post_table_columns ) ); + } + + /** + * Converts an array of fields to the format required by the class. + * + * @param array $fields + * + * @return array + * + * @since 4.6 + */ + protected function cast_to_strategy( array $fields ) { + $cast = []; + + foreach ( $fields as $key => $value ) { + if ( is_numeric( $key ) ) { + $cast[ $value ] = [ 'match' => 'same' ]; + } elseif ( is_array( $value ) ) { + if ( ! empty( $value['match'] ) ) { + $cast[ $key ] = $value; + } else { + $cast[ $key ] = array_merge( $value, [ 'match' => 'same' ] ); + } + } + } + + return $cast; + } + + /** + * Finds a duplicate with the data provided. + * + * The more post and custom fields are used to find a match the less likely it is to find one and the more + * likely it is for a duplicate to be a good match. + * + * @param array $postarr An array of post data, post fields and custom fields, that should be used to find the + * duplicate. + * + * @return bool|int `false` if a duplicate was not found, the post ID of the duplicate if found. + * + * @since 4.6 + */ + public function find_for( array $postarr ) { + if ( empty( $this->post_fields ) && empty( $this->custom_fields ) ) { + return false; + } + + $prepared = $this->prepare_queries( $postarr ); + + if ( false === $prepared ) { + return false; + } + + $id = false; + + /** @var wpdb $wpdb */ + global $wpdb; + foreach ( $prepared as $query ) { + $this_id = $wpdb->get_var( $query ); + + if ( self::AND_OPERATOR === $this->where_operator ) { + if ( empty( $this_id ) ) { + return false; + } + + $id = empty( $id ) + ? $this_id + : $this_id == $id; + + if ( empty( $id ) ) { + return false; + } + } else { + if ( ! empty( $this_id ) ) { + return $this_id; + } + } + } + + return $id; + } + + /** + * Finds all the duplicates with the data provided. + * + * The more post and custom fields are used to find a match the less likely it is to find one and the more + * likely it is for a duplicate to be a good match. + * + * @param array $postarr An array of post data, post fields and custom fields, that should be used to find the + * duplicate. + * + * @return bool|array `false` if a duplicate was not found, an array of the duplicate post IDs if any were found. + * + * @since 4.6 + */ + public function find_all_for( array $postarr ) { + if ( empty( $this->post_fields ) && empty( $this->custom_fields ) ) { + return false; + } + + $prepared = $this->prepare_queries( $postarr ); + + if ( false === $prepared ) { + return false; + } + + $ids = false; + + /** @var wpdb $wpdb */ + global $wpdb; + foreach ( $prepared as $query ) { + $this_ids = $wpdb->get_results( $query ); + $this_ids = ! empty( $this_ids ) + ? array_map( 'intval', wp_list_pluck( $this_ids, 'ID' ) ) + : false; + + if ( self::AND_OPERATOR === $this->where_operator ) { + if ( empty( $this_ids ) ) { + return false; + } + + $ids = empty( $ids ) + ? $this_ids + : array_intersect( (array) $ids, (array) $this_ids ); + + if ( empty( $ids ) ) { + return false; + } + } else { + $ids = empty( $ids ) + ? $this_ids + : array_unique( array_merge( (array) $ids, array_filter( (array) $this_ids ) ) ); + } + } + + return $ids; + } + + /** + * Sets the custom fields that should be used to find a duplicate in the database. + * + * Each entry should be in the [ => [ 'match' => ]] format. + * If not the strategy will be set to the default one. + * + * @param array $custom_fields + * + * @since 4.6 + */ + public function use_custom_fields( array $custom_fields ) { + $cast = $this->cast_to_strategy( $custom_fields ); + $this->custom_fields = $cast; + } + + /** + * Gets the SQL logic operator that will be used to join the WHERE queries frags. + * + * @return string + * + * @since 4.6 + */ + public function get_where_operator() { + return $this->where_operator; + } + + /** + * Sets the SQL logic operator that should be used to join the WHERE queries frags. + * + * @param string $where_operator + * + * @since 4.6 + */ + public function set_where_operator( $where_operator ) { + $this->where_operator = self::AND_OPERATOR === strtoupper( $where_operator ) + ? self::AND_OPERATOR + : self::OR_OPERATOR; + } + + /** + * Prepares the query that should be used to query for duplicates according + * to the current post and custom fields. + * + * @param array $postarr + * + * @return bool|array An array of prepared queries or `false` on failure. + * + * @since 4.6 + */ + protected function prepare_queries( array $postarr ) { + /** @var wpdb $wpdb */ + global $wpdb; + + $where_frags = []; + $custom_fields_where_frags = []; + $join = []; + + if ( ! empty( $this->post_fields ) ) { + $queryable_post_fields = array_intersect_key( $postarr, $this->post_fields ); + if ( empty( $queryable_post_fields ) ) { + return false; + } + foreach ( $queryable_post_fields as $key => $value ) { + $match_strategy = $this->factory->make( $this->post_fields[ $key ]['match'] ); + $where_frags[] = $match_strategy->where( $key, $postarr[ $key ] ); + } + } + + if ( ! empty( $this->custom_fields ) ) { + // we had post fields and found a match + $queryable_custom_fields = array_intersect_key( $postarr, $this->custom_fields ); + $i = 0; + foreach ( $queryable_custom_fields as $key => $value ) { + $match_strategy = $this->factory->make( $this->custom_fields[ $key ]['match'] ); + $meta_value = is_array( $value ) ? reset( $value ) : $value; + $custom_fields_where_frags[] = $match_strategy->where_custom_field( $key, $meta_value, "pm{$i}" ); + $i ++; + } + $count = count( $custom_fields_where_frags ); + for ( $i = 0; $i < $count; $i ++ ) { + $join[] = " \nLEFT JOIN {$wpdb->postmeta} pm{$i} ON pm{$i}.post_id = {$wpdb->posts}.ID "; + } + } + + /** + * Filters the JOIN limit. + * + * @param int $join_limit How many joins will be made per query at most. + * @param array $where_frags The WHERE components for this duplicate search query + * @param string $post_type The post type that's being used for this duplicate search query. + * + * @since 4.6 + */ + $join_limit = apply_filters( 'tribe_duplicate_post_join_limit', $this->join_limit, $where_frags, $this->post_type ); + + $excluded_status = [ + 'trash', + 'autodraft', + ]; + + /** + * Filters the excluded status. + * + * @param array $excluded_status The list of post_status to exclude from the query. + * @param string $post_type The post type that's being used for this duplicate search query. + * + * @since 4.6 + */ + $excluded_status = apply_filters( 'tribe_duplicate_post_excluded_status', $excluded_status, $where_frags, $this->post_type ); + + $post_type_conditional = $wpdb->prepare( "{$wpdb->posts}.post_type = %s", $this->post_type ); + + $post_status_conditional = ''; + + if ( $excluded_status ) { + $in_string = array_fill( 0, count( $excluded_status ), '%s' ); + $in_string = implode( ', ', $in_string ); + + // @codingStandardsIgnoreLine + $post_status_conditional = $wpdb->prepare( "{$wpdb->posts}.post_status NOT IN ( {$in_string} )", $excluded_status ); + } + + $queries = []; + + if ( ! empty( $join_limit ) && ! empty( $join ) ) { + while ( count( $join ) ) { + $current_wheres = array_splice( $custom_fields_where_frags, 0, $join_limit ); + $current_joins = array_splice( $join, 0, $join_limit ); + + $this_join = implode( "\n", $current_joins ); + + $this_where = "\n" . implode( " \n{$this->where_operator} ", array_merge( $where_frags, $current_wheres ) ); + $this_where = sprintf( '%s AND (%s)', $post_type_conditional, $this_where ); + + if ( '' !== $post_status_conditional ) { + $this_where = sprintf( '%s AND %s', $post_status_conditional, $this_where ); + } + + $queries[] = "SELECT DISTINCT {$wpdb->posts}.ID from {$wpdb->posts} {$this_join} \nWHERE {$this_where}"; + } + } else { + $where = implode( " \n{$this->where_operator} ", $where_frags ); + $where = sprintf( '%s AND (%s)', $post_type_conditional, $where ); + + if ( '' !== $post_status_conditional ) { + $where = sprintf( '%s AND %s', $post_status_conditional, $where ); + } + + $queries[] = "SELECT DISTINCT {$wpdb->posts}.ID from {$wpdb->posts} \nWHERE {$where}"; + } + + + return $queries; + } + + /** + * Gets the post type that will be used to find duplicates. + * + * @return string + * + * @since 4.6 + */ + public function get_post_type() { + return $this->post_type; + } + + /** + * Sets the post type that should be used to find duplicates. + * + * @param string $post_type + * + * @since 4.6 + */ + public function set_post_type( $post_type ) { + $this->post_type = $post_type; + } + + /** + * Sets the limit that should be applied to the number of JOIN in a single query. + * + * Setting the limit to an empty value will remove the limit (very bad idea). + * + * @param int $join_limit + * + * @since 4.6 + */ + public function set_join_limit( $join_limit ) { + $this->join_limit = empty( $join_limit ) + ? 999 + : intval( $join_limit ); + } + + /** + * Returns the limit that will be applied to the number of JOIN in a single query. + * + * @return int + * + * @since 4.6 + */ + public function get_join_limit() { + return $this->join_limit; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Duplicate/Strategy/Base.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Duplicate/Strategy/Base.php new file mode 100644 index 000000000..ebe5e955b --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Duplicate/Strategy/Base.php @@ -0,0 +1,24 @@ +is_a_numeric_post_field( $key ) ) { + return $wpdb->prepare( "{$key} = %d", $value ); + } + + $frags = $this->get_where_frags( $value ); + + $where_frags = []; + foreach ( $frags as $frag ) { + $formatted_frag = '%' . $wpdb->esc_like( strtolower( trim( $frag ) ) ) . '%'; + $where_frags[] = $wpdb->prepare( "{$key} LIKE %s", $formatted_frag ); + } + + return sprintf( '(%s)', implode( ' AND ', $where_frags ) ); + } + + /** + * Removes anything that's not letters, numbers, hypens and underscores from the string and returns its frags. + * + * @param string $value + * + * @return array + * + * @since 4.6 + */ + protected function get_where_frags( $value ) { + $snaked = preg_replace( '/[^a-z\d-]+/i', '_', $value ); + $frags = array_filter( explode( '_', $snaked ) ); + + return $frags; + } + + /** + * Returns a string suitable to be used as a WHERE clause in a SQL query for a custom field JOIN. + * + * @param string $key + * @param mixed $value + * @param string $table_alias + * + * @return string + * + * @since 4.6 + */ + public function where_custom_field( $key, $value, $table_alias ) { + /** @var wpdb $wpdb */ + global $wpdb; + + $frags = $this->get_where_frags( $value ); + + $where_frags = [ $wpdb->prepare( "{$table_alias}.meta_key = %s", $key ) ]; + foreach ( $frags as $frag ) { + $formatted_frag = '%' . $wpdb->esc_like( strtolower( trim( $frag ) ) ) . '%'; + $query = "{$table_alias}.meta_value LIKE %s"; + $where_frags[] = $wpdb->prepare( $query, $formatted_frag ); + } + + return sprintf( '(%s)', implode( " \n\tAND ", $where_frags ) ); + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Duplicate/Strategy/Same.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Duplicate/Strategy/Same.php new file mode 100644 index 000000000..b91e52c5c --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Duplicate/Strategy/Same.php @@ -0,0 +1,51 @@ +is_a_numeric_post_field( $key ) ) { + return $wpdb->prepare( "{$key} = %d", $value ); + } + + return $wpdb->prepare( "{$key} = %s", $value ); + } + + /** + * Returns a string suitable to be used as a WHERE clause in a SQL query for a custom field JOIN. + * + * @param string $key + * @param mixed $value + * @param string $table_alias + * + * @return string + * + * @since 4.6 + */ + public function where_custom_field( $key, $value, $table_alias ) { + /** @var wpdb $wpdb */ + global $wpdb; + + return $wpdb->prepare( "{$table_alias}.meta_key = %s AND {$table_alias}.meta_value = %s", $key, $value ); + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Duplicate/Strategy_Factory.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Duplicate/Strategy_Factory.php new file mode 100644 index 000000000..ebfde0219 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Duplicate/Strategy_Factory.php @@ -0,0 +1,108 @@ + 'Tribe__Duplicate__Strategy__Same', + 'same' => 'Tribe__Duplicate__Strategy__Same', + 'like' => 'Tribe__Duplicate__Strategy__Like', + ]; + + /** + * Filters the strategies managed by the strategy factory. + * + * If a 'default' slug is not provided the first strategy class in the map will be used as default. + * + * @param array $strategy_map An array that maps strategy slugs to strategy classes. + * @param Tribe__Duplicate__Strategy_Factory $this This factory object. + * + * @since 4.6 + */ + $this->strategy_map = apply_filters( 'tribe_duplicate_post_strategies', $strategy_map, $this ); + } + + /** + * Builds a strategy provided a strategy slug. + * + * @param string $strategy The slug for the strategy that should be built. + * + * @return Tribe__Duplicate__Strategy__Interface|bool A built strategy or `false` if the strategy could not be built. + * + * @since 4.6 + */ + public function make( $strategy ) { + /** + * Filters the strategy built by the factory. + * + * Returning a non `null` value here will override the factory operations. + * + * @param Tribe__Duplicate__Strategy__Interface $built_strategy The strategy that should be built + * for the slug. + * @param string $strategy The requested strategy slug. + * @param Tribe__Duplicate__Strategy_Factory $this This factory object. + * + * @since 4.6 + */ + $built_strategy = apply_filters( 'tribe_duplicate_post_strategy', null, $strategy, $this ); + + /** + * Filters the strategy built by the factory for a specific strategy. + * + * Returning a non `null` value here will override the factory operations. + * + * @param Tribe__Duplicate__Strategy__Interface $built_strategy The strategy that should be built + * for the slug. + * @param Tribe__Duplicate__Strategy_Factory $this This factory object. + * + * @since 4.6 + */ + $built_strategy = apply_filters( "tribe_duplicate_post_{$strategy}_strategy", $built_strategy, $this ); + + if ( null !== $built_strategy ) { + return $built_strategy; + } + + if ( isset( $this->strategy_map[ $strategy ] ) ) { + $strategy_class = $this->strategy_map[ $strategy ]; + } else { + $strategy_class = ! empty( $this->strategy_map['default'] ) + ? $this->strategy_map['default'] + : reset( $this->strategy_map ); + } + + return class_exists( $strategy_class ) + ? new $strategy_class + : false; + } + + /** + * Gets the unfiltered slug to strategy class map used by the factory. + * + * @return array + * + * @since 4.6 + */ + public function get_strategy_map() { + return $this->strategy_map; + } + + /** + * Sets the unfiltered slug to strategy class map used by the factory. + * + * @param array $strategy_map + * + * @since 4.6 + */ + public function set_strategy_map( array $strategy_map ) { + $this->strategy_map = $strategy_map; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Editor.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Editor.php new file mode 100644 index 000000000..7d4277e83 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Editor.php @@ -0,0 +1,297 @@ +are_blocks_enabled(); + + /** + * Filters whether the Blocks Editor should be activated or not for events. + * + * @since 4.12.0 + * + * @param bool $should_load_blocks Whether the blocks editor should be activated or not for events. + */ + $should_load_blocks= (bool) apply_filters( 'tribe_editor_should_load_blocks', $should_load_blocks ); + + return $should_load_blocks; + } + + /** + * Utility function to check if blocks are enabled based on two assumptions + * + * a) Is gutenberg active? + * 1) Via plugin or WP version + * b) Is the blocks editor active? + * 1) Based on the enqueue_block_assets action. + * + * @since 4.14.13 + * + * @return bool + */ + public function are_blocks_enabled() { + $gutenberg = $this->is_gutenberg_active() || $this->is_wp_version(); + $blocks_enabled = $gutenberg && $this->is_blocks_editor_active(); + + /** + * Filters whether the Blocks Editor is enabled or not. + * + * @since 4.14.13 + * + * @param bool $should_load_blocks Whether the Blocks Editor is enabled or not. + */ + return (bool) apply_filters( 'tribe_editor_are_blocks_enabled', $blocks_enabled ); + } + + /** + * Checks if we are on version 5.0-alpha or higher where we no longer have + * Gutenberg Project, but the Blocks Editor + * + * @since 4.8 + * + * @return boolean + */ + public function is_wp_version() { + global $wp_version; + + return version_compare( $wp_version, '5.0-alpha', '>=' ); + } + + /** + * Checks if we have the Gutenberg Project plugin active. + * + * @since 4.8 + * + * @return boolean + */ + private function is_gutenberg_active() { + return function_exists( 'gutenberg_register_scripts_and_styles' ); + } + + /** + * Checks if we have Editor Block active. + * + * @since 4.8 + * @since 4.14.13 Switch to using the `enqueue_block_assets` check that the Classic Editor plugin uses + * + * @return boolean + */ + public function is_blocks_editor_active() { + return has_action( 'enqueue_block_assets' ); + } + + /** + * Adds the required fields into the Post Type so that we can the Rest API to update it + * + * @since 4.8 + * + * @param array $args Arguments used to setup the Post Type + * + * @return array + */ + public function add_rest_support( $args = [] ) { + // Blocks Editor requires REST support + $args['show_in_rest'] = true; + + // Make sure we have the Support argument and it's an array + if ( ! isset( $args['supports'] ) || ! is_array( $args['supports'] ) ) { + $args['supports'] = []; + } + + if ( ! in_array( 'revisions', $args['supports'] ) ) { + $args['supports'][] = 'revisions'; + } + + // Add Custom Fields (meta) Support + if ( ! in_array( 'custom-fields', $args['supports'] ) ) { + $args['supports'][] = 'custom-fields'; + } + + // Add Post Title Support + if ( ! in_array( 'title', $args['supports'] ) ) { + $args['supports'][] = 'title'; + } + + // Add Post Excerpt Support + if ( ! in_array( 'excerpt', $args['supports'] ) ) { + $args['supports'][] = 'excerpt'; + } + + // Add Post Content Support + if ( ! in_array( 'editor', $args['supports'] ) ) { + $args['supports'][] = 'editor'; + } + + // Add Post Author Support + if ( ! in_array( 'author', $args['supports'] ) ) { + $args['supports'][] = 'author'; + } + + // Add Thumbnail Support + if ( ! in_array( 'thumbnail', $args['supports'] ) ) { + $args['supports'][] = 'thumbnail'; + } + + return $args; + } + + /** + * Detect if the Classic Editor is force-activated via plugin or if it comes from a request. + * + * @since 4.8 + * @todo Deprecate before 6.0. + * + * @return bool + */ + public function is_classic_editor() { + return ! $this->should_load_blocks(); + + _deprecated_function( __FUNCTION__, '4.14.13', 'should_load_blocks' ); + /** + * Allow other addons to disable Classic Editor based on options. + * + * @since 4.8.5 + * @deprecated 4.14.13 + * + * @param bool $classic_is_active Whether the Classic Editor should be used. + */ + return apply_filters_deprecated( + 'tribe_editor_classic_is_active', + [false], + '4.14.13', + 'tribe_editor_should_load_blocks', + 'This has been deprecated in favor of the filter in should_load_blocks(). Note however that the logic is inverted!' + ); + } + + /* DEPRECATED FUNCTIONS */ + + /** + * Adds the required fields into the Events Post Type so that we can use Block Editor + * + * @since 4.8 + * @deprecated 4.14.13 This is not used anywhere. + * + * @param array $args Arguments used to setup the Post Type + * + * @return array + */ + public function add_support( $args = [] ) { + _deprecated_function( __FUNCTION__, '4.14.13' ); + // Make sure we have the Support argument and it's an array + if ( ! isset( $args['supports'] ) || ! is_array( $args['supports'] ) ) { + $args['supports'] = []; + } + + // Add Editor Support + if ( ! in_array( 'editor', $args['supports'] ) ) { + $args['supports'][] = 'editor'; + } + + return $args; + } + + /** + * classic_editor_replace is function that is created by the plugin: + * used in ECP recurrence and TEC Meta + * + * @see https://wordpress.org/plugins/classic-editor/ + * + * prior 1.3 version the Classic Editor plugin was bundled inside of a unique function: + * `classic_editor_replace` now all is bundled inside of a class `Classic_Editor` + * + * @since 4.8 + * @deprecated 4.14.13 + * + * @return bool + */ + public function is_classic_plugin_active() { + _deprecated_function( __FUNCTION__, '4.14.13', 'Tribe\Editor\Compatibility\Classic_Editor::is_classic_plugin_active' ); + + return Tribe\Editor\Compatibility\Classic_Editor::is_classic_plugin_active(); + } + + /** + * Check if the setting `classic-editor-replace` is set to `replace` that option means to + * replace the gutenberg editor with the Classic Editor. + * + * Prior to 1.3 on Classic Editor plugin the value to identify if is on classic the value + * was `replace`, now the value is `classic` + * + * @since 4.8 + * @deprecated 4.14.13 + * + * @return bool + */ + public function is_classic_option_active() { + // _deprecated_function( __FUNCTION__, '4.14.13', 'Tribe\Editor\Compatibility\Classic_Editor::is_classic_option_active' ); + + return Tribe\Editor\Compatibility\Classic_Editor::is_classic_option_active(); + } + + /** + * Whether the TEC setting dictates Blocks or the Classic Editor. + * used in ET, ET+ and TEC + * + * @since 4.12.0 + * @todo Deprecate before 6.0. + * + * @return bool True if using Blocks. False if using the Classic Editor. + */ + public function is_events_using_blocks() { + return $this->should_load_blocks(); + + _deprecated_function( __FUNCTION__, '4.14.13', 'should_load_blocks'); + /** + * Whether the event is being served through blocks + * or the Classic Editor. + * + * @since 4.12.0 + * + * @param bool $is_using_blocks True if using blocks. False if using the Classic Editor. + */ + $is_using_blocks = apply_filters_deprecated( 'tribe_is_using_blocks', null, '4.14.13', 'tribe_editor_should_load_blocks', 'Function is slated for deprecation. Please use should_load_blocks, above.' ); + + // Early bail: The filter was overridden to return either true or false. + if ( null !== $is_using_blocks ) { + return (bool) $is_using_blocks; + } + + // Early bail: The site itself is not using blocks. + if ( ! $this->should_load_blocks() ) { + return false; + } + + return tribe_is_truthy( tribe_get_option( 'toggle_blocks_editor', false ) ); + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Editor/Assets.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Editor/Assets.php new file mode 100644 index 000000000..8a6c45926 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Editor/Assets.php @@ -0,0 +1,141 @@ + false, + 'localize' => [], + 'priority' => 10, + ] + ); + tribe_asset( + $plugin, + 'tribe-common-gutenberg-modules', + 'app/modules.js', + [ $this, 'filter_event_blocks_editor_deps' ], + 'enqueue_block_editor_assets', + [ + 'in_footer' => false, + 'localize' => [], + 'priority' => 11, + ] + ); + tribe_asset( + $plugin, + 'tribe-common-gutenberg-main', + 'app/main.js', + [ $this, 'filter_event_blocks_editor_deps' ], + 'enqueue_block_editor_assets', + [ + 'in_footer' => false, + 'localize' => [ + [ + 'name' => 'tribe_editor_config', + /** + * Array used to setup the FE with custom variables from the BE + * + * @since 4.8 + * + * @param array An array with the variables to be localized + */ + 'data' => tribe_callback( 'common.editor.configuration', 'localize' ), + ], + ], + 'priority' => 12, + ] + ); + + /** + * Block editor CSS. + */ + tribe_asset( + $plugin, + 'tribe-common-gutenberg-vendor-styles', + 'app/vendor.css', + [], + 'enqueue_block_editor_assets', + [ + 'in_footer' => false, + ] + ); + tribe_asset( + $plugin, + 'tribe-common-gutenberg-main-styles', + 'app/main.css', + [], + 'enqueue_block_editor_assets', + [ + 'in_footer' => false, + ] + ); + } + + /** + * Filter the dependencies for event blocks + * + * @since 4.14.2 + * + * @param array|object|null $assets Array of asset objects, single asset object, or null. + * + * @return array An array of dependency slugs. + */ + public function filter_event_blocks_editor_deps( $asset ) { + global $pagenow; + + $deps = [ + 'react', + 'react-dom', + 'wp-components', + 'wp-api', + 'wp-api-request', + 'wp-blocks', + 'wp-i18n', + 'wp-element', + 'wp-editor', + ]; + + if ( 'post.php' !== $pagenow && 'post-new.php' !== $pagenow ) { + if ( ( $key = array_search( 'wp-editor', $deps ) ) !== false ) { + unset( $deps[ $key ] ); + } + } + + return $deps; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Editor/Blocks/Abstract.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Editor/Blocks/Abstract.php new file mode 100644 index 000000000..919223ac6 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Editor/Blocks/Abstract.php @@ -0,0 +1,249 @@ +slug(), $this->namespace . '/' ) ) { + return $this->namespace . '/' . $this->slug(); + } else { + return $this->slug(); + } + } + + /** + * Return the namespace to child or external sources + * + * @since 4.8 + * + * @return string + */ + public function get_namespace() { + return $this->namespace; + } + + /** + * Return the block attributes + * + * @since 4.8 + * + * @param array $attributes + * + * @return array + */ + public function attributes( $params = [] ) { + + // get the default attributes + $default_attributes = $this->default_attributes(); + + // parse the attributes with the default ones + $attributes = wp_parse_args( + $params, + $default_attributes + ); + + /** + * Filters the default attributes for the block + * + * @param array $attributes The attributes + * @param object $this The current object + */ + $attributes = apply_filters( 'tribe_block_attributes_defaults_' . $this->slug(), $attributes, $this ); + + return $attributes; + } + + /** + * Return the block default attributes + * + * @since 4.8 + * + * @param array $attributes + * + * @return array + */ + public function default_attributes() { + + $attributes = []; + + /** + * Filters the default attributes + * + * @param array $params The attributes + * @param object $this The current object + */ + $attributes = apply_filters( 'tribe_block_attributes_defaults', $attributes, $this ); + + return $attributes; + } + + /** + * Since we are dealing with a Dynamic type of Block we need a PHP method to render it + * + * @since 4.8 + * + * @param array $attributes + * + * @return string + */ + public function render( $attributes = [] ) { + $json_string = json_encode( $attributes, JSON_PRETTY_PRINT ); + + return + '
    ' .
    +			'Block Name: ' . $this->name() . "\n" .
    +			'Block Attributes: ' . "\n" . $json_string .
    +		'
    '; + } + + /** + * Sends a valid JSON response to the AJAX request for the block contents + * + * @since 4.8 + * + * @return void + */ + public function ajax() { + wp_send_json_error( esc_attr__( 'Problem loading the block, please remove this block to restart.', 'tribe-common' ) ); + } + + /** + * Does the registration for PHP rendering for the Block, + * important due to being a dynamic Block + * + * @since 4.8 + * + * @return void + */ + public function register() { + $block_args = [ + 'render_callback' => [ $this, 'render' ], + ]; + + // Prevents a block from being registered twice. + if ( ! class_exists( 'WP_Block_Type_Registry' ) || WP_Block_Type_Registry::get_instance()->is_registered( $this->name() ) ) { + return; + } + + register_block_type( $this->name(), $block_args ); + } + + /** + * Registering the block and loading the assets and hooks should be handled separately. + * + * @since 4.14.13 + */ + public function load() { + add_action( 'wp_ajax_' . $this->get_ajax_action(), [ $this, 'ajax' ] ); + + $this->assets(); + $this->hook(); + } + + /** + * Determine whether a post or content string has this block. + * + * This test optimizes for performance rather than strict accuracy, detecting + * the pattern of a block but not validating its structure. For strict accuracy + * you should use the block parser on post content. + * + * @since 4.8 + * + * @see gutenberg_parse_blocks() + * + * @param int|string|WP_Post|null $post Optional. Post content, post ID, or post object. Defaults to global $post. + * + * @return bool Whether the post has this block. + */ + public function has_block( $post = null ) { + if ( ! is_numeric( $post ) ) { + $wp_post = get_post( $post ); + if ( $wp_post instanceof WP_Post ) { + $post = $wp_post->post_content; + } + } + + return false !== strpos( (string) $post, '\s*$/im'; + return $this->update_post_content( $post_id, $patttern, $replacement ); + } + + /** + * Function used to remove the inner blocks and the parent block as well inside of a post_content + * + * @since 4.8.2 + * + * @param $post_id + * @param $block_name The name of the block + * @param string $replacement The string used to replace the value of the searched block + * + * @return bool + */ + public function remove_inner_blocks( $post_id, $block_name, $replacement = '' ) { + $pattern = '/^\s*\s.*/ims'; + return $this->update_post_content( $post_id, $pattern, $replacement ); + } + + /** + * Update the content of a post using a pattern to search a specifc string, with a custom + * replacement + * + * @since 4.8.2 + * + * @param $post_id + * @param $pattern + * @param string $replacement The string used to replace the value of the searched block + * + * @return bool + */ + public function update_post_content( $post_id, $pattern, $replacement = '' ) { + $content = get_post_field( 'post_content', $post_id ); + + if ( empty( $content ) ) { + return false; + } + + $next_content = preg_replace( $pattern, $replacement, $content ); + + /** + * Don't update post content if preg_replace fails or content is the update_content + * is same as current content on the post to avoid a DB operation. + */ + if ( $next_content === null || $next_content === $content ) { + return false; + } + + return wp_update_post( [ + 'ID' => $post_id, + 'post_content' => $next_content, + ] ); + } + + /** + * Strip the dynamic blocks of the content + * + * @since 4.8.5 + * + * @param string $content The event content + * + * @return string + */ + public function strip_dynamic_blocks( $content = '' ) { + + if ( ! function_exists( 'strip_dynamic_blocks' ) ) { + return $content; + } + + return strip_dynamic_blocks( $content ); + + } + + /** + * Return the content without the tribe blocks + * + * @since 4.8.5 + * + * @param string $content The event content + * + * @return string + */ + public function exclude_tribe_blocks( $content = '' ) { + + $match_blocks_exp = '/\<\!\-\- \/?wp\:tribe.*\/?-->/i'; + + if ( ! preg_match( $match_blocks_exp, $content ) ) { + return $content; + } + + return preg_replace( $match_blocks_exp, '', $content ); + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Error.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Error.php new file mode 100644 index 000000000..c91d0c0d5 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Error.php @@ -0,0 +1,200 @@ +register( 'unknown', esc_html__( 'An Unknown error occurred' ) ); + } + + /** + * Make a quickly usable method to transform code/indexes to WP_Errors + * + * @see tribe_error() + * + * @param string|array $indexes Which Error we are looking for + * @param array $context Gives the Error context + * @param array $sprintf Allows variables on the message + * + * @return WP_Error + */ + public function send( $indexes, $context = [], $sprintf = [] ) { + if ( ! $this->exists( $indexes ) ) { + $indexes = [ 'unknown' ]; + } + + // Fetches the Errors + $messages = (array) $this->get( $indexes ); + $error = new WP_Error; + + foreach ( $messages as $key => $message ) { + // Allows variables when sending the message + if ( ! empty( $sprintf ) ) { + $message = vsprintf( $message, $sprintf ); + } + // Add this Message to the WP_Error + $error->add( $key, $message, $context ); + } + + return $error; + } + + /** + * Register a new error based on a Namespace + * + * @param string|array $indexes A list of the namespaces and last item should be the error name + * @param string $message What is going to be the message associate with this indexes + * + * @return boolean + */ + public function register( $indexes, $message ) { + if ( is_string( $indexes ) ) { + // Each namespace should come with `:` + $indexes = (array) explode( ':', $indexes ); + } + + // Couldn't register the error + if ( empty( $indexes ) ) { + return false; + } + + $variable = &$this->items; + $count = count( $indexes ); + + // Will create the Indexes based on the $slug + foreach ( $indexes as $i => $index ) { + if ( $count === $i + 1 ) { + $variable[ $index ] = $message; + } else { + $variable = &$variable[ $index ]; + } + } + + // Allows Chain Reactions + return true; + } + + /** + * Removes an error from the items + * + * @param string|array $indexes A list of the namespaces and last item should be the error name + * + * @return boolean + */ + public function remove( $indexes ) { + if ( ! $this->exists( $indexes ) ) { + return false; + } + + if ( is_string( $indexes ) ) { + // Each namespace should come with `:` + $indexes = (array) explode( ':', $indexes ); + } + + // Ensures that we don't modify the original + $variable = &$this->items; + $count = count( $indexes ); + + foreach ( $indexes as $i => $index ) { + if ( $count === $i + 1 ) { + unset( $variable[ $index ] ); + } else { + $variable = &$variable[ $index ]; + } + } + + return true; + } + + /** + * Fetches the error or namespace + * + * @param string|array $indexes (optional) A list of the namespaces and last item should be the error name + * + * @return null|array|string + */ + public function get( $indexes = null ) { + if ( is_null( $indexes ) ) { + return $this->items; + } + + if ( is_string( $indexes ) ) { + // Each namespace should come with `:` + $indexes = (array) explode( ':', $indexes ); + } + + // Ensures that we don't modify the original + $variable = $this->items; + $count = count( $indexes ); + + foreach ( $indexes as $i => $index ) { + if ( ! isset( $variable[ $index ] ) ) { + // If we are on the last item and we don't have it set make it Null + if ( $count === $i + 1 ) { + return null; + } + continue; + } + + $variable = $variable[ $index ]; + } + + $return = []; + $was_namespace = is_array( $variable ); + + /** + * @todo Allow fetching bigger groups + * Right now you can only fetch the first group of messages + * Trying to fetch Namespaces that contain other namespaces will bug + */ + foreach ( (array) $variable as $key => $value ) { + $key = implode( ':', $indexes ) . ( $was_namespace ? ':' . $key : '' ); + $return[ $key ] = $value; + } + + return $return; + } + + /** + * Checks if a given error or namespace exists + * + * @param string|array $indexes A list of the namespaces and last item should be the error name + * + * @return boolean + */ + public function exists( $indexes ) { + $variable = $this->get( $indexes ); + return ! empty( $variable ) || is_array( $variable ) ? true : false; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Exception.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Exception.php new file mode 100644 index 000000000..48e6c86c7 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Exception.php @@ -0,0 +1,90 @@ +original_exception = $original_exception; + } + + /** + * Handles the exception throwing the original when debugging (`WP_DEBUG` defined and `true`) + * or quietly logging when `WP_DEBUG` is `false` or not set. + * + * @return bool `true` if the message was logged, `false` otherwise. + * + * @throws Exception + */ + public function handle() { + $debug = defined( 'WP_DEBUG' ) && WP_DEBUG; + + if ( $debug ) { + $this->throw_original_exception(); + } + + return $this->log_original_exception_message(); + } + + /** + * @return string + */ + private function get_log_type_for_exception_code( $code ) { + $map = array( + // @todo [BTRIA-583]: Let's add a decent exception code to log type map here. + ); + + return isset( $map[ $code ] ) ? $map[ $code ] : Tribe__Log::ERROR; + } + + /** + * Throws the original exception. + * + * Provided as a manual override over the default `WP_DEBUG` dependent behaviour. + * + * @see Tribe__Exception::handle() + * + * @throws Exception + */ + public function throw_original_exception() { + throw $this->original_exception; + } + + /** + * Logs the original exception message. + * + * Provided as a manual override over the default `WP_DEBUG` dependent behaviour. + * + * @see Tribe__Exception::handle() + * + * @return bool `true` if the message was logged, `false` otherwise. + */ + private function log_original_exception_message() { + if ( ! class_exists( 'Tribe__Log' ) ) { + return false; + } + + $logger = new Tribe__Log(); + $message = $this->original_exception->getMessage(); + $log_type = $this->get_log_type_for_exception_code( $this->original_exception->getCode() ); + $src = $this->original_exception->getFile() . ':' . $this->original_exception->getLine(); + + $logger->log( $message, $log_type, $src ); + + return true; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Extension.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Extension.php new file mode 100644 index 000000000..021008beb --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Extension.php @@ -0,0 +1,454 @@ +args = $args; + $this->construct(); + + // The init() action/hook. + $init_hook = $this->get_init_hook(); + + // Continue plugin run after $init_hook has fired. + if ( did_action( $init_hook ) > 0 ) { + $this->register(); + } else { + add_action( $init_hook, [ $this, 'register' ] ); + } + } + + /** + * Empty function typically overridden by child class + */ + protected function construct() {} + + /** + * This is where the magic begins + * + * Declare this inside the child and put any custom code inside of it. + */ + abstract public function init(); + + /** + * Adds a Tribe Plugin to the list of plugins this extension depends upon. + * + * If this plugin is not present or does not exceed the specified version + * init() will not run. + * + * @param string $main_class The Main class for this Tribe plugin. + * @param string|null $minimum_version Minimum acceptable version of plugin. + */ + final protected function add_required_plugin( $main_class, $minimum_version = null ) { + $this->set( [ 'requires', $main_class ], $minimum_version ); + } + + /** + * Set the extension's tec.com URL + * + * @param string $url URL to the extension's page. + */ + final protected function set_url( $url ) { + $this->set( 'url', $url ); + + // Adds this as a tutorial link to Wp Admin > Plugins page. + Tribe__Plugin_Meta_Links::instance()->add_link( + $this->get_plugin_file(), + __( 'Tutorial', 'tribe-common' ), + $url, + [ 'class' => 'tribe-meta-link-extension' ] + ); + } + + /** + * Set the extension's version number + * + * @param string $version Extensions semantic version number. + */ + final protected function set_version( $version ) { + $this->set( 'version', $version ); + } + + /** + * Checks if the extension has permission to run, if so runs init() in child class + */ + final public function register() { + $extension_file = $this->get_plugin_file(); + $extension_class_name = $this->get( 'class' ); + $extension_version = $this->get_version(); + $plugins_required = $this->get( 'requires', [] ); + + tribe_register_plugin( + $extension_file, + $extension_class_name, + $extension_version, + $plugins_required + ); + + $dependency = Tribe__Dependency::instance(); + + // check requisite plugins are active for this extension + $is_plugin_authorized = $dependency->has_requisite_plugins( $plugins_required ); + + /** + * Explicitly disallow an extension, such as a core plugin having absorbed/replaced its functionality. + * + * @since 4.12.2 + * + * @param bool $is_disallowed    False by default. + * @param string $extension_class_name This extension's class name string + * (without initial forward slash for namespaced classes). + * @param Tribe__Extension $this_instance This extension class' instance. + */ + $is_disallowed = (bool) apply_filters( 'tribe_extension_is_disallowed', false, $extension_class_name, $this ); + + if ( $is_disallowed ) { + if ( + is_admin() + && current_user_can( 'activate_plugins' ) + ) { + tribe_notice( 'tribe_extension_is_disallowed', [ $this, 'notice_disallowed' ], [ 'type' => 'error' ] ); + } + + deactivate_plugins( $extension_file, true ); + + return; + } + + if ( $is_plugin_authorized ) { + $this->init(); + + // Add extension as active to dependency checker. + $dependency->add_active_plugin( $extension_class_name, $extension_version, $extension_file ); + } + } + + /** + * Gets the full path to the extension's plugin file + * + * Sets default if the arg is blank. + * + * @return string File path + */ + final public function get_plugin_file() { + $file = $this->get( 'file' ); + + // If this is not set assume the extension's plugin class is the plugin file. + if ( empty( $file ) ) { + $reflection = new ReflectionClass( $this->get( 'class' ) ); + $file = $reflection->getFileName(); + $this->set( 'file', $file ); + } + + return $file; + } + + /** + * Get the extension's version number + * + * @return string Semantic version number + */ + final public function get_version() { + return $this->get_arg_or_plugin_data( 'version', 'Version' ); + } + + /** + * Get the extension's plugin name + * + * @return string Plugin name + */ + final public function get_name() { + return $this->get_arg_or_plugin_data( 'name', 'Name' ); + } + + /** + * Get the extension's description + * + * @return string Plugin description + */ + final public function get_description() { + return $this->get_arg_or_plugin_data( 'description', 'Description' ); + } + + /** + * Gets the action/hook for the extensions' init(). + * + * @return string Action/hook + */ + final public function get_init_hook() { + return $this->get( 'hook', 'tribe_plugins_loaded' ); + } + + /** + * Gets the plugin data from the plugin file header + * + * This is somewhat resource intensive, so data is stored in $args + * in case of subsequent calls. + * + * @see get_plugin_data() for WP Admin only function this is similar to. + * + * @return array Plugin data; keys match capitalized file header declarations. + */ + final public function get_plugin_data() { + $plugin_data = $this->get( 'plugin_data' ); + + // Set the plugin data arg/cache to match. + if ( empty( $plugin_data ) ) { + $plugin_data = $this->set( 'plugin_data', Tribe__Utils__Plugins::get_plugin_data( $this->get_plugin_file() ) ); + } + + return $plugin_data; + } + + /** + * Retrieves any args whose default value is stored in the plugin file header + * + * @param string $arg The key for arg. + * @param string $plugin_data_key The key for the arg in the file header. + * + * @return string|null String if set, otherwise null. + */ + final public function get_arg_or_plugin_data( $arg, $plugin_data_key ) { + $arg_value = $this->get( $arg, null ); + + // See if the arg is already set, if not get default from plugin data and set it. + if ( null === $arg_value ) { + $pdata = $this->get_plugin_data(); + $arg_value = isset( $pdata[ $plugin_data_key ] ) ? $pdata[ $plugin_data_key ] : null; + } + + return $arg_value; + } + + /** + * Sets an arg, including one nested a few levels deep + * + * @param string|array $key To set an arg nested multiple levels deep pass an array + * specifying each key in order as a value. + * Example: array( 'lvl1', 'lvl2', 'lvl3' ); + * @param mixed $value The value. + */ + final protected function set( $key, $value ) { + $this->args = Tribe__Utils__Array::set( $this->args, $key, $value ); + } + + /** + * Retrieves arg, including one nested a few levels deep + * + * @param string|array $key To select an arg nested multiple levels deep pass an + * array specifying each key in order as a value. + * Example: array( 'lvl1', 'lvl2', 'lvl3' ); + * @param null $default Value to return if nothing is set. + * + * @return mixed Returns the args value or the default if arg is not found. + */ + final public function get( $key, $default = null ) { + return Tribe__Utils__Array::get( $this->args, $key, $default ); + } + + /** + * Gets the name of the class the method is called in; typically will be a child class + * + * This uses some hackery if the server is on PHP 5.2, and it can fail in rare + * circumstances causing a null value to be returned. + * + * @return string|null Class name + */ + final protected static function get_called_class() { + $class_name = null; + + if ( function_exists( 'get_called_class' ) ) { + // For PHP 5.3+ we can use the late static binding class name. + $class_name = get_called_class(); + } else { + // For PHP 5.2 and under we hack around the lack of late static bindings. + try { + $backtraces = debug_backtrace(); + + // Grab each class from the backtrace. + foreach ( $backtraces as $i ) { + $class = null; + + if ( array_key_exists( 'class', $i ) ) { + // Direct call to a class. + $class = $i['class']; + } elseif ( + array_key_exists( 'function', $i ) && + strpos( $i['function'], 'call_user_func' ) === 0 && + array_key_exists( 'args', $i ) && + is_array( $i['args'] ) && + is_array( $i['args'][0] ) && + isset( $i['args'][0][0] ) + ) { + // Found a call from call_user_func... and $i['args'][0][0] is present + // indicating a static call to a method. + $class = $i['args'][0][0]; + } else { + // Slight performance boost from skipping ahead. + continue; + } + + // Check to see if the parent is the current class. + // The first backtrace with a matching parent is our class. + if ( get_parent_class( $class ) === __CLASS__ ) { + $class_name = $class; + break; + } + } + } catch ( Exception $e ) { + // Host has disabled or misconfigured debug_backtrace(). + $exception = new Tribe__Exception( $e ); + $exception->handle(); + } + } + + // Class name was not set by debug_backtrace() hackery. + if ( null === $class_name ) { + tribe_notice( 'tribe_debug_backtrace_disabled', [ __CLASS__, 'notice_debug_backtrace' ] ); + } + + return $class_name; + } + + /** + * Echoes error message indicating user is on PHP 5.2 and debug_backtrace is disabled + */ + final public static function notice_debug_backtrace() { + printf( + '

    %s

    ', + esc_html__( 'Unable to run Tribe Extensions. Your website host is running PHP 5.2 or older, and has likely disabled or misconfigured debug_backtrace(). You, or your website host, will need to upgrade PHP or properly configure debug_backtrace() for Tribe Extensions to work.', 'tribe-common' ) + ); + } + + /** + * Gets the error message about being explicitly disallowed. + * + * @since 4.12.2 + * + * @return string Notice text. + */ + public function notice_disallowed() { + return sprintf( + '

    %1$s: %2$s

    ', + $this->get_name(), + esc_html_x( + "This extension has been programmatically disallowed. The most common reason is due to another The Events Calendar plugin having absorbed or replaced this extension's functionality. This extension plugin has been deactivated, and you should likely delete it.", + 'extension disallowed', + 'tribe-common' + ) + ); + } + + /** + * Prevent cloning the singleton with 'clone' operator + * + * @return void + */ + final public function __clone() { + _doing_it_wrong( + __FUNCTION__, + 'Can not use this method on singletons.', + '4.3' + ); + } + + /** + * Prevent unserializing the singleton instance + * + * @return void + */ + final public function __wakeup() { + _doing_it_wrong( + __FUNCTION__, + 'Can not use this method on singletons.', + '4.3' + ); + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Extension_Loader.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Extension_Loader.php new file mode 100644 index 000000000..da1271b87 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Extension_Loader.php @@ -0,0 +1,168 @@ +instantiate_extension( $plugin_file ); + } + } + + /** + * Gets tribe extension plugin foldername prefixes + * + * @return array Prefixes + */ + public static function get_extension_file_prefixes() { + $prefixes = [ 'tribe-ext-' ]; + + /** + * Filter which plugin folder prefixes are considered tribe extensions. + * + * @param array $prefixes Extension plugin folder name prefixes. + */ + return apply_filters( 'tribe_extension_prefixes', $prefixes ); + } + + /** + * Instantiates an extension based on info in its plugin file header. + * + * @param string $plugin_file Full path to extension's plugin file header. + * + * @return bool Indicates if extension was instantiated successfully. + */ + public function instantiate_extension( $plugin_file ) { + $p_data = $this->get_cached_plugin_data( $plugin_file ); + $p_folder = trailingslashit( dirname( $plugin_file ) ); + $success = false; + + // Nothing to instantiate if class is not set. + if ( empty( $p_data['ExtensionClass'] ) ) { + return $success; + } + + // Default to plugin file when empty. + $class_file = ! empty( $p_data['ExtensionFile'] ) ? $p_folder . $p_data['ExtensionFile'] : $plugin_file; + + // Include file. + if ( file_exists( $class_file ) ) { + // Prevent loading class twice in edge cases where require_once wouldn't work. + if ( ! class_exists( $p_data['ExtensionClass'] ) ) { + require( $class_file ); + } + } else { + _doing_it_wrong( + esc_html( $class_file ), + 'Extension file does not exist, please specify valid extension file.', + '4.3' + ); + } + + // Class instantiation. + if ( class_exists( $p_data['ExtensionClass'] ) ) { + $extension_args = [ + 'file' => $plugin_file, + 'plugin_data' => $p_data, + ]; + + // Instantiates extension instance. + $extension = call_user_func( + [ $p_data['ExtensionClass'], 'instance' ], + $p_data['ExtensionClass'], + $extension_args + ); + + if ( null !== $extension ) { + $success = true; + } + } else { + _doing_it_wrong( + esc_html( $p_data['ExtensionClass'] ), + 'Specified extension class does not exist. Please double check that this class is declared in the extension file.', + '4.3' + ); + } + + return $success; + } + + /** + * Retrieves plugin data from cache if it exists. + * + * @param string $plugin_path Path to plugin header file. + * + * @return array|null Plugin data or null. + */ + public function get_cached_plugin_data( $plugin_path ) { + $plugin_basename = plugin_basename( $plugin_path ); + + if ( ! array_key_exists( $plugin_basename, $this->plugin_data ) ) { + $this->plugin_data[ $plugin_basename ] = Tribe__Utils__Plugins::get_plugin_data( $plugin_path ); + } + + return $this->plugin_data[ $plugin_basename ]; + } + + /** + * Prevent cloning the singleton with 'clone' operator + * + * @return void + */ + public function __clone() { + _doing_it_wrong( + __FUNCTION__, + 'Can not use this method on singletons.', + '4.3' + ); + } + + /** + * Prevent unserializing the singleton instance + * + * @return void + */ + public function __wakeup() { + _doing_it_wrong( + __FUNCTION__, + 'Can not use this method on singletons.', + '4.3' + ); + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Feature_Detection.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Feature_Detection.php new file mode 100644 index 000000000..c10b59791 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Feature_Detection.php @@ -0,0 +1,250 @@ + 6000, + ]; + + /** + * The name of the option that will be used to indicate a feature detection is running. + * + * @var string + */ + protected $lock_option_name; + + /** + * Checks whether async, AJAX-based, background processing is supported or not. + * + * To avoid making this costly check on each load the result of this check is cached + * in the `tribe_feature_detection` transient, under the `supports_async_process` key. + * + * @since 4.7.23 + * + * @param bool $force Whether to use the cache value, if available, or force the check + * to be made again. + * + * @return bool Whether async, AJAX-based, background processing is supported or not. + */ + public function supports_async_process( $force = false ) { + /** + * Filters whether async, AJAX-based, processing is supported or not. + * + * Returning a non `null` value here will make this method bail and + * return the filtered value immediately. + * + * @since 4.7.23 + * + * @param bool $supports_async_process Whether async, AJAX-based, processing is supported or not. + * @param bool $force Whether the check is forcing the cached value to be refreshed + * or not. + */ + $supports_async_process = apply_filters( 'tribe_supports_async_process', null, $force ); + if ( null !== $supports_async_process ) { + return (bool) $supports_async_process; + } + + $this->lock_option_name = 'tribe_feature_support_check_lock'; + $transient_name = Tribe__Process__Tester::TRANSIENT_NAME; + $timed_option_exists = tec_timed_option()->exists( $transient_name ); + $supports_async_process = tec_timed_option()->get( $transient_name, null, $force ); + + if ( + $force + || null === $supports_async_process + || ! $timed_option_exists + ) { + if ( $this->is_locked() ) { + // We're already running this check, bail and return the safe option for the time being. + return false; + } + + // Let's avoid race conditions by running two or more checks at the same time. + $this->lock(); + + // Log that we're checking for AJAX-based async process support using the tester. + tribe( 'logger' )->log( 'Checking for AJAX-based async processing support triggering a test request.', Tribe__Log::DEBUG ); + + /* + * Build and dispatch the tester: if it works a transient should be set. + */ + $tester = new Tribe__Process__Tester(); + tribe( 'logger' )->log( 'Dispatching AJAX-based async processing support test request.', Tribe__Log::DEBUG ); + $tester->dispatch(); + + $wait_up_to = 10; + $start = time(); + $supports_async_process = false; + + while ( time() <= $start + $wait_up_to ) { + // We want to force a refetch from the database on each check. + $supports_async_process = (bool) tec_timed_option()->get( $transient_name ); + + if ( $supports_async_process ) { + break; + } + sleep( $wait_up_to / 5 ); + } + + $this->unlock(); + if ( $supports_async_process ) { + tribe( 'logger' )->log( 'AJAX-based async processing is supported.', Tribe__Log::DEBUG ); + } else { + tribe( 'logger' )->log( 'AJAX-based async processing is not supported; background processing will rely on WP Cron.', Tribe__Log::DEBUG ); + } + } + + return (bool) $supports_async_process; + } + + /** + * Sets the lock option to `1` to indicate a feature detection is running. + * + * @since 4.8.1 + */ + protected function lock() { + update_option( $this->lock_option_name, '1' ); + } + + /** + * Deletes the lock option to indicate the current feature detection process is done. + * + * @since 4.8.1 + */ + protected function unlock() { + delete_option( $this->lock_option_name ); + } + + /** + * Checks whether a feature detection lock is currently in place or not. + * + * @since 4.8.1 + * + * @return bool Whether a feature detection lock is currently in place or not. + */ + protected function is_locked() { + $lock_option = get_option( $this->lock_option_name ); + + return ! empty( $lock_option ); + } + + /** + * Returns the value of the `max_allowed_packet` MYSQL variable, if set, or a default value. + * + * @since 4.10.2 + * + * @return int The byte size of the `max_allowed_packet` MYSQL variable. + */ + public function get_mysql_max_packet_size() { + /** + * Filters the value of the `max_allowed_packet` variable before it's read from the database. + * + * If the value returned from this filter is not `null`, then it will be assumed to be the value. + * + * @since 4.10.2 + * + * @param int $mysql_max_packet_size The value of the `max_allowed_packet` variable, initially `null`. + */ + $mysql_max_packet_size = apply_filters( 'tribe_max_allowed_packet_size', null ); + + if ( null !== $mysql_max_packet_size ) { + return absint( $mysql_max_packet_size ); + } + + /** @var Tribe__Cache $cache */ + $cache = tribe( 'cache' ); + + $cached = $cache->get( 'max_allowed_packet' ); + + if ( false !== $cached ) { + return $cached; + } + + global $wpdb; + $mysql_max_packet_size = $wpdb->get_var( "SHOW VARIABLES LIKE 'max_allowed_packet'", 1 ); + // At min set it to 2 MBs. + $mysql_max_packet_size = absint( max( absint( $mysql_max_packet_size ), 2097152 ) ); + + $cache->set( 'max_allowed_packet', $mysql_max_packet_size, WEEK_IN_SECONDS ); + + return $mysql_max_packet_size; + } + + /** + * Returns the suggested SQL LIMIT value, based on the `max_allowed_packet` size and example string length. + * + * This is useful to size "reasonable" LIMITs when dealing with either very long queries or potentially long + * result sets. + * + * @since 4.10.2 + * + * @param string $example_string The example string. + * + * @return int The suggested LIMIT value. + */ + public function mysql_limit_for_string( $example_string ) { + $byte_size = function_exists( 'mb_strlen' ) + ? mb_strlen( $example_string ) + : strlen( $example_string ); + + return $this->mysql_limit_for_size( $byte_size ); + } + + /** + * Returns the SQL LIMIT for a byte size, in relation to the `max_allowed_packet` value. + * + * @since 4.10.2 + * + * @param int $byte_size The byte size to check. + * + * @return int The SQL LIMIT value. + */ + public function mysql_limit_for_size( $byte_size ) { + return absint( floor( $this->get_mysql_max_packet_size() / $byte_size ) * 0.8 ); + } + + /** + * Provides the SQL LIMIT value, in relation to the `max_allowed_packet` value, for a pre-existing example. + * + * Defaults to the complete post result example string if the example is not found. + * + * @since 4.10.2 + * + * @param string $example The name of the example to return. See the `Tribe__Feature_Detection::$example_sizes` + * prop for the available examples. Defaults to the `post_result` one. + * + * @return int The SQL LIMIT value for the example. + */ + public function mysql_limit_for_example( $example ) { + $example_size = Arr::get( static::$example_size, $example, static::$example_size['post_result'] ); + + return $this->mysql_limit_for_size( $example_size ); + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Field.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Field.php new file mode 100644 index 000000000..7c5dd3f4f --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Field.php @@ -0,0 +1,952 @@ +defaults = [ + 'type' => 'html', + 'name' => $id, + 'fieldset_attributes' => [], + 'attributes' => [], + 'class' => null, + 'label' => null, + 'label_attributes' => null, + 'placeholder' => null, + 'tooltip' => null, + 'size' => 'medium', + 'html' => null, + 'error' => false, + 'value' => $value, + 'options' => null, + 'conditional' => true, + 'display_callback' => null, + 'if_empty' => null, + 'can_be_empty' => false, + 'clear_after' => true, + 'tooltip_first' => false, + 'allow_clear' => false, + 'settings' => [], + ]; + + // a list of valid field types, to prevent screwy behavior + $this->valid_field_types = [ + 'heading', + 'html', + 'text', + 'textarea', + 'wysiwyg', + 'radio', + 'checkbox_bool', + 'checkbox_list', + 'dropdown', + 'dropdown', + 'dropdown_select2', // Deprecated use `dropdown` + 'dropdown_chosen', // Deprecated use `dropdown` + 'license_key', + 'number', + 'wrapped_html', + 'email', + 'color', + 'image', + 'toggle', + ]; + + $this->valid_field_types = apply_filters( 'tribe_valid_field_types', $this->valid_field_types ); + + // parse args with defaults and extract them + $args = wp_parse_args( $field, $this->defaults ); + + // sanitize the values just to be safe + $id = esc_attr( $id ); + $type = esc_attr( $args['type'] ); + $name = esc_attr( $args['name'] ); + $placeholder = esc_attr( $args['placeholder'] ); + $class = empty( $args['class'] ) ? '' : $this->sanitize_class_attribute( $args['class'] ); + $label = wp_kses( + $args['label'], [ + 'a' => [ 'href' => [], 'title' => [] ], + 'br' => [], + 'em' => [], + 'strong' => [], + 'b' => [], + 'i' => [], + 'u' => [], + 'img' => [ + 'title' => [], + 'src' => [], + 'alt' => [], + ], + 'span' => [ 'class' => [] ], + ] + ); + $label_attributes = $args['label_attributes']; + $tooltip = wp_kses( + $args['tooltip'], [ + 'a' => [ 'class' => [], 'href' => [], 'title' => [], 'target' => [], 'rel' => [] ], + 'br' => [], + 'em' => [ 'class' => [] ], + 'strong' => [ 'class' => [] ], + 'b' => [ 'class' => [] ], + 'i' => [ 'class' => [] ], + 'u' => [ 'class' => [] ], + 'img' => [ + 'class' => [], + 'title' => [], + 'src' => [], + 'alt' => [], + ], + 'code' => [ 'span' => [] ], + 'span' => [ 'class' => [] ], + ] + ); + $fieldset_attributes = []; + if ( is_array( $args['fieldset_attributes'] ) ) { + foreach ( $args['fieldset_attributes'] as $key => $val ) { + $fieldset_attributes[ $key ] = esc_attr( $val ); + } + } + $attributes = []; + if ( is_array( $args['attributes'] ) ) { + foreach ( $args['attributes'] as $key => $val ) { + $attributes[ $key ] = esc_attr( $val ); + } + } + if ( is_array( $args['options'] ) ) { + $options = []; + foreach ( $args['options'] as $key => $val ) { + $options[ $key ] = $val; + } + } else { + $options = $args['options']; + } + $size = esc_attr( $args['size'] ); + $html = $args['html']; + $error = (bool) $args['error']; + $value = is_array( $value ) ? array_map( 'esc_attr', $value ) : esc_attr( $value ); + $conditional = $args['conditional']; + $display_callback = $args['display_callback']; + $if_empty = is_string( $args['if_empty'] ) ? trim( $args['if_empty'] ) : $args['if_empty']; + $can_be_empty = (bool) $args['can_be_empty']; + $clear_after = (bool) $args['clear_after']; + $tooltip_first = (bool) $args['tooltip_first']; + $allow_clear = (bool) $args['allow_clear']; + $settings = $args['settings']; + + // set the ID + $this->id = apply_filters( 'tribe_field_id', $id ); + + // set each instance variable and filter + foreach ( array_keys( $this->defaults ) as $key ) { + $this->{$key} = apply_filters( 'tribe_field_' . $key, $$key, $this->id ); + } + + // epicness + $this->do_field(); + } + + /** + * Determines how to handle this field's creation + * either calls a callback function or runs this class' course of action + * logs an error if it fails + * + * @return void + */ + public function do_field() { + + if ( $this->conditional ) { + + if ( $this->display_callback && is_callable( $this->display_callback ) ) { + + // if there's a callback, run it + call_user_func( $this->display_callback ); + + } elseif ( in_array( $this->type, $this->valid_field_types ) ) { + + // the specified type exists, run the appropriate method + $field = call_user_func( [ $this, $this->type ] ); + + // filter the output + $field = apply_filters( 'tribe_field_output_' . $this->type, $field, $this->id, $this ); + echo apply_filters( 'tribe_field_output_' . $this->type . '_' . $this->id, $field, $this->id, $this ); + + } else { + + // fail, log the error + Tribe__Debug::debug( esc_html__( 'Invalid field type specified', 'tribe-common' ), $this->type, 'notice' ); + + } + } + } + + /** + * returns the field's start + * + * @return string the field start + */ + public function do_field_start() { + $return = '
    type; + $return .= ( $this->error ) ? ' tribe-error' : ''; + $return .= ( $this->size ) ? ' tribe-size-' . $this->size : ''; + $return .= ( $this->class ) ? ' ' . $this->class . '"' : '"'; + $return .= ( $this->fieldset_attributes ) ? ' ' . $this->do_fieldset_attributes() : ''; + $return .= '>'; + + return apply_filters( 'tribe_field_start', $return, $this->id, $this->type, $this->error, $this->class, $this ); + } + + /** + * returns the field's end + * + * @return string the field end + */ + public function do_field_end() { + $return = '
    '; + $return .= ( $this->clear_after ) ? '
    ' : ''; + + return apply_filters( 'tribe_field_end', $return, $this->id, $this ); + } + + /** + * returns the field's label + * + * @return string the field label + */ + public function do_field_label() { + $return = ''; + if ( $this->label ) { + if ( isset( $this->label_attributes ) ) { + $this->label_attributes['class'] = isset( $this->label_attributes['class'] ) ? + implode( ' ', array_merge( [ 'tribe-field-label' ], $this->label_attributes['class'] ) ) : + [ 'tribe-field-label' ]; + $this->label_attributes = $this->concat_attributes( $this->label_attributes ); + } + $return = sprintf( '%s', $this->label_attributes, $this->label ); + } + + return apply_filters( 'tribe_field_label', $return, $this->label, $this ); + } + + /** + * returns the field's div start + * + * @return string the field div start + */ + public function do_field_div_start() { + $return = '
    '; + + if ( true === $this->tooltip_first ) { + $return .= $this->do_tool_tip(); + // and empty it to avoid it from being printed again + $this->tooltip = ''; + } + + return apply_filters( 'tribe_field_div_start', $return, $this ); + } + + /** + * returns the field's div end + * + * @return string the field div end + */ + public function do_field_div_end() { + $return = $this->do_tool_tip(); + $return .= '
    '; + + return apply_filters( 'tribe_field_div_end', $return, $this ); + } + + /** + * returns the field's tooltip/description + * + * @return string the field tooltip + */ + public function do_tool_tip() { + $return = ''; + if ( $this->tooltip ) { + $return = '

    ' . $this->tooltip . '

    '; + } + + return apply_filters( 'tribe_field_tooltip', $return, $this->tooltip, $this ); + } + + /** + * returns the screen reader label + * + * @return string the screen reader label + */ + public function do_screen_reader_label() { + $return = ''; + if ( $this->tooltip ) { + $return = ''; + } + + return apply_filters( 'tribe_field_screen_reader_label', $return, $this->tooltip, $this ); + } + + /** + * returns the field's value + * + * @return string the field value + */ + public function do_field_value() { + $return = ''; + if ( $this->value ) { + $return = ' value="' . $this->value . '"'; + } + + return apply_filters( 'tribe_field_value', $return, $this->value, $this ); + } + + /** + * returns the field's name + * + * @param bool $multi + * + * @return string the field name + */ + public function do_field_name( $multi = false ) { + $return = ''; + if ( $this->name ) { + if ( $multi ) { + $return = ' name="' . $this->name . '[]"'; + } else { + $return = ' name="' . $this->name . '"'; + } + } + + return apply_filters( 'tribe_field_name', $return, $this->name, $this ); + } + + /** + * returns the field's placeholder + * + * @return string the field value + */ + public function do_field_placeholder() { + $return = ''; + if ( $this->placeholder ) { + $return = ' placeholder="' . $this->placeholder . '"'; + } + + return apply_filters( 'tribe_field_placeholder', $return, $this->placeholder, $this ); + } + + /** + * Return a string of attributes for the field + * + * @return string + **/ + public function do_field_attributes() { + $return = ''; + if ( ! empty( $this->attributes ) ) { + foreach ( $this->attributes as $key => $value ) { + $return .= ' ' . $key . '="' . $value . '"'; + } + } + + return apply_filters( 'tribe_field_attributes', $return, $this->name, $this ); + } + + /** + * Return a string of attributes for the fieldset + * + * @return string + **/ + public function do_fieldset_attributes() { + $return = ''; + if ( ! empty( $this->fieldset_attributes ) ) { + foreach ( $this->fieldset_attributes as $key => $value ) { + $return .= ' ' . $key . '="' . $value . '"'; + } + } + + return apply_filters( 'tribe_fieldset_attributes', $return, $this->name, $this ); + } + + /** + * generate a heading field + * + * @return string the field + */ + public function heading() { + $field = '

    ' . $this->label . '

    '; + + return $field; + } + + /** + * generate an html field + * + * @return string the field + */ + public function html() { + $field = $this->do_field_label(); + $field .= $this->html; + + return $field; + } + + /** + * generate a simple text field + * + * @return string the field + */ + public function text() { + $field = $this->do_field_start(); + $field .= $this->do_field_label(); + $field .= $this->do_field_div_start(); + $field .= 'do_field_name(); + $field .= $this->do_field_value(); + $field .= $this->do_field_placeholder(); + $field .= $this->do_field_attributes(); + $field .= '/>'; + $field .= $this->do_screen_reader_label(); + $field .= $this->do_field_div_end(); + $field .= $this->do_field_end(); + + return $field; + } + + /** + * generate a textarea field + * + * @return string the field + */ + public function textarea() { + $field = $this->do_field_start(); + $field .= $this->do_field_label(); + $field .= $this->do_field_div_start(); + $field .= 'do_field_name(); + $field .= $this->do_field_attributes(); + $field .= '>'; + $field .= esc_html( stripslashes( $this->value ) ); + $field .= ''; + $field .= $this->do_screen_reader_label(); + $field .= $this->do_field_div_end(); + $field .= $this->do_field_end(); + + return $field; + } + + /** + * generate a wp_editor field + * + * @return string the field + */ + public function wysiwyg() { + $mce = new Wysiwyg( $this->name, $this->value, $this->settings ); + $field = $this->do_field_start(); + $field .= $this->do_field_label(); + $field .= $this->do_field_div_start(); + $field .= $mce->get_html(); + $field .= $this->do_screen_reader_label(); + $field .= $this->do_field_div_end(); + $field .= $this->do_field_end(); + + return $field; + } + + /** + * generate a radio button field + * + * @return string the field + */ + public function radio() { + $field = $this->do_field_start(); + $field .= $this->do_field_label(); + $field .= $this->do_field_div_start(); + if ( is_array( $this->options ) ) { + foreach ( $this->options as $option_id => $title ) { + $field_id = sprintf( + '%1$s-%2$s', + sanitize_html_class( trim( $this->id ) ), + sanitize_html_class( trim( $option_id ) ) + ); + + $field .= ''; + } + } else { + $field .= '' . esc_html__( 'No radio options specified', 'tribe-common' ) . ''; + } + $field .= $this->do_field_div_end(); + $field .= $this->do_field_end(); + + return $field; + } + + /** + * generate a checkbox_list field + * + * @return string the field + */ + public function checkbox_list() { + $field = $this->do_field_start(); + $field .= $this->do_field_label(); + $field .= $this->do_field_div_start(); + + if ( ! is_array( $this->value ) ) { + if ( ! empty( $this->value ) ) { + $this->value = [ $this->value ]; + } else { + $this->value = []; + } + } + + if ( is_array( $this->options ) ) { + foreach ( $this->options as $option_id => $title ) { + $field .= ''; + } + } else { + $field .= '' . esc_html__( 'No checkbox options specified', 'tribe-common' ) . ''; + } + $field .= $this->do_field_div_end(); + $field .= $this->do_field_end(); + + return $field; + } + + /** + * generate a boolean checkbox field + * + * @return string the field + */ + public function checkbox_bool() { + $field = $this->do_field_start(); + $field .= $this->do_field_label(); + $field .= $this->do_field_div_start(); + $field .= 'do_field_name(); + $field .= ' value="1" ' . checked( $this->value, true, false ); + $field .= $this->do_field_attributes(); + $field .= '/>'; + $field .= $this->do_screen_reader_label(); + $field .= $this->do_field_div_end(); + $field .= $this->do_field_end(); + + return $field; + } + + /** + * generate a dropdown field + * + * @return string the field + */ + public function dropdown() { + $field = $this->do_field_start(); + $field .= $this->do_field_label(); + $field .= $this->do_field_div_start(); + if ( is_array( $this->options ) && ! empty( $this->options ) ) { + $field .= 'do_field_name(); + $field .= " id='{$this->id}-select'"; + $field .= " class='tribe-dropdown'"; + if ( empty( $this->allow_clear ) ) { + $field .= " data-prevent-clear='true'"; + } + $field .= $this->do_field_attributes(); + $field .= '>'; + foreach ( $this->options as $option_id => $title ) { + $field .= ''; + } + $field .= ''; + $field .= $this->do_screen_reader_label(); + } elseif ( $this->if_empty ) { + $field .= '' . (string) $this->if_empty . ''; + } else { + $field .= '' . esc_html__( 'No select options specified', 'tribe-common' ) . ''; + } + $field .= $this->do_field_div_end(); + $field .= $this->do_field_end(); + + return $field; + } + + /** + * generate a chosen dropdown field - the same as the + * regular dropdown but wrapped so it can have the + * right css class applied to it + * + * @deprecated + * + * @return string the field + */ + public function dropdown_chosen() { + $field = $this->dropdown(); + + return $field; + } + + /** + * generate a select2 dropdown field - the same as the + * regular dropdown but wrapped so it can have the + * right css class applied to it + * + * @deprecated + * + * @return string the field + */ + public function dropdown_select2() { + $field = $this->dropdown(); + + return $field; + } + + /** + * generate a license key field + * + * @return string the field + */ + public function license_key() { + $field = $this->do_field_start(); + $field .= $this->do_field_label(); + $field .= $this->do_field_div_start(); + $field .= 'do_field_name(); + $field .= $this->do_field_value(); + $field .= $this->do_field_attributes(); + $field .= '/>'; + $field .= '

    '; + $field .= ''; + $field .= $this->do_screen_reader_label(); + $field .= $this->do_field_div_end(); + $field .= $this->do_field_end(); + + return $field; + } + + /** + * Generate a color field. + * + * @since 5.0.0 + * + * @return string The field. + */ + public function color() { + + tribe( Settings::class )->maybe_load_color_field_assets(); + + $field = $this->do_field_start(); + $field .= $this->do_field_label(); + $field .= $this->do_field_div_start(); + $field .= 'do_field_name(); + $field .= $this->do_field_value(); + $field .= $this->do_field_attributes(); + $field .= '/>'; + $field .= $this->do_screen_reader_label(); + $field .= $this->do_field_div_end(); + $field .= $this->do_field_end(); + + return $field; + } + + /** + * Generate an image field. + * + * @since 5.0.0 + * + * @return string The field. + */ + public function image() { + + tribe( Settings::class )->maybe_load_image_field_assets(); + + $image_exists = ! empty( $this->value ); + $upload_image_text = esc_html__( 'Select Image', 'tribe-common' ); + $remove_image_text = esc_html__( 'Remove Image', 'tribe-common' ); + + // Add default fieldset attributes if none exist. + $image_fieldset_attributes = [ + 'data-select-image-text' => esc_html__( 'Select an image', 'tribe-common' ), + 'data-use-image-text' => esc_html__( 'Use this image', 'tribe-common' ), + ]; + $this->fieldset_attributes = array_merge( $image_fieldset_attributes, $this->fieldset_attributes ); + + $field = $this->do_field_start(); + $field .= $this->do_field_label(); + $field .= $this->do_field_div_start(); + $field .= 'do_field_name(); + $field .= $this->do_field_value(); + $field .= $this->do_field_attributes(); + $field .= '/>'; + $field .= ''; + $field .= '

    '; + $field .= ''; + $field .= $this->do_screen_reader_label(); + $field .= $this->do_field_div_end(); + $field .= $this->do_field_end(); + + return $field; + } + + /** + * Generate a toggle switch. + * + * @since 5.0.12 + * + * @return string the field + */ + public function toggle() { + $field = $this->do_field_start(); + $field .= $this->do_field_label(); + $field .= $this->do_field_div_start(); + $field .= 'do_field_name(); + $field .= ' value="1" ' . checked( $this->value, true, false ); + $field .= $this->do_field_attributes(); + $field .= '/>'; + $field .= ''; + $field .= $this->do_screen_reader_label(); + $field .= $this->do_field_div_end(); + $field .= $this->do_field_end(); + + return $field; + } + + /* deprecated camelCase methods */ + public function doField() { + _deprecated_function( __METHOD__, '4.3', __CLASS__ . '::do_field' ); + return $this->do_field(); + } + + public function doFieldStart() { + _deprecated_function( __METHOD__, '4.3', __CLASS__ . '::do_field_start' ); + return $this->do_field_start(); + } + + public function doFieldEnd() { + _deprecated_function( __METHOD__, '4.3', __CLASS__ . '::do_field_end' ); + return $this->do_field_end(); + } + + public function doFieldLabel() { + _deprecated_function( __METHOD__, '4.3', __CLASS__ . '::do_field_label' ); + return $this->do_field_label(); + } + + public function doFieldDivStart() { + _deprecated_function( __METHOD__, '4.3', __CLASS__ . '::do_field_div_start' ); + return $this->do_field_div_start(); + } + + public function doFieldDivEnd() { + _deprecated_function( __METHOD__, '4.3', __CLASS__ . '::do_field_div_end' ); + return $this->do_field_div_end(); + } + + public function doToolTip() { + _deprecated_function( __METHOD__, '4.3', __CLASS__ . '::do_tool_tip' ); + return $this->do_tool_tip(); + } + + public function doFieldValue() { + _deprecated_function( __METHOD__, '4.3', __CLASS__ . '::do_field_value' ); + return $this->do_field_value(); + } + + public function doFieldName( $multi = false ) { + _deprecated_function( __METHOD__, '4.3', __CLASS__ . '::do_field_name' ); + return $this->do_field_name( $multi ); + } + + public function doFieldAttributes() { + _deprecated_function( __METHOD__, '4.3', __CLASS__ . '::do_field_attributes' ); + return $this->do_field_attributes(); + } + + public function doScreenReaderLabel() { + _deprecated_function( __METHOD__, '4.3', __CLASS__ . '::do_screen_reader_label' ); + return $this->do_screen_reader_label(); + } + + /** + * Generate a wrapped html field. + * + * This is useful to print some HTML that should be inline with the other fieldsets. + * + * @return string The field markup. + */ + public function wrapped_html() { + $field = $this->do_field_start(); + $field .= $this->do_field_label(); + $field .= $this->do_field_div_start(); + $field .= $this->html; + $field .= $this->do_field_div_start(); + $field .= $this->do_field_end(); + + return $field; + } + + /** + * Concatenates an array of attributes to use in HTML tags. + * + * Example usage: + * + * $attrs = [ 'class' => ['one', 'two'], 'style' => 'color:red;' ]; + * printf ( '

    %s

    ', tribe_concat_attributes( $attrs ), 'bar' ); + * + * //

    class="one two" style="color:red;">bar

    + * + * @param array $attributes An array of attributes in the format + * [ => , => ] + * where `value` can be a string or an array. + * + * @return string The concatenated attributes. + */ + protected function concat_attributes( array $attributes = [] ) { + if ( empty( $attributes ) ) { + return ''; + } + + $concat = []; + foreach ( $attributes as $attribute => $value ) { + if ( is_array( $value ) ) { + $value = implode( ' ', $value ); + } + $quote = false !== strpos( $value, '"' ) ? "'" : '"'; + $concat[] = esc_attr( $attribute ) . '=' . $quote . esc_attr( $value ) . $quote; + } + + return implode( ' ', $concat ); + } + + /** + * Generate an email address field + * + * @since 4.7.4 + * + * @return string The field + */ + public function email() { + $this->value = trim( $this->value ); + return $this->text(); + } + + /** + * Sanitizes a space-separated or array of classes. + * + * @since 4.7.7 + * + * @param string|array $class A single class, a space-separated list of classes + * or an array of classes. + * + * @return string A space-separated list of classes. + */ + protected function sanitize_class_attribute( $class ) { + $classes = is_array( $class ) ? $class : explode( ' ', $class ); + $sanitized = array_map( 'sanitize_html_class', $classes ); + + return implode( ' ', $sanitized ); + } + } // end class +} // endif class_exists diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Field_Conditional.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Field_Conditional.php new file mode 100644 index 000000000..ca1af0537 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Field_Conditional.php @@ -0,0 +1,79 @@ + array( + * 'type' => 'checkbox_bool', + * // ... + * ), + * 'bar' => array( + * 'type' => 'text' + * 'validate_if' => new Tribe__Field_Conditional( 'foo', 'tribe_is_truthy' ) + * 'conditional' => tribe_is_truthy( tribe_get_option( 'foo' ) ), + * // ... + * ), + * ); + * + * The above will modify the validation logic to make it so that the field will not be validated if + * the parent (`foo` in the example) is not "truthy". + * If you need to hide/show the field conditionally use the `conditional` attribute of the field. + * + * @since 4.7.7 + */ +class Tribe__Field_Conditional { + + /** + * @var string The slug of the field the condition + */ + protected $depends_on; + /** + * @var bool + */ + protected $condition; + + /** + * Tribe__Field_Conditional constructor. + * + * @since 4.7.7 + * + * @param string $depends_on_field The slug or identifier of the parent field. + * @param mixed|callable $condition Either a valid callable function or method or a + * value that will be used for a shallow comparison. + */ + public function __construct( $depends_on_field, $condition = true ) { + $this->depends_on = $depends_on_field; + $this->condition = $condition; + } + + /** + * @param mixed $value The value to check, typically the parent field value. + * @param array $fields An array of all the current fields; this will be passed to + * the condition callback function for context if the condition + * is a callable function or method. + * + * @return bool Whether the check was successful (the parent field does have the required + * value) or not. + */ + public function check( $value, array $fields ) { + return is_callable( $this->condition ) + ? call_user_func( $this->condition, $value, $fields ) + : $value == $this->condition; + } + + /** + * Return the id/slug of the field this condition depends on. + * + * @since 4.7.7 + * + * @return string + */ + public function depends_on() { + return $this->depends_on; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Image/Uploader.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Image/Uploader.php new file mode 100644 index 000000000..6b321a053 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Image/Uploader.php @@ -0,0 +1,319 @@ +featured_image = $featured_image; + } + + /** + * Resets the static "cache" of the class. + */ + public static function reset_cache() { + self::$attachment_guids_cache = false; + self::$original_urls_cache = false; + } + + /** + * Uploads a file and creates the media attachment or simply returns the attachment ID if existing. + * + * @return int|bool The attachment post ID if the uploading and attachment is successful or the ID refers to an + * attachment; + * `false` otherwise. + */ + public function upload_and_get_attachment_id() { + if ( empty( $this->featured_image ) ) { + return false; + } + + $existing = false; + + if ( is_string( $this->featured_image ) && ! is_numeric( $this->featured_image ) ) { + // Assume image exists in the local file system. + $id = $this->get_attachment_ID_from_url( $this->featured_image ); + if ( ! $id ) { + $id = $this->upload_file( $this->featured_image ); + $id = $this->maybe_retry_upload( $id ); + } + $existing = (bool) $id; + } elseif ( $post = get_post( $this->featured_image ) ) { + $id = $post && 'attachment' === $post->post_type ? $this->featured_image : false; + } else { + $id = false; + } + + do_action( + 'tribe_log', + 'debug', + __CLASS__, + [ + 'featured_image' => $this->featured_image, + 'exists' => $existing, + 'id' => $id, + ] + ); + + return $id; + } + + /** + * Retry to upload an image after it failed as was provided, try to decode the URL as in some cases the + * original URL might be encoded HTML components such as: "&" and some CDNs does not handle well different URLs + * as they were provided so we try to recreate the original URL where it might be required. + * + * @since 4.11.5 + * + * @param int|bool $id The id of the attachment if was uploaded correctly, false otherwise. + * + * @return int The ID of the attachment after the upload retry. + */ + protected function maybe_retry_upload( $id ) { + if ( $id ) { + do_action( 'tribe_log', 'debug', __CLASS__, [ 'message' => "ID: {$id} is already a valid one." ] ); + + return $id; + } + + $decoded = esc_url_raw( html_entity_decode( $this->featured_image ) ); + + do_action( 'tribe_log', 'debug', __CLASS__, [ + 'message' => 'Retry upload decoding the URL of the image', + 'url' => $this->featured_image, + 'decoded' => $decoded, + ] ); + + // Maybe the URL was encoded and we need to convert it to a valid URL. + return $this->upload_file( $decoded ); + } + + /** + * @param string $file_url + * + * @return int + */ + protected function upload_file( $file_url ) { + /** + * Allow plugins to enable local URL uploads, mainly used for testing. + * + * @since 4.9.5 + * + * @param bool $allow_local_urls Whether to allow local URLs. + * @param string $file_url File URL. + */ + $allow_local_urls = apply_filters( 'tribe_image_uploader_local_urls', false, $file_url ); + + if ( ! $allow_local_urls && ! filter_var( $file_url, FILTER_VALIDATE_URL ) ) { + return false; + } + + // These files need to be included as dependencies + require_once( ABSPATH . 'wp-admin/includes/image.php' ); + require_once( ABSPATH . 'wp-admin/includes/file.php' ); + require_once( ABSPATH . 'wp-admin/includes/media.php' ); + + $is_local = false; + // This is a local file no need to fetch it from the wire. + if ( $allow_local_urls && file_exists( $file_url ) ) { + $file = $file_url; + $is_local = true; + } else { + /** + * Some CDN services will append query arguments to the image URL; removing + * them now has the potential of blocking the image fetching completely so we + * let them be here. + */ + $file = download_url( $file_url ); + if ( is_wp_error( $file ) ) { + do_action( 'tribe_log', 'error', __CLASS__, [ + 'message' => $file->get_error_message(), + 'url' => $file_url, + 'error' => $file, + ] ); + + return false; + } + } + + // Upload file into WP and leave WP handle the resize and such. + $attachment_id = media_handle_sideload( + [ + 'name' => $this->create_file_name( $file ), + 'tmp_name' => $file, + 'post_mime_type' => 'image', + ], + 0 + ); + + // Remove the temporary file as is no longer required at this point. + if ( ! $is_local && file_exists( $file ) ) { + @unlink( $file ); + } + + if ( is_wp_error( $attachment_id ) ) { + do_action( 'tribe_log', 'error', __CLASS__, [ + 'message' => $attachment_id->get_error_message(), + 'url' => $file_url, + 'error' => $attachment_id, + ] ); + + return false; + } + + update_post_meta( $attachment_id, '_tribe_importer_original_url', $file_url ); + + $this->maybe_init_attachment_guids_cache(); + $this->maybe_init_attachment_original_urls_cache(); + + $attachment_post = get_post( $attachment_id ); + // Only update the cache if is a valid attachment. + if ( $attachment_post instanceof WP_Post ) { + self::$attachment_guids_cache[ $attachment_post->guid ] = $attachment_id; + self::$original_urls_cache[ $file_url ] = $attachment_id; + } + + return $attachment_id; + } + + /** + * WordPress requires to have an extension in all all files as uses `wp_check_filetype` which uses the extension + * of the file to define if a file is valid or not, in this case the extension might not be present in some URLs of + * attachments or media files, in those cases we try to guess the right extension using the mime of the file as + * an alternative, if the $filename is a path we can verify the mime type using native WP functions. + * + * @since 4.11.5 + * + * @param string $filename The name of the file or URL. + * + * @return string Returned a file name with an extension if is not already part of the file name. + */ + protected function create_file_name( $filename ) { + /** + * We use the path basename only here to provided WordPress with a good filename + * that will allow it to correctly detect and validate the extension. + */ + $path = wp_parse_url( $filename, PHP_URL_PATH ); + + $name = basename( $path ); + $properties = wp_check_filetype( $name ); + + // Type can be defined from the name use that one instead. + if ( ! empty( $properties['type'] ) ) { + return $name; + } + + // This is not a file that exists on the system, use the name instead. + if ( ! file_exists( $filename ) ) { + return $name; + } + + $mime = wp_get_image_mime( $filename ); + + // There's no mime defined for the file use the plain name instead. + if ( $mime === '' ) { + return $name; + } + + // create an array with the mimes as the keys and extensions as values. + $mime_to_extensions = array_flip( wp_get_mime_types() ); + + // No mime was found for the file on the array of allowed mime types, fallback to the name. + if ( ! isset( $mime_to_extensions[ $mime ] ) ) { + return $name; + } + + // If there are more than one extension just ose one. + $parts = explode( '|', $mime_to_extensions[ $mime ] ); + + // Create a new name with extension. + return implode( '.', [ $name, reset( $parts ) ] ); + } + + protected function get_attachment_ID_from_url( $featured_image ) { + $this->maybe_init_attachment_guids_cache(); + $this->maybe_init_attachment_original_urls_cache(); + + $guids_cache = self::$attachment_guids_cache; + $original_urls_cache = self::$original_urls_cache; + if ( isset( $guids_cache[ $featured_image ] ) ) { + return $guids_cache[ $featured_image ]; + } + + if ( isset( $original_urls_cache[ $featured_image ] ) ) { + return $original_urls_cache[ $featured_image ]; + } + + return false; + } + + protected function maybe_init_attachment_guids_cache() { + if ( false === self::$attachment_guids_cache ) { + /** @var \wpdb $wpdb */ + global $wpdb; + $guids = $wpdb->get_results( "SELECT ID, guid FROM $wpdb->posts where post_type = 'attachment'" ); + + if ( $guids ) { + $keys = wp_list_pluck( $guids, 'guid' ); + $values = wp_list_pluck( $guids, 'ID' ); + self::$attachment_guids_cache = array_combine( $keys, $values ); + } else { + self::$attachment_guids_cache = []; + } + } + } + + protected function maybe_init_attachment_original_urls_cache() { + if ( false === self::$original_urls_cache ) { + /** @var \wpdb $wpdb */ + global $wpdb; + $original_urls = $wpdb->get_results( " + SELECT p.ID, pm.meta_value FROM $wpdb->posts p + JOIN $wpdb->postmeta pm + ON p.ID = pm.post_id + WHERE p.post_type = 'attachment' + AND pm.meta_key = '_tribe_importer_original_url' + " ); + + if ( $original_urls ) { + $keys = wp_list_pluck( $original_urls, 'meta_value' ); + $values = wp_list_pluck( $original_urls, 'ID' ); + self::$original_urls_cache = array_combine( $keys, $values ); + } else { + self::$original_urls_cache = []; + } + } + } + + /** + * Handles errors generated during the use of `file_get_contents` to + * make them run-time exceptions. + * + * @since 4.7.22 + * + * @param string $unused_error_code The error numeric code. + * @param string $message The error message. + * + * @throws RuntimeException To pass the error as an exception to + * the handler. + */ + public function handle_error( $unused_error_code, $message ) { + throw new RuntimeException( $message ); + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/JSON_LD/Abstract.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/JSON_LD/Abstract.php new file mode 100644 index 000000000..fc86b9054 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/JSON_LD/Abstract.php @@ -0,0 +1,349 @@ +exists( $post_id ) && $this->type_exists( $post_id, $this->type ) ) { + return []; + } + + $post = get_post( $post_id ); + + if ( empty( $post->ID ) ) { + return []; + } + + $data = (object) []; + + // We may need to prevent the context to be triggered + if ( ! isset( $args['context'] ) || false !== $args['context'] ) { + $data->{'@context'} = 'http://schema.org'; + } + + $data->{'@type'} = $this->type; + + $data->name = esc_js( get_the_title( $post ) ); + $data->description = esc_js( tribe_post_excerpt( $post ) ); + + if ( has_post_thumbnail( $post ) ) { + $data->image = wp_get_attachment_url( get_post_thumbnail_id( $post ) ); + } + + $data->url = esc_url_raw( $this->get_link( $post ) ); + + $type = strtolower( esc_attr( $this->type ) ); + $data = $this->apply_object_data_filter( $data, $args, $post ); + + // Index by ID: this will allow filter code to identify the actual event being referred to + // without injecting an additional property + return [ $post->ID => $data ]; + } + + /** + * Filters the JSON LD object data. + * + * The expectation is that any sub-classes overriding the get_data() method will ensure they + * call this method for consistency. + * + * @param string $type + * @param object $data + * @param array $args + * @param WP_Post $post + * + * @return mixed + */ + protected function apply_object_data_filter( $data, $args, $post ) { + $type = strtolower( esc_attr( $this->type ) ); + + /** + * Allows the event data to be modifed by themes and other plugins. + * + * @example tribe_json_ld_thing_object + * @example tribe_json_ld_event_object + * + * @param object $data The JSON-LD object + * @param array $args The arguments used to get data + * @param WP_Post $post The post object + */ + return apply_filters( "tribe_json_ld_{$type}_object", $data, $args, $post ); + } + + /** + * puts together the actual html/json javascript block for output + * + * @return string + */ + public function get_markup( $post = null, $args = [] ) { + $data = $this->get_data( $post, $args ); + $type = strtolower( esc_attr( $this->type ) ); + $this->set_type( $post, $type ); + + foreach ( $data as $post_id => $_data ) { + // Register this post as done already + $this->register( $post_id ); + } + + /** + * Allows the event data to be modifed by themes and other plugins. + * + * @example tribe_json_ld_thing_data + * @example tribe_json_ld_event_data + * + * @param array $data objects representing the Google Markup for each event. + * @param array $args the arguments used to get data + */ + $data = apply_filters( "tribe_json_ld_{$type}_data", $data, $args ); + + // Strip the post ID indexing before returning + $data = array_values( $data ); + + if ( ! empty( $data ) ) { + $html[] = ''; + } + + return ! empty( $html ) ? implode( "\r\n", $html ) : ''; + } + + public function markup( $post = null, $args = [] ) { + $html = $this->get_markup( $post, $args ); + + /** + * Allows users to filter the end markup of JSON-LD + * + * @param string The HTML for the JSON LD markup + */ + $html = apply_filters( 'tribe_json_ld_markup', $html ); + echo $html; + } + + /** + * Get a link to the post + * + * Children of this class are likely to override it with their + * own functions that only work with their designated post type. + * + * @since 4.5.10 + * + * @param int|WP_Post $post The Post Object or ID + * + * @return false|string Link to the post or false + */ + protected function get_link( $post ) { + return get_permalink( $post ); + } + + /** + * Gets from the Posts index a specific post or fetch all of them + * + * @param int|WP_Post $post The Post Object or ID + * + * @return null|array|WP_Post Returns an Indexed Array of Posts, a found Post or Null if not found + */ + public function get( $post = null ) { + if ( is_null( $post ) ) { + return self::$posts; + } + $id = Tribe__Main::post_id_helper( $post ); + + if ( $this->exists( $id ) ) { + return self::$posts[ $id ]; + } + + return null; + } + + /** + * Checks if a Post has been registered to the JSON-LD index + * + * @param int|WP_Post $post The Post Object or ID + * + * @return bool + */ + public function exists( $post ) { + return isset( self::$posts[ Tribe__Main::post_id_helper( $post ) ] ); + } + + /** + * Register the new Post on the Index of created ones + * + * @param int|WP_Post $post The Post Object or ID + * + * @return WP_Post The Post Object that was registered + */ + public function register( $post ) { + $id = Tribe__Main::post_id_helper( $post ); + if ( $this->exists( $id ) ) { + return self::$posts[ $id ]; + } + self::$posts[ $id ] = get_post( $id ); + return self::$posts[ $id ]; + } + + + /** + * Public method to have access to the types + * + * @since 4.7.12 + * + * @return array + */ + public function get_types() { + return self::$types; + } + + /** + * Register the current $type to prevent duplicates entries with different $types and IDs + * + * @since 4.7.12 + * + * @param $post + * @param $type + * + * @return mixed + */ + public function set_type( $post, $type ) { + $id = Tribe__Main::post_id_helper( $post ); + + if ( $this->type_exists( $id, $type ) ) { + return self::$types[ $id ]; + } + + if ( empty( self::$types[ $id ] ) ) { + self::$types[ $id ] = [ $this->type ]; + } else { + self::$types[ $id ][] = $this->type; + } + + return self::$types[ $id ]; + } + + /** + * Remove an Post from the Indexed list + * + * @param int|WP_Post $post The Post Object or ID + * + * @return bool + */ + public function remove( $post ) { + $id = Tribe__Main::post_id_helper( $post ); + + if ( ! $this->exists( $id ) ) { + return false; + } + + unset( self::$posts[ $id ] ); + + return true; + } + + /** + * Return `true` if the $type has been already registered for the specified $id. + * + * @since 4.7.12 + * + * @param $id + * @param $type + * + * @return bool + */ + public function type_exists( $id, $type ) { + return isset( self::$types[ $id ] ) && false !== array_search( $type, self::$types[ $id ] ); + } + + /** + * Empties the registered posts cache variable. + * + * Added for testing purposes. + */ + public static function unregister_all() { + self::$posts = []; + self::$types = []; + } + + /** + * Returns an array of the registered post IDs. + * + * @return array + */ + public static function get_registered_post_ids() { + return array_keys( self::$posts ); + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Languages/Locations.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Languages/Locations.php new file mode 100644 index 000000000..36449a907 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Languages/Locations.php @@ -0,0 +1,413 @@ +get( $cache_key , '', null ); + + if ( null === $countries ) { + $countries = $this->build_country_array(); + + if ( $escape ) { + $countries = array_map( static function( $country ) { + return html_entity_decode( $country, ENT_QUOTES ); + }, $countries ); + } + + // Actually set the cache in case it's not in place. + $cache->set( $cache_key, $countries ); + } + + return $countries; + } + + /** + * Returns an array of countries and their codes. + * + * Adds array to object cache to speed up subsequent retrievals. + * + * @return array { + * List of countries + * + * @type string $country_code Country name. + * } + */ + public function get_us_states() { + return tribe( 'cache' )->get( 'tribe_us_states_list', '', [ $this, 'build_us_states_array' ] ); + } + + /** + * Get a translated array of countries. + * + * @return array { + * List of countries + * + * @type string $country_code Country name. + * } + */ + public function build_country_array() { + $countries = [ + 'US' => esc_html__( 'United States', 'tribe-common' ), + 'AF' => esc_html__( 'Afghanistan', 'tribe-common' ), + 'AX' => esc_html__( 'Åland Islands', 'tribe-common' ), + 'AL' => esc_html__( 'Albania', 'tribe-common' ), + 'DZ' => esc_html__( 'Algeria', 'tribe-common' ), + 'AS' => esc_html__( 'American Samoa', 'tribe-common' ), + 'AD' => esc_html__( 'Andorra', 'tribe-common' ), + 'AO' => esc_html__( 'Angola', 'tribe-common' ), + 'AI' => esc_html__( 'Anguilla', 'tribe-common' ), + 'AQ' => esc_html__( 'Antarctica', 'tribe-common' ), + 'AG' => esc_html__( 'Antigua and Barbuda', 'tribe-common' ), + 'AR' => esc_html__( 'Argentina', 'tribe-common' ), + 'AM' => esc_html__( 'Armenia', 'tribe-common' ), + 'AW' => esc_html__( 'Aruba', 'tribe-common' ), + 'AU' => esc_html__( 'Australia', 'tribe-common' ), + 'AT' => esc_html__( 'Austria', 'tribe-common' ), + 'AZ' => esc_html__( 'Azerbaijan', 'tribe-common' ), + 'BS' => esc_html__( 'Bahamas', 'tribe-common' ), + 'BH' => esc_html__( 'Bahrain', 'tribe-common' ), + 'BD' => esc_html__( 'Bangladesh', 'tribe-common' ), + 'BB' => esc_html__( 'Barbados', 'tribe-common' ), + 'BY' => esc_html__( 'Belarus', 'tribe-common' ), + 'BE' => esc_html__( 'Belgium', 'tribe-common' ), + 'BZ' => esc_html__( 'Belize', 'tribe-common' ), + 'BJ' => esc_html__( 'Benin', 'tribe-common' ), + 'BM' => esc_html__( 'Bermuda', 'tribe-common' ), + 'BT' => esc_html__( 'Bhutan', 'tribe-common' ), + 'BO' => esc_html__( 'Bolivia', 'tribe-common' ), + 'BA' => esc_html__( 'Bosnia and Herzegovina', 'tribe-common' ), + 'BW' => esc_html__( 'Botswana', 'tribe-common' ), + 'BV' => esc_html__( 'Bouvet Island', 'tribe-common' ), + 'BR' => esc_html__( 'Brazil', 'tribe-common' ), + 'IO' => esc_html__( 'British Indian Ocean Territory', 'tribe-common' ), + 'BN' => esc_html__( 'Brunei Darussalam', 'tribe-common' ), + 'BG' => esc_html__( 'Bulgaria', 'tribe-common' ), + 'BF' => esc_html__( 'Burkina Faso', 'tribe-common' ), + 'BI' => esc_html__( 'Burundi', 'tribe-common' ), + 'KH' => esc_html__( 'Cambodia', 'tribe-common' ), + 'CM' => esc_html__( 'Cameroon', 'tribe-common' ), + 'CA' => esc_html__( 'Canada', 'tribe-common' ), + 'CV' => esc_html__( 'Cape Verde', 'tribe-common' ), + 'KY' => esc_html__( 'Cayman Islands', 'tribe-common' ), + 'CF' => esc_html__( 'Central African Republic', 'tribe-common' ), + 'TD' => esc_html__( 'Chad', 'tribe-common' ), + 'CL' => esc_html__( 'Chile', 'tribe-common' ), + 'CN' => esc_html__( 'China', 'tribe-common' ), + 'CX' => esc_html__( 'Christmas Island', 'tribe-common' ), + 'CC' => esc_html__( 'Cocos (Keeling) Islands', 'tribe-common' ), + 'MF' => esc_html__( 'Collectivity of Saint Martin', 'tribe-common' ), + 'CO' => esc_html__( 'Colombia', 'tribe-common' ), + 'KM' => esc_html__( 'Comoros', 'tribe-common' ), + 'CG' => esc_html__( 'Congo', 'tribe-common' ), + 'CD' => esc_html__( 'Congo, Democratic Republic of the', 'tribe-common' ), + 'CK' => esc_html__( 'Cook Islands', 'tribe-common' ), + 'CR' => esc_html__( 'Costa Rica', 'tribe-common' ), + 'CI' => esc_html__( "Côte d'Ivoire", 'tribe-common' ), + 'HR' => esc_html__( 'Croatia (Local Name: Hrvatska)', 'tribe-common' ), + 'CU' => esc_html__( 'Cuba', 'tribe-common' ), + 'CW' => esc_html__( 'Curaçao', 'tribe-common' ), + 'CY' => esc_html__( 'Cyprus', 'tribe-common' ), + 'CZ' => esc_html__( 'Czech Republic', 'tribe-common' ), + 'DK' => esc_html__( 'Denmark', 'tribe-common' ), + 'DJ' => esc_html__( 'Djibouti', 'tribe-common' ), + 'DM' => esc_html__( 'Dominica', 'tribe-common' ), + 'DO' => esc_html__( 'Dominican Republic', 'tribe-common' ), + 'TP' => esc_html__( 'East Timor', 'tribe-common' ), + 'EC' => esc_html__( 'Ecuador', 'tribe-common' ), + 'EG' => esc_html__( 'Egypt', 'tribe-common' ), + 'SV' => esc_html__( 'El Salvador', 'tribe-common' ), + 'GQ' => esc_html__( 'Equatorial Guinea', 'tribe-common' ), + 'ER' => esc_html__( 'Eritrea', 'tribe-common' ), + 'EE' => esc_html__( 'Estonia', 'tribe-common' ), + 'ET' => esc_html__( 'Ethiopia', 'tribe-common' ), + 'FK' => esc_html__( 'Falkland Islands (Malvinas)', 'tribe-common' ), + 'FO' => esc_html__( 'Faroe Islands', 'tribe-common' ), + 'FJ' => esc_html__( 'Fiji', 'tribe-common' ), + 'FI' => esc_html__( 'Finland', 'tribe-common' ), + 'FR' => esc_html__( 'France', 'tribe-common' ), + 'GF' => esc_html__( 'French Guiana', 'tribe-common' ), + 'PF' => esc_html__( 'French Polynesia', 'tribe-common' ), + 'TF' => esc_html__( 'French Southern Territories', 'tribe-common' ), + 'GA' => esc_html__( 'Gabon', 'tribe-common' ), + 'GM' => esc_html__( 'Gambia', 'tribe-common' ), + 'GE' => esc_html_x( 'Georgia', 'The country', 'tribe-common' ), + 'DE' => esc_html__( 'Germany', 'tribe-common' ), + 'GH' => esc_html__( 'Ghana', 'tribe-common' ), + 'GI' => esc_html__( 'Gibraltar', 'tribe-common' ), + 'GR' => esc_html__( 'Greece', 'tribe-common' ), + 'GL' => esc_html__( 'Greenland', 'tribe-common' ), + 'GD' => esc_html__( 'Grenada', 'tribe-common' ), + 'GP' => esc_html__( 'Guadeloupe', 'tribe-common' ), + 'GU' => esc_html__( 'Guam', 'tribe-common' ), + 'GT' => esc_html__( 'Guatemala', 'tribe-common' ), + 'GN' => esc_html__( 'Guinea', 'tribe-common' ), + 'GW' => esc_html__( 'Guinea-Bissau', 'tribe-common' ), + 'GY' => esc_html__( 'Guyana', 'tribe-common' ), + 'HT' => esc_html__( 'Haiti', 'tribe-common' ), + 'HM' => esc_html__( 'Heard and McDonald Islands', 'tribe-common' ), + 'VA' => esc_html__( 'Holy See (Vatican City State)', 'tribe-common' ), + 'HN' => esc_html__( 'Honduras', 'tribe-common' ), + 'HK' => esc_html__( 'Hong Kong', 'tribe-common' ), + 'HU' => esc_html__( 'Hungary', 'tribe-common' ), + 'IS' => esc_html__( 'Iceland', 'tribe-common' ), + 'IN' => esc_html__( 'India', 'tribe-common' ), + 'ID' => esc_html__( 'Indonesia', 'tribe-common' ), + 'IR' => esc_html__( 'Iran, Islamic Republic of', 'tribe-common' ), + 'IQ' => esc_html__( 'Iraq', 'tribe-common' ), + 'IE' => esc_html__( 'Ireland', 'tribe-common' ), + 'IL' => esc_html__( 'Israel', 'tribe-common' ), + 'IT' => esc_html__( 'Italy', 'tribe-common' ), + 'JM' => esc_html__( 'Jamaica', 'tribe-common' ), + 'JP' => esc_html__( 'Japan', 'tribe-common' ), + 'JO' => esc_html__( 'Jordan', 'tribe-common' ), + 'KZ' => esc_html__( 'Kazakhstan', 'tribe-common' ), + 'KE' => esc_html__( 'Kenya', 'tribe-common' ), + 'KI' => esc_html__( 'Kiribati', 'tribe-common' ), + 'KP' => esc_html__( "Korea, Democratic People's Republic of", 'tribe-common' ), + 'KR' => esc_html__( 'Korea, Republic of', 'tribe-common' ), + 'KW' => esc_html__( 'Kuwait', 'tribe-common' ), + 'KG' => esc_html__( 'Kyrgyzstan', 'tribe-common' ), + 'LA' => esc_html__( "Lao People's Democratic Republic", 'tribe-common' ), + 'LV' => esc_html__( 'Latvia', 'tribe-common' ), + 'LB' => esc_html__( 'Lebanon', 'tribe-common' ), + 'LS' => esc_html__( 'Lesotho', 'tribe-common' ), + 'LR' => esc_html__( 'Liberia', 'tribe-common' ), + 'LY' => esc_html__( 'Libya', 'tribe-common' ), + 'LI' => esc_html__( 'Liechtenstein', 'tribe-common' ), + 'LT' => esc_html__( 'Lithuania', 'tribe-common' ), + 'LU' => esc_html__( 'Luxembourg', 'tribe-common' ), + 'MO' => esc_html__( 'Macau', 'tribe-common' ), + 'MG' => esc_html__( 'Madagascar', 'tribe-common' ), + 'MW' => esc_html__( 'Malawi', 'tribe-common' ), + 'MY' => esc_html__( 'Malaysia', 'tribe-common' ), + 'MV' => esc_html__( 'Maldives', 'tribe-common' ), + 'ML' => esc_html__( 'Mali', 'tribe-common' ), + 'MT' => esc_html__( 'Malta', 'tribe-common' ), + 'MH' => esc_html__( 'Marshall Islands', 'tribe-common' ), + 'MQ' => esc_html__( 'Martinique', 'tribe-common' ), + 'MR' => esc_html__( 'Mauritania', 'tribe-common' ), + 'MU' => esc_html__( 'Mauritius', 'tribe-common' ), + 'YT' => esc_html__( 'Mayotte', 'tribe-common' ), + 'MX' => esc_html__( 'Mexico', 'tribe-common' ), + 'FM' => esc_html__( 'Micronesia, Federated States of', 'tribe-common' ), + 'MD' => esc_html__( 'Moldova, Republic of', 'tribe-common' ), + 'MC' => esc_html__( 'Monaco', 'tribe-common' ), + 'MN' => esc_html__( 'Mongolia', 'tribe-common' ), + 'ME' => esc_html__( 'Montenegro', 'tribe-common' ), + 'MS' => esc_html__( 'Montserrat', 'tribe-common' ), + 'MA' => esc_html__( 'Morocco', 'tribe-common' ), + 'MZ' => esc_html__( 'Mozambique', 'tribe-common' ), + 'MM' => esc_html__( 'Myanmar', 'tribe-common' ), + 'NA' => esc_html__( 'Namibia', 'tribe-common' ), + 'NR' => esc_html__( 'Nauru', 'tribe-common' ), + 'NP' => esc_html__( 'Nepal', 'tribe-common' ), + 'NL' => esc_html__( 'Netherlands', 'tribe-common' ), + 'NC' => esc_html__( 'New Caledonia', 'tribe-common' ), + 'NZ' => esc_html__( 'New Zealand', 'tribe-common' ), + 'NI' => esc_html__( 'Nicaragua', 'tribe-common' ), + 'NE' => esc_html__( 'Niger', 'tribe-common' ), + 'NG' => esc_html__( 'Nigeria', 'tribe-common' ), + 'NU' => esc_html__( 'Niue', 'tribe-common' ), + 'NF' => esc_html__( 'Norfolk Island', 'tribe-common' ), + 'MK' => esc_html__( 'North Macedonia', 'tribe-common' ), + 'MP' => esc_html__( 'Northern Mariana Islands', 'tribe-common' ), + 'NO' => esc_html__( 'Norway', 'tribe-common' ), + 'OM' => esc_html__( 'Oman', 'tribe-common' ), + 'PK' => esc_html__( 'Pakistan', 'tribe-common' ), + 'PW' => esc_html__( 'Palau', 'tribe-common' ), + 'PA' => esc_html__( 'Panama', 'tribe-common' ), + 'PG' => esc_html__( 'Papua New Guinea', 'tribe-common' ), + 'PY' => esc_html__( 'Paraguay', 'tribe-common' ), + 'PE' => esc_html__( 'Peru', 'tribe-common' ), + 'PH' => esc_html__( 'Philippines', 'tribe-common' ), + 'PN' => esc_html__( 'Pitcairn', 'tribe-common' ), + 'PL' => esc_html__( 'Poland', 'tribe-common' ), + 'PT' => esc_html__( 'Portugal', 'tribe-common' ), + 'PR' => esc_html__( 'Puerto Rico', 'tribe-common' ), + 'QA' => esc_html__( 'Qatar', 'tribe-common' ), + 'RE' => esc_html__( 'Reunion', 'tribe-common' ), + 'RO' => esc_html__( 'Romania', 'tribe-common' ), + 'RU' => esc_html__( 'Russian Federation', 'tribe-common' ), + 'RW' => esc_html__( 'Rwanda', 'tribe-common' ), + 'BL' => esc_html__( 'Saint Barthélemy', 'tribe-common' ), + 'SH' => esc_html__( 'Saint Helena', 'tribe-common' ), + 'KN' => esc_html__( 'Saint Kitts and Nevis', 'tribe-common' ), + 'LC' => esc_html__( 'Saint Lucia', 'tribe-common' ), + 'PM' => esc_html__( 'Saint Pierre and Miquelon', 'tribe-common' ), + 'VC' => esc_html__( 'Saint Vincent and The Grenadines', 'tribe-common' ), + 'WS' => esc_html__( 'Samoa', 'tribe-common' ), + 'SM' => esc_html__( 'San Marino', 'tribe-common' ), + 'ST' => esc_html__( 'São Tomé and Príncipe', 'tribe-common' ), + 'SA' => esc_html__( 'Saudi Arabia', 'tribe-common' ), + 'SN' => esc_html__( 'Senegal', 'tribe-common' ), + 'RS' => esc_html__( 'Serbia', 'tribe-common' ), + 'SC' => esc_html__( 'Seychelles', 'tribe-common' ), + 'SL' => esc_html__( 'Sierra Leone', 'tribe-common' ), + 'SG' => esc_html__( 'Singapore', 'tribe-common' ), + 'SX' => esc_html__( 'Sint Maarten', 'tribe-common' ), + 'SK' => esc_html__( 'Slovakia (Slovak Republic)', 'tribe-common' ), + 'SI' => esc_html__( 'Slovenia', 'tribe-common' ), + 'SB' => esc_html__( 'Solomon Islands', 'tribe-common' ), + 'SO' => esc_html__( 'Somalia', 'tribe-common' ), + 'ZA' => esc_html__( 'South Africa', 'tribe-common' ), + 'GS' => esc_html__( 'South Georgia, South Sandwich Islands', 'tribe-common' ), + 'ES' => esc_html__( 'Spain', 'tribe-common' ), + 'LK' => esc_html__( 'Sri Lanka', 'tribe-common' ), + 'SD' => esc_html__( 'Sudan', 'tribe-common' ), + 'SR' => esc_html__( 'Suriname', 'tribe-common' ), + 'SJ' => esc_html__( 'Svalbard and Jan Mayen Islands', 'tribe-common' ), + 'SZ' => esc_html__( 'Swaziland', 'tribe-common' ), + 'SE' => esc_html__( 'Sweden', 'tribe-common' ), + 'CH' => esc_html__( 'Switzerland', 'tribe-common' ), + 'SY' => esc_html__( 'Syrian Arab Republic', 'tribe-common' ), + 'TW' => esc_html__( 'Taiwan', 'tribe-common' ), + 'TJ' => esc_html__( 'Tajikistan', 'tribe-common' ), + 'TZ' => esc_html__( 'Tanzania, United Republic of', 'tribe-common' ), + 'TH' => esc_html__( 'Thailand', 'tribe-common' ), + 'TG' => esc_html__( 'Togo', 'tribe-common' ), + 'TK' => esc_html__( 'Tokelau', 'tribe-common' ), + 'TO' => esc_html__( 'Tonga', 'tribe-common' ), + 'TT' => esc_html__( 'Trinidad and Tobago', 'tribe-common' ), + 'TN' => esc_html__( 'Tunisia', 'tribe-common' ), + 'TR' => esc_html__( 'Turkey', 'tribe-common' ), + 'TM' => esc_html__( 'Turkmenistan', 'tribe-common' ), + 'TC' => esc_html__( 'Turks and Caicos Islands', 'tribe-common' ), + 'TV' => esc_html__( 'Tuvalu', 'tribe-common' ), + 'UG' => esc_html__( 'Uganda', 'tribe-common' ), + 'UA' => esc_html__( 'Ukraine', 'tribe-common' ), + 'AE' => esc_html__( 'United Arab Emirates', 'tribe-common' ), + 'GB' => esc_html__( 'United Kingdom', 'tribe-common' ), + 'UM' => esc_html__( 'United States Minor Outlying Islands', 'tribe-common' ), + 'UY' => esc_html__( 'Uruguay', 'tribe-common' ), + 'UZ' => esc_html__( 'Uzbekistan', 'tribe-common' ), + 'VU' => esc_html__( 'Vanuatu', 'tribe-common' ), + 'VE' => esc_html__( 'Venezuela', 'tribe-common' ), + 'VN' => esc_html__( 'Viet Nam', 'tribe-common' ), + 'VG' => esc_html__( 'Virgin Islands (British)', 'tribe-common' ), + 'VI' => esc_html__( 'Virgin Islands (U.S.)', 'tribe-common' ), + 'WF' => esc_html__( 'Wallis and Futuna Islands', 'tribe-common' ), + 'EH' => esc_html__( 'Western Sahara', 'tribe-common' ), + 'YE' => esc_html__( 'Yemen', 'tribe-common' ), + 'ZM' => esc_html__( 'Zambia', 'tribe-common' ), + 'ZW' => esc_html__( 'Zimbabwe', 'tribe-common' ), + ]; + + // Perform a natural sort, ensures the countries are in the expected order even once translated. + natsort( $countries ); + + /** + * Filter that allows to change the list and the output of the countries names. + * + * @since 4.7.12 + * + * @param array associative array with: Country Code => Country Name + */ + return (array) apply_filters( 'tribe_countries', $countries ); + } + + /** + * Get a translated array of US States. + * + * @return array { + * List of States + * + * @type string $state_abbreviation State. + * } + */ + public function build_us_states_array() { + $states = [ + 'AL' => esc_html__( 'Alabama', 'tribe-common' ), + 'AK' => esc_html__( 'Alaska', 'tribe-common' ), + 'AZ' => esc_html__( 'Arizona', 'tribe-common' ), + 'AR' => esc_html__( 'Arkansas', 'tribe-common' ), + 'CA' => esc_html__( 'California', 'tribe-common' ), + 'CO' => esc_html__( 'Colorado', 'tribe-common' ), + 'CT' => esc_html__( 'Connecticut', 'tribe-common' ), + 'DE' => esc_html__( 'Delaware', 'tribe-common' ), + 'DC' => esc_html__( 'District of Columbia', 'tribe-common' ), + 'FL' => esc_html__( 'Florida', 'tribe-common' ), + 'GA' => esc_html_x( 'Georgia', 'The US state Georgia', 'tribe-common' ), + 'HI' => esc_html__( 'Hawaii', 'tribe-common' ), + 'ID' => esc_html__( 'Idaho', 'tribe-common' ), + 'IL' => esc_html__( 'Illinois', 'tribe-common' ), + 'IN' => esc_html__( 'Indiana', 'tribe-common' ), + 'IA' => esc_html__( 'Iowa', 'tribe-common' ), + 'KS' => esc_html__( 'Kansas', 'tribe-common' ), + 'KY' => esc_html__( 'Kentucky', 'tribe-common' ), + 'LA' => esc_html__( 'Louisiana', 'tribe-common' ), + 'ME' => esc_html__( 'Maine', 'tribe-common' ), + 'MD' => esc_html__( 'Maryland', 'tribe-common' ), + 'MA' => esc_html__( 'Massachusetts', 'tribe-common' ), + 'MI' => esc_html__( 'Michigan', 'tribe-common' ), + 'MN' => esc_html__( 'Minnesota', 'tribe-common' ), + 'MS' => esc_html__( 'Mississippi', 'tribe-common' ), + 'MO' => esc_html__( 'Missouri', 'tribe-common' ), + 'MT' => esc_html__( 'Montana', 'tribe-common' ), + 'NE' => esc_html__( 'Nebraska', 'tribe-common' ), + 'NV' => esc_html__( 'Nevada', 'tribe-common' ), + 'NH' => esc_html__( 'New Hampshire', 'tribe-common' ), + 'NJ' => esc_html__( 'New Jersey', 'tribe-common' ), + 'NM' => esc_html__( 'New Mexico', 'tribe-common' ), + 'NY' => esc_html__( 'New York', 'tribe-common' ), + 'NC' => esc_html__( 'North Carolina', 'tribe-common' ), + 'ND' => esc_html__( 'North Dakota', 'tribe-common' ), + 'OH' => esc_html__( 'Ohio', 'tribe-common' ), + 'OK' => esc_html__( 'Oklahoma', 'tribe-common' ), + 'OR' => esc_html__( 'Oregon', 'tribe-common' ), + 'PA' => esc_html__( 'Pennsylvania', 'tribe-common' ), + 'RI' => esc_html__( 'Rhode Island', 'tribe-common' ), + 'SC' => esc_html__( 'South Carolina', 'tribe-common' ), + 'SD' => esc_html__( 'South Dakota', 'tribe-common' ), + 'TN' => esc_html__( 'Tennessee', 'tribe-common' ), + 'TX' => esc_html__( 'Texas', 'tribe-common' ), + 'UT' => esc_html__( 'Utah', 'tribe-common' ), + 'VT' => esc_html__( 'Vermont', 'tribe-common' ), + 'VA' => esc_html__( 'Virginia', 'tribe-common' ), + 'WA' => esc_html__( 'Washington', 'tribe-common' ), + 'WV' => esc_html__( 'West Virginia', 'tribe-common' ), + 'WI' => esc_html__( 'Wisconsin', 'tribe-common' ), + 'WY' => esc_html__( 'Wyoming', 'tribe-common' ), + ]; + + // Perform a natural sort, ensures the states are in the expected order even once translated. + natsort( $states ); + + /** + * Filter that allows to change the names of US states before output. + * + * @since 4.7.12 + * + * @param array Associative array with the format: State Code => State Name + */ + return (array) apply_filters( 'tribe_us_states', $states ); + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Languages/Map_Interface.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Languages/Map_Interface.php new file mode 100644 index 000000000..725a4f81d --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Languages/Map_Interface.php @@ -0,0 +1,32 @@ + => ] + * e.g. [ 'pt-BR' => 'Portuguese (Brazil)' ] + */ + public function get_supported_languages(); + + /** + * Checks whether a language code is supported by the language map or not. + * + * @param string $language_code + * + * @return bool Whether a language code is supported by the language map or not. + */ + public function is_supported( $language_code ); + + /** + * Converts a language code from the format used by WP to the one used by the language map. + * + * @param string $language_code A language code in the format used by WP; e.g. `en_US`. + * + * @return string|false The converted language code or `false` if the language code is not supported. + */ + public function convert_language_code( $language_code ); +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Languages/Recaptcha_Map.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Languages/Recaptcha_Map.php new file mode 100644 index 000000000..e818a312e --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Languages/Recaptcha_Map.php @@ -0,0 +1,125 @@ + => ] + * e.g. [ 'pt-BR' => 'Portuguese (Brazil)' ] + */ + public function get_supported_languages() { + return [ + 'ar' => 'Arabic', + 'af' => 'Afrikaans', + 'am' => 'Amharic', + 'hy' => 'Armenian', + 'az' => 'Azerbaijani', + 'eu' => 'Basque', + 'bn' => 'Bengali', + 'bg' => 'Bulgarian', + 'ca' => 'Catalan', + 'zh-HK' => 'Chinese (Hong Kong)', + 'zh-CN' => 'Chinese (Simplified)', + 'zh-TW' => 'Chinese (Traditional)', + 'hr' => 'Croatian', + 'cs' => 'Czech', + 'da' => 'Danish', + 'nl' => 'Dutch', + 'en-GB' => 'English (UK)', + 'en' => 'English (US)', + 'et' => 'Estonian', + 'fil' => 'Filipino', + 'fi' => 'Finnish', + 'fr' => 'French', + 'fr-CA' => 'French (Canadian)', + 'gl' => 'Galician', + 'ka' => 'Georgian', + 'de' => 'German', + 'de-AT' => 'German (Austria)', + 'de-CH' => 'German (Switzerland)', + 'el' => 'Greek', + 'gu' => 'Gujarati', + 'iw' => 'Hebrew', + 'hi' => 'Hindi', + 'hu' => 'Hungarain', + 'is' => 'Icelandic', + 'id' => 'Indonesian', + 'it' => 'Italian', + 'ja' => 'Japanese', + 'kn' => 'Kannada', + 'ko' => 'Korean', + 'lo' => 'Laothian', + 'lv' => 'Latvian', + 'lt' => 'Lithuanian', + 'ms' => 'Malay', + 'ml' => 'Malayalam', + 'mr' => 'Marathi', + 'mn' => 'Mongolian', + 'no' => 'Norwegian', + 'fa' => 'Persian', + 'pl' => 'Polish', + 'pt' => 'Portuguese', + 'pt-BR' => 'Portuguese (Brazil)', + 'pt-PT' => 'Portuguese (Portugal)', + 'ro' => 'Romanian', + 'ru' => 'Russian', + 'sr' => 'Serbian', + 'si' => 'Sinhalese', + 'sk' => 'Slovak', + 'sl' => 'Slovenian', + 'es' => 'Spanish', + 'es-419' => 'Spanish (Latin America)', + 'sw' => 'Swahili', + 'sv' => 'Swedish', + 'ta' => 'Tamil', + 'te' => 'Telugu', + 'th' => 'Thai', + 'tr' => 'Turkish', + 'uk' => 'Ukrainian', + 'ur' => 'Urdu', + 'vi' => 'Vietnamese', + 'zu' => 'Zulu', + ]; + } + + /** + * Checks whether a language code is supported by the language map or not. + * + * @param string $language_code + * + * @return bool Whether a language code is supported by the language map or not. + */ + public function is_supported( $language_code ) { + return (bool) $this->convert_language_code( $language_code ); + } + + /** + * Converts a language code from the format used by WP to the one used by the language map. + * + * @param string $language_code A language code in the format used by WP; e.g. `en_US`. + * + * @return string|false The converted language code or `false` if the language code is not supported. + */ + public function convert_language_code( $language_code ) { + $converted_code = $language_code; + if ( strlen( $language_code ) > 2 ) { + // go from `en_US` to `en-US` + $converted_code = str_replace( '_', '-', $language_code ); + $exists = array_key_exists( $converted_code, $this->get_supported_languages() ); + // try with just the two first chars + $converted_code = $exists ? $converted_code : $this->convert_language_code( substr( $language_code, 0, 2 ) ); + } else { + $converted_code = array_key_exists( $language_code, $this->get_supported_languages() ) ? $converted_code : false; + } + + return $converted_code ? $converted_code : false; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Log.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Log.php new file mode 100644 index 000000000..b25ac9581 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Log.php @@ -0,0 +1,437 @@ + description ]. + * + * @var array + */ + protected $levels = []; + + /** + * Alternative representation of the $levels property allowing quick look + * up of levels by priority. + * + * @var array + */ + protected $prioritized_levels = []; + + /** + * Instantiated loggers, stored for re-use. + * + * @var array + */ + protected $loggers = []; + + public function __construct() { + if ( is_admin() ) { + $this->admin = new Tribe__Log__Admin(); + } + + $this->current_level = $this->get_level(); + $this->log_cleanup(); + } + + /** + * @return Tribe__Log__Admin + */ + public function admin() { + return $this->admin; + } + + /** + * Facilitates daily cleanup and log rotation. + */ + protected function log_cleanup() { + $this->register_cleanup_task(); + do_action( self::CLEANUP, [ $this, 'do_cleanup' ] ); + } + + /** + * Schedules a daily cleanup task if one is not already in place. + */ + protected function register_cleanup_task() { + if ( ! wp_next_scheduled( self::CLEANUP ) ) { + wp_schedule_event( strtotime( '+1 day' ), 'daily', self::CLEANUP ); + } + } + + /** + * Call the cleanup() method for each available logging engine. + * + * We don't just call it on the current engine since, if there was a recent change, + * we'll generally still want the now unused engine's output to be cleaned up. + */ + public function do_cleanup() { + foreach ( $this->get_logging_engines() as $engine ) { + /** + * @var Tribe__Log__Logger $engine + */ + $engine->cleanup(); + } + } + + /** + * Logs a debug-level entry. + * + * @param string $entry + * @param string $src + */ + public function log_debug( $entry, $src ) { + $this->log( $entry, self::DEBUG, $src ); + } + + /** + * Logs a warning. + * + * @param string $entry + * @param string $src + */ + public function log_warning( $entry, $src ) { + $this->log( $entry, self::WARNING, $src ); + } + + /** + * Logs an error. + * + * @param string $entry + * @param string $src + */ + public function log_error( $entry, $src ) { + $this->log( $entry, self::ERROR, $src ); + } + + /** + * Logs a successful operation. + * + * @param string $entry + * @param string $src + */ + public function log_success( $entry, $src ) { + $this->log( $entry, self::SUCCESS, $src ); + } + + /** + * Logs an entry colorizing it. + * + * This will only apply to WP-CLI based logging. + * + * @param string $entry + * @param string $src + */ + public function log_colorized( $entry, $src ) { + $this->log( $entry, self::COLORIZE, $src ); + } + + /** + * Adds an entry to the log (if it is at the appropriate level, etc) and outputs information using WP-CLI if available. + * + * This is simply a shorthand for calling log() on the current logger. + */ + public function log( $entry, $type = self::DEBUG, $src = '' ) { + $original_type = $type; + + // some levels are really just debug information + $debug_types = [ self::SUCCESS, self::COLORIZE ]; + + if ( in_array( $type, $debug_types ) ) { + $type = self::DEBUG; + } + + if ( $this->should_log( $type ) ) { + $this->get_current_logger()->log( $entry, $type, $src ); + } + + /** + * Whether to log the message to wp-cli, if available, or not. + * + * @since 4.9.6 + * + * @param bool $log_to_wpcli Whether to log to wp-cli, if available, or not. + * @param string $entry The message entry. + * @param string $type The message type. + * @param string $src The message source. + */ + $log_to_wpcli = apply_filters( 'tribe_common_log_to_wpcli', true, $entry, $type, $src ); + + // Only go further if we have WP_CLI or if we want to log to WP-CLI. + if ( ! class_exists( 'WP_CLI' ) || false === $log_to_wpcli ) { + return false; + } + + // We are always logging to WP-CLI if available + switch ( $original_type ) { + case self::ERROR: + WP_CLI::error( $entry ); + break; + case self::WARNING: + WP_CLI::warning( $entry ); + break; + case self::SUCCESS: + WP_CLI::success( $entry ); + break; + case self::DEBUG: + WP_CLI::debug( $entry, $src ); + break; + + case self::COLORIZE: + default: + WP_CLI::log( WP_CLI::colorize( $entry ) ); + break; + } + } + + /** + * Returns a list of available logging engines as an array where each + * key is the classname and the value is the logger itself. + * + * @return array + */ + public function get_logging_engines() { + $available_engines = []; + $bundled_engines = [ + 'Tribe__Log__File_Logger', + ]; + + foreach ( $bundled_engines as $engine_class ) { + $engine = $this->get_engine( $engine_class ); + + // Check that we have a valid engine that is available for use in the current environment + if ( $engine && $engine->is_available() ) { + $available_engines[ $engine_class ] = $engine; + } + } + + /** + * Offers a chance to modify the array of currently available logging engines. + * + * The array is organized with each key as the class name of the logging + * implementation and the matching value is the actual logger object. + * + * @var array $available_engines + */ + return apply_filters( 'tribe_common_logging_engines', $available_engines ); + } + + /** + * Returns the currently active logger. + * + * If no loggers are available, this will be the null logger which is a no-op + * implementation (making it safe to call Tribe__Log__Logger methods on the + * return value of this method at all times). + * + * @since 4.6.2 altered the return signature to only return instances of Tribe__Log__Logger + * + * @return Tribe__Log__Logger + */ + public function get_current_logger() { + if ( ! $this->current_logger ) { + $engine = tribe_get_option( 'logging_class', null ); + $available = $this->get_logging_engines(); + + if ( empty( $engine ) || ! isset( $available[ $engine ] ) ) { + return $this->current_logger = new Tribe__Log__Null_Logger(); + } else { + $this->current_logger = $this->get_engine( $engine ); + } + } + + return $this->current_logger; + } + + /** + * Sets the current logging engine to the provided class (if it is a valid + * and currently available logging class, else will set this to null - ie + * no logging). + * + * @param string $engine + * + * @throws Exception if the specified logging engine is invalid + */ + public function set_current_logger( $engine ) { + $available_engines = $this->get_logging_engines(); + + // Make sure to de-duplicate the slashes on class names. + $engine = str_replace( '\\\\', '\\', $engine ); + + if ( ! isset( $available_engines[ $engine ] ) ) { + throw new Exception( sprintf( __( 'Cannot set %s as the current logging engine', 'tribe-common' ), $engine ) ); + } + + tribe_update_option( 'logging_class', $engine ); + $this->current_logger = $available_engines[ $engine ]; + } + + /** + * Attempts to return the requested logging object or null if that + * is not possible. + * + * @param $class_name + * + * @return Tribe__Log__Logger|null + */ + public function get_engine( $class_name ) { + if ( ! isset( $this->loggers[ $class_name ] ) ) { + $object = new $class_name; + + if ( $object instanceof Tribe__Log__Logger ) { + $this->loggers[ $class_name ] = new $class_name(); + } + } + + if ( isset( $this->loggers[ $class_name ] ) ) { + return $this->loggers[ $class_name ]; + } + + return null; + } + + /** + * Sets the current logging level to the provided level (if it is a valid + * level, else will set the level to 'default'). + * + * @param string $level + */ + public function set_level( $level ) { + $available_levels = wp_list_pluck( $this->get_logging_levels(), 0 ); + + if ( ! in_array( $level, $available_levels ) ) { + $level = self::DISABLE; + } + + tribe_update_option( 'logging_level', $level ); + $this->current_level = $level; + } + + /** + * Returns the current logging level as a string. + * + * @return string + */ + public function get_level() { + $current_level = tribe_get_option( 'logging_level', null ); + $available_levels = wp_list_pluck( $this->get_logging_levels(), 0 ); + + if ( ! in_array( $current_level, $available_levels ) ) { + $current_level = self::DISABLE; + } + + return $current_level; + } + + /** + * Returns a list of logging levels. + * + * The format is an array of arrays, each inner array being comprised of the + * level code (index 0) and a human readable description (index 1). + * + * The ordering of the inner arrays is critical as it dictates what will be logged + * when a given logging level is in effect. Example: if the current logging level + * is "error" mode (only record error-level problems) then debug-level notices will + * *not* be recorded and nor will warnings. + * + * On the other hand, if the current logging level is "debug" then debug level + * notices *and* all higher levels (including warnings and errors) will be recorded. + * + * @return array + */ + public function get_logging_levels() { + if ( empty( $this->levels ) ) { + /** + * Provides an opportunity to add or remove logging levels. This is expected + * to be organized as an array of arrays: the ordering of each inner array + * is critical, see Tribe__Log::get_logging_levels() docs. + * + * General form: + * + * [ + * [ 'disable' => 'description' ], // * Do not log anything + * [ 'error' => 'description' ], // ^ Log only the most critical problems + * [ 'warning' => 'description' ], // | ... + * [ 'debug' => 'description' ] // v Log as much data as possible, including less important trivia + * ] + * + * @param array $logging_levels + */ + $this->levels = (array) apply_filters( 'tribe_common_logging_levels', [ + [ self::DISABLE, __( 'Disabled', 'tribe-common' ) ], + [ self::ERROR, __( 'Only errors', 'tribe-common' ) ], + [ self::WARNING, __( 'Warnings and errors', 'tribe-common' ) ], + [ self::DEBUG, __( 'Full debug (all events)', 'tribe-common' ) ], + ] ); + } + + return $this->levels; + } + + /** + * Indicates if errors relating to the specified logging level should indeed + * be logged. + * + * Examples if the current logging level is "warning" (log all warnings and errors): + * + * * Returns true for "error" + * * Returns true for "warning" + * * Returns false for "debug" + * + * The above assumes we are using the default logging levels. + * + * @param string $level_code + * + * @return bool + */ + protected function should_log( $level_code ) { + if ( empty( $this->prioritized_levels ) ) { + $this->build_prioritized_levels(); + } + + // Protect against the possibility non-existent level codes might be passed in + if ( ! isset( $this->prioritized_levels[ $level_code ] ) ) { + return false; + } + + return $this->prioritized_levels[ $level_code ] <= $this->prioritized_levels[ $this->current_level ]; + } + + /** + * Creates a second list of logging levels allowing easy lookup of + * their relative priorities (ie, a means of quickly checking if + * an "error" level entry should be recorded when we're in debug + * mode). + */ + protected function build_prioritized_levels() { + foreach ( $this->get_logging_levels() as $index => $level_data ) { + $this->prioritized_levels[ $level_data[ 0 ] ] = $index; + } + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Log/Action_Logger.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Log/Action_Logger.php new file mode 100644 index 000000000..9d4545294 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Log/Action_Logger.php @@ -0,0 +1,125 @@ +translate_log_level( $type ), $message ); + } + + /** + * Translates the log types used by `Tribe__Log` to those used by Monolog. + * + * @since 4.9.16 + * + * @param string $type The `Tribe__Log` log type. + * + * @return int The Monolog equivalent of the current level. + */ + protected function translate_log_level( $type ) { + switch ( $type ) { + case Tribe__Log::DEBUG: + return Logger::DEBUG; + case Tribe__Log::ERROR: + return Logger::ERROR; + case Tribe__Log::WARNING: + return Logger::WARNING; + case Tribe__Log::SUCCESS: + default: + return Logger::INFO; + } + } + + /** + * {@inheritDoc} + * + * @since 4.9.16 + */ + public function retrieve( $limit = 0, array $args = [] ) { + return [ + [ + 'message' => __( + 'The Action Logger will dispatch any logging message using the "tribe_log" action writing, by ' . + 'default, to the PHP error log.', + 'tribe-common' ) + ], + ]; + } + + /** + * {@inheritDoc} + * + * @since 4.9.16 + */ + public function list_available_logs() { + return []; + } + + /** + * Changes the Monolog logger channel to the specified one. + * + * @since 4.9.16 + * + * @param string $log_identifier The channel to switch to. + * @param bool $create Unused by this class. + * + * @return bool The exit status of the channel change. + * + * @uses \Tribe\Log\Monolog_Logger::set_channel(). + */ + public function use_log( $log_identifier, $create = false ) { + return tribe( 'monolog' )->set_global_channel( $log_identifier ); + } + + /** + * {@inheritDoc} + * + * @since 4.9.16 + */ + public function cleanup() { + return true; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Log/Admin.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Log/Admin.php new file mode 100644 index 000000000..c5aa2472d --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Log/Admin.php @@ -0,0 +1,283 @@ + Settings > Help screen. + * + * @return string + */ + public function display_log() { + $log_choices = $this->get_available_logs(); + $log_engines = $this->get_log_engines(); + $log_levels = $this->get_logging_levels(); + $log_entries = $this->get_log_entries(); + $download_url = $this->get_log_url(); + + ob_start(); + include trailingslashit( Tribe__Main::instance()->plugin_path ) . 'src/admin-views/event-log.php'; + return ob_get_clean(); + } + + /** + * Listens for changes to the event log settings updating and returning + * an appropriate response. + */ + public function listen() { + $fields = wp_parse_args( $_POST, [ + 'check' => '', + 'log-level' => '', + 'log-engine' => '', + ] ); + + foreach ( $fields as &$single_field ) { + $single_field = sanitize_text_field( $single_field ); + } + + if ( ! wp_verify_nonce( $fields['check'], 'logging-controls' ) ) { + return; + } + + /** + * Fires before log settings are committed. + * + * This will not happen unless a nonce check has already passed. + */ + do_action( 'tribe_common_update_log_settings' ); + + $this->update_logging_level( $fields['log-level'] ); + $this->update_logging_engine( $fields['log-engine'] ); + + /** + * Fires immediately after log settings have been committed. + */ + do_action( 'tribe_common_updated_log_settings' ); + + $data = [ + 'logs' => $this->get_available_logs(), + ]; + + if ( ! empty( $fields['log-view'] ) ) { + $data['entries'] = $this->get_log_entries( $fields['log-view'] ); + } + + wp_send_json_success( $data ); + } + + /** + * Sets the current logging level to the provided level (if it is a valid + * level, else will set the level to 'default'). + * + * @param string $level + */ + protected function update_logging_level( $level ) { + $this->log_manager()->set_level( $level ); + } + + /** + * Sets the current logging engine to the provided class (if it is a valid + * and currently available logging class, else will set this to null - ie + * no logging). + * + * @param string $engine + */ + protected function update_logging_engine( $engine ) { + try { + $this->log_manager()->set_current_logger( $engine ); + } + catch ( Exception $e ) { + // The class name did not relate to a valid logging engine + } + } + + /** + * Register our script early. + */ + public function register_script() { + tribe_asset( + Tribe__Main::instance(), + 'tribe-common-logging-controls', + 'admin-log-controls.js', + [ 'jquery' ], + 'admin_enqueue_scripts', + [ + 'conditionals' => [ $this, 'should_enqueue_assets' ], + 'localize' => (object) [ + 'name' => 'tribe_logger_data', + 'data' => [ + 'check' => wp_create_nonce( 'logging-controls' ), + ], + ], + ] + ); + } + + /** + * Checks wether the assets should be enqueued. + * + * @since 4.15.0 + * + * @return boolean True if the assets should be enqueued. + */ + public function should_enqueue_assets() { + return Tribe__Admin__Help_Page::instance()->is_current_page() || tribe( Troubleshooting::class )->is_current_page(); + } + + /** + * Returns a list of logs that are available for perusal. + * + * @return array + */ + protected function get_available_logs() { + $current_logger = $this->current_logger(); + + if ( $current_logger ) { + $available_logs = $this->current_logger()->list_available_logs(); + } + + if ( empty( $available_logs ) ) { + return [ '' => _x( 'None currently available', 'log selector', 'tribe-common' ) ]; + } + + return $available_logs; + } + + /** + * Returns a list of logging engines that are available for use. + * + * @return array + */ + protected function get_log_engines() { + $available_engines = $this->log_manager()->get_logging_engines(); + + if ( empty( $available_engines ) ) { + return [ '' => _x( 'None currently available', 'log engines', 'tribe-common' ) ]; + } + + $engine_list = []; + + foreach ( $available_engines as $class_name => $engine ) { + /** + * @var Tribe__Log__Logger $engine + */ + $engine_list[ $class_name ] = $engine->get_name(); + } + + return $engine_list; + } + + /** + * Returns all log entries for the current or specified log. + * + * @return array + */ + public function get_log_entries( $log = null ) { + if ( $logger = $this->current_logger() ) { + $logger->use_log( $log ); + return (array) $logger->retrieve(); + } + + return []; + } + + /** + * Returns an array of logging levels arranged as key:value pairs, with + * each key being the level code and the value being the human-friendly + * description. + * + * @return array + */ + protected function get_logging_levels() { + $levels = []; + $available_levels = $this->log_manager()->get_logging_levels(); + + foreach ( $available_levels as $logging_level ) { + $levels[ $logging_level[0] ] = $logging_level[1]; + } + + return $levels; + } + + /** + * Provides a URL that can be used to download the current or specified + * log. + * + * @param $log + * + * @return string + */ + protected function get_log_url( $log = null ) { + $query = [ + 'tribe-common-log' => 'download', + 'check' => wp_create_nonce( 'download_log' ), + ]; + + $log_download_url = add_query_arg( $query, get_admin_url( null, 'edit.php' ) ); + + return esc_url( $log_download_url ); + } + + /** + * Facilitate downloading of logs. + */ + public function serve_log_downloads() { + if ( empty( $_GET['tribe-common-log'] ) || 'download' !== $_GET['tribe-common-log'] ) { + return; + } + + if ( ! wp_verify_nonce( @$_GET['check'], 'download_log' ) ) { + return; + } + + if ( empty( $_GET['log'] ) || ! in_array( $_GET['log'], $this->get_available_logs() ) ) { + return; + } + + $log_name = sanitize_file_name( $_GET['log'] ); + $this->current_logger()->use_log( $log_name ); + + /** + * Provides an opportunity to modify the recommended filename for a downloaded + * log file. + * + * @param string $log_name + */ + $log_name = apply_filters( 'tribe_common_log_download_filename', $log_name ); + + header( 'Content-Disposition: attachment; filename="tribe-log-' . $log_name . '"' ); + $output = fopen( 'php://output', 'w' ); + + foreach ( $this->current_logger()->retrieve() as $log_entry ) { + fputcsv( $output, $log_entry ); + } + + fclose( $output ); + exit(); + } + + /** + * Returns a reference to the main log management object. + * + * @return Tribe__Log + */ + protected function log_manager() { + return tribe( 'logger' ); + } + + /** + * Returns the currently enabled logging object or null if it is not + * available. + * + * @return Tribe__Log__Logger|null + */ + protected function current_logger() { + return tribe( 'logger' )->get_current_logger(); + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Log/Canonical_Formatter.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Log/Canonical_Formatter.php new file mode 100644 index 000000000..94c8e3da8 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Log/Canonical_Formatter.php @@ -0,0 +1,174 @@ +format_record_message( $record ); + $formatter = $this->get_context_formatter(); + } else { + // Fall-back on a standard format if the message does not have a context. + $formatter = $this->get_standard_formatter(); + } + + return $formatter->format( $record ); + } + + /** + * Gets a LineFormatter whose format is context aware. + * + * @since 4.12.13 + * + * @return LineFormatter + */ + public function get_context_formatter() { + if ( empty( $this->context_formatter ) ) { + $this->context_formatter = new LineFormatter( $this->context_format ); + } + + return $this->context_formatter; + } + + /** + * Gets a LineFormatter whose format is our standard logging format. + * + * @since 4.12.13 + * + * @return LineFormatter + */ + public function get_standard_formatter() { + if ( empty( $this->standard_formatter ) ) { + $this->standard_formatter = new LineFormatter( $this->standard_format ); + } + + return $this->standard_formatter; + } + + /** + * Formats a set of log records. + * + * This simply hands off the work of formatting Batches to the LineFormatter. + * + * @since 4.12.13 + * + * @param array $records A set of records to format + * @return mixed The formatted set of records + */ + public function formatBatch( array $records ) { + $line_formatter = new LineFormatter(); + + return $line_formatter->formatBatch( $records ); + } + + /** + * Formats the record to the canonical format. + * + * @since 4.9.16 + * + * @param array $record The record to process. + * + * @return string The formatted message, as built from the record context and message, in the format `=`. + */ + protected function format_record_message( array $record ) { + $message = []; + $extra = []; + + $extra['level'] = isset( $record['level_name'] ) ? strtolower( $record['level_name'] ) : 'debug'; + + if ( ! empty( $record['message'] ) ) { + // Use the message as the source. + $extra['source'] = $this->escape_quotes( $record['message'] ); + } + + $context = $record['context']; + $context = array_merge( $extra, $context ); + + foreach ( $context as $key => $value ) { + $escape = false; + + if ( is_bool( $value ) ) { + $value = $value ? 'true' : 'false'; + } elseif ( ! is_scalar( $value ) ) { + $value = json_encode( $value ); + if ( false === $value ) { + $value = 'malformed'; + } else { + $escape = true; + } + } + + if ( $escape || ( is_string( $value ) && preg_match( '~[\\\\/\\s]+~', $value ) ) ) { + $value = '"' . $this->escape_quotes( $value ) . '"'; + } + + $message[] = "{$key}={$value}"; + } + + return implode( ' ', $message ); + } + + /** + * Escapes the double quotes in a string. + * + * @since 4.9.16 + * + * @param string $string The string to escape the quotes in. + * + * @return string The string, with the quotes escaped. + */ + protected function escape_quotes( $string ) { + return str_replace( '"', '\\"', $string ) ; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Log/File_Logger.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Log/File_Logger.php new file mode 100644 index 000000000..ef69a7f38 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Log/File_Logger.php @@ -0,0 +1,314 @@ +set_log_dir(); + $this->set_log_file(); + } + + public function __destruct() { + $this->close_handle(); + } + + protected function set_log_dir() { + /** + * Controls the directory used for logging. + * + * @var string $log_dir + */ + $this->log_dir = apply_filters( 'tribe_file_logger_directory', get_temp_dir() ); + } + + /** + * Sets the path for the log file we're currently interested in using. + * + * @param string $date = null + */ + protected function set_log_file( $date = null ) { + $this->log_file = $this->get_log_file_name( $date ); + $this->obtain_handle(); + } + + /** + * Used to switch between contexts for reading ('r') and writing + * ('a' := append) modes. + * + * @see fopen() documentation + * + * @param string $context + */ + protected function set_context( $context ) { + $this->context = $context; + $this->close_handle(); + $this->obtain_handle(); + } + + /** + * Attempts to obtain a file handle for the current log file. + */ + protected function obtain_handle() { + $this->close_handle(); + + if ( ! file_exists( $this->log_file ) && $this->is_available() ) { + touch( $this->log_file ); + } + + // Bail if we're attempting to write but don't have permission. + if ( 'r' !== $this->context && ! is_writable( $this->log_file ) ) { + return; + } + + if ( is_readable( $this->log_file ) ) { + $this->handle = fopen( $this->log_file, $this->context ); + } + } + + /** + * Closes the current file handle, if one is open. + */ + protected function close_handle() { + // is_resource() only returns true for open resources + if ( is_resource( $this->handle ) ) { + fclose( $this->handle ); + } + } + + /** + * Returns the log name to be used for reading/writing events for a specified date + * (defaulting to today, if no date is specified). + * + * @param string $date = null + * + * @return string + */ + protected function get_log_file_name( $date = null ) { + if ( null === $date ) { + $date = date_i18n( 'Y-m-d' ); + } + + $filename = $this->log_dir . DIRECTORY_SEPARATOR . $this->get_log_file_basename() . $date . '.log'; + + /** + * Dictates the filename of the log used to record events for the specified date. + * + * @var string $filename + * @var string $date + */ + return apply_filters( 'tribe_file_logger_filename', $filename, $date ); + } + + protected function get_log_file_basename() { + /** + * Log files share a common prefix, which aids identifying archived/rotated logs. + * This filter allows a degree of control to be exercised over the prefix to avoid + * conflicts, etc. + * + * @var string $log_file_base_name + */ + return apply_filters( 'tribe_file_logger_file_prefix', $this->module_id . '_' ); + } + + /** + * Returns a 'human friendly' name for the logging implementation. + * + * @return string + */ + public function get_name() { + return __( 'Default (uses temporary files)', 'tribe-common' ); + } + + /** + * Indicates if the logger will work in the current environment. + * + * @return bool + */ + public function is_available() { + return is_writable( $this->log_dir ) && is_readable( $this->log_dir ); + } + + /** + * Responsible for commiting the entry to the log. + * + * @param string $entry + * @param string $type + * @param string $src + */ + public function log( $entry, $type = Tribe__Log::DEBUG, $src = '' ) { + // Ensure we're in 'append' mode before we try to write + if ( 'a' !== $this->context ) { + $this->set_context( 'a' ); + } + + // Couldn't obtain the file handle? We'll bail out without causing further disruption + if ( ! $this->handle ) { + return; + } + + fputcsv( $this->handle, [ date_i18n( 'Y-m-d H:i:s' ), $entry, $type, $src ] ); + } + + /** + * Retrieve up to $limit most recent log entries in reverse chronological + * order. If $limit is a negative or zero value, there is no limit. + * + * Supports passing a 'log' argument to recover + * + * @see Tribe__Log__Logger::list_available_logs() + * + * @param int $limit + * @param array $args + * + * @return array + */ + public function retrieve( $limit = 0, array $args = [] ) { + // Ensure we're in 'read' mode before we try to retrieve + if ( 'r' !== $this->context ) { + $this->set_context( 'r' ); + } + + // Couldn't obtain the file handle? We'll bail out without causing further disruption + if ( ! $this->handle ) { + return []; + } + + $rows = []; + + while ( $current_row = fgetcsv( $this->handle ) ) { + if ( $limit && $limit === count( $rows ) ) { + array_shift( $rows ); + } + + $rows[] = $current_row; + } + + return array_reverse( $rows ); + } + + /** + * Returns a list of currently accessible logs (current first, oldest last). + * Each is refered to by date. + * + * Example: + * + * [ '2016-12-31', + * '2016-12-30', + * '2016-12-30', + * '2016-12-30', + * '2016-12-30', ... ] + * + * @since 4.6.2 added extra safety checks before attempting to access log directory + * + * @return array + */ + public function list_available_logs() { + $logs = []; + + // This could be called when the log dir is not accessible. + if ( ! $this->is_available() ) { + return $logs; + } + + $basename = $this->get_log_file_basename(); + + /** + * Though the is_available() method tests to see if the log directory is + * readable and writeable there are situations where that isn't a + * sufficient check by itself, hence the try/catch block. + * + * @see https://central.tri.be/issues/90436 + */ + try { + $log_files_dir = new DirectoryIterator( $this->log_dir ); + + // Look through the log storage directory + foreach ( $log_files_dir as $node ) { + if ( ! $node->isReadable() ) { + continue; + } + + $name = $node->getFilename(); + $ext = $node->getExtension(); + + // Skip unless it is a .log file with the expected prefix + if ( 'log' !== $ext || 0 !== strpos( $name, $basename ) ) { + continue; + } + + if ( preg_match( '/([0-9]{4}\-[0-9]{2}\-[0-9]{2})/', $name, $matches ) ) { + $logs[] = $matches[1]; + } + } + + rsort( $logs ); + } catch ( Exception $e ) { + return $logs; + } + + return $logs; + } + + /** + * Switches to the specified log. The $log_identifier should take the + * form of a "yyyy-mm-dd" format date string. + * + * If optional param $create is true then it will try to create a log + * using the provided identifier. If the log does not exist, cannot be + * created or an invalid identifier has been passed in then boolean false + * will be returned, otherwise it will attempt to switch to the new log. + * + * @param mixed $log_identifier + * @param bool $create + * + * @return bool + */ + public function use_log( $log_identifier, $create = false ) { + $log_file = $this->get_log_file_name( $log_identifier ); + $exists = file_exists( $log_file ); + + if ( ! $exists && ! $create ) { + return false; + } + + if ( ! $exists && $create && preg_match( '/^([0-9]{4}\-[0-9]{2}\-[0-9]{2})$/', $log_file ) ) { + if ( false === file_put_contents( $log_file, '' ) ) { + return false; + } + } + + $this->set_log_file( $log_identifier ); + return true; + } + + /** + * Performs routine maintenance and cleanup work (such as log rotation) + * whenever it is called. + */ + public function cleanup() { + // Default to retaining 7 days worth of logs + $cutoff = date_i18n( 'Y-m-d', current_time( 'timestamp' ) - WEEK_IN_SECONDS ); + + /** + * Logs falling on or earlier than this date will be removed. + * + * @param string $cutoff 'Y-m-d' format date string + */ + $cutoff = apply_filters( 'tribe_file_logger_cutoff', $cutoff ); + + foreach ( $this->list_available_logs() as $available_log ) { + if ( $available_log <= $cutoff ) { + unlink( $this->get_log_file_name( $available_log ) ); + } + } + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Log/Logger.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Log/Logger.php new file mode 100644 index 000000000..0ffeb0937 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Log/Logger.php @@ -0,0 +1,88 @@ +set_global_channel( static::DEFAULT_CHANNEL ); + } + + /** + * Clones this logger and replaces it in the `tribe` container. + * + * @since 4.9.16 + * + * @param string $channel The new logger name, also referred to as "channel" (hence the method name). + * + * @return bool Whether the channel change was successful or not. + */ + public function set_global_channel( $channel ) { + $new = $this->withName( $channel ); + tribe_register( Logger::class, $new ); + tribe_register( 'monolog', $new ); + + return $channel === tribe( 'monolog' )->getName(); + } +} \ No newline at end of file diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Log/Null_Logger.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Log/Null_Logger.php new file mode 100644 index 000000000..953215731 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Log/Null_Logger.php @@ -0,0 +1,106 @@ + Pro tip: if you, as a developer, find yourself using `var_dump` and `error_log` a lot, then you should log instead. Someone, someday, will have your same issue. + +Worried about "spamming" the logs? [Read here](#logging-levels--or-stuff-does-not-appear-in-the-log). + +## This will deprecate the old logger, but not yet + +At first we're not replacing the "old" logger with this new one, we're just asking you **to stop using the old logger** in your code from now on and use the new, Monolog-based, one. +The old logger still offers file-based logging and connections to the UI the new logger is not yet offering; the current implementation will allow us, in the future, to log **everything** with the Monolog-based logger, but, currently, intercepting log messages from the "old" logger requires manual activation, see the following section. + +To be clear: this is what we mean by "old" or "legacy" logger: + +```php +log_debug( 'Some debug information', 'The source' ); +tribe( 'logger' )->log( 'Some information', Tribe__Log::DEBUG, 'The source' ); +``` + +### Intercepting legacy logger logs with the new Monolog logger + +The Monolog-based logger will handle logging coming from the legacy logger only if explicitly told so. + + You can activate this function with this code: + + ```php + 'updated', 'post_id' => $id, 'origin' => $origin ] +); +``` + +The logger listening on the action will consume three parameters: + +1. `level` - `debug` in the example; is the level of the log; available levels, in increasing value of urgency are: `debug`, `info`, `notice`, `warning`, `error`, `critical`, `alert`, `emergency`. Use each level wisely. +2. `source` - `ea_client` in this example; is the source of the log; this is a human-readable value; consistency is king here. +3. `context` - the array in this example; this is an associative array that will be logged to define the context of the log. Think of this as something that will provide the details required to unpack what **was** happening when the log entry was created. Provide enough context to make it clear, but avoid bloating it. + +## Where are my logs? + +The initial implementation of the new logger will write, by default, to the **PHP error** log. + +We're using Monolog to allow us, and third parties, to "attach" and "deatach" loggers as required. +By default we're formatting logs using canonical lines( read more [here](https://brandur.org/logfmt) and [here](https://blog.codeship.com/logfmt-a-log-format-thats-easy-to-read-and-write/)) to make our log entries both human-readable and machine parsable (e.g. by a tool like [this](https://www.npmjs.com/package/logfmt)). + +The output format of the example above would be this: + +``` +[22-Aug-2019 15:50:42 UTC] tribe-canonical-line channel=default level=debug source=ea_client action=updated post_id=23 origin=ical +``` + +What about legacy logs? +Their format would not be formatted to the canonical line style: + +``` +[22-Aug-2019 16:03:33 UTC] tribe.default.DEBUG: The source: debug information +``` + +### Logging levels ( or "stuff does not appear in the log") + +By default we're only logging Warnings and above. +This means all your `debug` level logs are being ignored. + +In production we do not want to fill people logs with pointless information, but you can control the level of logging: any log equal or above the specified level will be logged. + +You can control the logging level with the `tribe_log_level` filter: + +```php +set_global_channel( 'my_channel' ); +do_action( 'tribe_log', 'debug', 'my_source', [ 'foo' => 'bar' ] ); +tribe( 'logger' )->log_debug( 'Some debug information', 'My source' ); +``` + +You can do the same using the legacy logger, [if enabled][0527-0003]: + +```php +use_log( 'my_channel' ); +``` + +Any log produced after the call will log to the `my_channel` channel; this will apply to the legacy logger too ([if redirected][0527-0003]): + +``` +[22-Aug-2019 15:50:42 UTC] tribe-canonical-line channel=default level=debug source=ea_client action=updated post_id=23 origin=ical +[22-Aug-2019 15:51:13 UTC] tribe-canonical-line channel=my_channel level=debug source=my_source foo=bar +[22-Aug-2019 16:03:33 UTC] tribe.my_channel.DEBUG: My source: Some debug information +``` + +## I want to use this right now to debug my code + +Copy and paste this in a plugin, or must-use plugin. +If you're using a plugin remember to activate it. + +```php +container->singleton( 'log', $this ); + $this->container->singleton( static::class, $this ); + $this->container->singleton( Logger::class, [ $this, 'build_logger' ] ); + $this->container->singleton( 'monolog', + function () { + return $this->container->make( Logger::class ); + } + ); + + add_action( 'tribe_log', [ $this, 'dispatch_log' ], 10, 3 ); + + /** + * Filters whether to make the Action Logger available as logger or not. + * + * @since 4.9.16 + * + * @param bool $use_action_logger Whether to allow logging messages from the \Tribe\Log\Logger class using the + * `tribe_log` action or not. + */ + $use_action_logger = apply_filters( 'tribe_log_use_action_logger', false ); + + if ( $use_action_logger ) { + add_filter( 'tribe_common_logging_engines', [ $this, 'add_logging_engine' ] ); + } + } + + /** + * Builds and returns the Monolog Logger instance that will listen to the `tribe_log` action. + * + * To avoid the over-head introduced by filtering the filters are applied here, only once, when the instance is + * first built. Any later call will use the singleton instance stored in the container. + * + * @since 4.9.16 + * + * @return Logger + */ + public function build_logger() { + /* + * Disable logging by either setting the `TEC_DISABLE_LOGGING` constant or by setting the + * `TRIBE_DISABLE_LOGGING` environment variable to a truthy value. Use the environment variable + * in the context of integration tests. + */ + $logger_disabled = ( defined( 'TEC_DISABLE_LOGGING' ) && TEC_DISABLE_LOGGING ) + || ! empty( $_ENV['TEC_DISABLE_LOGGING'] ) + || getenv( 'TEC_DISABLE_LOGGING' ); + + /** + * Allow filtering the logger enabled status. + * + * @since 5.0.10 + * + * @param bool $logger_disabled Whether the logger should be disabled or not. + */ + $logger_disabled = apply_filters( 'tec_disable_logging', $logger_disabled ); + + if ( $logger_disabled ) { + // Logging is disabled, still build a logger to allow fine usage of it and make sure it exists. + $logger = new Monolog_Logger( Monolog_Logger::DEFAULT_CHANNEL ); + + // Set the handlers to one NullHandler: setting them to an empty array would make it use the STDERR one. + return $logger->setHandlers( [ new NullHandler() ] ); + } + + // Provide more information in debug mode. + $level_threshold = defined( 'WP_DEBUG' ) && WP_DEBUG ? Logger::DEBUG : Logger::WARNING; + + /** + * Filters the level of the messages that will be logged. + * + * The threshold is inclusive of the level; it default to log any warning and above. + * + * @since 4.9.16 + * + * @param int The threshold level; if the level of a message is this level or above, then it will be logged. + * + * @see \Monolog\Logger for possible levels. + */ + $level_threshold = apply_filters( 'tribe_log_level', $level_threshold ); + + $error_log_handler = new ErrorLogHandler( ErrorLogHandler::OPERATING_SYSTEM, $level_threshold ); + + /** + * Filters whether to use canonical format for the logs or not. + * + * @since 4.9.16 + * + * @param bool $use_canonical_format Whether to use canonical format for the logs or not; defaults to `true`. + */ + $use_canonical_format = apply_filters( 'tribe_log_canonical', true ); + + if ( $use_canonical_format ) { + $error_log_handler->setFormatter( new Canonical_Formatter() ); + } + + $handlers = [ + 'default' => $error_log_handler + ]; + + /** + * Filters the list of handlers that will handle dispatched log messages. + * + * All handlers should implement the `\Monolog\Handler\HandlerInterface`. + * + * @since 4.9.16 + * + * @param array $handlers An array of default log handlers. + */ + $handlers = apply_filters( 'tribe_log_handlers', $handlers ); + + // Monolog will log to stderr when no handlers are set. + $logger = new Monolog_Logger( Monolog_Logger::DEFAULT_CHANNEL ); + + $logger->setHandlers( $handlers ); + + return $logger; + } + + /** + * Dispatch a message of a specific level. + * + * Available levels are: `debug`, `info`, `notice`, `warning`, `error`, `critical`, `alert`, `emergency`. + * + * @since 4.9.16 + * + * @param string|int $level Either the log level or the log pretty name, see long description. + * @param string $message The message to log. + * @param array $context An array of values to define the context. + * + * @see \Monolog\Logger for the log level constants and names. + */ + public function dispatch_log( $level = 'debug', $message = '', array $context = [] ) { + // Goes from something like `debug` to `100`. + $level = is_numeric( $level ) ? $level : Logger::toMonologLevel( $level ); + + /** @var Logger $logger */ + $logger = $this->container->make( Logger::class ); + + $logger->log( $level, $message, $context ); + } + + /** + * Makes the action-based logging engine available in the backend. + * + * @since 4.9.16 + * + * @param array $logging_engines An array of available logging engines. + * + * @return array The updated array of logging engines. + */ + public function add_logging_engine( array $logging_engines = [] ) { + $logging_engines[ Action_Logger::class ] = new Action_Logger(); + + return $logging_engines; + } + + /** + * Enables logging in the service provider, if not already enabled. + * + * @since 4.12.15 + */ + public function enable() { + if ( has_action( 'tribe_log', [ $this, 'dispatch_log' ] ) ) { + return; + } + + add_action( 'tribe_log', [ $this, 'dispatch_log' ] ); + } + + /** + * Disables the logging functions. + * + * @since 4.12.15 + */ + public function disable() { + if ( ! has_action( 'tribe_log', [ $this, 'dispatch_log' ] ) ) { + return; + } + + remove_action( 'tribe_log', [ $this, 'dispatch_log' ] ); + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Main.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Main.php new file mode 100644 index 000000000..3b9d0ba1b --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Main.php @@ -0,0 +1,858 @@ +plugin_context = $context; + $this->plugin_context_class = get_class( $context ); + } + + $this->plugin_path = trailingslashit( dirname( dirname( dirname( __FILE__ ) ) ) ); + $this->plugin_dir = trailingslashit( basename( $this->plugin_path ) ); + $this->parent_plugin_dir = trailingslashit( plugin_basename( $this->plugin_path ) ); + $this->plugin_url = plugins_url( $this->parent_plugin_dir === $this->plugin_dir ? $this->plugin_dir : $this->parent_plugin_dir ); + + $this->promoter_connector(); + + add_action( 'plugins_loaded', [ $this, 'plugins_loaded' ], 1 ); + add_action( 'tribe_common_loaded', [ $this, 'tribe_common_app_store' ], 10 ); + add_action( 'customize_controls_print_styles', [ $this, 'load_tec_variables' ], 10 ); + + if ( did_action( 'plugins_loaded' ) && ! doing_action( 'plugins_loaded' ) ) { + /* + * This might happen in the context of a plugin activation. + * Complete the loading now and set the singleton instance to avoid infinite loops. + */ + self::$instance = $this; + $this->plugins_loaded(); + } + } + + /** + * + */ + public function plugins_loaded() { + + $this->init_autoloading(); + + $this->init_early_libraries(); + $this->bind_implementations(); + $this->init_libraries(); + $this->add_hooks(); + + /** + * Runs once all common libs are loaded and initial hooks are in place. + * + * @since 4.3 + */ + do_action( 'tribe_common_loaded' ); + + /** + * Runs to register loaded plugins + * + * @since 4.9 + */ + do_action( 'tribe_plugins_loaded ' ); + } + + /** + * Setup the autoloader for common files + */ + protected function init_autoloading() { + if ( ! class_exists( 'Tribe__Autoloader' ) ) { + require_once dirname( __FILE__ ) . '/Autoloader.php'; + } + + // Aliases for backwards compatibility with our Extensions and Pods. + require_once realpath( dirname( dirname( __FILE__ ) ) . '/functions/aliases.php' ); + + $autoloader = Tribe__Autoloader::instance(); + + $prefixes = [ + 'TEC\\Common\\' => dirname( __DIR__ ) . '/Common', + 'Tribe__' => __DIR__, + ]; + $autoloader->register_prefixes( $prefixes ); + + foreach ( glob( $this->plugin_path . 'src/deprecated/*.php' ) as $file ) { + $class_name = str_replace( '.php', '', basename( $file ) ); + $autoloader->register_class( $class_name, $file ); + } + + $autoloader->register_autoloader(); + } + + public function tribe_common_app_store() { + Tribe__Extension_Loader::instance(); + } + + /** + * Gets the instantiated context of this class. I.e. the object that instantiated this one. + */ + public function context() { + return $this->plugin_context; + } + + /** + * Gets the class name of the instantiated plugin context of this class. I.e. the class name of the object that instantiated this one. + */ + public function context_class() { + return $this->plugin_context_class; + } + + /** + * Initializes all libraries used/required by our singletons. + * + * @since 4.14.18 + */ + public function init_early_libraries() { + require_once $this->plugin_path . 'src/functions/editor.php'; + } + + /** + * initializes all required libraries + */ + public function init_libraries() { + require_once $this->plugin_path . 'src/functions/utils.php'; + require_once $this->plugin_path . 'src/functions/conditionals.php'; + require_once $this->plugin_path . 'src/functions/transient.php'; + require_once $this->plugin_path . 'src/functions/url.php'; + require_once $this->plugin_path . 'src/functions/query.php'; + require_once $this->plugin_path . 'src/functions/multibyte.php'; + require_once $this->plugin_path . 'src/functions/files.php'; + require_once $this->plugin_path . 'src/functions/template-tags/general.php'; + require_once $this->plugin_path . 'src/functions/template-tags/date.php'; + require_once $this->plugin_path . 'src/functions/template-tags/html.php'; + require_once $this->plugin_path . 'src/functions/template-tags/post.php'; + + Tribe__Debug::instance(); + tec_timed_option(); + + tribe( 'assets' ); + tribe( 'assets.pipeline' ); + tribe( 'settings.manager' ); + tribe( 'tracker' ); + tribe( 'plugins.api' ); + tribe( 'ajax.dropdown' ); + tribe( 'logger' ); + } + + /** + * Registers resources that can/should be enqueued + */ + public function load_assets() { + // These ones are only registered + tribe_assets( + $this, + [ + [ 'tribe-accessibility-css', 'accessibility.css' ], + [ 'tribe-query-string', 'utils/query-string.js' ], + [ 'tribe-clipboard', 'node_modules/clipboard/dist/clipboard.min.js' ], + [ 'datatables', 'vendor/datatables/datatables.js', [ 'jquery' ] ], + [ 'tribe-select2', 'vendor/tribe-selectWoo/dist/js/selectWoo.full.js', [ 'jquery' ] ], + [ 'tribe-select2-css', 'vendor/tribe-selectWoo/dist/css/selectWoo.css' ], + [ 'tribe-utils-camelcase', 'utils-camelcase.js', [ 'underscore' ] ], + [ 'tribe-moment', 'vendor/momentjs/moment.js' ], + [ 'tribe-moment-locales', 'vendor/momentjs/locale.min.js' ], + [ 'tribe-tooltipster', 'vendor/tooltipster/tooltipster.bundle.js', [ 'jquery' ] ], + [ 'tribe-tooltipster-css', 'vendor/tooltipster/tooltipster.bundle.css' ], + [ 'datatables-css', 'datatables.css' ], + [ 'tribe-datatables', 'tribe-datatables.js', [ 'datatables' ] ], + [ 'tribe-bumpdown', 'bumpdown.js', [ 'jquery', 'underscore', 'hoverIntent' ] ], + [ 'tribe-bumpdown-css', 'bumpdown.css' ], + [ 'tribe-buttonset-style', 'buttonset.css' ], + [ 'tribe-dropdowns', 'dropdowns.js', [ 'jquery', 'underscore', 'tribe-select2', 'tribe-common' ] ], + [ 'tribe-jquery-timepicker', 'vendor/jquery-tribe-timepicker/jquery.timepicker.js', [ 'jquery' ] ], + [ 'tribe-jquery-timepicker-css', 'vendor/jquery-tribe-timepicker/jquery.timepicker.css' ], + [ 'tribe-timepicker', 'timepicker.js', [ 'jquery', 'tribe-jquery-timepicker' ] ], + [ 'tribe-attrchange', 'vendor/attrchange/js/attrchange.js' ], + [ 'tec-ky-module', 'vendor/ky/ky.js', [], null, [ 'module' => true ] ], + [ 'tec-ky', 'vendor/ky/tec-ky.js', [ 'tec-ky-module' ], null, [ 'module' => true ] ], + ] + ); + + tribe_assets( + $this, + [ + [ 'tec-variables-skeleton', 'variables-skeleton.css', ], + [ 'tribe-common-skeleton-style', 'common-skeleton.css', [ 'tec-variables-skeleton' ] ], + [ 'tec-variables-full', 'variables-full.css', [ 'tec-variables-skeleton' ] ], + [ 'tribe-common-full-style', 'common-full.css', [ 'tec-variables-full', 'tribe-common-skeleton-style' ] ], + ], + null + ); + + // These ones will be enqueued on `admin_enqueue_scripts` if the conditional method on filter is met + tribe_assets( + $this, + [ + [ 'tribe-ui', 'tribe-ui.css', [ 'tec-variables-full' ] ], + [ 'tribe-buttonset', 'buttonset.js', [ 'jquery', 'underscore' ] ], + [ 'tribe-common-admin', 'tribe-common-admin.css', [ 'tec-variables-skeleton', 'tec-variables-full', 'tribe-dependency-style', 'tribe-bumpdown-css', 'tribe-buttonset-style', 'tribe-select2-css' ] ], + [ 'tribe-validation', 'validation.js', [ 'jquery', 'underscore', 'tribe-common', 'tribe-utils-camelcase', 'tribe-tooltipster' ] ], + [ 'tribe-validation-style', 'validation.css', [ 'tec-variables-full', 'tribe-tooltipster-css' ] ], + [ 'tribe-dependency', 'dependency.js', [ 'jquery', 'underscore', 'tribe-common' ] ], + [ 'tribe-dependency-style', 'dependency.css', [ 'tribe-select2-css' ] ], + [ 'tribe-pue-notices', 'pue-notices.js', [ 'jquery' ] ], + [ 'tribe-datepicker', 'datepicker.css' ], + ], + 'admin_enqueue_scripts', + [ + 'conditionals' => [ $this, 'should_load_common_admin_css' ], + 'priority' => 5, + ] + ); + + tribe_asset( + $this, + 'tribe-common', + 'tribe-common.js', + [], + 'admin_enqueue_scripts', + [ + 'priority' => 0, + ] + ); + + tribe_asset( + $this, + 'tribe-admin-url-fragment-scroll', + 'admin/url-fragment-scroll.js', + [ 'tribe-common' ], + 'admin_enqueue_scripts', + [ + 'conditionals' => [ $this, 'should_load_common_admin_css' ], + 'priority' => 5, + ] + ); + + tribe_asset( + $this, + 'tec-admin-settings-image-field', + 'admin-image-field.js', + [ 'jquery' ], + 'in_admin_footer', + [ + 'conditionals' => [ tribe( Settings::class ), 'should_load_image_field_assets' ] + ] + ); + + // Register the asset for Customizer controls. + tribe_asset( + $this, + 'tribe-customizer-controls', + 'customizer-controls.css', + [ 'tec-variables-full' ], + 'customize_controls_print_styles' + ); + + // Register the asset for color fields. + tribe_asset( + $this, + 'tec-settings-color-field', + 'admin-color-field.js', + [ 'jquery', 'wp-color-picker' ], + 'admin_footer', + [ + 'conditionals' => [ tribe( Settings::class ), 'should_load_color_field_assets' ] + ] + ); + + tribe( Tribe__Admin__Help_Page::class )->register_assets(); + } + + /** + * Ensure that the customizer styles get the variables they need. + * + * @since 4.14.13 + */ + public function load_tec_variables() { + tribe_asset_enqueue( 'tec-variables-skeleton' ); + tribe_asset_enqueue( 'tec-variables-full' ); + } + + /** + * Load Common's text domain, then fire the hook for other plugins to do the same. + * + * Make sure this fires on 'init', per WordPress best practices. + * + * @since 4.12.0 + * + * @return bool + */ + public function hook_load_text_domain() { + $loaded = $this->load_text_domain( + 'tribe-common', + basename( dirname( __FILE__, 4 ) ) . '/common/lang/' + ); + + /** + * After attempting (hopefully successfully) to load Common's text domain. + * + * Load other plugin text domains on this hook, but make sure they're setup on this hook prior to 'init'. + * + * @since 4.12.0 + * + * @param bool $loaded Whether or not Common's text domain was loaded. + * + * @return bool + */ + do_action( 'tribe_load_text_domains', $loaded ); + + return $loaded; + } + + /** + * Load All localization data create by `asset.data` + * + * @since 4.7 + * + * @return void + */ + public function load_localize_data() { + $datepicker_months = array_values( Tribe__Date_Utils::get_localized_months_full() ); + + tribe( 'asset.data' )->add( 'tribe_l10n_datatables', [ + 'aria' => [ + 'sort_ascending' => __( ': activate to sort column ascending', 'tribe-common' ), + 'sort_descending' => __( ': activate to sort column descending', 'tribe-common' ), + ], + 'length_menu' => __( 'Show _MENU_ entries', 'tribe-common' ), + 'empty_table' => __( 'No data available in table', 'tribe-common' ), + 'info' => __( 'Showing _START_ to _END_ of _TOTAL_ entries', 'tribe-common' ), + 'info_empty' => __( 'Showing 0 to 0 of 0 entries', 'tribe-common' ), + 'info_filtered' => __( '(filtered from _MAX_ total entries)', 'tribe-common' ), + 'zero_records' => __( 'No matching records found', 'tribe-common' ), + 'search' => __( 'Search:', 'tribe-common' ), + 'all_selected_text' => __( 'All items on this page were selected. ', 'tribe-common' ), + 'select_all_link' => __( 'Select all pages', 'tribe-common' ), + 'clear_selection' => __( 'Clear Selection.', 'tribe-common' ), + 'pagination' => [ + 'all' => __( 'All', 'tribe-common' ), + 'next' => __( 'Next', 'tribe-common' ), + 'previous' => __( 'Previous', 'tribe-common' ), + ], + 'select' => [ + 'rows' => [ + 0 => '', + '_' => __( ': Selected %d rows', 'tribe-common' ), + 1 => __( ': Selected 1 row', 'tribe-common' ), + ], + ], + 'datepicker' => [ + 'dayNames' => Tribe__Date_Utils::get_localized_weekdays_full(), + 'dayNamesShort' => Tribe__Date_Utils::get_localized_weekdays_short(), + 'dayNamesMin' => Tribe__Date_Utils::get_localized_weekdays_initial(), + 'monthNames' => $datepicker_months, + 'monthNamesShort' => $datepicker_months, // We deliberately use full month names here, + 'monthNamesMin' => array_values( Tribe__Date_Utils::get_localized_months_short() ), + 'nextText' => esc_html__( 'Next', 'tribe-common' ), + 'prevText' => esc_html__( 'Prev', 'tribe-common' ), + 'currentText' => esc_html__( 'Today', 'tribe-common' ), + 'closeText' => esc_html__( 'Done', 'tribe-common' ), + 'today' => esc_html__( 'Today', 'tribe-common' ), + 'clear' => esc_html__( 'Clear', 'tribe-common' ), + ], + ] ); + } + + /** + * Adds core hooks + */ + public function add_hooks() { + add_action( 'plugins_loaded', [ 'Tribe__App_Shop', 'instance' ] ); + add_action( 'plugins_loaded', [ $this, 'tribe_plugins_loaded' ], PHP_INT_MAX ); + + // Register for the assets to be available everywhere + add_action( 'tribe_common_loaded', [ $this, 'load_assets' ], 1 ); + add_action( 'init', [ $this, 'hook_load_text_domain' ] ); + add_action( 'switch_locale', [ $this, 'hook_load_text_domain' ] ); + add_action( 'restore_previous_locale', [ $this, 'hook_load_text_domain' ] ); + add_action( 'init', [ $this, 'load_localize_data' ] ); + add_action( 'plugins_loaded', [ 'Tribe__Admin__Notices', 'instance' ], 1 ); + add_action( 'admin_enqueue_scripts', [ $this, 'store_admin_notices' ] ); + + add_filter( 'body_class', [ $this, 'add_js_class' ] ); + add_action( 'wp_footer', [ $this, 'toggle_js_class' ] ); + } + + /** + * Adds `tribe-no-js` class to all pages when common is active. + * + * @since 4.3.4 + * + * @param array|string $classes Previous classes on body. + * + * @return array All classes that will be printed on ``. + */ + public function add_js_class( $classes = [] ) { + if ( ! is_array( $classes ) ) { + $classes = explode( ' ', $classes ); + } + + $classes[] = 'tribe-no-js'; + + return array_filter( array_unique( $classes ) ); + } + + /** + * Removes `tribe-no-js` and replaces with `tribe-js` when the Javascript of the page is enabled. + * + * @since 4.3.4 + * + * @return void This method only prints HTML to the screen no return. + */ + public function toggle_js_class() { + ?> + + is_post_type_screen(); + + // Are we on the Plugins page? + $is_plugins = $helper->is_screen( 'plugins' ); + + // Are we on the Widgets page? + $is_widgets = $helper->is_screen( 'widgets' ); + + // Are we viewing a generic Tribe screen? + // Includes: Events > Settings, Events > Help, App Shop page, and more. + $is_tribe_screen = $helper->is_screen(); + + return $is_post_type || $is_plugins || $is_widgets || $is_tribe_screen; + } + + /** + * A Helper method to load text domain + * First it tries to load the wp-content/languages translation then if falls to the try to load $dir language files. + * + * @since 4.0.1 Introduced. + * @since 4.2 Included $domain and $dir params. + * + * @param string $domain The text domain that will be loaded. + * @param string|false $dir What directory should be used to try to load if the default doesn't work. + * + * @return bool If it was able to load the text domain. + */ + public function load_text_domain( $domain, $dir = false ) { + // Added safety just in case this runs twice... + if ( is_textdomain_loaded( $domain ) && ! $GLOBALS['l10n'][ $domain ] instanceof NOOP_Translations ) { + return true; + } + + $locale = get_locale(); + $plugin_rel_path = WP_LANG_DIR . '/plugins/'; + + /** + * Allows users to filter the file location for a given text domain.. + * Be careful when using this filter, it will apply across the whole plugin suite. + * + * @param string $plugin_rel_path The relative path for the language files. + * @param string $domain Which plugin domain we are trying to load. + * @param string $locale Which Language we will load. + * @param string|bool $dir If there was a custom directory passed on the method call. + */ + $plugin_rel_path = apply_filters( 'tribe_load_text_domain', $plugin_rel_path, $domain, $locale, $dir ); + + $loaded = load_plugin_textdomain( $domain, false, $plugin_rel_path ); + + if ( $dir !== false && ! $loaded ) { + return load_plugin_textdomain( $domain, false, $dir ); + } + + return $loaded; + } + + /** + * Returns the post types registered by Tribe plugins. + * + * @since 4.0.1 Introduced the method. + * + * @return array Slugs for all Post Types registered. + */ + public static function get_post_types() { + /** + * We default the post type array to empty in tribe-common. Plugins like TEC add to it. + * + * @since 4.0.1 + * + * @param array Slugs for all Post Types registered. + */ + return apply_filters( 'tribe_post_types', [] ); + } + + /** + * Insert an array after a specified key within another array. + * + * @param $key + * @param $source_array + * @param $insert_array + * + * @return array + */ + public static function array_insert_after_key( $key, $source_array, $insert_array ) { + if ( array_key_exists( $key, $source_array ) ) { + $position = array_search( $key, array_keys( $source_array ) ) + 1; + $source_array = array_slice( $source_array, 0, $position, true ) + $insert_array + array_slice( $source_array, $position, null, true ); + } else { + // If no key is found, then add it to the end of the array. + $source_array += $insert_array; + } + + return $source_array; + } + + /** + * Insert an array immediately before a specified key within another array. + * + * @param $key + * @param $source_array + * @param $insert_array + * + * @return array + */ + public static function array_insert_before_key( $key, $source_array, $insert_array ) { + if ( array_key_exists( $key, $source_array ) ) { + $position = array_search( $key, array_keys( $source_array ) ); + $source_array = array_slice( $source_array, 0, $position, true ) + $insert_array + array_slice( $source_array, $position, null, true ); + } else { + // If no key is found, then add it to the end of the array. + $source_array += $insert_array; + } + + return $source_array; + } + + /** + * Get the Post ID from a passed integer, a passed WP_Post object, or the current post. + * + * Helper function for getting Post ID. Accepts `null` or a Post ID. If attempting + * to detect $post object and it is not found, returns `false` to avoid a PHP Notice. + * + * @param null|int|WP_Post $candidate Post ID or object, `null` to get the ID of the global post object. + * + * @return int|false The ID of the passed or global post, `false` if the passes object is not a post or the global + * post is not set. + */ + public static function post_id_helper( $candidate = null ) { + $candidate_post = get_post( $candidate ); + + $post_id = $candidate_post instanceof WP_Post ? $candidate_post->ID : false; + + /** + * Allows modifying the post ID in order to allow redirection of values before any other additional + * WordPress action is called from on result. + * + * @since 4.12.13 + * + * @param int|bool $post_id The ID of the post if the $candidate value is a valid WP_Post Object, `false` otherwise. + * @param null|int|WP_Post $candidate Post ID or object, `null` to get the ID of the global post object. + * + * @return int|bool The ID of the post. + */ + return apply_filters( 'tribe_post_id', $post_id, $candidate ); + } + + /** + * Adds a hook + * + */ + public function store_admin_notices( $page ) { + if ( 'plugins.php' !== $page ) { + return; + } + $notices = apply_filters( 'tribe_plugin_notices', [] ); + wp_localize_script( 'tribe-pue-notices', 'tribe_plugin_notices', $notices ); + } + + /** + * Runs tribe_plugins_loaded action, should be hooked to the end of plugins_loaded + */ + public function tribe_plugins_loaded() { + tribe( 'cache' ); + tribe_singleton( 'feature-detection', 'Tribe__Feature_Detection' ); + tribe_register_provider( 'Tribe__Service_Providers__Processes' ); + + tribe( \Tribe\Admin\Notice\WP_Version::class ); + tribe( \Tribe\Admin\Troubleshooting::class ); + + /** + * Runs after all plugins including Tribe ones have loaded + * + * @since 4.3 + */ + do_action( 'tribe_plugins_loaded' ); + } + + /** + * Registers the slug bound to the implementations in the container. + * + * @since 4.4 + * + * @return void Implementation of components loader doesn't return anything. + */ + public function bind_implementations() { + tribe_singleton( \TEC\Common\Storage\Timed_Option::class, \TEC\Common\Storage\Timed_Option::class ); + tribe_singleton( 'settings.manager', 'Tribe__Settings_Manager' ); + tribe_singleton( 'settings', 'Tribe__Settings', [ 'hook' ] ); + tribe_singleton( 'ajax.dropdown', 'Tribe__Ajax__Dropdown', [ 'hook' ] ); + tribe_singleton( 'assets', 'Tribe__Assets' ); + tribe_singleton( 'assets.pipeline', 'Tribe__Assets_Pipeline', [ 'hook' ] ); + tribe_singleton( 'asset.data', 'Tribe__Asset__Data', [ 'hook' ] ); + tribe_singleton( 'admin.helpers', 'Tribe__Admin__Helpers' ); + tribe_singleton( 'tracker', 'Tribe__Tracker', [ 'hook' ] ); + tribe_singleton( 'chunker', 'Tribe__Meta__Chunker', [ 'set_post_types', 'hook' ] ); + tribe_singleton( 'cache', 'Tribe__Cache', [ 'hook' ] ); + tribe_singleton( 'languages.locations', 'Tribe__Languages__Locations' ); + tribe_singleton( 'plugins.api', new Tribe__Plugins_API ); + tribe_singleton( 'logger', 'Tribe__Log' ); + tribe_singleton( 'cost-utils', [ 'Tribe__Cost_Utils', 'instance' ] ); + tribe_singleton( 'post-duplicate.strategy-factory', 'Tribe__Duplicate__Strategy_Factory' ); + tribe_singleton( 'post-duplicate', 'Tribe__Duplicate__Post' ); + tribe_singleton( 'context', 'Tribe__Context' ); + tribe_singleton( 'post-transient', 'Tribe__Post_Transient' ); + tribe_singleton( 'db', 'Tribe__Db' ); + tribe_singleton( 'db-lock', DB_Lock::class ); + tribe_singleton( 'customizer', 'Tribe__Customizer' ); + tribe_singleton( Tribe__Dependency::class, Tribe__Dependency::class ); + tribe_singleton( \Tribe\Admin\Troubleshooting::class, \Tribe\Admin\Troubleshooting::class, [ 'hook' ] ); + + tribe_singleton( 'callback', 'Tribe__Utils__Callback' ); + tribe_singleton( Tribe__Admin__Help_Page::class, Tribe__Admin__Help_Page::class, [ 'hook' ] ); + tribe_singleton( 'admin.pages', '\Tribe\Admin\Pages' ); + tribe_singleton( 'admin.activation.page', 'Tribe__Admin__Activation_Page' ); + tribe_singleton( Translations_Loader::class, Translations_Loader::class ); + + tribe_register_provider( Tribe__Editor__Provider::class ); + tribe_register_provider( Tribe__Service_Providers__Debug_Bar::class ); + tribe_register_provider( Tribe__Service_Providers__Promoter::class ); + tribe_register_provider( Tribe\Service_Providers\Tooltip::class ); + tribe_register_provider( Tribe\Service_Providers\Dialog::class ); + tribe_register_provider( Tribe\Service_Providers\PUE::class ); + tribe_register_provider( Tribe\Service_Providers\Shortcodes::class ); + tribe_register_provider( Tribe\Service_Providers\Body_Classes::class ); + tribe_register_provider( Tribe\Log\Service_Provider::class ); + tribe_register_provider( Tribe\Service_Providers\Crons::class ); + tribe_register_provider( Tribe\Service_Providers\Widgets::class ); + tribe_register_provider( Tribe\Service_Providers\Onboarding::class ); + tribe_register_provider( Tribe\Admin\Notice\Service_Provider::class ); + tribe_register_provider( Tribe\Admin\Conditional_Content\Service_Provider::class ); + tribe_register_provider( Libraries\Provider::class ); + + // Load the new third-party integration system. + tribe_register_provider( TEC\Common\Integrations\Provider::class ); + tribe_register_provider( TEC\Common\Site_Health\Provider::class ); + tribe_register_provider( TEC\Common\Telemetry\Provider::class ); + } + + /** + * Create the Promoter connector singleton early to allow hook into the filters early. + * + * Add a filter to determine_current_user during the setup of common library. + * + * @since 4.9.20 + * + * @return void Internal method without any return. + */ + public function promoter_connector() { + tribe_singleton( 'promoter.connector', 'Tribe__Promoter__Connector' ); + + add_filter( + 'determine_current_user', + tribe_callback( 'promoter.connector', 'authenticate_user_with_connector' ) + ); + } + + /** + * Get the common library's parent plugin file path. + * + * @since 5.1.0 + * + * @return string The path to the parent plugin file. + */ + public function get_parent_plugin_file_path(): string { + /** + * Allows plugins to hook in and declare themselves the parent of common. + * Used by Telemetry to determine which plugin to associate with. + * + * @since 5.1.0 + * + * @var string $parent_plugin_file The current path to the parent plugin file. + * + */ + $paths = apply_filters( 'tec_common_parent_plugin_file', [] ); + + foreach( $paths as $path ) { + $path = wp_normalize_path( $path ); + $test_path = str_ireplace( '/common', '', $this->parent_plugin_dir ); + + if ( stripos( $path, $test_path ) ) { + return $path; + } + } + + return ''; + } + + + /************************ + * * + * Deprecated Methods * + * * + ************************/ + // @codingStandardsIgnoreStart + + /** + * Helper function to indicate whether the current execution context is AJAX + * + * This method exists to allow us test code that behaves differently depending on the execution + * context. + * + * @since 4.0 + * + * @todo Add warning with '_deprecated_function' + * + * @param bool $doing_ajax An injectable status to override the `DOING_AJAX` check. + * + * @deprecated 4.7.12 + * + * @return boolean + */ + public function doing_ajax( $doing_ajax = null ) { + return tribe( 'context' )->doing_ajax( $doing_ajax ); + } + + /** + * Manages PUE license key notifications. + * + * It's important for the sanity of our users that only one instance of this object + * be created. However, multiple Tribe__Main objects can and will be instantiated, hence + * why for the time being we need to make this field static. + * + * @see https://central.tri.be/issues/65755 + * + * @deprecated 4.7.10 + * + * @return Tribe__PUE__Notices + */ + public function pue_notices() { + return tribe( 'pue.notices' ); + } + + /** + * + * @deprecated 4.7.10 + * + * @return Tribe__Log + */ + public function log() { + return tribe( 'logger' ); + } + // @codingStandardsIgnoreEnd +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Meta/Chunker.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Meta/Chunker.php new file mode 100644 index 000000000..f24018d64 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Meta/Chunker.php @@ -0,0 +1,965 @@ +register_for_chunking( $post_id, 'my_meta_key' ); + * + * // ... some code later... + * + * // data will be transparently chunked if needed... + * update_meta( $post_id, 'my_meta_key', $some_looooooooooong_string ); + * + * // ...and glued back together when reading + * get_post_meta( $post_id, 'my_meta_key', true ); + * + * By default the Chunker supports the `post` post type only, filter the `tribe_meta_chunker_post_types` to add yours: + * + * add_filter( 'tribe_meta_chunker_post_types`, 'my_chunkable_post_types' ); + * function my_chunkable_post_types( $post_types ) { + * $post_types[] = 'book'; + * + * return $post_types; + * } + * + * or filter the `tribe_meta_chunker_post_types` filter. + */ +class Tribe__Meta__Chunker { + /** + * @var string The key used to cache the class results in the WordPress object cache. + */ + protected $cache_group = 'post_meta'; + + /** + * @var string + */ + protected $chunked_keys_option_name = '_tribe_chunker_chunked_keys'; + + /** + * @var array The cache that will store chunks to avoid middleware operations from fetching the database. + */ + protected $chunks_cache = null; + + /** + * @var array The cache that will store the IDs of the posts that have at least one meta key registered + * for chunking. + */ + protected $post_ids_cache = null; + + /** + * @var string The separator that's used to mark the start of each chunk. + */ + protected $chunk_separator = '{{{TCSEP}}}'; + + /** + * @var array The post types supported by the Chunker. + */ + protected $post_types = []; + + /** + * @var int The filter priority at which Chunker will operate on meta CRUD operations. + */ + protected $filter_priority = - 1; + + /** + * @var string The meta key prefix applied ot any Chunker related post meta. + */ + protected $meta_key_prefix = '_tribe_chunker_'; + + /** + * @var int The largest size allowed by the Chunker. + */ + protected $max_chunk_size; + + /** + * Hooks the chunker on metadata operations for each supported post types. + * + * When changing post types unhook and rehook it like: + * + * $chunker = tribe( 'chunker' ); + * $chunker->set_post_types( array_merge( $my_post_types, $chunker->get_post_types() ); + * $chunker->unhook(); + * $chunker->hook(); + */ + public function hook() { + if ( empty( $this->post_types ) ) { + return; + } + + add_filter( 'update_post_metadata', [ $this, 'filter_update_metadata' ], $this->filter_priority, 4 ); + add_filter( 'delete_post_metadata', [ $this, 'filter_delete_metadata' ], $this->filter_priority, 3 ); + add_filter( 'add_post_metadata', [ $this, 'filter_add_metadata' ], $this->filter_priority, 4 ); + add_filter( 'get_post_metadata', [ $this, 'filter_get_metadata' ], $this->filter_priority, 4 ); + add_action( 'deleted_post', [ $this, 'remove_post_entry' ] ); + } + + /** + * Primes the chunked cache. + * + * This will just fetch the keys for the supported post types, not the values. + * + * @param bool $force Whether the cache should be reprimed even if already primed. + */ + public function prime_chunks_cache( $force = false ) { + if ( false === $force && null !== $this->chunks_cache ) { + return; + } + + $this->chunks_cache = []; + $this->post_ids_cache = []; + + $chunked_keys = get_option( $this->chunked_keys_option_name ); + + if ( empty( $chunked_keys ) ) { + return; + } + + foreach ( $chunked_keys as $post_id => $keys ) { + if ( ! is_array( $keys ) || empty( $keys ) ) { + continue; + } + $this->post_ids_cache[] = $post_id; + foreach ( $keys as $key ) { + $this->chunks_cache[ $this->get_key( $post_id, $key ) ] = null; + } + } + } + + /** + * Gets the key used to identify a post ID and meta key in the chunks cache. + * + * @param int $post_id + * @param string $meta_key + * @return string + */ + public function get_key( $post_id, $meta_key ) { + return "{$post_id}::{$meta_key}"; + } + + /** + * Register a post ID and meta key to be chunked if needed. + * + * @param int $post_id + * @param string $meta_key + * + * @return bool `false` if the post type is not supported, `true` otherwise + */ + public function register_chunking_for( $post_id, $meta_key ) { + if ( ! $this->is_supported_post_type( $post_id ) ) { + return false; + } + + $this->tag_as_chunkable( $post_id, $meta_key ); + + return true; + } + + /** + * Whether a post type is supported or not. + * + * @param int $object_id + * + * @return bool + */ + protected function is_supported_post_type( $object_id ) { + $post = get_post( $object_id ); + if ( empty( $post ) || ! in_array( $post->post_type, $this->post_types ) ) { + return false; + } + + return true; + } + + /** + * Tags a post ID and meta key couple as "chunkable" if needed. + * + * @param $post_id + * @param $meta_key + */ + protected function tag_as_chunkable( $post_id, $meta_key ) { + $key = $this->get_key( $post_id, $meta_key ); + + $this->prime_chunks_cache(); + + if ( ! array_key_exists( $key, $this->chunks_cache ) ) { + $this->chunks_cache[ $key ] = null; + } + + $this->post_ids_cache[] = $post_id; + + $option = (array) get_option( $this->chunked_keys_option_name ); + + if ( ! isset( $option[ $post_id ] ) ) { + $option[ $post_id ] = [ $meta_key ]; + } else { + $option[ $post_id ][] = $meta_key; + } + + update_option( $this->chunked_keys_option_name, array_filter( $option ), true ); + } + + /** + * Returns the meta key used to indicate if a meta key for a post is marked as chunkable. + * + * @param string $meta_key + * + * @return string + */ + public function get_chunkable_meta_key( $meta_key ) { + return $this->meta_key_prefix . $meta_key; + } + + /** + * Filters the add operations. + * + * Due to how the system works no more than one chunked entry can be stored. + * + * @param mixed $check + * @param int $object_id + * @param string $meta_key + * @param string $meta_value + * + * @see add_metadata() + * + * @return bool + */ + public function filter_add_metadata( $check, $object_id, $meta_key, $meta_value ) { + return $this->filter_update_metadata( $check, $object_id, $meta_key, $meta_value ); + } + + /** + * Filters the updated operations. + * + * @param mixed $check + * @param int $object_id + * @param string $meta_key + * @param string $meta_value + * + * @see update_metadata() + * + * @return bool + */ + public function filter_update_metadata( $check, $object_id, $meta_key, $meta_value ) { + if ( ! $this->applies( $object_id, $meta_key ) ) { + return $check; + } + + /** + * Filters the chunked meta update operation. + * + * Returning a non null value here will make the function return that value immediately. + * + * @param mixed $updated + * @param int $object_id The post ID + * @param string $meta_key + * @param mixed $meta_value + * + * @since 4.5.6 + */ + $updated = apply_filters( 'tribe_meta_chunker_update_meta', null, $object_id, $meta_key, $meta_value ); + if ( null !== $updated ) { + return $updated; + } + + $this->delete_chunks( $object_id, $meta_key ); + $this->remove_checksum_for( $object_id, $meta_key ); + wp_cache_delete( $object_id, $this->cache_group ); + + if ( $this->should_be_chunked( $object_id, $meta_key, $meta_value ) ) { + $this->insert_chunks( $object_id, $meta_key ); + + return true; + } else { + $this->cache_delete( $object_id, $meta_key ); + $this->insert_meta( $object_id, $meta_key, $meta_value ); + + return true; + } + + return $check; + } + + /** + * Whether the chunker should operate on this post ID and meta key couple or not. + * + * @param int $object_id + * @param string $meta_key + * + * @return bool + */ + protected function applies( $object_id, $meta_key ) { + $applies = ! $this->is_chunker_logic_meta_key( $meta_key ) + && $this->is_supported_post_type( $object_id ) + && $this->is_chunkable( $object_id, $meta_key ); + + /** + * Filters whether the meta chunker will apply to a post ID and meta key or not. + * + * The `$meta_key` parameter might be empty. + * + * @param bool $applies + * @param int $object_id + * @param string $meta_key + * + * @since 4.5.6 + */ + return apply_filters( 'tribe_meta_chunker_applies', $applies, $object_id, $meta_key ); + } + + /** + * Whether the meta key is one used by the chunker to keep track of its operations or not. + * + * @param string $meta_key + * + * @return bool + */ + protected function is_chunker_logic_meta_key( $meta_key ) { + if ( ! is_string( $meta_key ) ) { + return false; + } + + return 0 === strpos( $meta_key, $this->meta_key_prefix ); + } + + /** + * Whether a post ID and meta key couple is registered as chunkable or not. + * + * If no meta key is passed then the function will check if there is at least + * one meta key registered for chunking for the specified post ID. + * + * @param int $post_id + * @param string $meta_key + * + * @return bool + */ + public function is_chunkable( $post_id, $meta_key = null ) { + $this->prime_chunks_cache(); + + return ! empty( $meta_key ) + ? array_key_exists( $this->get_key( $post_id, $meta_key ), $this->chunks_cache ) + : in_array( $post_id, $this->post_ids_cache ); + } + + /** + * Deletes all the chunks for a post ID and meta key couple. + * + * @param int $object_id + * @param string $meta_key + */ + protected function delete_chunks( $object_id, $meta_key ) { + /** @var wpdb $wpdb */ + global $wpdb; + $chunk_meta_key = $this->get_chunk_meta_key( $meta_key ); + $delete = "DELETE FROM {$wpdb->postmeta} WHERE (meta_key = %s OR meta_key = %s) AND post_id = %d"; + $wpdb->query( $wpdb->prepare( $delete, $chunk_meta_key, $meta_key, $object_id ) ); + } + + /** + * Returns the meta key used to indicate a chunk for a meta key. + * + * @param string $meta_key + * + * @return string + */ + public function get_chunk_meta_key( $meta_key ) { + return $this->get_chunkable_meta_key( $meta_key ) . '_chunk'; + } + + /** + * Removes the checksum used to verify the integrity of the chunked values. + * + * @param int $object_id + * @param string $meta_key + */ + protected function remove_checksum_for( $object_id, $meta_key ) { + /** @var wpdb $wpdb */ + global $wpdb; + $data = [ + 'post_id' => $object_id, + 'meta_key' => $this->get_checksum_key( $meta_key ), + ]; + $wpdb->delete( $wpdb->postmeta, $data ); + } + + /** + * Returns the meta_key used to store the chunked meta checksum for a specified meta key. + * + * @param string $meta_key + * + * @return string + */ + public function get_checksum_key( $meta_key ) { + return $this->meta_key_prefix . $meta_key . '_checksum'; + } + + /** + * Whether a value should be chunked or not. + * + * @param int $post_id + * @param string $meta_key + * @param mixed $meta_value + * + * @return bool + */ + public function should_be_chunked( $post_id, $meta_key, $meta_value ) { + $should_be_chunked = false; + + $max_allowed_packet = $this->get_max_chunk_size(); + $serialized = maybe_serialize( $meta_value ); + $byte_size = $this->get_byte_size( $serialized ); + + $this->prime_chunks_cache(); + + // we use .8 and not 1 to allow for MySQL instructions to use 20% of the string size + if ( $byte_size > .8 * $max_allowed_packet ) { + $chunk_size = ceil( $max_allowed_packet * 0.75 ); + $key = $this->get_key( $post_id, $meta_key ); + $this->chunks_cache[ $key ] = $this->prefix_chunks( $this->chunk( $serialized, $chunk_size ) ); + $should_be_chunked = true; + } + + return $should_be_chunked; + } + + /** + * Returns the max chunk size in bytes. + * + * @return array|int|null|object + */ + public function get_max_chunk_size() { + if ( ! empty( $this->max_chunk_size ) ) { + return $this->max_chunk_size; + } + + $max_size = tribe( 'db' )->get_max_allowed_packet_size(); + + /** + * Filters the max size of the of the chunks in bytes. + * + * @param int $max_size By default the `max_allowed_packet` from the database. + */ + $this->max_chunk_size = apply_filters( 'tribe_meta_chunker_max_size', $max_size ); + + return $max_size; + } + + /** + * Sets the max chunk size. + * + * @param int $max_chunk_size The max chunk size in bytes. + */ + public function set_max_chunk_size( $max_chunk_size ) { + $this->max_chunk_size = $max_chunk_size; + } + + /** + * Gets the size in bytes of something. + * + * @param mixed $data + * + * @return int + */ + public function get_byte_size( $data ) { + return strlen( utf8_decode( maybe_serialize( $data ) ) ); + } + + /** + * Prefixes each chunk with a sequence number. + * + * @param array $chunks + * + * @return array An array of chunks each prefixed with sequence number. + */ + protected function prefix_chunks( array $chunks ) { + $count = count( $chunks ); + $prefixed = []; + for ( $i = 0; $i < $count; $i ++ ) { + $prefixed[] = "{$i}{$this->chunk_separator}{$chunks[$i]}"; + } + + return $prefixed; + } + + /** + * Chunks a string. + * + * The chunks are not prefixed! + * + * @param string $serialized + * @param int $chunk_size + * + * @return array An array of unprefixed chunks. + */ + protected function chunk( $serialized, $chunk_size ) { + $sep = $this->chunk_separator; + $chunks = array_filter( explode( $sep, chunk_split( $serialized, $chunk_size, $sep ) ) ); + + return $chunks; + } + + /** + * Inserts the chunks for a post ID and meta key couple in the database. + * + * The chunks are read from the array cache. + * + * @param int $object_id + * @param string $meta_key + */ + protected function insert_chunks( $object_id, $meta_key ) { + /** @var wpdb $wpdb */ + global $wpdb; + + $this->prime_chunks_cache(); + + $key = $this->get_key( $object_id, $meta_key ); + $chunks = $this->chunks_cache[ $key ]; + $chunk_meta_key = $this->get_chunk_meta_key( $meta_key ); + $this->insert_meta( $object_id, $meta_key, $chunks[0] ); + foreach ( $chunks as $chunk ) { + $wpdb->insert( $wpdb->postmeta, [ + 'post_id' => $object_id, + 'meta_key' => $chunk_meta_key, + 'meta_value' => $chunk, + ] ); + } + + $glued = $this->glue_chunks( $this->get_chunks_for( $object_id, $meta_key ) ); + $checksum_key = $this->get_checksum_key( $meta_key ); + $wpdb->delete( $wpdb->postmeta, [ 'post_id' => $object_id, 'meta_key' => $checksum_key ] ); + $wpdb->insert( $wpdb->postmeta, [ + 'post_id' => $object_id, + 'meta_key' => $checksum_key, + 'meta_value' => md5( $glued ), + ] ); + } + + /** + * Inserts a meta value in the database. + * + * Convenience method to avoid infinite loop in hooks. + * + * @param int $object_id + * @param string $meta_key + * @param mixed $meta_value + */ + protected function insert_meta( $object_id, $meta_key, $meta_value ) { + /** @var wpdb $wpdb */ + global $wpdb; + $data = [ + 'post_id' => $object_id, + 'meta_key' => $meta_key, + 'meta_value' => maybe_serialize( $meta_value ), + ]; + $wpdb->insert( $wpdb->postmeta, $data ); + } + + /** + * Glues the provided chunks. + * + * This method is sequence aware and should be used with what the `get_chunks_for` method returns. + * + * @param array $chunks + * + * @return string + * + * @see Tribe__Meta__Chunker::get_chunks_for() + */ + public function glue_chunks( array $chunks ) { + $ordered_chunks = []; + foreach ( $chunks as $chunk ) { + preg_match( '/(\\d+)' . preg_quote( $this->chunk_separator ) . '(.*)/', $chunk, $matches ); + $ordered_chunks[ $matches[1] ] = $matches[2]; + } + ksort( $ordered_chunks ); + + return implode( '', array_values( $ordered_chunks ) ); + } + + /** + * Returns the chunks stored in the database for a post ID and meta key couple. + * + * The chunks are returned as they are with prefix. + * + * @param int $object_id + * @param string $meta_key + * + * @return array|mixed + */ + public function get_chunks_for( $object_id, $meta_key ) { + $key = $this->get_key( $object_id, $meta_key ); + + $this->prime_chunks_cache(); + + if ( ! empty( $this->chunks_cache[ $key ] ) ) { + return $this->chunks_cache[ $key ]; + } + + /** @var wpdb $wpdb */ + global $wpdb; + + $chunk_meta_key = $this->get_chunk_meta_key( $meta_key ); + + $meta_ids = $wpdb->get_col( $wpdb->prepare( "SELECT meta_id FROM {$wpdb->postmeta} + WHERE post_id = %d + AND meta_key = %s", + $object_id, $chunk_meta_key + ) ); + + $meta_values = []; + foreach ( $meta_ids as $meta_id ) { + $query = $wpdb->prepare( "SELECT meta_value FROM {$wpdb->postmeta} WHERE meta_id = %d", $meta_id ); + $meta_values[] = $wpdb->get_var( $query ); + } + + if ( ! empty( $meta_values ) ) { + $this->chunks_cache[ $this->get_key( $object_id, $meta_key ) ] = $meta_values; + } else { + $this->chunks_cache[ $this->get_key( $object_id, $meta_key ) ] = null; + } + + return $meta_values; + } + + /** + * Resets a post ID and meta key couple cache. + * + * @param int $object_id + * @param string $meta_key + */ + protected function cache_delete( $object_id, $meta_key ) { + $key = $this->get_key( $object_id, $meta_key ); + + $this->prime_chunks_cache(); + + if ( isset( $this->chunks_cache[ $key ] ) ) { + $this->chunks_cache[ $key ] = null; + } + } + + /** + * Filters the delete operations. + * + * @param mixed $check + * @param int $object_id + * @param string $meta_key + * + * @return bool + * + * @see delete_metadata() + */ + public function filter_delete_metadata( $check, $object_id, $meta_key ) { + if ( ! $this->applies( $object_id, $meta_key ) ) { + return $check; + } + + /** + * Filters the value returned when deleting a specific meta for a post. + * + * Returning a non null value here will make the function return that value immediately. + * + * @param mixed $deleted + * @param int $object_id The post ID + * @param string $meta_key The requested meta key + * + * @since 4.5.6 + */ + $deleted = apply_filters( 'tribe_meta_chunker_delete_meta', null, $object_id, $meta_key ); + if ( null !== $deleted ) { + return $deleted; + } + + $has_chunked_meta = $this->is_chunked( $object_id, $meta_key ); + if ( ! $has_chunked_meta ) { + return $check; + } + $this->cache_delete( $object_id, $meta_key ); + $this->delete_chunks( $object_id, $meta_key ); + wp_cache_delete( $object_id, $this->cache_group ); + + return true; + } + + /** + * Whether a post ID and meta key couple has chunked meta or not. + * + * @param int $object_id + * @param string $meta_key + * @param bool $check_db Do verify the chunking state on the database. + * + * @return mixed + */ + public function is_chunked( $object_id, $meta_key, $check_db = false ) { + $key = $this->get_key( $object_id, $meta_key ); + + $this->prime_chunks_cache(); + + $chunked_in_cache = array_key_exists( $key, $this->chunks_cache ) && is_array( $this->chunks_cache[ $key ] ); + + return $chunked_in_cache; + } + + /** + * Returns the checksum for the stored meta key to spot meta value corruption malforming. + * + * @param int $object_id + * @param string $meta_key + * + * @return string + */ + public function get_checksum_for( $object_id, $meta_key ) { + /** @var wpdb $wpdb */ + global $wpdb; + + $query = "SELECT meta_value FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key = %s"; + $checksum = $wpdb->get_var( $wpdb->prepare( $query, $object_id, $this->get_checksum_key( $meta_key ) ) ); + + return ! empty( $checksum ) ? $checksum : ''; + } + + /** + * Handles the object destruction cycle to leave no traces behind. + */ + public function __destruct() { + $this->unhook(); + } + + /** + * Unhooks the Chunker from the metadata operations. + */ + public function unhook() { + remove_filter( 'update_post_metadata', [ $this, 'filter_update_metadata' ], $this->filter_priority ); + remove_filter( 'delete_post_metadata', [ $this, 'filter_delete_metadata' ], $this->filter_priority ); + remove_filter( 'add_post_metadata', [ $this, 'filter_add_metadata' ], $this->filter_priority ); + remove_filter( 'get_post_metadata', [ $this, 'filter_get_metadata' ], $this->filter_priority ); + remove_action( 'deleted_post', [ $this, 'remove_post_entry' ] ); + } + + /** + * Filters the get operations. + * + * @param mixed $check + * @param int $object_id + * @param string $meta_key + * @param bool $single + * + * @return array|mixed + * + * @see get_metadata() + */ + public function filter_get_metadata( $check, $object_id, $meta_key, $single ) { + if ( ! $this->applies( $object_id, $meta_key ) ) { + return $check; + } + + $all_meta = wp_cache_get( $object_id, $this->cache_group ); + + if ( ! $all_meta ) { + $all_meta = $this->get_all_meta_for( $object_id ); + wp_cache_set( $object_id, $all_meta, $this->cache_group ); + } + + // getting all the meta + if ( empty( $meta_key ) ) { + return $all_meta; + } + + // why not take $single into account? See condition check on the filter to understand. + $meta_value = isset( $all_meta[ $meta_key ] ) + ? array_map( 'maybe_unserialize', $all_meta[ $meta_key ] ) + : ''; + + /** + * Filters the value returned when getting a specific meta for a post. + * + * Returning a non null value here will make the function return that value immediately. + * + * @param mixed $meta_value + * @param int $object_id The post ID + * @param string $meta_key The requested meta key + * + * @since 4.5.6 + */ + $meta_value = apply_filters( 'tribe_meta_chunker_get_meta', $meta_value, $object_id, $meta_key ); + + if ( $single ) { + return (array) $meta_value; + } else { + return ! empty( $meta_value ) ? $meta_value : ''; + } + } + + /** + * Returns all the meta for a post ID. + * + * The meta includes the chunked one but not the chunker logic meta keys. + * The return format is the same used by the `get_post_meta( $post_id )` function. + * + * @param int $object_id + * + * @return array An array containing all meta including the chunked one. + * + * @see get_post_meta() with empty `$meta_key` argument. + */ + public function get_all_meta_for( $object_id ) { + $all_meta = $this->get_all_meta( $object_id ); + + if ( empty( $all_meta ) ) { + return []; + } + + $grouped = []; + foreach ( $all_meta as $entry ) { + if ( ! isset( $grouped[ $entry['meta_key'] ] ) ) { + $grouped[ $entry['meta_key'] ] = [ $entry['meta_value'] ]; + } else { + $grouped[ $entry['meta_key'] ][] = $entry['meta_value']; + } + } + + $chunker_meta_keys = array_filter( array_keys( $grouped ), [ $this, 'is_chunker_logic_meta_key' ] ); + + if ( empty( $chunker_meta_keys ) ) { + return $grouped; + } + + $checksum_keys = array_filter( $chunker_meta_keys, [ $this, 'is_chunker_checksum_key' ] ); + + if ( empty( $checksum_keys ) ) { + return $grouped; + } + + $chunker_meta = array_intersect_key( $grouped, array_combine( $chunker_meta_keys, $chunker_meta_keys ) ); + $normal_meta = array_diff_key( $grouped, array_combine( $chunker_meta_keys, $chunker_meta_keys ) ); + foreach ( $checksum_keys as $checksum_key ) { + $normal_meta_key = str_replace( [ $this->meta_key_prefix, '_checksum' ], '', $checksum_key ); + $chunk_meta_key = $this->get_chunk_meta_key( $normal_meta_key ); + + if ( empty( $chunker_meta[ $chunk_meta_key ] ) ) { + continue; + } + + $normal_meta[ $normal_meta_key ] = [ $this->glue_chunks( $chunker_meta[ $chunk_meta_key ] ) ]; + } + + return $normal_meta; + } + + /** + * Fetches all the meta for a post. + * + * @param int $object_id + * + * @return array|null|object + */ + protected function get_all_meta( $object_id ) { + /** + * Filters the value returned when getting all the meta for a post. + * + * Returning a non null value here will make the function return that value immediately. + * + * @param mixed $all_meta + * @param int $object_id The post ID + * + * @since 4.5.6 + */ + $all_meta = apply_filters( 'tribe_meta_chunker_get_all_meta', null, $object_id ); + if ( null !== $all_meta ) { + return $all_meta; + } + + /** @var wpdb $wpdb */ + global $wpdb; + $query = $wpdb->prepare( "SELECT meta_key, meta_value FROM {$wpdb->postmeta} WHERE post_id = %d", $object_id ); + $results = $wpdb->get_results( $query, ARRAY_A ); + + return ! empty( $results ) && is_array( $results ) ? $results : []; + } + + /** + * Returns the post types supported by the chunker. + * + * @return array + */ + public function get_post_types() { + return $this->post_types; + } + + /** + * Sets the post types the Chunker should support. + * + * @param array $post_types + */ + public function set_post_types( array $post_types = null ) { + if ( null === $post_types ) { + /** + * Filters the chunk-able post types. + * + * @param array $post_types + */ + $this->post_types = apply_filters( 'tribe_meta_chunker_post_types', $this->post_types ); + + return; + } + + $this->post_types = $post_types; + } + + /** + * Returns the name of the option that stores the keys registered for chunking for each post. + * + * @return string + */ + public function get_key_option_name() { + return $this->chunked_keys_option_name; + } + + /** + * Returns the cache group used by the meta chunker. + * + * @return string + */ + public function get_cache_group() { + return $this->cache_group; + } + + /** + * Asserts that a meta key is not a chunk meta key. + * + * @param string $meta_key + * + * @return bool + */ + protected function is_chunker_checksum_key( $meta_key ) { + return preg_match( "/^{$this->meta_key_prefix}.*_checksum$/", $meta_key ); + } + + /** + * Removes the entries associated with a deleted post from the cache and the database option. + * + * @param int $post_id A post ID + */ + public function remove_post_entry( $post_id ) { + $this->prime_chunks_cache(); + + foreach ( $this->chunks_cache as $key => $value ) { + if ( 0 === strpos( $key, (string) $post_id ) ) { + unset( $this->chunks_cache[ $key ] ); + } + } + + if ( ! empty( $this->chunks_cache ) ) { + update_option( $this->chunked_keys_option_name, $this->chunks_cache ); + } else { + delete_option( $this->chunked_keys_option_name ); + } + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Models/Post_Types/Base.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Models/Post_Types/Base.php new file mode 100644 index 000000000..439e1faf9 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Models/Post_Types/Base.php @@ -0,0 +1,369 @@ +post = $post; + + return $instance; + } + + /** + * Returns the slug that will be prefixed to the cache key for the model. + * + * @since 4.9.18 + * + * @return string The slug that will be prefixed to the cache key for the model. + */ + abstract protected function get_cache_slug(); + + /** + * Returns the cached model properties for the specified filter, if any. + * + * @since 4.9.18 + * + * @param string $filter Type of filter to apply, used here as the stored post values might change. + * + * @return array|false An array of model properties, or `false` if not found. + */ + protected function get_cached_properties( $filter ) { + $cache_slug = $this->get_cache_slug(); + + if ( empty( $cache_slug ) ) { + return false; + } + + // Cache by post ID and filter. + $cache_key = $this->get_properties_cache_key( $filter ); + + return ( new Cache() )->get( $cache_key, Cache_Listener::TRIGGER_SAVE_POST ); + } + + /** + * Builds and returns the properties for the model. + * + * In this method child classes should also implement any caching trigger mechanism, if any. + * + * @since 4.9.18 + * + * @param string $filter The type of filter to build the properties for. + * + * @return array An array of built properties. + */ + abstract protected function build_properties( $filter ); + + /** + * Returns an array of the model properties. + * + * @since 4.9.18 + * + * @param string $filter The type of filter to get the properties for. + * @param bool $force Whether to force a rebuild of the properties or not. + * + * @return array The model properties. This value might be cached. + */ + protected function get_properties( $filter, bool $force = false ) { + $cached = ! $force ? $this->get_cached_properties( $filter ) : false; + + if ( false !== $cached ) { + // Un-serialize the pre-serialized properties now, when classes will be most likely defined. + $pre_serialized_properties = $cached[ self::PRE_SERIALIZED_PROPERTY ] ?? []; + + foreach ( $pre_serialized_properties as $key => $value ) { + try { + $cached[ $key ] = unserialize( $value, [ 'allowed_classes' => true ] ); + } catch ( \Throwable $t ) { + /* + * Deal with the case where plugin A, B, C were active when the cache was built, + * but B and C are now inactive. In this case the un-serialization will fail for + * any pre-serialized value using classes from B and C: here we gracefully ignore + * each one of those. + */ + } + } + + try { + // Allow models to apply further unserialization operations. + $cached = $this->scalar_unserialize_properties( $cached ); + + /** + * Allows filtering the properties of the post type model after they have been unserialized from the + * cache.. + * + * @since 5.0.3 + * + * @param array $cached The key-value map of the properties of the post type model. + * @param \WP_Post $post The post object of the post type model. + */ + $cached = apply_filters( "tec_model_{$this->get_cache_slug()}_read_cache_properties", $cached, $this->post ); + + // Remove the pre-serialized properties from the cached properties. + unset( $cached[ self::PRE_SERIALIZED_PROPERTY ] ); + + return $cached; + } catch ( \Throwable $t ) { + // Rebuid the properties from cache failed, move on. + } + } + + $props = $this->build_properties( $filter ); + + $cache_slug = $this->get_cache_slug(); + + /** + * Filters the array of properties that will be used to decorate the post object handled by the class. + * + * @since 4.9.18 + * + * @param array $props An associative array of all the properties that will be set on the "decorated" post + * object. + * @param \WP_Post $post The post object handled by the class. + */ + $props = apply_filters( "tribe_post_type_{$cache_slug}_properties", $props, $this->post ); + + return $props; + } + + /** + * Returns the WP_Post version of this model. + * + * @since 4.9.18 + * @since 5.0.3 Added the `$force` parameter. + * + * @param string $output The required return type. One of OBJECT, ARRAY_A, or ARRAY_N, which correspond to a WP_Post + * object,an associative array, or a numeric array, respectively. + * @param string $filter Type of filter to apply. Accepts 'raw', 'edit', 'db', or 'display' and other formats + * supported by the specific type implementation. + * @param bool $force Whether to force the post to be reloaded from the database or not. + * + * @return \WP_Post|array|null The post object version of this post type model or `null` if the post is not valid. + */ + public function to_post( $output = OBJECT, $filter = 'raw', bool $force = false ) { + $properties = $this->get_properties( $filter, $force ); + + switch ( $output ) { + case ARRAY_A: + return array_merge( (array) $this->post, $properties ); + case ARRAY_N: + return array_values( array_merge( (array) $this->post, $properties ) ); + case OBJECT: + default: + // Clone the post to avoid side effects. + $clone = clone $this->post; + // And decorate the clone with the properties. + foreach ( $properties as $key => $value ) { + $clone->{$key} = $value; + } + + return $clone; + } + } + + /** + * Returns the closure that should be used to cache the post type model when, and if, caching it is required. + * + * @since 4.9.18 + * + * @param string $filter The kind of filter applied to the model. + * @return callable The closure, or callable, that should be used to cache this model when, and if, required. + */ + protected function get_caching_callback( $filter ) { + $cache_slug = $this->get_cache_slug(); + + if ( empty( $cache_slug ) ) { + return '__return_true'; + } + + $callback = null; + + if ( wp_using_ext_object_cache() ) { + $callback = $this->get_object_cache_callback( $filter ); + } + + return $callback; + } + + /** + * Further scalarizes the properties of the post type model. + * + * Extending classes should implement this method to handle + * specific scalarization of the model properties. + * + * @since 5.0.3 + * + * @param array $properties A key-value map of the + * properties of the post type model. + * + * @return array The scalarized properties key-value map. + */ + protected function scalar_serialize_properties( array $properties ): array { + return $properties; + } + + /** + * Further un-scalarizes the properties of the post type model. + * + * Extending classes should implement this method to handle + * specific un-scalarization of the model properties. + * + * @since 5.0.3 + * + * @param array $properties A key-value map of the + * properties of the post type model. + * + * @return array The un-scalarized properties key-value map. + */ + protected function scalar_unserialize_properties( array $properties ): array { + return $properties; + } + + /** + * Returns the callback function that should be used to cache the model using object caching. + * + * If any real caching is in place , then define a function to cache this event when, and if, one of the + * lazy properties is loaded. + * Cache by post ID and filter. + * Cache could be pre-fetched: in that case only built-in PHP classes will be supported: for this reason + * object properties will be "scalarized". + * + * @since 5.0.3 + * + * @param string $cache_slug The cache slug of the post type model. + * @param string $filter The filter to cache the model for. + * + * @return \Closure The callback function that should be used to cache the model using object caching. + */ + protected function get_object_cache_callback( string $filter ): \Closure { + $cache_key = $this->get_properties_cache_key( $filter ); + $cache = new Cache(); + + return function () use ( $cache, $cache_key, $filter ) { + $properties = $this->get_properties( $filter ); + $pre_serialized_properties = []; + + try { + // Pre-serialize each object property and store it in a separate cache entry. + foreach ( $properties as $key => &$value ) { + try { + if ( is_object( $value ) && ! $value instanceof \stdClass ) { + // We might end up pre-serializing other built-in objects here, but let's play it safe. + $pre_serialized_properties[ $key ] = serialize( $value ); + } + } catch ( \Throwable $t ) { + // Null the property: an object that cannot be serialized correctly is not cacheable. + $value = null; + } + } + unset( $value ); + + // Remove the pre-serialized properties from the main cache entry. + $properties = array_diff_key( $properties, $pre_serialized_properties ); + + // Allow models to customize the pre-serialization further. + $properties = $this->scalar_serialize_properties( $properties ); + + // Add the pre-serialized properties to the main cache entry. + if ( count( $pre_serialized_properties ) ) { + $properties[ self::PRE_SERIALIZED_PROPERTY ] = $pre_serialized_properties; + } + + /** + * Allows filtering the properties of the post type model before they are cached. + * + * @since 5.0.3 + * + * @param array $properties The key-value map of the properties of the post type model. + * @param \WP_Post $post The post object of the post type model. + */ + $properties = apply_filters( "tec_model_{$this->get_cache_slug()}_put_cache_properties", $properties, $this->post ); + } catch ( \Throwable $t ) { + // If we can't serialize the properties, bail. + return; + } + + /* + * Cache without expiration, but only until a post of the types managed by The Events Calendar is + * updated or created. + */ + $cache->set( $cache_key, $properties, 0, Cache_Listener::TRIGGER_SAVE_POST ); + }; + } + + /** + * Returns the cache key to be used to cache the model properties. + * + * @since 5.0.3 + * + * @param string $filter The filter to cache the model for. + * + * @return string The cache key to be used to cache the model properties. + */ + public function get_properties_cache_key( string $filter ): string { + return $this->get_cache_slug() . '_' . $this->post->ID . '_' . $filter; + } + + /** + * Commits the model properties to cache immediately. + * + * @since 5.0.3 + * + * @param string $filter The filter to cache the model properties for. + * + * @return void The model properties are cached immediately. + */ + public function commit_to_cache( string $filter = 'raw' ): void { + $caching_callback = $this->get_object_cache_callback( $filter ); + $caching_callback(); + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Models/Post_Types/Nothing.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Models/Post_Types/Nothing.php new file mode 100644 index 000000000..b238e2210 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Models/Post_Types/Nothing.php @@ -0,0 +1,44 @@ +notices[ $key ] = $notice; + + return true; + } + + /** + * Check to see if an admin notice exists + * + * @param string $key + * + * @return bool + */ + public static function is_notice( $key ) { + return ! empty( self::instance()->notices[ $key ] ) ? true : false; + } + + /** + * Remove an admin notice + * + * @param string $key + * + * @return bool + */ + public static function remove_notice( $key ) { + if ( self::is_notice( $key ) ) { + unset( self::instance()->notices[ $key ] ); + + return true; + } else { + return false; + } + } + + /** + * Get the admin notices + * + * @return array + */ + public static function get() { + return self::instance()->notices; + } + + /** + * Static Singleton Factory Method + * + * @return Tribe__Notices + */ + public static function instance() { + static $instance; + + if ( ! $instance ) { + $class_name = __CLASS__; + $instance = new $class_name; + } + + return $instance; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Onboarding/Hints_Abstract.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Onboarding/Hints_Abstract.php new file mode 100644 index 000000000..e654a2783 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Onboarding/Hints_Abstract.php @@ -0,0 +1,98 @@ +is_on_page() ) { + return false; + } + + // Bail if the `Times to display` is set and it was reached. + if ( + is_numeric( $this->times_to_display ) + && ( tribe( 'onboarding' )->get_views( $this->hints_id ) > $this->times_to_display ) + ) { + return false; + } + + return true; + } + + /** + * Return the hints data. + * + * @since 4.14.9 + * + * @return array The hints. + */ + abstract function hints(); + + /** + * Return the CSS classes. + * + * @since 4.14.9 + * + * @return array The CSS classes. + */ + public function css_classes() { + return []; + } + + /** + * The hints data, publicly accessible. + * + * @since 4.14.9. + * + * @param array $data An array with the hints data. + * @return array + */ + public function hints_data( array $data = [] ) { + $data['hints'] = $this->hints(); + $data['classes'] = $this->css_classes(); + + return $data; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Onboarding/Main.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Onboarding/Main.php new file mode 100644 index 000000000..3b9e77e56 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Onboarding/Main.php @@ -0,0 +1,223 @@ +get_registered_tours(); + + // Try to populate, if it should display. + foreach ( $registered_tours as $tour => $class_name ) { + $tour_class = new $class_name(); + + if ( $tour_class->should_display() ) { + // Increment the views when the tour is displayed. + $this->increment_views( $tour_class->tour_id ); + $data = $tour_class->tour_data( $data ); + + /** + * We're displaying the tour. + * + * @since 4.14.9. + * + * @param string $tour_id The tour id. + */ + do_action( 'tribe_onboarding_tour_display', $tour_class->tour_id ); + + break; + } + } + + /** + * Filter the data we're using to localize the tour steps. + * + * Since 4.14.9 + * + * @param array $data An array with the tour data. + * + * @return array $data An array with the tour data. + */ + $data = apply_filters( 'tribe_onboarding_tour_data', $data ); + + return $data; + } + + /** + * Get the hints. + * + * @since 4.14.9 + * + * @return array $steps The hints data. + */ + protected function hints_data() { + $data = []; + $registered_hints = $this->get_registered_hints(); + + // Try to populate, and check if it should display. + foreach ( $registered_hints as $hints => $class_name ) { + $hints_class = new $class_name(); + + if ( $hints_class->should_display() ) { + // Increment the views when the tour is displayed. + $this->increment_views( $hints_class->tour_id ); + $data = $hints_class->hints_data( $data ); + + /** + * We're displaying the hints. + * + * @since 4.14.9. + * + * @param string $hints_id The hints id. + */ + do_action( 'tribe_onboarding_hints_display', $hints_class->hints_id ); + + break; + } + } + + /** + * Filter the data we're using to localize the hints. + * + * Since 4.14.9 + * + * @param array $data An array with the hints data. + * + * @return array $data An array with the hints data. + */ + $data = apply_filters( 'tribe_onboarding_hints_data', $data ); + + return $data; + } + + /** + * Localize tour data. + * + * @since 4.14.9 + * + * @param string $hook The current admin page. + */ + public function localize_tour( $hook ) { + $data = $this->tour_data(); + + wp_localize_script( 'tec-onboarding-js', 'TribeOnboardingTour', $data ); + } + + /** + * Localize hints data. + * + * @since 4.14.9 + * + * @param string $hook The current admin page. + */ + public function localize_hints( $hook ) { + $data = $this->hints_data(); + + wp_localize_script( 'tec-onboarding-js', 'TribeOnboardingHints', $data ); + } + + /** + * Get the views for an onboarding element. + * + * @since 4.14.9 + * + * @param string $id The onboarding ID (tour or hint). + * + * @return mixed The views for the given ID. + */ + public function get_views( $id = '' ) { + + if ( empty( $id ) ) { + return; + } + + $option = tribe_get_option( 'tribe_onboarding_views', [] ); + + if ( ! isset( $option[ $id ] ) ) { + return; + } + + return intval( $option[ $id ] ); + } + + /** + * Increment views for an onboarding element. + * + * @since 4.14.9 + * + * @param string $id The onboarding ID (tour or hint). + * @return int The views count for the particular `$id`. + */ + public function increment_views( $id ) { + $option = tribe_get_option( 'tribe_onboarding_views', [] ); + $views = 0; + + if ( isset( $option[ $id ] ) ) { + $views = intval( $option[ $id ] ); + } + + // Increment views and save. + $views++; + $option[ $id ] = $views; + + tribe_update_option( 'tribe_onboarding_views', $option ); + + return $views; + } + + /** + * Get the list of tours available for handling. + * + * @since 4.14.9 + * + * @return array An associative array of shortcodes in the shape `[ => ]` + */ + public function get_registered_tours() { + $tours = []; + + /** + * Allow the registering of tours into our plugins. + * + * @since 4.14.9 + * + * @var array An associative array of tours in the shape `[ => ]`. + */ + $tours = apply_filters( 'tribe_onboarding_tours', $tours ); + + return $tours; + } + + /** + * Get the list of hints available for handling. + * + * @since 4.14.9 + * + * @return array An associative array of hints in the shape `[ => ]` + */ + public function get_registered_hints() { + $hints = []; + + /** + * Allow the registering of tours into our plugins. + * + * @since 4.14.9 + * + * @var array An associative array of hints in the shape `[ => ]`. + */ + $tours = apply_filters( 'tribe_onboarding_hints', $hints ); + + return $hints; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Onboarding/README.md b/wp-content/plugins/the-events-calendar/common/src/Tribe/Onboarding/README.md new file mode 100644 index 000000000..d13a01a0b --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Onboarding/README.md @@ -0,0 +1,242 @@ +# Onboarding + +Onboarding consists of two components. Tours & Hints. The idea of this components is to enhance the onboarding experience and add some contextual help for elements. + +These components work as a wrapper of [IntroJS](https://introjs.com/). + +If for any reason you want to disable the Onboarding library, you can use the following filter: + +`add_filter( 'tribe_onboarding_disable, '__return_true' );` + +## Tours + +**Tours** provides an easy way to onboard users on a step by step basis. The information is provided to the user on a modal. + +Users can navigate through the different steps and close the modal at any time by clicking outside of it. + +Setting up tours is fairly simple. It all comes down to hooking onto `tribe_onboarding_tour_data`. + +The information to be sent there is an array in the following format: + +``` +$tour_data = [ + 'steps' = [], // An array of the steps you'd like for the tour. + 'classes' = [], // An array of CSS classes to apply to the modal. (Optional) +]; +``` + +The format of each step can contain the following: + +``` +$step = [ + 'title' => __( 'Welcome to this screen' ), // The step title. + 'intro' => __( 'This is the description of the "Welcome to this screen" message.' ); + 'element' => '#my-html-id', // If you want to highlight a certain part of the HTML for this step. If not defined, it'll show just the modal with the information. (Optional) +]; +``` + +So for example, if you want to add a simple welcome tour for a settings panel you could add the following. + +``` +add_filter( 'tribe_onboarding_tour_data', 'my_fancy_tour' ); + +function my_fancy_tour( $data ) { + + // Here you can do some checks to see if you're in the page you want to show to tour. + + $steps = [ + [ + 'title' => __( '🤘 Welcome to the settings panel' ), + 'intro' => __( 'It is actually great that you are using our plugins! From this settings panel you should be able to access all the settings to configure your site.' ), + ], + [ + 'title' => __( '⚙️ Different sections' ), + 'element' => '#tribe-settings-tabs', + 'intro' => __( 'On this section you can access all of the different settings of our plugins, if you have questions about which settings we have you can go to
    our knowledgebase article' ), + ], + [ + 'title' => __( '🛠️ Change the settings' ), + 'element' => '#tribe-field-postsPerPage', + 'intro' => __( 'If you need to change any configuration, you can do it! If you have questions about which settings we have you can go to our knowledgebase article' ), + ], + [ + 'title' => __( '💡 Save the Settings' ), + 'element' => '#tribeSaveSettings', + 'intro' => __( 'Please remember to save the settings, if you have questions about which settings we have you can go to our knowledgebase article' ), + ], + ]; + + $data['steps'] = $steps; + $data['classes'] = [ 'my__fancy-css-class', 'my__fancy-css-class--modifier' ]; + + return $data; +} +``` + +### Setting up Tours from TEC plugins + +Setting up new tours from our plugins should be easy with the abstract classes we have in place. + +We should be registering the tours we want, hooking them into the `tribe_onboarding_tours` filter. + +The function to hook onto `tribe_onboarding_tours` should have the following format: + +``` +/** + * Register tours. + * + * @see \Tribe\Onboarding\Main::get_registered_tours() + * + * @since 1.0 + * + * @param array $tours An associative array of tours in the shape `[ => ]`. + * + * @return array +*/ +public function filter_register_tours( array $tours ) { + $tours['my_awesome_tour_id'] = MyAwesomeTourClass::class; + + return $tours; +} +``` + +And then `MyAwesomeTourClass` should have the following format: + +``` +use Tribe\Onboarding\Tour_Abstract; +/** + * Class MyAwesomeTourClass + */ +class MyAwesomeTourClass extends Tour_Abstract { + + /** + * The tour ID. + * + * @var string + */ + public $tour_id = 'my_awesome_tour_id'; + + /** + * Times to display the tour. + * If you set '5', then it'll be displayed FIVE times. + * + * @var int + */ + public $times_to_display = 5; + + /** + * Returns if it's on the page we want to display the tour for. + * + * @return bool True if it's on page. + */ + public function is_on_page() { + + // Perform any check you want, to see if the tour should display or not. + return $admin_helpers->is_screen( 'tribe_events_page_tribe-common' ); + } + + /** + * Tour steps. + * + * @since 1.0 + * + * @return array $steps The tour steps + */ + public function steps() { + + $steps = [ + [ + 'title' => __( '🤘 Welcome to the settings panel' ), + 'intro' => __( 'It is actually great that you are using our plugins! From this settings panel you should be able to access all the settings to configure your site.' ), + ], + [ + 'title' => __( '⚙️ Different sections' ), + 'element' => '#tribe-settings-tabs', + 'intro' => __( 'On this section you can access all of the different settings of our plugins, if you have questions about which settings we have you can go to our knowledgebase article' ), + ], + [ + 'title' => __( '🛠️ Change the settings' ), + 'element' => '#tribe-field-postsPerPage', + 'intro' => __( 'If you need to change any configuration, you can do it! If you have questions about which settings we have you can go to our knowledgebase article' ), + ], + [ + 'title' => __( '💡 Save the Settings' ), + 'element' => '#tribeSaveSettings', + 'intro' => __( 'Please remember to save the settings, if you have questions about which settings we have you can go to our knowledgebase article' ), + ], + ]; + + return $steps; + } + + /** + * Tour CSS Classes. + * + * Here you can set additional CSS classes for the particular tour. + * + * @return array $css_classes The tour extra CSS classes. + */ + public function css_classes() { + + return [ 'my-awesome-css-class' ]; + } +} + +``` + +## Hints + +**Hints** are great for providing non-intrusive contextual help. Each hing will be associated to a particular HTML element (which you can define by a CSS class or an ID) and it'll add kind of a "infinite bouncing dot" besides that element. When clicked you'll have some more context on what's the purpose of that. + +Technically speaking **Hints** work pretty similarly to how **Tours** do. The mechanics of adding a set of hints is almost the same. + +It comes down to hooking onto `tribe_onboarding_hints_data`. + +The information to be sent there is an array in the following format: + +``` +$hints_data = [ + 'hints' = [], // An array of the hints you'd like to have. + 'classes' = [], // An array of CSS classes to apply to the modal/tooltip. (Optional) +]; +``` + +So for example, if you want to add a hint for a newly added button: + +``` +add_filter( 'tribe_onboarding_hints_data', 'my_fancy_hints' ); + +function my_fancy_hints( $data ) { + $hints = [ + [ + 'hint' => __( 'You can now add attendees for this event!' ), + 'element' => '.add_attendee', + ], + ]; + + $data['hints'] = $hints; + $data['classes'] = [ 'my__fancy-css-class', 'my__fancy-css-class--modifier' ]; + + return $data; +} +``` + +## CSS classes that you may want to use: + +- `.tribe-onboarding__tooltip--large` - Use if if you want your tooltip to be bigger/wider. +- `.tribe-onboarding__tooltip--dark` - Use if if you want your tooltip to have a dark skin (to use an image for the background, or just a plain dark color). +- `.tribe-onboarding__tooltip--squared` - Use it if you want a squared tooltip. +- `.tribe-onboarding__tooltip--no-bullets` - Use it if you want to hide the navigation bullets. +- `.tribe-onboarding__tooltip--title-large` - Use it if you want to have a bigger title. +- `.tribe-onboarding__tooltip--content-centered` - Use it if you want to center the content. +- `.tribe-onboarding__tooltip--button-centered` - Use it if you want to center the buttons. +- `.tribe-onboarding__tooltip--button-large` - Use it if you want to have a bigger button. +- `.tribe-onboarding__tooltip--button-rounded` - Use it if you want to have a rounded button. +- `.tribe-onboarding__tooltip--button-dark-skin` - Use it if you want to have a button for dark skin (white background / dark text button). + + +### 💡 To-Do's / Ideas: + +- [ ] Add some more styles variations. +- [ ] Maybe add the possibility of having animated GIFs/images on each step. +- [ ] Add some abstraction to extend this anywhere, and make it easier to check if it's in the page, and load the tours and/or hints we would like to add. diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Onboarding/Tour_Abstract.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Onboarding/Tour_Abstract.php new file mode 100644 index 000000000..adfbb0068 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Onboarding/Tour_Abstract.php @@ -0,0 +1,98 @@ +is_on_page() ) { + return false; + } + + // Bail if the `Times to display` is set and it was reached. + if ( + is_numeric( $this->times_to_display ) + && ( tribe( 'onboarding' )->get_views( $this->tour_id ) > $this->times_to_display ) + ) { + return false; + } + + return true; + } + + /** + * Return the tour steps. + * + * @since 4.14.9 + * + * @return array The tour steps. + */ + abstract function steps(); + + /** + * Return the CSS classes. + * + * @since 4.14.9 + * + * @return array The CSS classes. + */ + public function css_classes() { + return []; + } + + /** + * The tour data, publicly accessible. + * + * @since 4.14.9. + * + * @param array $data An array with the tour data. + * @return array + */ + public function tour_data( array $data = [] ) { + $data['steps'] = $this->steps(); + $data['classes'] = $this->css_classes(); + + return $data; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/PUE/Checker.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/PUE/Checker.php new file mode 100644 index 000000000..6c29d5ec3 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/PUE/Checker.php @@ -0,0 +1,1917 @@ +set_slug( $slug ); + $this->set_plugin_file( $plugin_file ); + $this->set_options( $options ); + $this->hooks(); + $this->set_key_status_name(); + } + + /** + * Gets whether the license key is valid or not. + * + * @since 4.14.9 + */ + public function is_key_valid() { + // @todo remove transient in a major feature release where we release all plugins. + $status = get_transient( $this->pue_key_status_transient_name ); + + if ( empty( $status ) ) { + $status = get_option( $this->pue_key_status_option_name, 'invalid' ); + } + + return 'valid' === $status; + } + + /** + * Gets whether or not the PUE key validation check is expired. + * + * @since 4.14.9 + */ + public function is_key_validation_expired() { + // If we have a transient, then we're good. Not expired. + // @todo remove transient in a major feature release where we release all plugins. + if ( get_transient( $this->pue_key_status_transient_name ) ) { + return false; + } + + $option_expiration = get_option( "{$this->pue_key_status_option_name}_timeout", null ); + return is_null( $option_expiration ) || ( time() > $option_expiration ); + } + + /** + * Set the PUE key status property names. + * + * @since 4.14.9 + */ + public function set_key_status_name() { + $this->pue_key_status_option_name = 'pue_key_status_' . $this->get_slug() . '_' . $this->get_site_domain(); + + // @todo remove transient in a major feature release where we release all plugins. + $this->pue_key_status_transient_name = md5( $this->get_slug() . $this->get_site_domain() ); + } + + /** + * Creates a hash for the transient name that holds the current key status. + * + * @todo remove transient in a major feature release where we release all plugins. + * + * @since 4.14.14 + */ + public function set_key_status_transient_name() { + _deprecated_function( __METHOD__, '4.14.9', __CLASS__ . '::set_key_status_name()' ); + } + + /** + * Sets the key status based on the key validation check results. + * + * @since 4.14.9 + * + * @param int $valid 0 for invalid, 1 or 2 for valid. + */ + public function set_key_status( $valid ) { + $status = tribe_is_truthy( $valid ) ? 'valid' : 'invalid'; + update_option( $this->pue_key_status_option_name, $status ); + update_option( "{$this->pue_key_status_option_name}_timeout", $this->check_period * HOUR_IN_SECONDS ); + + // We set a transient in addition to an option for compatibility reasons. + // @todo remove transient in a major feature release where we release all plugins. + set_transient( $this->pue_key_status_transient_name, $status, $this->check_period * HOUR_IN_SECONDS ); + } + + /** + * Sets the key status transient based on the key validation check results. + * + * @since 4.14.9 + * + * @param int $valid 0 for invalid, 1 or 2 for valid. + */ + public function set_key_status_transient( $valid ) { + _deprecated_function( __METHOD__, '4.14.9', __CLASS__ . '::set_key_status()' ); + $this->set_key_status( $valid ); + } + + /** + * Install the hooks required to run periodic update checks and inject update info + * into WP data structures. + * Also other hooks related to the automatic updates (such as checking agains API and what not (@from Darren) + */ + public function hooks() { + // Override requests for plugin information + add_filter( 'plugins_api', [ $this, 'inject_info' ], 10, 3 ); + + // Check for updates when the WP updates are checked and inject our update if needed. + // Only add filter if the TRIBE_DISABLE_PUE constant is not set as true and where + // the context is not 'service' + if ( ( ! defined( 'TRIBE_DISABLE_PUE' ) || true !== TRIBE_DISABLE_PUE ) && 'service' !== $this->context ) { + add_filter( 'pre_set_site_transient_update_plugins', [ $this, 'check_for_updates' ] ); + } + + add_filter( 'tribe_licensable_addons', [ $this, 'build_addon_list' ] ); + add_action( 'tribe_license_fields', [ $this, 'do_license_key_fields' ] ); + add_action( 'tribe_settings_after_content_tab_licenses', [ $this, 'do_license_key_javascript' ] ); + add_action( 'tribe_settings_success_message', [ $this, 'do_license_key_success_message' ], 10, 2 ); + add_action( 'load-plugins.php', [ $this, 'remove_default_inline_update_msg' ], 50 ); + + // Key validation + add_filter( 'tribe_settings_save_field_value', [ $this, 'check_for_api_key_error' ], 10, 3 ); + add_action( 'wp_ajax_pue-validate-key_' . $this->get_slug(), [ $this, 'ajax_validate_key' ] ); + add_filter( 'tribe-pue-install-keys', [ $this, 'return_install_key' ] ); + add_action( 'admin_enqueue_scripts', [ $this, 'maybe_display_json_error_on_plugins_page' ], 1 ); + add_action( 'admin_init', [ $this, 'general_notifications' ] ); + + // Package name + add_filter( 'upgrader_pre_download', [ Tribe__PUE__Package_Handler::instance(), 'filter_upgrader_pre_download' ], 5, 3 ); + } + + /********************** Getter / Setter Functions **********************/ + + /** + * Get the slug + * + * @return string + */ + public function get_slug() { + return apply_filters( 'pue_get_slug', $this->slug ); + } + + /** + * Set the slug + * + * @param string $slug + */ + private function set_slug( $slug = '' ) { + $this->slug = $slug; + $clean_slug = str_replace( '-', '_', $this->slug ); + $this->dismiss_upgrade = 'pu_dismissed_upgrade_' . $clean_slug; + $this->pue_install_key = 'pue_install_key_' . $clean_slug; + } + + /** + * Get the PUE update API endpoint url + * + * @return string + */ + public function get_pue_update_url() { + $pue_update_url = 'https://pue.theeventscalendar.com'; + + if ( defined( 'PUE_UPDATE_URL' ) ) { + $pue_update_url = PUE_UPDATE_URL; + } + + $pue_update_url = apply_filters( 'pue_get_update_url', $pue_update_url, $this->get_slug() ); + + $pue_update_url = untrailingslashit( $pue_update_url ); + + return $pue_update_url; + } + + /** + * Get the plugin file path + * + * @return string + */ + public function get_plugin_file() { + return apply_filters( 'pue_get_plugin_file', $this->plugin_file, $this->get_slug() ); + } + + /** + * Set the plugin file path + * + * @param string $plugin_file + */ + private function set_plugin_file( $plugin_file = '' ) { + + if ( ! empty( $plugin_file ) ) { + $this->plugin_file = $plugin_file; + + return; + } + + $slug = $this->get_slug(); + if ( ! empty( $slug ) ) { + $this->plugin_file = $slug . '/' . $slug . '.php'; + } + } + + /** + * Set the plugin name + * + * @param string $plugin_name + */ + private function set_plugin_name( $plugin_name = '' ) { + if ( ! empty( $plugin_name ) ) { + $this->plugin_name = $plugin_name; + } else { + //get name from plugin file itself + if ( ! function_exists( 'get_plugins' ) ) { + require_once( ABSPATH . 'wp-admin/includes/plugin.php' ); + } + + // Prevents get_plugins from throwing a weird notice + if ( ! file_exists( WP_PLUGIN_DIR . '/' . $this->get_plugin_file() ) ) { + return; + } + + $plugin_details = explode( '/', $this->get_plugin_file() ); + $plugin_folder = get_plugins( '/' . $plugin_details[0] ); + $this->plugin_name = isset( $plugin_details[1] ) && isset( $plugin_folder[ $plugin_details[1] ] ) ? $plugin_folder[ $plugin_details[1] ]['Name'] : null; + } + } + + /** + * Get the plugin name + * + * @return string + */ + public function get_plugin_name() { + if ( empty( $this->plugin_name ) ) { + $this->set_plugin_name(); + } + + return apply_filters( 'pue_get_plugin_name', $this->plugin_name, $this->get_slug() ); + } + + /** + * Set all the PUE instantiation options + * + * @param array $options + */ + private function set_options( $options = [] ) { + + $options = wp_parse_args( + $options, [ + 'pue_option_name' => 'external_updates-' . $this->get_slug(), + 'apikey' => '', + 'check_period' => 12, + 'context' => 'component', + 'plugin_name' => '', + ] + ); + + $this->pue_option_name = $options['pue_option_name']; + $this->check_period = (int) $options['check_period']; + $this->context = $options['context']; + $this->plugin_name = $options['plugin_name']; + + } + + /** + * Set all the download query array + * + * @param array $download_query + */ + private function set_download_query( $download_query = [] ) { + + if ( ! empty( $download_query ) ) { + $this->download_query = $download_query; + + return; + } + + // plugin slug + $this->download_query['plugin'] = sanitize_text_field( $this->get_slug() ); + + // include current version + $this->download_query['installed_version'] = sanitize_text_field( $this->get_installed_version() ); + + $this->download_query['domain'] = sanitize_text_field( $this->get_domain() ); + + // get general stats + $stats = $this->get_stats(); + + $this->download_query['multisite'] = $stats['network']['multisite']; + $this->download_query['network_activated'] = $stats['network']['network_activated']; + $this->download_query['active_sites'] = $stats['network']['active_sites']; + $this->download_query['wp_version'] = $stats['versions']['wp']; + + // the following is for install key inclusion (will apply later with PUE addons.) + $this->download_query['key'] = sanitize_text_field( $this->get_key() ); + $this->download_query['dk'] = sanitize_text_field( $this->get_key( 'default' ) ); + $this->download_query['o'] = sanitize_text_field( $this->get_key( 'any', 'origin' ) ); + + } + + /** + * Get the download_query args + * + * @return array + */ + public function get_download_query() { + if ( empty( $this->download_query ) ) { + $this->set_download_query(); + } + + return apply_filters( 'pue_get_download_query', $this->download_query, $this->get_slug() ); + } + + /** + * Set all the validate query array + * + * @param array $validate_query + */ + private function set_validate_query( $validate_query = [] ) { + + if ( ! empty( $validate_query ) ) { + $this->validate_query = $validate_query; + + return; + } + + // the following is for install key inclusion (will apply later with PUE addons.) + $this->validate_query['key'] = sanitize_text_field( $this->get_key() ); + + // include default key + $this->validate_query['default_key'] = sanitize_text_field( $this->get_key( 'default' ) ); + + // include license origin + $this->validate_query['license_origin'] = sanitize_text_field( $this->get_key( 'any', 'origin' ) ); + + // plugin slug + $this->validate_query['plugin'] = sanitize_text_field( $this->get_slug() ); + + // include current version + $this->validate_query['version'] = sanitize_text_field( $this->get_installed_version() ); + + // include current domain + $this->validate_query['domain'] = sanitize_text_field( $this->get_domain() ); + + // include plugin stats + $this->validate_query['stats'] = $this->get_stats(); + + } + + /** + * Get the validate_query args + * + * @return array + */ + public function get_validate_query() { + if ( empty( $this->validate_query ) ) { + $this->set_validate_query(); + } + + return apply_filters( 'pue_get_validate_query', $this->validate_query, $this->get_slug() ); + } + + /** + * Get current domain + * + * @return string + */ + public function get_domain() { + + $domain = self::$domain; + + if ( empty( $domain ) ) { + $url = wp_parse_url( get_option( 'siteurl' ) ); + if ( ! empty( $url ) && isset( $url['host'] ) ) { + $domain = $url['host']; + } elseif ( isset( $_SERVER['SERVER_NAME'] ) ) { + $domain = $_SERVER['SERVER_NAME']; + } + + if ( is_multisite() ) { + // For multisite, return the network-level siteurl + $domain = $this->get_network_domain(); + } + + self::$domain = $domain; + } + + return $domain; + + } + + + /********************** General Functions **********************/ + + /** + * Compile a list of addons + * + * @param array $addons list of addons + * + * @return array list of addons + */ + public function build_addon_list( $addons = [] ) { + $addons[] = $this->get_plugin_name(); + + return $addons; + } + + /** + * Inserts license key fields on license key page + * + * @param array $fields List of fields + * + * @return array Modified list of fields. + */ + public function do_license_key_fields( $fields ) { + // common fields whether licenses should be hidden or not + $to_insert = [ + $this->pue_install_key . '-heading' => [ + 'type' => 'heading', + 'label' => $this->get_plugin_name(), + ], + ]; + + $no_license_tooltip = esc_html__( 'A valid license key is required for support and updates', 'tribe-common' ); + if ( 'event-aggregator' === $this->get_slug() ) { + $no_license_tooltip = sprintf( + esc_html__( '%1$sBuy a license%2$s for the Event Aggregator service to access additional import features.', 'tribe-common' ), + '', + '' + ); + } + + // we want to inject the following license settings at the end of the licenses tab + if ( $this->should_show_network_editable_license() ) { + $to_insert[ $this->pue_install_key ] = [ + 'type' => 'license_key', + 'size' => 'large', + 'validation_type' => 'license_key', + 'label' => sprintf( esc_attr__( 'License Key', 'tribe-common' ) ), + 'default' => $this->get_key( 'default' ), + 'tooltip' => $no_license_tooltip, + 'parent_option' => false, + 'network_option' => true, + ]; + } elseif ( $this->should_show_subsite_editable_license() ) { + $to_insert[ $this->pue_install_key ] = [ + 'type' => 'license_key', + 'size' => 'large', + 'validation_type' => 'license_key', + 'label' => sprintf( esc_attr__( 'License Key', 'tribe-common' ) ), + 'default' => $this->get_key( 'default' ), + 'tooltip' => $no_license_tooltip, + 'parent_option' => false, + 'network_option' => false, + ]; + } elseif ( $this->should_show_overrideable_license() ) { + $to_insert[ $this->pue_install_key . '-state' ] = [ + 'type' => 'html', + 'label' => sprintf( esc_attr__( 'License Key Status:', 'tribe-common' ) ), + 'label_attributes' => [ 'style' => 'width:auto;' ], + 'html' => sprintf( '

    %s

    ', $this->get_network_license_state_string() ), + ]; + + $override_id = $this->pue_install_key . '-override'; + + $to_insert[ $override_id ] = [ + 'type' => 'checkbox_bool', + 'label' => esc_html__( 'Override network license key', 'tribe-common' ), + 'tooltip' => esc_html__( 'Check this box if you wish to override the network license key with your own', 'tribe-common' ), + 'default' => false, + 'validation_type' => 'boolean', + 'parent_option' => false, + 'attributes' => [ 'id' => $override_id . '-field' ], + ]; + + $to_insert[ $this->pue_install_key ] = [ + 'type' => 'license_key', + 'size' => 'large', + 'validation_type' => 'license_key', + 'label' => sprintf( esc_attr__( 'Site License Key', 'tribe-common' ) ), + 'tooltip' => $no_license_tooltip, + 'parent_option' => false, + 'network_option' => false, + 'class' => 'tribe-dependent', + 'fieldset_attributes' => [ + 'data-depends' => '#' . $override_id . '-field', + 'data-condition-checked' => true, + ], + ]; + } else { + $to_insert[ $this->pue_install_key . '-state' ] = [ + 'type' => 'html', + 'label' => sprintf( esc_attr__( 'License Key Status:', 'tribe-common' ) ), + 'label_attributes' => [ 'style' => 'width:auto;' ], + 'html' => sprintf( '

    %s

    ', $this->get_network_license_state_string() ), + ]; + } + + $fields = self::array_insert_after_key( 'tribe-form-content-start', $fields, $to_insert ); + + return $fields; + } + + /** + * Inserts the javascript that makes the ajax checking + * work on the license key page + * + */ + public function do_license_key_javascript() { + ?> + +

    ' . esc_html__( 'License key(s) updated.', 'tribe-common' ) . '

    '; + + } + + /** + * Build stats for endpoints + * + * @return array + */ + public function build_stats() { + + global $wpdb; + + $stats = [ + 'versions' => [ + 'wp' => sanitize_text_field( $GLOBALS['wp_version'] ), + ], + 'network' => [ + 'multisite' => 0, + 'network_activated' => 0, + 'active_sites' => 1, + ], + ]; + + if ( is_multisite() ) { + $sql_count = " + SELECT COUNT( `blog_id` ) + FROM `{$wpdb->blogs}` + WHERE + `public` = '1' + AND `archived` = '0' + AND `spam` = '0' + AND `deleted` = '0' + "; + + $stats['network']['multisite'] = 1; + $stats['network']['network_activated'] = (int) $this->is_plugin_active_for_network(); + $stats['network']['active_sites'] = (int) $wpdb->get_var( $sql_count ); + } + + self::$stats = $stats; + + return $stats; + + } + + /** + * Build full stats for endpoints + * + * @param array $stats Initial stats + * + * @return array + */ + public function build_full_stats( $stats ) { + + global $wpdb; + + $theme = wp_get_theme(); + + $current_offset = (int) get_option( 'gmt_offset', 0 ); + $tzstring = get_option( 'timezone_string' ); + + // Remove old Etc mappings. Fallback to gmt_offset. + if ( false !== strpos( $tzstring, 'Etc/GMT' ) ) { + $timezone = ''; + } + + // Create a UTC+- zone if no timezone string exists + if ( empty( $tzstring ) ) { + if ( 0 === $current_offset ) { + $timezone = 'UTC+0'; + } elseif ( $current_offset < 0 ) { + $timezone = 'UTC' . $current_offset; + } else { + $timezone = 'UTC+' . $current_offset; + } + } + + $stats['versions'] = [ + 'wp' => sanitize_text_field( $GLOBALS['wp_version'] ), + 'php' => sanitize_text_field( phpversion() ), + 'mysql' => sanitize_text_field( $wpdb->db_version() ), + ]; + + $stats['theme'] = [ + 'name' => sanitize_text_field( $theme->get( 'Name' ) ), + 'version' => sanitize_text_field( $theme->get( 'Version' ) ), + 'stylesheet' => sanitize_text_field( $theme->get_stylesheet() ), + 'template' => sanitize_text_field( $theme->get_template() ), + ]; + + $stats['site_language'] = sanitize_text_field( get_locale() ); + $stats['user_language'] = sanitize_text_field( get_user_locale() ); + $stats['is_public'] = (int) get_option( 'blog_public', 0 ); + $stats['wp_debug'] = (int) ( defined( 'WP_DEBUG' ) && WP_DEBUG ); + $stats['site_timezone'] = sanitize_text_field( $timezone ); + + $stats['totals'] = [ + 'all_post_types' => (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$wpdb->posts}`" ), + 'events' => (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM `{$wpdb->posts}` WHERE post_type = %s", 'tribe_events' ) ), + 'venues' => (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM `{$wpdb->posts}` WHERE post_type = %s", 'tribe_venue' ) ), + 'organizers' => (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM `{$wpdb->posts}` WHERE post_type = %s", 'tribe_organizer' ) ), + 'event_categories' => (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM `{$wpdb->term_taxonomy}` WHERE taxonomy = %s", 'tribe_events_cat' ) ), + ]; + + self::$stats_full = $stats; + + return $stats; + + } + + /** + * Build and get the stats + * + * @return array + */ + public function get_stats() { + + $stats = self::$stats; + + if ( empty( $stats ) ) { + $stats = $this->build_stats(); + } + + /** + * Allow full stats data to be built and sent. + * + * @param boolean $use_full_stats Whether to send full stats + * + * @since 4.5.1 + */ + $use_full_stats = apply_filters( 'pue_use_full_stats', false ); + + if ( $use_full_stats ) { + $stats_full = self::$stats_full; + + if ( empty( $stats_full ) ) { + $stats = $this->build_full_stats( $stats ); + } + } + + /** + * Filter stats and allow plugins to add their own stats + * for tracking specific points of data. + * + * @param array $stats Stats gathered by PUE Checker class + * @param boolean $use_full_stats Whether to send full stats + * @param \Tribe__PUE__Checker $checker PUE Checker class object + * + * @since 4.5.1 + */ + $stats = apply_filters( 'pue_stats', $stats, $use_full_stats, $this ); + + return $stats; + } + + /** + * Get current license key, optionally of a specific type. + * + * @param string $type The type of key to get (any, network, local, default) + * @param string $return_type The type of data to return (key, origin) + * + * @return string + */ + public function get_key( $type = 'any', $return_type = 'key' ) { + + $license_key = ''; + $license_origin = 'm'; + + /* + * Even if we have a network key if the plugin is not active on the network then it should + * not be used. + */ + if ( + ( 'network' === $type || 'any' === $type ) + && is_multisite() + && $this->is_plugin_active_for_network() + ) { + $license_key = get_network_option( null, $this->pue_install_key, '' ); + } + + if ( ( 'local' === $type || 'any' === $type ) && empty( $license_key ) ) { + $license_key = get_option( $this->pue_install_key, '' ); + } + + if ( empty( $license_key ) && ( 'default' === $type || 'any' === $type ) ) { + $autoloader = Tribe__Autoloader::instance(); + + $class_name = $autoloader->get_prefix_by_slug( $this->get_slug() ); + + if ( $class_name ) { + $is_namespaced = false !== strpos( $class_name, '\\' ); + + if ( $is_namespaced ) { + // Handle class prefixes like Tribe\Plugin\. + $class_name .= 'PUE\Helper'; + } else { + // Handle class prefixes like Tribe__Plugin__. + $class_name .= 'PUE__Helper'; + } + + if ( constant( $class_name . '::DATA' ) ) { + $license_key = constant( $class_name . '::DATA' ); + + $license_origin = 'e'; + } + } + } + + if ( 'origin' === $return_type ) { + if ( 'm' === $license_origin ) { + $default_key = $this->get_key( 'default' ); + + if ( $license_key !== $default_key ) { + $license_origin = 'o'; + } + } + + return $license_origin; + } + + return $license_key; + + } + + /** + * Update license key for specific type of license. + * + * @param string $license_key The new license key value + * @param string $type The type of key to update (network or local) + */ + public function update_key( $license_key, $type = 'local' ) { + + if ( 'network' === $type && is_multisite() ) { + update_network_option( null, $this->pue_install_key, sanitize_text_field( $license_key ) ); + } elseif ( 'local' === $type ) { + update_option( $this->pue_install_key, sanitize_text_field( $license_key ) ); + } + + } + + /** + * Checks for the license key status with MT servers. + * + * @param string $key + * @param bool $network Whether the key to check for is a network one or not. + * + * @return array An associative array containing the license status response. + */ + public function validate_key( $key, $network = false ) { + $response = []; + $response['status'] = 0; + + if ( ! $key ) { + $response['message'] = sprintf( esc_html__( 'Hmmm... something\'s wrong with this validator. Please contact %ssupport%s.', 'tribe-common' ), '', '' ); + return $response; + } + + $query_args = $this->get_validate_query(); + + $query_args['key'] = sanitize_text_field( $key ); + + // This method is primarily used during when validating keys by ajax, before they are + // formally committed or saved by the user: for that reason we call request_info() + // rather than license_key_status() as at this stage invalid or missing keys should + // not result in admin notices being generated + $plugin_info = $this->request_info( $query_args ); + $expiration = isset( $plugin_info->expiration ) ? $plugin_info->expiration : __( 'unknown date', 'tribe-common' ); + + $pue_notices = Tribe__Main::instance()->pue_notices(); + $plugin_name = $this->get_plugin_name(); + + if ( empty( $plugin_info ) ) { + $response['message'] = __( 'Sorry, key validation server is not available.', 'tribe-common' ); + } elseif ( isset( $plugin_info->api_expired ) && 1 === (int) $plugin_info->api_expired ) { + $response['message'] = $this->get_license_expired_message(); + $response['api_expired'] = true; + } elseif ( isset( $plugin_info->api_upgrade ) && 1 === (int) $plugin_info->api_upgrade ) { + $response['message'] = $this->get_api_message( $plugin_info ); + $response['api_upgrade'] = true; + } elseif ( isset( $plugin_info->api_invalid ) && 1 === (int) $plugin_info->api_invalid ) { + $response['message'] = $this->get_api_message( $plugin_info ); + $response['api_invalid'] = true; + } else { + $key_type = 'local'; + + if ( $network ) { + $key_type = 'network'; + } + + $current_install_key = $this->get_key( $key_type ); + $replacement_key = $query_args['key']; + + if ( ! empty( $plugin_info->replacement_key ) ) { + // The PUE service might send over a new key upon validation. + $replacement_key = $plugin_info->replacement_key; + } + + if ( $current_install_key && $current_install_key === $replacement_key ) { + $default_success_msg = esc_html( sprintf( __( 'Valid Key! Expires on %s', 'tribe-common' ), $expiration ) ); + } else { + // Set the key. + $this->update_key( $replacement_key, $key_type ); + + $default_success_msg = esc_html( sprintf( __( 'Thanks for setting up a valid key. It will expire on %s', 'tribe-common' ), $expiration ) ); + + // Set system info key on TEC.com after successful validation of license. + $optin_key = get_option( 'tribe_systeminfo_optin' ); + if ( $optin_key ) { + Tribe__Support::send_sysinfo_key( $optin_key, $query_args['domain'], false, true ); + } + } + + $pue_notices->clear_notices( $plugin_name ); + + $response['status'] = isset( $plugin_info->api_message ) ? 2 : 1; + $response['message'] = isset( $plugin_info->api_message ) ? $plugin_info->api_message : $default_success_msg; + $response['expiration'] = esc_html( $expiration ); + + if ( isset( $plugin_info->daily_limit ) ) { + $response['daily_limit'] = intval( $plugin_info->daily_limit ); + } + } + + $response['message'] = wp_kses( $response['message'], 'data' ); + + $this->set_key_status( $response['status'] ); + + return $response; + } + + public function get_license_expired_message() { + return '' . + __( 'Renew Your License Now', 'tribe-common' ) . + '' . + __( ' (opens in a new window)', 'tribe-common' ) . + ''; + } + + /** + * Echo JSON results for key validation + */ + public function ajax_validate_key() { + $key = isset( $_POST['key'] ) ? wp_unslash( $_POST['key'] ) : null; + $nonce = isset( $_POST['_wpnonce'] ) ? wp_unslash( $_POST['_wpnonce'] ) : null; + + if ( + empty( $nonce ) + || false === wp_verify_nonce( $nonce, 'pue-validate-key_' . $this->get_slug() ) + ) { + $response = [ + 'status' => 0, + 'message' => __( 'Please refresh the page and try your request again.', 'tribe-common' ), + ]; + } else { + $response = $this->validate_key( $key ); + } + + echo json_encode( $response ); + exit; + } + + /** + * Processes variable substitutions for server-side API message. + * + * @param Tribe__PUE__Plugin_Info $info + * + * @return string + */ + private function get_api_message( $info ) { + // this default message should never show, but is here as a fallback just in case. + $message = sprintf( + esc_html__( 'There is an update for %s. You\'ll need to %scheck your license%s to have access to updates, downloads, and support.', 'tribe-common' ), + $this->get_plugin_name(), + '', + '' + ); + + if ( ! empty( $info->api_inline_invalid_message ) ) { + $message = wp_kses( $info->api_inline_invalid_message, 'post' ); + } + + $message = str_replace( '%plugin_name%', $this->get_plugin_name(), $message ); + $message = str_replace( '%plugin_slug%', $this->get_slug(), $message ); + $message = str_replace( '%update_url%', $this->get_pue_update_url() . '/', $message ); + $message = str_replace( '%version%', $info->version, $message ); + $message = str_replace( '%changelog%', 'what\'s new', $message ); + + return $message; + } + + /** + * Whether the plugin is network activated and licensed or not. + * + * @return bool + */ + public function is_network_licensed() { + $is_network_licensed = false; + + if ( ! is_network_admin() && $this->is_plugin_active_for_network() ) { + $network_key = $this->get_key( 'network' ); + $local_key = $this->get_key( 'local' ); + + // Check whether the network is licensed and NOT overridden by local license + if ( $network_key && ( empty( $local_key ) || $local_key === $network_key ) ) { + $is_network_licensed = true; + } + } + + return $is_network_licensed; + } + + /** + * Returns tet name of the option that stores the license key. + * + * @return string + */ + public function get_license_option_key() { + return $this->pue_install_key; + } + + private function get_api_update_message() { + $plugin_info = $this->plugin_info; + + if ( ! isset( $plugin_info->api_invalid_message ) ) { + return false; + } + + $message = sprintf( + esc_html__( 'There is an update for %s. %sRenew your license%s to get access to bug fixes, security updates, and new features.', 'tribe-common' ), + $this->get_plugin_name(), + '', + '' + ); + + return $message; + } + + /** + * Displays a PUE message on the page if it is relevant + * + * @param string $page + */ + public function maybe_display_json_error_on_plugins_page( $page ) { + if ( 'plugins.php' !== $page ) { + return; + } + + $state = $this->get_state(); + $messages = []; + $plugin_updates = get_plugin_updates(); + $update_available = isset( $plugin_updates[ $this->plugin_file ] ); + + // Check to see if there is an licensing error or update message we should show + if ( ! empty( $state->update->license_error ) ) { + $messages[] = $state->update->license_error; + } elseif ( $update_available && current_user_can( 'update_plugins' ) ) { + // A plugin update is available + $update_now = sprintf( + esc_html__( 'Update now to version %s.', 'tribe-common' ), + $state->update->version + ); + + $update_now_link = sprintf( + ' %2$s', + wp_nonce_url( self_admin_url( 'update.php?action=upgrade-plugin&plugin=' ) . $this->plugin_file, 'upgrade-plugin_' . $this->plugin_file ), + $update_now + ); + + $update_message = sprintf( + esc_html__( 'There is a new version of %1$s available. %2$s', 'tribe-common' ), + $this->plugin_name, + $update_now_link + ); + + $messages[] = sprintf( + '

    %s

    ', + $update_message + ); + } + + if ( empty( $messages ) ) { + return; + } + + $message_row_html = ''; + + foreach ( $messages as $message ) { + $message_row_html .= sprintf( + '
    %s
    ', + $message + ); + } + + $message_row_html = sprintf( + '%s', + $message_row_html + ); + + $this->plugin_notice = [ + 'slug' => $this->plugin_file, + 'message_row_html' => $message_row_html, + ]; + + add_filter( 'tribe_plugin_notices', [ $this, 'add_notice_to_plugin_notices' ] ); + + } + + public function add_notice_to_plugin_notices( $notices ) { + if ( ! $this->plugin_notice || $this->is_network_licensed() ) { + return $notices; + } + + $notices[ $this->plugin_notice['slug'] ] = $this->plugin_notice; + + return $notices; + } + + /** + * Returns plugin/license key data based on the provided query arguments. + * + * Calling this method will also take care of setting up admin notices for any + * keys that are invalid or have expired, etc. + * + * @see Tribe__PUE__Checker::request_info() + * + * @param $query_args + * + * @return Tribe__PUE__Plugin_Info|null + */ + public function license_key_status( $query_args ) { + $pue_notices = Tribe__Main::instance()->pue_notices(); + $plugin_info = $this->request_info( $query_args ); + $plugin_name = empty( $this->plugin_name ) ? $this->get_plugin_name() : $this->plugin_name; + + if ( empty( $plugin_name ) ) { + return $plugin_info; + } + + $install_key = $this->get_key(); + + // Check for expired keys + if ( ! empty( $plugin_info->api_expired ) ) { + $pue_notices->add_notice( Tribe__PUE__Notices::EXPIRED_KEY, $plugin_name ); + } elseif ( ! empty( $plugin_info->api_upgrade ) ) { + // Check for keys that are out of installs (*must* happen before the api_invalid test) + $pue_notices->add_notice( Tribe__PUE__Notices::UPGRADE_KEY, $plugin_name ); + } elseif ( + // Check for invalid keys last of all (upgrades/empty keys will be flagged as invalid) + ! empty( $plugin_info->api_invalid ) + && ( + 'component' === $this->context + || ( + 'service' === $this->context + && $install_key + ) + ) + ) { + $pue_notices->add_notice( Tribe__PUE__Notices::INVALID_KEY, $plugin_name ); + } else { + // If none of the above were satisfied we can assume the key is valid + $pue_notices->clear_notices( $plugin_name ); + } + + return $plugin_info; + } + + /** + * Sets up and manages those license key notifications which don't depend on communicating with a remote + * PUE server, etc. + */ + public function general_notifications() { + $plugin_name = empty( $this->plugin_name ) ? $this->get_plugin_name() : $this->plugin_name; + + // Register our plugin name for use in messages (thus if we're deactivated, any previously + // added persistent messaging can be cleared) + Tribe__Main::instance()->pue_notices()->register_name( $plugin_name ); + + // Detect and setup notices for missing keys + $install_key = $this->get_key(); + + if ( empty( $install_key ) && 'service' !== $this->context ) { + Tribe__Main::instance()->pue_notices()->add_notice( Tribe__PUE__Notices::INVALID_KEY, $plugin_name ); + } + } + + /** + * Retrieve plugin info from the configured API endpoint. + * + * In general, this method should not be called directly and it is preferable to call + * the license_key_status() method instead. That method returns the same result, but + * also analyses each response to set up appropriate license key notifications in the + * admin environment. + * + * @uses wp_remote_get() + * @see Tribe__PUE__Checker::license_key_status() + * + * @param array $query_args Additional query arguments to append to the request. Optional. + * + * @return string $plugin_info + */ + public function request_info( $query_args = [] ) { + $query_args = apply_filters( 'tribe_puc_request_info_query_args-' . $this->get_slug(), $query_args ); + + // Cache the API call so it only needs to be made once per plugin per page load. + static $plugin_info_cache; + + // Sort parameter keys + $hash_data = $query_args; + + ksort( $hash_data ); + + // Flatten hashed data + $hash_data = json_encode( $hash_data ); + + // Generate unique hash + $key = hash( 'sha256', $hash_data ); + + if ( isset( $plugin_info_cache[ $key ] ) ) { + return $plugin_info_cache[ $key ]; + } + + //Various options for the wp_remote_get() call. Plugins can filter these, too. + $options = [ + 'body' => $query_args, + 'timeout' => 15, //seconds + 'headers' => [ + 'Accept' => 'application/json', + ], + ]; + $options = apply_filters( 'tribe_puc_request_info_options-' . $this->get_slug(), $options ); + + $url = sprintf( '%s/api/plugins/v2/license/validate', $this->get_pue_update_url() ); + + $result = wp_remote_post( + $url, + $options + ); + + // Try to parse the response + $plugin_info = null; + if ( ! is_wp_error( $result ) && isset( $result['response']['code'] ) && ( 200 === (int) $result['response']['code'] ) && ! empty( $result['body'] ) ) { + $plugin_info = Tribe__PUE__Plugin_Info::from_json( $result['body'] ); + } + $plugin_info = apply_filters( 'tribe_puc_request_info_result-' . $this->get_slug(), $plugin_info, $result ); + + $plugin_info_cache[ $key ] = $plugin_info; + + return $plugin_info; + } + + /** + * Returns the domain contained in the network's siteurl option (not the full URL). + * + * @return string + */ + public function get_network_domain() { + $site_url = wp_parse_url( get_site_option( 'siteurl' ) ); + if ( ! $site_url || ! isset( $site_url['host'] ) ) { + return ''; + } else { + return strtolower( $site_url['host'] ); + } + } + + /** + * Retrieve the latest update (if any) from the configured API endpoint. + * + * @uses Tribe__PUE__Checker::request_info() + * + * @return Tribe__PUE__Utility An instance of Tribe__PUE__Utility, or NULL when no updates are available. + */ + public function request_update() { + // For the sake of simplicity, this function just calls request_info() + // and transforms the result accordingly. + $query_args = $this->get_validate_query(); + + if ( ! empty( $_POST['key'] ) ) { + $query_args['key'] = sanitize_text_field( $_POST['key'] ); + } elseif ( ! empty( $_POST[ $this->pue_install_key ] ) ) { + $query_args['key'] = sanitize_text_field( $_POST[ $this->pue_install_key ] ); + } + + $this->plugin_info = $plugin_info = $this->license_key_status( $query_args ); + + if ( null === $plugin_info ) { + return null; + } + + // admin display for if the update check reveals that there is a new version but the API key isn't valid. + if ( isset( $plugin_info->api_invalid ) ) { + $plugin_info = Tribe__PUE__Utility::from_plugin_info( $plugin_info ); + $plugin_info->license_error = $this->get_api_message( $plugin_info ); + + return $plugin_info; + } + + if ( ! empty( $plugin_info->new_install_key ) ) { + $this->update_key( $plugin_info->new_install_key ); + } + + // Need to correct the download url so it contains the custom user data (e.g. api and any other parameters). + $download_query = $this->get_download_query(); + + if ( ! empty( $download_query ) ) { + $plugin_info->download_url = esc_url_raw( add_query_arg( $download_query, $plugin_info->download_url ) ); + } + + // Add plugin dirname/file (this will be expected by WordPress when it builds the plugin list table) + $plugin_info->plugin = $this->get_plugin_file(); + + return Tribe__PUE__Utility::from_plugin_info( $plugin_info ); + } + + /** + * Display a changelog when the api key is missing. + */ + public function display_changelog() { + //contents of changelog display page when api-key is invalid or missing. It will ONLY show the changelog (hook into existing thickbox?) + } + + /** + * Get the currently installed version of the plugin. + * + * @return string Version number. + */ + public function get_installed_version() { + if ( function_exists( 'get_plugins' ) ) { + $all_plugins = get_plugins(); + if ( array_key_exists( $this->get_plugin_file(), $all_plugins ) && array_key_exists( 'Version', $all_plugins[ $this->get_plugin_file() ] ) ) { + return $all_plugins[ $this->get_plugin_file() ]['Version']; + } + } + } + + /** + * Get plugin update state + * + * @param boolean $force_recheck + * + * @return object + */ + public function get_state( $force_recheck = false ) { + + $state = null; + + if ( ! $force_recheck ) { + $state = get_site_option( $this->pue_option_name, false, false ); + } + + if ( empty( $state ) ) { + $state = new stdClass; + $state->lastCheck = 0; + $state->checkedVersion = ''; + $state->update = null; + } + + return $state; + + } + + /** + * Update plugin update state + * + * @param object $value + */ + public function update_state( $value ) { + + update_site_option( $this->pue_option_name, $value ); + + } + + /** + * Check for plugin updates. + * + * The results are stored in the DB option specified in $pue_option_name. + * + * @param array $updates + * @param boolean $force_recheck + * + * @return array + */ + public function check_for_updates( $updates = [], $force_recheck = false ) { + $state = $this->get_state( $force_recheck ); + + $state->lastCheck = time(); + $state->checkedVersion = $this->get_installed_version(); + + // Save before checking in case something goes wrong + $this->update_state( $state ); + + $state->update = $this->request_update(); + + // If a null update was returned, skip to the end of the function. + if ( null !== $state->update ) { + // Is there an update to insert? + if ( version_compare( $state->update->version, $this->get_installed_version(), '>' ) ) { + if ( empty( $updates ) ) { + $updates = (object) [ 'response' => [] ]; + } + + $updates->response[ $this->get_plugin_file() ] = $state->update->to_wp_format(); + + // If the key has expired we should register an appropriate admin notice + if ( $this->plugin_info->api_expired ) { + Tribe__Main::instance()->pue_notices()->add_notice( Tribe__PUE__Notices::EXPIRED_KEY, $this->plugin_name ); + } + } + } + + $this->update_state( $state ); + + return $updates; + } + + /** + * Clears out the site external site option and re-checks the license key + * + * @param string $value + * @param string $field_id + * @param string $validated_field + * + * @return string + */ + public function check_for_api_key_error( $value, $field_id, $validated_field ) { + + // Only hook into our option + if ( $this->pue_install_key !== $field_id ) { + return $value; + } + + if ( 'service' !== $this->context ) { + $this->check_for_updates( [], true ); + } + + $network_option = false; + + if ( ! empty( $validated_field->field['network_option'] ) ) { + $network_option = (boolean) $validated_field->field['network_option']; + } + + $key_type = 'local'; + + if ( $network_option ) { + $key_type = 'network'; + } + + $current_key = $this->get_key( $key_type ); + + // if we are saving this PUE key, we need to make sure we update the license key notices + // appropriately. Otherwise, we could have an invalid license key in place but the notices + // aren't being thrown globally + + $query_args = $this->get_validate_query(); + + $query_args['key'] = sanitize_text_field( $value ); + + $this->license_key_status( $query_args ); + + return $value; + + } + + /** + * Intercept plugins_api() calls that request information about our plugin and + * use the configured API endpoint to satisfy them. + * + * @see plugins_api() + * + * @param mixed $result + * @param string $action + * @param array|object $args + * + * @return mixed + */ + public function inject_info( $result, $action = null, $args = null ) { + $relevant = ( 'plugin_information' === $action ) && isset( $args->slug ) && ( $args->slug === $this->slug ); + if ( ! $relevant ) { + return $result; + } + + $query_args = $this->get_validate_query(); + + $plugin_info = $this->license_key_status( $query_args ); + + if ( $plugin_info ) { + return $plugin_info->to_wp_format(); + } + + return $result; + } + + /** + * Register a callback for filtering query arguments. + * + * The callback function should take one argument - an associative array of query arguments. + * It should return a modified array of query arguments. + * + * @uses add_filter() This method is a convenience wrapper for add_filter(). + * + * @param callback $callback + * + */ + public function add_query_arg_filter( $callback ) { + add_filter( 'tribe_puc_request_info_query_args-' . $this->get_slug(), $callback ); + } + + /** + * Register a callback for filtering arguments passed to wp_remote_get(). + * + * The callback function should take one argument - an associative array of arguments - + * and return a modified array or arguments. See the WP documentation on wp_remote_get() + * for details on what arguments are available and how they work. + * + * @uses add_filter() This method is a convenience wrapper for add_filter(). + * + * @param callback $callback + * + */ + public function add_http_request_arg_filter( $callback ) { + add_filter( 'tribe_puc_request_info_options-' . $this->get_slug(), $callback ); + } + + /** + * Register a callback for filtering the plugin info retrieved from the external API. + * + * The callback function should take two arguments. If the plugin info was retrieved + * successfully, the first argument passed will be an instance of Tribe__PUE__Plugin_Info. Otherwise, + * it will be NULL. The second argument will be the corresponding return value of + * wp_remote_get (see WP docs for details). + * + * The callback function should return a new or modified instance of Tribe__PUE__Plugin_Info or NULL. + * + * @uses add_filter() This method is a convenience wrapper for add_filter(). + * + * @param callback $callback + * + */ + public function add_result_filter( $callback ) { + add_filter( 'tribe_puc_request_info_result-' . $this->get_slug(), $callback, 10, 2 ); + } + + /** + * Insert an array after a specified key within another array. + * + * @param $key + * @param $source_array + * @param $insert_array + * + * @return array + * + */ + public static function array_insert_after_key( $key, $source_array, $insert_array ) { + if ( array_key_exists( $key, $source_array ) ) { + $position = array_search( $key, array_keys( $source_array ), true ) + 1; + $source_array = array_slice( $source_array, 0, $position, true ) + $insert_array + array_slice( $source_array, $position, null, true ); + } else { + // If no key is found, then add it to the end of the array. + $source_array += $insert_array; + } + + return $source_array; + } + + /** + * Add this plugin key to the list of keys + * + * @param array $keys + * + * @return array $keys + * + */ + public function return_install_key( $keys = [] ) { + $key = $this->get_key(); + + if ( ! empty( $key ) ) { + $keys[ $this->get_slug() ] = $key; + } + + return $keys; + } + + /** + * Prevent the default inline update-available messages from appearing, as we + * have implemented our own. + * + * @see resources/js/pue-notices.js + */ + public function remove_default_inline_update_msg() { + remove_action( "after_plugin_row_{$this->plugin_file}", 'wp_plugin_update_row' ); + } + + /** + * Returns the domain of the single site installation + * + * Will try to read it from the $_SERVER['SERVER_NAME'] variable + * and fall back on the one contained in the siteurl option. + * + * @return string + */ + protected function get_site_domain() { + if ( isset( $_SERVER['SERVER_NAME'] ) ) { + return $_SERVER['SERVER_NAME']; + } + $site_url = wp_parse_url( get_option( 'siteurl' ) ); + if ( ! $site_url || ! isset( $site_url['host'] ) ) { + return ''; + } else { + return strtolower( $site_url['host'] ); + } + } + + /** + * Check whether the current plugin is active for the network or not. + * + * @return boolean Whether the plugin is network activated + */ + protected function is_plugin_active_for_network() { + + if ( ! is_multisite() ) { + return false; + } + + $map = [ + 'event-aggregator/event-aggregator.php' => 'the-events-calendar/the-events-calendar.php', + ]; + + $plugin_file = $this->get_plugin_file(); + + if ( isset( $map[ $this->plugin_file ] ) ) { + $plugin_file = $map[ $this->plugin_file ]; + } + + if ( function_exists( 'is_plugin_active_for_network' ) ) { + // If is_plugin_active_for_network() is available, let's use it! + return is_plugin_active_for_network( $plugin_file ); + } else { + // When this method is called sufficiently early in the request, + // is_plugin_active_for_network() may not be available (#115826) + $plugins = get_site_option( 'active_sitewide_plugins' ); + return isset( $plugins[ $plugin_file ] ); + } + } + + /** + * Returns the localized string for a plugin or component license state. + * + * @return string The localized state string. + */ + protected function get_network_license_state_string() { + $states = [ + 'licensed' => esc_html__( 'A valid license has been entered by your network administrator.', 'tribe-common' ), + 'not-licensed' => esc_html__( 'No license entered. Consult your network administrator.', 'tribe-common' ), + 'expired' => esc_html__( 'Expired license. Consult your network administrator.', 'tribe-common' ), + ]; + + $response = $this->validate_key( $this->get_key( 'network' ), true ); + + if ( isset( $response['status'] ) && 1 === (int) $response['status'] ) { + $state = 'licensed'; + } elseif ( isset( $response['api_expired'] ) && true === (boolean) $response['api_expired'] ) { + $state = 'expired'; + } else { + $state = 'not-licensed'; + } + + return $states[ $state ]; + } + + /** + * Whether the user should be shown the fully editable subsite license field or not. + * + * This check will happen in the context of the plugin administration area; checks on the user + * capability to edit the plugin settings have been made before. + * + * @return bool + */ + public function should_show_subsite_editable_license() { + if ( ! is_multisite() ) { + return true; + } + + if ( is_network_admin() ) { + return false; + } + + if ( $this->is_plugin_active_for_network() && ! is_super_admin() ) { + return false; + } + + return true; + } + + /** + * Whether the user should be shown the override control to override the network license key or not. + * + * This check will happen in the context of the plugin administration area; checks on the user + * capability to edit the plugin settings have been made before. + * + * @return bool + */ + public function should_show_overrideable_license() { + if ( is_network_admin() ) { + return false; + } + + if ( is_super_admin() ) { + return false; + } + + if ( ! $this->is_plugin_active_for_network() ) { + return false; + } + + return true; + } + + /** + * Whether the user should be shown the fully editable network license field or not. + * + * This check will happen in the context of the network plugin administration area; checks on the user + * capability to edit the network plugin settings have been made before. + * + * @return bool + */ + public function should_show_network_editable_license() { + return is_network_admin() && is_super_admin(); + } + + /** + * Determines if the value on the DB is the correct format. + * + * @since 4.15.0 + * + * @return bool + */ + public function is_valid_key_format() { + $license_opt = (string) get_option( $this->get_license_option_key() ); + if ( empty( $license_opt ) ) { + return false; + } + + if ( ! preg_match( "/([0-9a-z]+)/i", $license_opt, $matches ) ) { + return false; + } + + // Pull the matching string into a variable + $license = $matches[1]; + + if ( 40 !== strlen( $license ) ) { + return false; + } + + return true; + } + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/PUE/Notices.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/PUE/Notices.php new file mode 100644 index 000000000..4f9c8702d --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/PUE/Notices.php @@ -0,0 +1,533 @@ + 'Event Tickets Plus', + 'pue_install_key_events_community' => 'The Events Calendar: Community Events', + 'pue_install_key_events_community_tickets' => 'The Events Calendar: Community Events Tickets', + 'pue_install_key_image_widget_plus' => 'Image Widget Plus', + 'pue_install_key_tribe_eventbrite' => 'The Events Calendar: Eventbrite Tickets', + 'pue_install_key_tribe_filterbar' => 'The Events Calendar: Filter Bar', + 'pue_install_key_event_aggregator' => 'Event Aggregator', + 'pue_install_key_events_calendar_pro' => 'The Events Calendar PRO', + ]; + + /** + * Sets up license key related admin notices. + */ + public function __construct() { + $this->populate(); + add_action( 'current_screen', [ $this, 'setup_notices' ] ); + add_action( 'tribe_pue_notices_save_notices', [ $this, 'maybe_undismiss_notices' ] ); + } + + /** + * Registers a plugin name that should be used in license key notifications. + * + * If, on a given request, the name is not registered then the plugin name will not + * feature in any notifications. The benefit is that if a plugin is suddenly removed, + * it's name can be automatically dropped from any pre-registered persistent + * notifications. + * + * @param string $plugin_name + */ + public function register_name( $plugin_name ) { + $this->registered[] = $plugin_name; + } + + /** + * Restores plugins added on previous requests to the relevant notification + * groups. + */ + protected function populate() { + $this->saved_notices = (array) get_option( self::STORE_KEY, [] ); + + if ( empty( $this->saved_notices ) ) { + return; + } + + $this->notices = array_merge_recursive( $this->notices, $this->saved_notices ); + + // Cleanup + foreach ( $this->notices as $key => &$plugin_lists ) { + // Purge any elements that are not arrays + if ( ! is_array( $plugin_lists ) ) { + unset( $this->notices[ $key ] ); + continue; + } + } + } + + /** + * Saves any license key notices already added. + */ + public function save_notices() { + update_option( self::STORE_KEY, $this->notices ); + + /** + * Fires after PUE license key notices have been saved. + * + * @param array $current_notices + * @param array $previously_saved_notices + */ + do_action( 'tribe_pue_notices_save_notices', $this->notices, $this->saved_notices ); + } + + /** + * Undismisses license key notifications where appropriate. + * + * The idea is that if an invalid key is detected for one or more plugins, we show a notification + * until a user dismisses it. That user will not then see the notification again unless or until + * an additional plugin name is added to the invalid key list. + * + * Example: + * + * - Notification listing "Eventbrite" and "Pro" keys as invalid shows + * - User X dismisses the notification + * - The "Pro" license is fixed/corrected - notification remains in a "dismissed" status for User X + * - "Filter Bar" is added to the list of invalid keys + * - The invalid key notification is undismissed, to make all users (including User X) aware of + * the problem re Filter Bar + */ + public function maybe_undismiss_notices() { + foreach ( $this->notices as $notice_type => $plugin_list ) { + if ( is_array( $this->saved_notices ) && ! empty( $this->saved_notices[ $notice_type ] ) ) { + $new_plugins = array_diff_key( $this->notices[ $notice_type ], $this->saved_notices[ $notice_type ] ); + } else { + $new_plugins = $this->notices[ $notice_type ]; + } + + if ( ! empty( $new_plugins ) ) { + Tribe__Admin__Notices::instance()->undismiss_for_all( 'pue_key-' . $notice_type ); + } + } + } + + /** + * Used to include a plugin in a notification. + * + * For example, this could be used to add "My Plugin" to the expired license key + * notification by passing Tribe__PUE__Notices::EXPIRED_KEY as the second param. + * + * Plugins can only be added to one notification group at a time, so if a plugin + * was already added to the MISSING_KEY group and is subsequently added to the + * INVALID_KEY group, the previous entry (under MISSING_KEY) will be cleared. + * + * @param string $notice_type + * @param string $plugin_name + */ + public function add_notice( $notice_type, $plugin_name ) { + $this->clear_notices( $plugin_name, true ); + $this->notices[ $notice_type ][ $plugin_name ] = true; + $this->save_notices(); + } + + /** + * Returns whether or not a given plugin name has a specific notice + * + * @param string $plugin_name + * @param string|null $notice_type + * + * @return boolean + */ + public function has_notice( $plugin_name, $notice_type = null ) { + // If we match a pue key we use that value + if ( isset( $this->plugin_names[ $plugin_name ] ) ) { + $plugin_name = $this->plugin_names[ $plugin_name ]; + } + + if ( $notice_type ) { + return ! empty( $this->notices[ $notice_type ][ $plugin_name ] ); + } + + foreach ( $this->notices as $notice_type => $plugins ) { + if ( ! empty( $plugins[ $plugin_name ] ) ) { + return true; + } + } + + return false; + } + + /** + * Removes any notifications for the specified plugin. + * + * Useful when a valid license key is detected for a plugin, where previously + * it might have been included under a warning notification. + * + * If the optional second param is set to true then this change will not + * immediately be committed to storage (useful if we know this will happen in + * any case later on in the same request). + * + * @param string $plugin_name + * @param bool $defer_saving_change = false + */ + public function clear_notices( $plugin_name, $defer_saving_change = false ) { + foreach ( $this->notices as $notice_type => &$list_of_plugins ) { + unset( $list_of_plugins[ $plugin_name ] ); + } + + if ( ! $defer_saving_change ) { + $this->save_notices(); + } + } + + /** + * Tests to see if there are any extant notifications and renders them if so. + * + * This must run prior to Tribe__Admin__Notices::hook() (which currently runs during + * "current_screen" priority 20). + */ + public function setup_notices() { + // Don't allow this to run multiple times + remove_action( 'current_screen', [ $this, 'setup_notices' ] ); + + // No need to display license key notices to users without appropriate capabilities + if ( ! current_user_can( 'install_plugins' ) ) { + return; + } + + foreach ( $this->notices as $notice_type => $plugin_names ) { + if ( empty( $plugin_names ) ) { + continue; + } + + $callback = [ $this, 'render_' . $notice_type ]; + + if ( is_callable( $callback ) ) { + tribe_notice( 'pue_key-' . $notice_type, $callback, 'dismiss=1&type=warning' ); + } + } + } + + /** + * Select all products with empty license keys + * and format their names + * + * This information will be used to remove products + * with no license keys from $this->notices['invalid_key'] + * + * @since 4.8 + * + * @return array + */ + public function select_empty_keys() { + /** @var $wpdb */ + global $wpdb; + + $sql = " + SELECT option_name + FROM {$wpdb->options} + WHERE option_name LIKE 'pue_install_key_%' + AND option_value='' + "; + + $empty_keys = $wpdb->get_results( $sql, ARRAY_N ); + + $formatted_empty_keys = []; + foreach ( $empty_keys as $empty_key ) { + $empty_key = Tribe__Utils__Array::get( $empty_key, [ 0 ] ); + $formatted_empty_keys[] = Tribe__Utils__Array::get( $this->plugin_names, $empty_key ); + } + + return $formatted_empty_keys; + } + + /** + * Generate a notice listing any plugins for which license keys have been entered but + * are invalid (in the sense of not matching PUE server records or having been revoked + * rather than having expired which is handled separately). + * + * In the context of the plugin admin screen, will not render if the key-has-expired + * notice is also scheduled to display. + */ + public function render_invalid_key() { + global $pagenow; + + $empty_keys = $this->select_empty_keys(); + + if ( empty( $empty_keys ) ) { + return; + } + + // Remove the invalid_key notice for products with an empty license key + foreach ( $empty_keys as $empty_key ) { + if ( array_key_exists( $empty_key, $this->notices['invalid_key'] ) ) { + unset( $this->notices['invalid_key'][ $empty_key ] ); + } + } + + if ( 'plugins.php' === $pagenow && ! empty( $this->notices[ self::EXPIRED_KEY ] ) ) { + return; + } + + $plugin_names = $this->get_formatted_plugin_names( self::INVALID_KEY ); + + /** + * Filters the list of plugins that should trigger an invalid key notice in PUE. + * + * @since 5.0.0 + * + * @param array $plugin_names Array of plugin names that should trigger the invalid key notice. + */ + $plugin_names = apply_filters( 'tec_pue_invalid_key_notice_plugins', $plugin_names ); + + if ( empty( $plugin_names ) ) { + return; + } + + $prompt = sprintf( + _n( + "It looks like you're using %1\$s, but the license key is invalid. Please download the latest version %2\$sfrom your account%3\$s.", + "It looks like you're using %1\$s, but the license keys are invalid. Please download the latest versions %2\$sfrom your account%3\$s.", + count( $this->notices[ self::INVALID_KEY ] ), + 'tribe-common' + ), + $plugin_names, + '', + '' + ); + + /** + * Filters the actions that can be taken if an invalid key is present + * + * @param string $actions Actions + * @param array $plugin_names Plugin names the message applies to + */ + $action_steps = apply_filters( 'tribe_notice_invalid_key_actions', $this->find_your_key_text(), $plugin_names ); + + if ( $action_steps ) { + $action_steps = "

    {$action_steps}

    "; + } + + $this->render_notice( 'pue_key-' . self::INVALID_KEY, "

    {$prompt}

    {$action_steps}" ); + } + + /** + * Generate a notice listing any plugins for which license keys have expired. + * + * This notice should only appear at the top of the plugin admin screen and "trumps" + * the missing/invalid key notice on that screen only. + */ + public function render_expired_key() { + global $pagenow; + + if ( 'plugins.php' !== $pagenow ) { + return; + } + + $plugin_names = $this->get_formatted_plugin_names( self::EXPIRED_KEY ); + + /** + * Filters the list of plugins that should trigger an expired key notice in PUE. + * + * @since 5.0.0 + * + * @param array $plugin_names Array of plugin names that should trigger the expired key notice. + */ + $plugin_names = apply_filters( 'tec_pue_expired_key_notice_plugins', $plugin_names ); + + if ( empty( $plugin_names ) ) { + return; + } + + $prompt = sprintf( _n( + 'There is an update available for %1$s but your license has expired. %2$sVisit the Events Calendar website to renew your license.%3$s', + 'Updates are available for %1$s but your license keys have expired. %2$sVisit the Events Calendar website to renew your licenses.%3$s', + count( $this->notices[ self::EXPIRED_KEY ] ), + 'tribe-common' + ), + $plugin_names, + '', + '' + ); + + $renew_action = + '' . + __( 'Renew Your License Now', 'tribe-common' ) . + '' . + __( ' (opens in a new window)', 'tribe-common' ) . + ''; + + $this->render_notice( 'pue_key-' . self::EXPIRED_KEY, "

    $prompt

    $renew_action

    " ); + } + + /** + * Generate a notice listing any plugins which have valid license keys, but those keys + * have met or exceeded the permitted number of installations they can be applied to. + */ + public function render_upgrade_key() { + $plugin_names = $this->get_formatted_plugin_names( self::UPGRADE_KEY ); + + /** + * Filters the list of plugins that should trigger an upgrade key notice in PUE. + * + * @since 5.0.0 + * + * @param array $plugin_names Array of plugin names that should trigger the upgrade key notice. + */ + $plugin_names = apply_filters( 'tec_pue_upgrade_key_notice_plugins', $plugin_names ); + + if ( empty( $plugin_names ) ) { + return; + } + + $prompt = sprintf( _n( + 'You have a license key for %1$s but the key is out of installs. %2$sVisit the Events Calendar website%3$s to manage your installs, upgrade your license, or purchase a new one.', + 'You have license keys for %1$s but your keys are out of installs. %2$sVisit the Events Calendar website%3$s to manage your installs, upgrade your licenses, or purchase new ones.', count( $this->notices[ self::UPGRADE_KEY ] ), + 'tribe-common' + ), + $plugin_names, + '', + '' + ); + + $this->render_notice( 'pue_key-' . self::UPGRADE_KEY, "

    $prompt

    " ); + } + + /** + * Renders the notice itself (the provided HTML will be wrapped in a suitable container div). + * + * @param string $slug + * @param string $inner_html + */ + protected function render_notice( $slug, $inner_html ) { + + // Enqueue the notice CSS. + tribe( 'assets' )->enqueue( [ 'tribe-common-admin' ] ); + + $mascot = esc_url( Tribe__Main::instance()->plugin_url . 'src/resources/images/mascot.png' ); + + $html = + '
    +
    + +
    +
    ' . $inner_html . '
    +
    '; + + Tribe__Admin__Notices::instance()->render( $slug, $html, false ); + } + + /** + * @return string + */ + protected function find_your_key_text() { + return sprintf( + __( 'You can always check the status of your licenses by logging in to %1$syour account on theeventscalendar.com%2$s.', 'tribe-common' ), + '', + '' + ); + } + + /** + * Transforms a list of plugins into human readable string. + * + * Examples of output: + * + * # One name + * "Ticket Pro" + * + * # Two names + * "Ticket Pro and Calendar Legend" + * + * # Three names + * "Ticket Pro, Calendar Legend and Date Stars" + * + * + * @since 4.9.12 + * + * @param array|string $plugins Array of plugin classes. + * + * @return string|false + */ + public function get_formatted_plugin_names_from_classes( $plugins ) { + $plugin_list = []; + + foreach ( (array) $plugins as $class_name ) { + $pue = tribe( Tribe__Dependency::class )->get_pue_from_class( $class_name ); + + if ( ! $pue ) { + continue; + } + + if ( ! isset( $this->plugin_names[ $pue->pue_install_key ] ) ) { + continue; + } + + $plugin_list[] = $this->plugin_names[ $pue->pue_install_key ]; + } + + $num_plugins = count( $plugin_list ); + + if ( 0 === $num_plugins ) { + return false; + } + + if ( 1 === $num_plugins ) { + $html = current( $plugin_list ); + } elseif ( 1 < $num_plugins ) { + $all_but_last = join( ', ', array_slice( $plugin_list, 0, count( $plugin_list ) - 1 ) ); + $last = current( array_slice( $plugin_list, count( $plugin_list ) - 1, 1 ) ); + $html = sprintf( _x( '%1$s and %2$s', 'formatted plugin list', 'tribe-common' ), $all_but_last, $last ); + } + + return '' . $html . ''; + } + + /** + * Transforms the array referenced by group into a human readable, + * comma delimited list. + * + * Examples of output: + * + * # One name + * "Ticket Pro" + * + * # Two names + * "Ticket Pro and Calendar Legend" + * + * # Three names + * "Ticket Pro, Calendar Legend and Date Stars" + * + * # Fallback + * "Unknown Plugin(s)" + * + * @param string $group + * + * @return string + */ + protected function get_formatted_plugin_names( $group ) { + if ( ! count( $this->notices[ $group ] ) ) { + return ''; + } + + $plugin_list = array_intersect( $this->registered, array_keys( $this->notices[ $group ] ) ); + $num_plugins = count( $plugin_list ); + + if ( 0 === $num_plugins ) { + return ''; + } elseif ( 1 === $num_plugins ) { + $html = current( $plugin_list ); + } elseif ( 1 < $num_plugins ) { + $all_but_last = join( ', ', array_slice( $plugin_list, 0, count( $plugin_list ) - 1 ) ); + $last = current( array_slice( $plugin_list, count( $plugin_list ) - 1, 1 ) ); + $html = sprintf( _x( '%1$s and %2$s', 'formatted plugin list', 'tribe-common' ), $all_but_last, $last ); + } + + return '' . $html . ''; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/PUE/Package_Handler.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/PUE/Package_Handler.php new file mode 100644 index 000000000..960d471f5 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/PUE/Package_Handler.php @@ -0,0 +1,157 @@ +filesystem = $wp_filesystem; + } + + /** + * @return Tribe__PUE__Package_Handler + */ + public static function instance() { + if ( empty( self::$instance ) ) { + self::$instance = new self(); + } + + return self::$instance; + } + + /** + * Filters the package download step to store the downloaded file with a shorter file name. + * + * @param bool $reply Whether to bail without returning the package. + * Default false. + * @param string $package The package file name or URL. + * @param WP_Upgrader $upgrader The WP_Upgrader instance. + */ + public function filter_upgrader_pre_download( $reply, $package, WP_Upgrader $upgrader ) { + if ( $this->is_mt_package( $package ) ) { + $this->upgrader = $upgrader; + + return $this->download( $package ); + } + + return $reply; + } + + /** + * Whether the current package is an MT plugin package or not. + * + * @param string $package The package file name or URL. + * + * @return bool + */ + protected function is_mt_package( $package ) { + if ( + empty( $package ) + || ! preg_match( '!^(http|https|ftp)://!i', $package ) + ) { + return false; + } + + $query_vars = parse_url( $package, PHP_URL_QUERY ); + + if ( empty( $query_vars ) ) { + return false; + } + + wp_parse_str( $query_vars, $parsed ); + + return isset( $parsed['pu_get_download'] ) && $parsed['pu_get_download'] == 1; + } + + /** + * A mimic of the `WP_Upgrader::download_package` method that adds a step to store the temp file with a shorter + * file name. + * + * @see WP_Upgrader::download_package() + * + * @param string $package The URI of the package. If this is the full path to an + * existing local file, it will be returned untouched. + * + * @return string|WP_Error The full path to the downloaded package file, or a WP_Error object. + */ + protected function download( $package ) { + if ( empty( $this->filesystem ) ) { + // try to connect + $this->upgrader->fs_connect( [ WP_CONTENT_DIR, WP_PLUGIN_DIR ] ); + + global $wp_filesystem; + + // still empty? + if ( empty( $wp_filesystem ) ) { + // bail + return false; + } + + $this->filesystem = $wp_filesystem; + } + + $this->upgrader->skin->feedback( 'downloading_package', $package ); + + $download_file = download_url( $package ); + + if ( is_wp_error( $download_file ) ) { + return new WP_Error( 'download_failed', $this->upgrader->strings['download_failed'], + $download_file->get_error_message() ); + } + + $file = $this->get_short_filename( $download_file ); + + $moved = $this->filesystem->move( $download_file, $file ); + + if ( empty( $moved ) ) { + // we tried, we failed, we bail and let WP do its job + return false; + } + + return $file; + } + + /** + * Returns the absolute path to a shorter filename version of the original download temp file. + * + * The path will point to the same temp dir (WP handled) but shortening the filename to a + * 6 chars hash to cope with OSes limiting the max number of chars in a file path. + * The original filename would be a sanitized version of the URL including query args. + * + * @param string $download_file The absolute path to the original download file. + * + * @return string The absolute path to a shorter name version of the downloaded file. + */ + protected function get_short_filename( $download_file ) { + $extension = pathinfo( $download_file, PATHINFO_EXTENSION ); + $filename = substr( md5( $download_file ), 0, 5 ); + $file = dirname( $download_file ) . '/' . $filename . '.' . $extension; + + return $file; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/PUE/Plugin_Info.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/PUE/Plugin_Info.php new file mode 100644 index 000000000..a3dc71592 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/PUE/Plugin_Info.php @@ -0,0 +1,141 @@ +results ) ) { + $apiResponse = current( $apiResponse->results ); + } + + if ( empty( $apiResponse ) || ! is_object( $apiResponse ) ) { + return null; + } + + //Very, very basic validation. + $valid = ( isset( $apiResponse->name ) && ! empty( $apiResponse->name ) && isset( $apiResponse->version ) && ! empty( $apiResponse->version ) ) || ( isset( $apiResponse->api_invalid ) || isset( $apiResponse->no_api ) ); + if ( ! $valid ) { + return null; + } + + $info = new Tribe__PUE__Plugin_Info(); + + foreach ( get_object_vars( $apiResponse ) as $key => $value ) { + $key = str_replace( 'plugin_', '', $key ); // let's strip out the "plugin_" prefix we've added in plugin-updater-classes. + $info->$key = $value; + } + + return $info; + } + + /** + * Transform plugin info into the format used by the native WordPress.org API + * + * @return object + */ + public function to_wp_format() { + $info = new StdClass; + + // The custom update API is built so that many fields have the same name and format + // as those returned by the native WordPress.org API. These can be assigned directly. + $sameFormat = [ + 'name', + 'slug', + 'version', + 'requires', + 'tested', + 'rating', + 'upgrade_notice', + 'num_ratings', + 'downloaded', + 'homepage', + 'last_updated', + 'api_expired', + 'api_upgrade', + 'api_invalid', + ]; + + foreach ( $sameFormat as $field ) { + if ( isset( $this->$field ) ) { + $info->$field = $this->$field; + } else { + $info->$field = null; + } + } + + //Other fields need to be renamed and/or transformed. + $info->download_link = $this->download_url; + + if ( ! empty( $this->author_homepage ) ) { + $info->author = sprintf( '%s', esc_url( $this->author_homepage ), $this->author ); + } else { + $info->author = $this->author; + } + + if ( is_object( $this->sections ) ) { + $info->sections = get_object_vars( $this->sections ); + } elseif ( is_array( $this->sections ) ) { + $info->sections = $this->sections; + } else { + $info->sections = [ 'description' => '' ]; + } + + return $info; + } + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/PUE/Update_Prevention.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/PUE/Update_Prevention.php new file mode 100644 index 000000000..c5164b44f --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/PUE/Update_Prevention.php @@ -0,0 +1,199 @@ + version ] or empty if it didn't find it. + */ + public function get_dependencies( $content ) { + $regex = "/'(?[^']*)'(?:[^']*)'(?[^']*)',/"; + + if ( ! preg_match_all( $regex, $content, $matches ) ) { + return []; + } + + $dependencies = array_combine( $matches['plugin'], $matches['version'] ); + + return $dependencies; + } + + /** + * Checks for the list of constants associate with plugin to make sure we are dealing + * with a plugin owned by The Events Calendar. + * + * @since 4.9.12 + * + * @param string $plugin Plugin file partial path, folder and main php file. + * + * @return bool + */ + public function is_tribe_plugin( $plugin ) { + $path_constants_list = [ + // The Events Calendar + 'TRIBE_EVENTS_FILE', + + // Events Pro + 'EVENTS_CALENDAR_PRO_FILE', + + // Filter bar + 'TRIBE_EVENTS_FILTERBAR_FILE', + + // Eventbrite Tickets + 'EVENTBRITE_PLUGIN_FILE', + ]; + + foreach ( $path_constants_list as $constant_name ) { + if ( ! defined( $constant_name ) ) { + continue; + } + + if ( false === strpos( constant( $constant_name ), $plugin ) ) { + continue; + } + + return true; + } + + return false; + } + + /** + * Filters the source file location for the upgrade package for the PUE Update_Prevention engine. + * + * @since 4.9.12 + * + * @param string $source File source location. + * @param string $remote_source Remote file source location. + * @param WP_Upgrader $upgrader WP_Upgrader instance. + * @param array $extra Extra arguments passed to hooked filters. + */ + public function filter_upgrader_source_selection( $source, $remote_source, $upgrader, $extras ) { + if ( ! isset( $extras['plugin'] ) ) { + return $source; + } + + $plugin = $extras['plugin']; + + // Bail if we are not dealing with a plugin we own. + if ( ! $this->is_tribe_plugin( $plugin ) ) { + return $source; + } + + $register_path = $source . '/src/Tribe/Plugin_Register.php'; + + // Bail when the Plugin Register file doesn't exist. + if ( ! file_exists( $register_path ) ) { + return $source; + } + + $register_contents = file_get_contents( $register_path ); + + $dependencies = $this->get_dependencies( $register_contents ); + $incompatible_plugins = []; + + foreach ( $dependencies as $class_name => $required_version ) { + // Skip inactive plugin checks. + if ( ! class_exists( $class_name ) ) { + continue; + } + + $constant_name = $class_name . '::VERSION'; + + // Skip if we can't find the version constant. + if ( ! defined( $constant_name ) ) { + continue; + } + + $current_version = constant( $constant_name ); + + // Skip when the version is equal or higher than the required. + if ( version_compare( $current_version, $required_version, '>=' ) ) { + continue; + } + + $pue = tribe( Dependency::class )->get_pue_from_class( $class_name ); + $has_pue_notice = $pue ? tribe( 'pue.notices' )->has_notice( $pue->pue_install_key ) : false; + + // Only throw warning for customers with notices of invalid/expired licenses. + if ( ! $has_pue_notice ) { + continue; + } + + // Flag that we should prevent the Update + $incompatible_plugins[ $class_name ] = $required_version; + } + + // Bail when there are no incompatible plugins. + if ( empty( $incompatible_plugins ) ) { + return $source; + } + + /** + * Filter the if we should prevent the update. + * + * @since 4.9.12 + * + * @param bool $should_revent Flag false to skip the prevention. + * @param array $incompatible_plugins Which plugins were incompatible with new version of the plugin. + * @param string $source File source location. + * @param string $remote_source Remote file source location. + * @param WP_Upgrader $upgrader WP_Upgrader instance. + * @param array $extra Extra arguments passed to hooked filters. + */ + $should_prevent_update = apply_filters( + 'tribe_pue_should_prevent_update_without_license', + true, + $incompatible_plugins, + $source, + $remote_source, + $upgrader, + $extras + ); + + // Bail if the filter above returns anything but true. + if ( true !== $should_prevent_update ) { + return $source; + } + + $full_plugin_path = $remote_source . '/' . $plugin; + $plugin_data = get_plugin_data( $full_plugin_path ); + + $plugins_classes = array_keys( $incompatible_plugins ); + $plugins_list_html = tribe( 'pue.notices' )->get_formatted_plugin_names_from_classes( $plugins_classes ); + + $link_read_more = '' . esc_html__( 'Read more', 'tribe-common' ) . '.'; + + $message = sprintf( + esc_html__( 'Your update failed due to an incompatibility between the version (%1$s) of the %2$s you tried to update to and the version of %3$s that you are using. %4$s', 'tribe-common' ), + esc_html( $plugin_data['Version'] ), + esc_html( $plugin_data['Name'] ), + $plugins_list_html, + $link_read_more + ); + + $error = new WP_Error( + 'tribe-updater-failed-prevention', + $message, + [] + ); + + return $error; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/PUE/Utility.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/PUE/Utility.php new file mode 100644 index 000000000..07b6bbf5d --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/PUE/Utility.php @@ -0,0 +1,126 @@ +$field ) ) { + continue; + } + + $update->$field = $info->$field; + } + + return $update; + } + + /** + * Transform the update into the format used by WordPress native plugin API. + * + * @return object + */ + public function to_wp_format() { + $update = new StdClass; + + $update->id = $this->id; + $update->plugin = $this->plugin; + $update->slug = $this->slug; + $update->new_version = $this->version; + $update->url = $this->homepage; + $update->package = $this->download_url; + if ( ! empty( $this->upgrade_notice ) ) { + $update->upgrade_notice = $this->upgrade_notice; + } + + // Support custom $update properties coming straight from PUE + if ( ! empty( $this->custom_update ) ) { + $custom_update = get_object_vars( $this->custom_update ); + + foreach ( $custom_update as $field => $custom_value ) { + if ( is_object( $custom_value ) ) { + $custom_value = get_object_vars( $custom_value ); + } + + $update->$field = $custom_value; + } + } + + return $update; + } + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Plugin_Meta_Links.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Plugin_Meta_Links.php new file mode 100644 index 000000000..fd674cbfb --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Plugin_Meta_Links.php @@ -0,0 +1,147 @@ + Plugins list + */ +class Tribe__Plugin_Meta_Links { + /** + * Class instance + * + * @var Tribe__Plugin_Meta_Links The singleton instance. + */ + private static $instance; + + /** + * The various meta links that will be added + * + * @var array { + * Each plugin that will be filtered. + * + * @type array $plugin_basename { + * Meta links added to this plugin. + * + * @type array { + * Each individual link. + * + * @type string $html The full HTML for this link. + * @type bool $remove Whether we are adding or removing this link. + * } + * } + * } + */ + private $meta_links = []; + + /** + * Returns the singleton instance of this class. + * + * @return Tribe__Plugin_Meta_Links instance. + */ + public static function instance() { + return null === self::$instance ? new self() : self::$instance; + } + + private function __construct() { + add_action( 'plugin_row_meta', [ $this, 'filter_meta_links' ], 10, 2 ); + } + + /** + * Adds an link to the meta list + * + * @param string $plugin Path to plugin file. + * @param string $text Inner text for HTML element. + * @param string $href URL for the link. + * @param array $attributes Key => value attributes for element. + */ + public function add_link( $plugin, $title, $href, $attributes = [] ) { + $attributes['href'] = $href; + + // Build the element. + $html = ' $val ) { + $html .= ' ' . $att . '="' . esc_attr( $val ) . '"'; + } + + $html .= '>' . esc_html( $title ) . ''; + + $this->set( $plugin, $html, false ); + } + + /** + * Adds or removes the specified HTML link + * + * @param string $plugin Path to plugin file. + * @param string $html Full HTML for this link. + * @param bool $remove Whether to add this HTML/link or match and remove it. + */ + public function set( $plugin, $html, $remove = false ) { + $basename = plugin_basename( $plugin ); + + // Get any current links for this plugin. + $cur_links = Tribe__Utils__Array::get( $this->meta_links, $basename, [] ); + + $cur_links[] = [ + 'html' => $html, + 'remove' => $remove, + ]; + + $this->meta_links = Tribe__Utils__Array::set( $this->meta_links, $basename, $cur_links ); + } + + /** + * Filters meta links on the plugins list page + * + * @param array $links The current plugin's links. + * @param string $basename The plugin currently being filtered. + * + * @return array Filtered action links array. + */ + public function filter_meta_links( $links, $basename ) { + // Gets any links that are set for this plugin, defaults to an empty array. + $set_links = Tribe__Utils__Array::get( $this->meta_links, $basename, [] ); + + foreach ( $set_links as $link ) { + + if ( true === $link['remove'] ) { + // Remove a link. + $pos = array_search( $link['html'], $links ); + + if ( false !== $pos ) { + unset( $links[ $pos ] ); + } + } else { + // Add a link. + $links[] = $link['html']; + } + } + + return $links; + } + + /** + * Prevent cloning the singleton with 'clone' operator + * + * @return void + */ + final public function __clone() { + _doing_it_wrong( + __FUNCTION__, + 'Can not use this method on singletons.', + '4.3' + ); + } + + /** + * Prevent unserializing the singleton instance + * + * @return void + */ + final public function __wakeup() { + _doing_it_wrong( + __FUNCTION__, + 'Can not use this method on singletons.', + '4.3' + ); + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Plugins.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Plugins.php new file mode 100644 index 000000000..5d1482fc1 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Plugins.php @@ -0,0 +1,184 @@ + Common name for the plugin, used in places such as WP Admin messages + * 'class' => Main plugin class + * 'thickbox_url' => Download or purchase URL for plugin from within /wp-admin/ thickbox + * ] + */ + private $tribe_plugins = [ + [ + 'short_name' => 'Event Tickets', + 'class' => 'Tribe__Tickets__Main', + 'thickbox_url' => 'plugin-install.php?tab=plugin-information&plugin=event-tickets&TB_iframe=true', + ], + [ + 'short_name' => 'Event Tickets Plus', + 'class' => 'Tribe__Tickets_Plus__Main', + 'thickbox_url' => 'https://theeventscalendar.com/product/wordpress-event-tickets-plus/?TB_iframe=true', + ], + [ + 'short_name' => 'The Events Calendar', + 'class' => 'Tribe__Events__Main', + 'thickbox_url' => 'plugin-install.php?tab=plugin-information&plugin=the-events-calendar&TB_iframe=true', + ], + [ + 'short_name' => 'Events Calendar Pro', + 'class' => 'Tribe__Events__Pro__Main', + 'thickbox_url' => 'https://theeventscalendar.com/product/wordpress-events-calendar-pro/?TB_iframe=true', + ], + [ + 'short_name' => 'Community Events', + 'class' => 'Tribe__Events__Community__Main', + 'thickbox_url' => 'https://theeventscalendar.com/product/wordpress-community-events/?TB_iframe=true', + ], + [ + 'short_name' => 'Community Tickets', + 'class' => 'Tribe__Events__Community__Tickets__Main', + 'thickbox_url' => 'https://theeventscalendar.com/product/community-tickets/?TB_iframe=true', + ], + [ + 'short_name' => 'Filter Bar', + 'class' => 'Tribe__Events__Filterbar__View', + 'thickbox_url' => 'https://theeventscalendar.com/product/wordpress-events-filterbar/?TB_iframe=true', + ], + [ + 'short_name' => 'Facebook Events', + 'class' => 'Tribe__Events__Facebook__Importer', + 'thickbox_url' => 'https://theeventscalendar.com/product/facebook-events/?TB_iframe=true', + ], + [ + 'short_name' => 'iCal Importer', + 'class' => 'Tribe__Events__Ical_Importer__Main', + 'thickbox_url' => 'https://theeventscalendar.com/product/ical-importer/?TB_iframe=true', + ], + [ + 'short_name' => 'Eventbrite Tickets', + 'class' => 'Tribe__Events__Tickets__Eventbrite__Main', + 'thickbox_url' => 'https://theeventscalendar.com/product/wordpress-eventbrite-tickets/?TB_iframe=true', + ], + [ + 'short_name' => 'Advanced Post Manager', + 'class' => 'Tribe_APM', + 'thickbox_url' => 'plugin-install.php?tab=plugin-information&plugin=advanced-post-manager&TB_iframe=true', + ], + ]; + + /** + * Searches the plugin list for key/value pair and return the full details for that plugin + * + * @param string $search_key The array key this value will appear in + * @param string $search_val The value itself + * + * @return array|null + */ + public function get_plugin_by_key( $search_key, $search_val ) { + foreach ( $this->get_list() as $plugin ) { + if ( isset( $plugin[ $search_key ] ) && $plugin[ $search_key ] === $search_val ) { + return $plugin; + } + } + + return null; + } + + /** + * Retrieves plugins details by plugin name + * + * @param string $name Common name for the plugin, not necessarily the lengthy name in the WP Admin Plugins list + * + * @return array|null + */ + public function get_plugin_by_name( $name ) { + return $this->get_plugin_by_key( 'short_name', $name ); + } + + /** + * Retrieves plugins details by class name + * + * @param string $main_class Main/base class for this plugin + * + * @return array|null + */ + public function get_plugin_by_class( $main_class ) { + return $this->get_plugin_by_key( 'class', $main_class ); + } + + /** + * Retrieves the entire list + * + * @return array + */ + public function get_list() { + /** + * Gives an opportunity to filter the list of tribe plugins + * + * @since 4.7.18 + * + * @param array Contains a list of all tribe plugins + */ + return apply_filters( 'tribe_plugins_get_list', $this->tribe_plugins ); + } + + /** + * Checks if given plugin is active. Usually a The Events Calendar plugin. + * + * @param string $plugin_name The name of the plugin. Each plugin defines their name upon hooking on the filter. + * + * @since 4.12.1 + * + * @return bool True if plugin is active. False if plugin is not active. + */ + public static function is_active( $plugin_name ) { + if ( ! did_action( "plugins_loaded" ) ) { + _doing_it_wrong( + __METHOD__, + __( 'Using this function before "plugins_loaded" action has fired can return unreliable results.', 'tribe-common' ), + '4.12.6' + ); + } + + /** + * Filters the array that each Tribe plugin overrides to + * set itself as active when this function is called. + * + * @example [ 'the-events-calendar' => true, 'event-tickets' => true ] + * + * @since 4.12.1 + * + * @return array Plugin slugs as keys and bool as value for whether it's active or not. + */ + $plugins = apply_filters( 'tribe_active_plugins', [] ); + + return isset( $plugins[ $plugin_name ] ) && tribe_is_truthy( $plugins[ $plugin_name ] ); + } + + /** + * Retrieves a list of active plugins. + * + * @since 5.1.3 + * + * @return array List of active plugins. + */ + public function get_active_plugins() { + $plugins = $this->get_list(); + + $plugins = array_filter( $plugins, function ( $plugin ) { + return self::is_active( $plugin['short_name'] ); + } ); + + return $plugins; + } + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Plugins_API.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Plugins_API.php new file mode 100644 index 000000000..471986ec9 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Plugins_API.php @@ -0,0 +1,289 @@ + [ + 'title' => __( 'The Events Calendar', 'tribe-common' ), + 'slug' => 'the-events-calendar', + 'link' => 'https://evnt.is/1ai-', + 'plugin-dir' => 'the-events-calendar', + 'main-file' => 'the-events-calendar.php', + 'description' => __( 'Our flagship free calendar', 'tribe-common' ), + 'description-help' => __( 'The #1 calendar for WordPress', 'tribe-common' ), + 'features' => [ + __( 'Customizable', 'tribe-common' ), + __( 'Import & export events', 'tribe-common' ), + __( 'Timezone support', 'tribe-common' ), + __( 'Multiple views', 'tribe-common' ), + ], + 'image' => 'images/shop/calendar.jpg', + 'logo' => 'images/logo/the-events-calendar.svg', + 'is_installed' => class_exists( 'Tribe__Events__Main' ), + 'free' => true, + 'active_installs' => 800000, + ], + 'event-aggregator' => [ + 'title' => __( 'Event Aggregator', 'tribe-common' ), + 'slug' => 'event-aggregator', + 'link' => 'https://evnt.is/1aj0', + 'plugin-dir' => '', + 'main-file' => '', + 'description' => __( 'Automated imports for your calendar', 'tribe-common' ), + 'description-help' => __( 'Import events from Meetup, Eventbrite, iCal, Google Calendar, and more.', 'tribe-common' ), + 'features' => [ + __( 'Schedule automated imports', 'tribe-common' ), + __( 'Customizable', 'tribe-common' ), + __( 'Works with Google Calendar, Meetup, and more', 'tribe-common' ), + __( 'Refine by date, location, or keyword', 'tribe-common' ), + ], + 'image' => 'images/shop/aggregator.jpg', + 'logo' => 'images/logo/event-aggregator.svg', + 'is_installed' => class_exists( 'Tribe__Events__Aggregator' ) && Tribe__Events__Aggregator::is_service_active(), + 'free' => false, + 'active_installs' => 20000, + ], + 'event-automator' => [ + 'title' => __( 'Event Automator', 'tribe-common' ), + 'slug' => 'event-automator', + 'link' => 'https://evnt.is/1bc7', + 'plugin-dir' => '', + 'main-file' => '', + 'description' => __( 'Automate your event workflow with Zapier—no code required!', 'tribe-common' ), + 'description-help' => __( 'Connect The Events Calendar and Event Tickets with thousands of the most popular apps.', 'tribe-common' ), + 'features' => [ + __( 'Lead Management', 'tribe-common' ), + __( 'Communication Outreach', 'tribe-common' ), + __( 'Internal Processes', 'tribe-common' ), + __( 'Data Management', 'tribe-common' ), + ], + 'image' => 'images/shop/automator.svg', + 'logo' => 'images/logo/event-automator.svg', + 'is_installed' => class_exists( 'TEC\Event_Automator\Plugin' ), + 'free' => false, + 'active_installs' => 0, + ], + 'events-calendar-pro' => [ + 'title' => __( 'Events Calendar Pro', 'tribe-common' ), + 'slug' => 'events-calendar-pro', + 'link' => 'https://evnt.is/1ai-', + 'plugin-dir' => 'events-calendar-pro', + 'main-file' => 'events-calendar-pro.php', + 'description' => __( 'Power up your calendar with Pro', 'tribe-common' ), + 'description-help' => __( 'The #1 calendar for WordPress', 'tribe-common' ), + 'features' => [ + __( 'Premium support', 'tribe-common' ), + __( 'Recurring events & series', 'tribe-common' ), + __( 'Additional views', 'tribe-common' ), + __( 'Shortcodes', 'tribe-common' ), + __( 'Duplicate events', 'tribe-common' ), + ], + 'image' => 'images/shop/pro.jpg', + 'logo' => 'images/logo/events-calendar-pro.svg', + 'is_installed' => class_exists( 'Tribe__Events__Pro__Main' ), + 'free' => false, + 'active_installs' => 100000, + ], + 'event-tickets' => [ + 'title' => __( 'Event Tickets', 'tribe-common' ), + 'slug' => 'event-tickets', + 'link' => 'https://evnt.is/1aj1', + 'plugin-dir' => 'event-tickets', + 'main-file' => 'event-tickets.php', + 'description' => __( 'Manage ticketing and RSVPs', 'tribe-common' ), + 'description-help' => __( 'Collect RSVPs and sell tickets', 'tribe-common' ), + 'features' => [ + __( 'Add tickets and RSVP to any post', 'tribe-common' ), + __( 'Paypal integration', 'tribe-common' ), + __( 'Attendee reports', 'tribe-common' ), + __( 'Customizable ticket template', 'tribe-common' ), + ], + 'image' => 'images/shop/tickets.jpg', + 'logo' => 'images/logo/event-tickets.svg', + 'is_installed' => class_exists( 'Tribe__Tickets__Main' ), + 'free' => true, + 'active_installs' => 20000, + ], + 'event-tickets-plus' => [ + 'title' => __( 'Event Tickets Plus', 'tribe-common' ), + 'slug' => 'event-tickets-plus', + 'link' => 'http://evnt.is/1aj1', + 'plugin-dir' => 'event-tickets-plus', + 'main-file' => 'event-tickets-plus.php', + 'description' => __( 'Monetize your events', 'tribe-common' ), + 'description-help' => __( 'Collect RSVPs and sell tickets', 'tribe-common' ), + 'features' => [ + __( 'Custom registration fields', 'tribe-common' ), + __( 'WooCommerce compatibility', 'tribe-common' ), + __( 'Ticket scanning with mobile app', 'tribe-common' ), + __( 'Custom attendee registration fields', 'tribe-common' ), + ], + 'image' => 'images/shop/tickets-plus.jpg', + 'logo' => 'images/logo/event-tickets-plus.svg', + 'is_installed' => class_exists( 'Tribe__Tickets_Plus__Main' ), + 'free' => false, + 'active_installs' => 10000, + ], + 'promoter' => [ + 'title' => __( 'Promoter', 'tribe-common' ), + 'slug' => 'promoter', + 'link' => 'https://evnt.is/1acy', + 'plugin-dir' => '', + 'main-file' => '', + 'description' => __( 'An email marketing solution for events and the people running them', 'tribe-common' ), + 'description-help' => __( 'Email marketing to promote your events', 'tribe-common' ), + 'features' => [ + __( 'Automate email touchpoints', 'tribe-common' ), + __( 'Customize email templates', 'tribe-common' ), + __( 'Streamline your email process', 'tribe-common' ), + __( 'Segment your attendee lists', 'tribe-common' ), + ], + 'image' => 'images/shop/promoter.jpg', + 'logo' => 'images/logo/promoter.svg', + 'is_installed' => tribe( 'promoter.pue' )->has_license_key(), + 'free' => false, + 'active_installs' => 1000, + ], + 'tribe-filterbar' => [ + 'title' => __( 'Filter Bar', 'tribe-common' ), + 'slug' => 'tribe-filterbar', + 'link' => 'https://evnt.is/19o6', + 'plugin-dir' => 'the-events-calendar-filterbar', + 'main-file' => 'the-events-calendar-filter-view.php', + 'description' => __( 'Help users find exactly the right event', 'tribe-common' ), + 'description-help' => __( 'Allow users to search for events by category, tag, venue, organizer, day of the week, time of day, and price.', 'tribe-common' ), + 'features' => [ + __( 'Configurable set of filters', 'tribe-common' ), + __( 'Horizontal or vertical', 'tribe-common' ), + __( 'Filter category, price, and more', 'tribe-common' ), + __( 'Filter distance (for Events Calendar Pro)', 'tribe-common' ), + ], + 'image' => 'images/shop/filter-bar.jpg', + 'logo' => 'images/logo/filterbar.svg', + 'is_installed' => class_exists( 'Tribe__Events__Filterbar__View' ), + 'free' => false, + 'active_installs' => 20000, + ], + 'events-community' => [ + 'title' => __( 'Community Events', 'tribe-common' ), + 'slug' => 'events-community', + 'link' => 'https://evnt.is/19o7', + 'plugin-dir' => 'the-events-calendar-community-events', + 'main-file' => 'tribe-community-events.php', + 'description' => __( 'Users submit events to your calendar', 'tribe-common' ), + 'description-help' => __( 'Enable 3rd party event submissions.', 'tribe-common' ), + 'features' => [ + __( 'Publishing Control', 'tribe-common' ), + __( 'Event Submission Form', 'tribe-common' ), + __( 'Registered User Settings', 'tribe-common' ), + __( 'Email notifications', 'tribe-common' ), + ], + 'image' => 'images/shop/community.jpg', + 'logo' => 'images/logo/community-events.svg', + 'is_installed' => class_exists( 'Tribe__Events__Community__Main' ), + 'free' => false, + 'active_installs' => 20000, + ], + 'events-community-tickets' => [ + 'title' => __( 'Community Tickets', 'tribe-common' ), + 'slug' => 'events-community-tickets', + 'link' => 'https://evnt.is/19o8', + 'plugin-dir' => 'the-events-calendar-community-events-tickets', + 'main-file' => 'events-community-tickets.php', + 'description' => __( 'Run your own events marketplace', 'tribe-common' ), + 'description-help' => __( 'Let users create and sell tickets for events they submit to your calendar.', 'tribe-common' ), + 'features' => [ + __( 'Users submit events and sell tickets', 'tribe-common' ), + __( 'Split commission with users', 'tribe-common' ), + __( 'No admin access required', 'tribe-common' ), /* code review: fail this */ + __( 'Sales reporting', 'tribe-common' ), + ], + 'requires' => _x( 'Event Tickets Plus and Community Events', 'Names of required plugins for Community Tickets', 'tribe-common' ), + 'image' => 'images/shop/community-tickets.jpg', + 'logo' => 'images/logo/community-tickets.svg', + 'is_installed' => class_exists( 'Tribe__Events__Community__Tickets__Main' ), + 'free' => false, + 'active_installs' => 10000, + ], + 'tribe-eventbrite' => [ + 'title' => __( 'Eventbrite Tickets', 'tribe-common' ), + 'slug' => 'tribe-eventbrite', + 'link' => 'https://evnt.is/19o9', + 'plugin-dir' => 'the-events-calendar-eventbrite-tickets', + 'main-file' => 'tribe-eventbrite.php', + 'description' => __( 'Unite the power of TEC with the ticketing of Eventbrite', 'tribe-common' ), + 'description-help' => __( 'Create Eventbrite tickets and events right from your WordPress dashboard.', 'tribe-common' ), + 'features' => [ + __( 'Manage tickets from WordPress', 'tribe-common' ), + __( 'Ticket availability automatically updates', 'tribe-common' ), + __( 'Integrated with your events on Eventbrite', 'tribe-common' ), + __( 'Automatically import your events', 'tribe-common' ), + ], + 'image' => 'images/shop/eventbrite.jpg', + 'logo' => 'images/logo/eventbrite-tickets.svg', + 'is_installed' => class_exists( 'Tribe__Events__Tickets__Eventbrite__Main' ), + 'free' => false, + 'active_installs' => 20000, + ], + 'image-widget-plus' => [ + 'title' => __( 'Image Widget Plus', 'tribe-common' ), + 'slug' => 'image-widget-plus', + 'link' => 'https://evnt.is/19nv', + 'plugin-dir' => 'image-widget-plus', + 'main-file' => 'image-widget-plus.php', + 'description' => __( 'Beautiful display options for your favorite photos.', 'tribe-common' ), + 'features' => [ + __( 'Multi-Image Support', 'tribe-common' ), + __( 'Lightbox', 'tribe-common' ), + __( 'Slideshow', 'tribe-common' ), + __( 'Random Images', 'tribe-common' ), + ], + 'image' => 'images/shop/image-widget-plus.jpg', + 'logo' => 'images/logo/image-widget-plus.svg', + 'is_installed' => class_exists( 'Tribe__Image__Plus__Main' ), + 'free' => false, + 'active_installs' => 2500, + ], + 'events-virtual' => [ + 'title' => __( 'Virtual Events', 'tribe-common' ), + 'slug' => 'events-virtual', + 'link' => 'http://evnt.is/virtual-events', + 'plugin-dir' => 'events-virtual', + 'main-file' => 'events-virtual.php', + 'description' => __( 'Features to optimize your calendar for virtual events.', 'tribe-common' ), + 'description-help' => __( 'Highlight virtual events on you calendar and integrate with your favorite online meeting tools.', 'tribe-common' ), + 'features' => [ + __( 'Zoom integration', 'tribe-common' ), + __( 'Virtual event labels', 'tribe-common' ), + __( 'Status control for canceled or postponed events', 'tribe-common' ), + __( 'Embed livestreams and videos', 'tribe-common' ), + ], + 'image' => 'images/shop/virtual-events.jpg', + 'logo' => 'images/logo/virtual-events.svg', + 'is_installed' => defined( 'EVENTS_VIRTUAL_FILE' ), + 'free' => false, + 'active_installs' => 2500, + ], + ]; + + return $products; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Post_History.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Post_History.php new file mode 100644 index 000000000..fc21e25b8 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Post_History.php @@ -0,0 +1,134 @@ +post_id = $post_id; + } + + /** + * Records a new history entry for the current post. + * + * @param string $message + * @param array $data + */ + public function add_entry( $message, array $data = [] ) { + $datetime = current_time( 'mysql' ); + $checksum = uniqid( substr( hash( 'md5', $datetime . $message . serialize( $data ) ), 0, 8 ) . '_' ); + + $log_entry = wp_slash( json_encode( [ + 'datetime' => $datetime, + 'message' => $message, + 'data' => $data, + 'checksum' => $checksum, + ] ) ); + + add_post_meta( $this->post_id, self::HISTORY_KEY, $log_entry ); + } + + /** + * Indicates if any history exists for the current post. + * + * @return bool + */ + public function has_entries() { + $first_available_entry = get_post_meta( $this->post_id, self::HISTORY_KEY, true ); + return ! empty( $first_available_entry ); + } + + /** + * Returns all historical records for the current post as an array + * of objects, each object taking the form: + * + * { + * "datetime": "yyyy-mm-dd hh:ii:ss", + * "message": "...", + * "data": [] + * } + * + * @return array + */ + public function get_entries() { + $entries = []; + + foreach ( get_post_meta( $this->post_id, self::HISTORY_KEY ) as $log_entry ) { + $log_entry = json_decode( $log_entry ); + + if ( ! $log_entry ) { + continue; + } + + $entries[] = $log_entry; + } + + return $entries; + } + + /** + * Deletes all entries for the current post that match the provided datetime + * string and (optionally) also match the provided checksum. + * + * Returns the total number of deleted entries, which may be zero if none were matched; + * can also be more than one if multiple entries were logged at the same time and no + * checksum is provided. + * + * @param string $datetime + * @param string $checksum optional value to more precisely specify the entry to be deleted + * + * @return int + */ + public function delete_entry( $datetime, $checksum = null ) { + $deleted = 0; + + foreach ( $this->get_entries() as $entry ) { + if ( $entry->datetime !== $datetime ) { + continue; + } + + if ( null !== $checksum && $entry->checksum !== $checksum ) { + continue; + } + + if ( delete_post_meta( $this->post_id, self::HISTORY_KEY, json_encode( $entry ) ) ) { + $deleted++; + } + } + + return $deleted; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Post_Transient.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Post_Transient.php new file mode 100644 index 000000000..65655ffa7 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Post_Transient.php @@ -0,0 +1,214 @@ +ID; + } + + if ( has_filter( 'tribe_pre_post_meta_transient_' . $transient ) ) { + /** + * Attach an action before getting the new Transient + * + * @since 4.1 + * + * @param int $post_id Post ID + * @param string $transient The Post Meta Key + */ + $pre = apply_filters( 'tribe_pre_post_meta_transient_' . $transient, $post_id, $transient ); + if ( false !== $pre ) { + return $pre; + } + } + + if ( $_wp_using_ext_object_cache ) { + $value = wp_cache_get( "tribe_{$transient}-{$post_id}", "tribe_post_meta_transient-{$post_id}" ); + } else { + $meta_timeout = '_transient_timeout_' . $transient; + $meta = '_transient_' . $transient; + $value = get_post_meta( $post_id, $meta, false ); + + // if there aren't any values, communicate that it did not fetch data from post transient + if ( ! is_array( $value ) || 0 === count( $value ) ) { + return false; + } + + // grab the first value, because that's all we care about + $value = current( $value ); + + if ( $value && ! defined( 'WP_INSTALLING' ) ) { + if ( get_post_meta( $post_id, $meta_timeout, true ) < time() ) { + $this->delete( $post_id, $transient ); + + return false; + } + } + } + + /** + * Attach an action after getting the new Transient + * + * @since 4.1 + * + * @param int $post_id Post ID + * @param string $transient The Post Meta Key + */ + return has_filter( 'tribe_post_meta_transient_' . $transient ) + ? apply_filters( 'tribe_post_meta_transient_' . $transient, $value, $post_id ) + : $value; + } + + /** + * Delete a post meta transient. + * + * @since 4.1 + * + * @param int $post_id The Post ID, can also be a WP_Post. + * @param string $transient Post Meta to Delete. + * @param string $value Only delete if the value Matches. + * + * @return boolean If we were able to delete the transient. + */ + public function delete( $post_id, $transient, $value = null ) { + global $_wp_using_ext_object_cache; + + if ( is_numeric( $post_id ) ) { + $post_id = (int) $post_id; + } else { + $post = get_post( $post_id ); + $post_id = $post->ID; + } + + /** + * Use this to pre attach an action to deleting a Post Transient + * + * @since 4.1 + * + * @param int $post_id Post ID + * @param string $transient The Post Meta Key + */ + do_action( 'tribe_delete_post_meta_transient_' . $transient, $post_id, $transient ); + + if ( $_wp_using_ext_object_cache ) { + $result = wp_cache_delete( "tribe_{$transient}-{$post_id}", "tribe_post_meta_transient-{$post_id}" ); + } else { + $meta_timeout = '_transient_timeout_' . $transient; + $meta = '_transient_' . $transient; + $result = delete_post_meta( $post_id, $meta, $value ); + if ( $result ) { + delete_post_meta( $post_id, $meta_timeout, $value ); + } + } + + if ( $result ) { + /** + * Use this to attach an Action to when the Transient is deleted + * + * @since 4.1 + * + * @param int $post_id Post ID + * @param string $transient The Post Meta Key + */ + do_action( 'tribe_deleted_post_meta_transient', $transient, $post_id, $transient ); + } + + return $result; + } + + /** + * Sets a new value for the Transient. + * + * @since 4.1 + * + * @param int $post_id The Post ID, can also be a WP_Post. + * @param string $transient Post Meta to set. + * @param string $value Only delete if the value Matches. + * @param int $expiration How long this transient will be valid, in seconds. + * + * @return int|false Meta ID on success, false on failure. + */ + public function set( $post_id, $transient, $value, $expiration = 0 ) { + global $_wp_using_ext_object_cache; + + if ( is_numeric( $post_id ) ) { + $post_id = (int) $post_id; + } else { + $post = get_post( $post_id ); + $post_id = $post->ID; + } + + $this->delete( $post_id, $transient ); + + /** + * Attach an action before setting the new Transient + * + * @since 4.1 + * + * @param int $post_id Post ID + * @param string $transient The Post Meta Key + */ + if ( has_filter( 'tribe_pre_set_post_meta_transient_' . $transient ) ) { + $value = apply_filters( 'tribe_pre_set_post_meta_transient_' . $transient, $value, $post_id, $transient ); + } + + if ( $_wp_using_ext_object_cache ) { + $result = wp_cache_set( "tribe_{$transient}-{$post_id}", $value, "tribe_post_meta_transient-{$post_id}", $expiration ); + } else { + $meta_timeout = '_transient_timeout_' . $transient; + $meta = '_transient_' . $transient; + if ( $expiration ) { + add_post_meta( $post_id, $meta_timeout, time() + $expiration, true ); + } + $result = add_post_meta( $post_id, $meta, $value, true ); + } + + if ( $result ) { + /** + * Attach an action after setting the new Transient + * + * @since 4.1 + * + * @param int $post_id Post ID + * @param string $transient The Post Meta Key + */ + do_action( 'tribe_set_post_meta_transient_' . $transient, $post_id, $transient ); + } + + return $result; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Process/Handler.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Process/Handler.php new file mode 100644 index 000000000..ccad1f680 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Process/Handler.php @@ -0,0 +1,361 @@ +action = call_user_func( [ $class, 'action' ] ); + $this->identifier = $this->prefix . '_' . $this->action; + + add_action( 'wp_ajax_' . $this->identifier, [ $this, 'maybe_handle' ] ); + + /** + * Filters whether background processing should be triggered and handled on + * non-private AJAX requests as the ones triggered by a non logged in user. + * Defaults to `true` to exploit any possible chance to process. + * + * @since 4.9.5 + * + * @param bool $allow_nopriv Whether background processing should be triggered and handled on + * non-private AJAX requests or not. + * @param static $this This handler instance. + */ + $allow_nopriv = apply_filters( 'tribe_process_allow_nopriv_handling', true, $this ); + + if ( $allow_nopriv ) { + add_action( 'wp_ajax_nopriv_' . $this->identifier, [ $this, 'maybe_handle' ] ); + } + + $this->healthcheck_cron_hook_id = $this->identifier; + $this->feature_detection = tribe( 'feature-detection' ); + + /* + * This object might have been built while processing crons so + * we hook on the the object cron identifier to handle the task + * if the cron-triggered action ever fires. + */ + add_action( $this->healthcheck_cron_hook_id, [ $this, 'maybe_handle' ] ); + } + + /** + * Returns the async process action name. + * + * Extending classes must override this method to return their unique action slug. + * + * @since 4.7.12 + * + * @return string + * + * @throws RuntimeException If the extending class does not override this method. + */ + public static function action() { + $class = static::class; + throw new RuntimeException( "Class {$class} should override the `action` method to define its own unique identifier." ); + } + + /** + * Handles the process request if valid and if authorized. + * + * @since 4.7.23 + * @since 4.9.5 Pulled the `maybe_handle` implementation of the `WP_Async_Request` class. + * + * @param array|null $data_source A source of data if not provided in the request; used for + * cron-based fallback. + */ + public function maybe_handle( $data_source = null ) { + $data_source = (array) $data_source; + + if ( $this->feature_detection->supports_async_process() ) { + // Don't lock up other requests while processing. + session_write_close(); + + check_ajax_referer( $this->identifier, 'nonce' ); + + // Let's make sure to hydrate date from the request if not set. + if ( count( array_filter( $data_source ) ) < 1 ) { + $data_source = $_POST; + } + + do_action( + 'tribe_log', + 'debug', + $this->identifier, + [ + 'action' => 'async_handling', + 'data_source' => $data_source, + 'payload' => $_POST, + ] + ); + + $this->handle( $data_source ); + + wp_die(); + } + + /* + * If the environment does not support AJAX-based async processing then + * fallback to use the cron-based approach and just call the handle method + * removing it first from the action to avoid multiple calls. + */ + wp_clear_scheduled_hook( $this->healthcheck_cron_hook_id, [ $data_source ] ); + + do_action( + 'tribe_log', + 'debug', + $this->identifier, + array_merge( [ 'action' => 'cron_handling' ], $data_source ) ); + + $this->handle( $data_source ); + } + + /** + * Overrides the base `dispatch` method to allow for constants and/or environment vars to run + * async requests in sync mode. + * + * @since 4.7.12 + * @since 4.9.5 Pulled `dispatch` method logic from the `WP_Async_Request` class. + * + * @return mixed + */ + public function dispatch() { + if ( + ( defined( 'TRIBE_NO_ASYNC' ) && true === TRIBE_NO_ASYNC ) + || true === (bool) getenv( 'TRIBE_NO_ASYNC' ) + ) { + do_action( 'tribe_log', 'debug', $this->identifier, [ 'action' => 'sync_handle', 'data' => $this->data ] ); + + return $this->sync_handle( $this->data ); + } + + if ( $this->feature_detection->supports_async_process() ) { + $url = add_query_arg( $this->get_query_args(), $this->get_query_url() ); + $args = $this->get_post_args(); + + do_action( 'tribe_log', 'debug', $this->identifier, [ 'action' => 'async_dispatch', 'data' => $this->data ] ); + + return wp_remote_post( esc_url_raw( $url ), $args ); + } + + /* + * If async AJAX-based processing is not available then we "dispatch" + * by scheduling a single cron event immediately (as soon as possible) + * for this handler cron identifier. + */ + if ( ! wp_next_scheduled( $this->healthcheck_cron_hook_id, [ $this->data ] ) ) { + // Schedule the event to happen as soon as possible. + $scheduled = wp_schedule_single_event( time() - 1, $this->healthcheck_cron_hook_id, [ $this->data ] ); + + if ( false === $scheduled ) { + /** @var Tribe__Log__Logger $logger */ + $logger = tribe( 'logger' ); + $class = get_class( $this ); + $src = call_user_func( [ $class, 'action' ] ); + $logger->log( 'Could not schedule event for cron-based handling', Tribe__Log::ERROR, $src ); + + do_action( + 'tribe_log', + 'error', + $this->identifier, + [ 'action' => 'schedule_cron', 'data' => $this->data ] + ); + } + + do_action( + 'tribe_log', + 'debug', + $this->identifier, + [ 'action' => 'schedule_cron', 'data' => $this->data ] + ); + } + + return true; + } + + /** + * Handles the process immediately, not in an async manner. + * + * @since 4.7.12 + * + * @param array|null $data_source If not provided the method will read the handler data from the + * request array. + * + * @return mixed|null The result of the synchronous handling. + */ + abstract public function sync_handle( array $data_source = null ); + + /** + * Returns an array of arguments that will be used to send the POST request. + * + * @since 4.9.5 Pulled from the `WP_Async_Request` class. + * + * @return array An array of arguments for the POST request. + */ + protected function get_query_args() { + if ( null !== $this->query_args ) { + return $this->query_args; + } + + return [ + 'action' => $this->identifier, + 'nonce' => wp_create_nonce( $this->identifier ), + ]; + } + + /** + * Returns the URL that wil be used to post the request. + * + * @since 4.9.5 Pulled from the `WP_Async_Request` class. + * + * @return string The URL that will be used to POST the dispatch request; defaults + * to the `admin-ajax.php` one. + */ + protected function get_query_url() { + if ( null !== $this->query_url ) { + return $this->query_url; + } + + return admin_url( 'admin-ajax.php' ); + } + + /** + * Returns the arguments that will be used to send the POST request. + * + * @since 4.9.5 Pulled from the `WP_Async_Request` class. + * + * @return array An array of arguments that will be used to send the POST request. + */ + protected function get_post_args() { + if ( null !== $this->post_args ) { + return $this->post_args; + } + + return [ + 'timeout' => 0.01, + 'blocking' => false, + 'body' => $this->data, + 'cookies' => $_COOKIE, + 'sslverify' => apply_filters( 'https_local_ssl_verify', false ), + ]; + } + + /** + * Returns this handler cron hook identifier. + * + * The handler cron hook identifier is the one that the handler + * will use to schedule a single cron event when the `dispatch` + * method is called and the environment does not support async + * processing. + * + * @since 4.7.23 + * + * @return string The complete cron hook name (identifier) for + * this handler. + */ + public function get_healthcheck_cron_hook_id() { + return $this->healthcheck_cron_hook_id; + } + + /** + * Sets the that will be used during the request. + * + * @since 4.9.5 Pulled from the `WP_Async_Request` class. + * + * @param array $data Data. + * + * @return $this This handler instance. + */ + public function data( $data ) { + $this->data = $data; + + return $this; + } + + /** + * Handles the request and performs an action. + * + * @since 4.9.5 Pulled from the `WP_Async_Request` class. + * + * @param array|null $data_source A source of data if not provided in the request; used for + * cron-based fallback. + * + * @return null|array Depending on the context of the call, cron or async, either the result + * of the handling (cron) or nothing (async). + */ + abstract protected function handle( array $data_source = null ); +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Process/Post_Thumbnail_Setter.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Process/Post_Thumbnail_Setter.php new file mode 100644 index 000000000..f019d1dfa --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Process/Post_Thumbnail_Setter.php @@ -0,0 +1,176 @@ +set_post_id( $post_id ); + * $post_thumbnail_setter->set_post_thumbnail( 'http://foo.com/random-image.jpg' ); + * $post_thumbnail_setter->dispatch(); + * + * @since 4.7.12 + */ +class Tribe__Process__Post_Thumbnail_Setter extends Tribe__Process__Handler { + /** + * @var int The ID of the post the post thumbnail should be assigned to. + */ + protected $post_id; + + /** + * @var int|string Either the ID of an attachment that should be set as the post thumbnail + * or the full URL, or file path, to it. + */ + protected $post_thumbnail; + + /** + * {@inheritdoc} + */ + public static function action() { + return 'post_thumbnail_setter'; + } + + /** + * {@inheritdoc} + */ + public function dispatch() { + if ( ! isset( $this->post_id, $this->post_thumbnail ) ) { + // since this is a developer error we are not localizing this error string + throw new InvalidArgumentException( 'Post ID and featured image should be set before trying to dispatch.' ); + } + + $data = [ + 'post_id' => $this->post_id, + 'post_thumbnail' => trim( $this->post_thumbnail ), + ]; + + $this->data( $data ); + + do_action( 'tribe_log', 'debug', $this->identifier, $data ); + + return parent::dispatch(); + } + + /** + * Sets the ID of the post the post thumbnail (aka "featured image") should be attached + * and set for. + * + * @since 4.7.12 + * + * @param int $post_id The target post ID. + */ + public function set_post_id( $post_id ) { + $this->post_id = $post_id; + } + + /** + * Sets the post thumbnail ID or source the process should set. + * + * @since 4.7.12 + * + * @param int|string $post_thumbnail Either an attachment ID or the full URL, or path, to + * the post thumbnail image. + */ + public function set_post_thumbnail( $post_thumbnail ) { + $this->post_thumbnail = $post_thumbnail; + } + + /** + * Handles the post thumbnail setting async process. + * + * The post thumbnail will be uploaded, if not uploaded already, using the `tribe_upload_image` function. + * This method is an alias of the publicly accessible `sync_handle` one. + * + * @since 4.7.12 + * + * @param array|null $data_source An optional source of data. + * + * @see Tribe__Process__Post_Thumbnail_Setter::sync_handle() + * + * @see tribe_upload_image() + */ + protected function handle( array $data_source = null ) { + $this->sync_handle( $data_source ); + } + + /** + * {@inheritdoc} + */ + public function sync_handle( array $data_source = null ) { + do_action( 'tribe_log', 'debug', $this->identifier, [ 'status' => 'handling request' ] ); + + $data_source = isset( $data_source ) ? $data_source : $_POST; + + if ( ! isset( $data_source['post_id'], $data_source['post_thumbnail'] ) ) { + do_action( 'tribe_log', 'error', $this->identifier, [ 'data' => $data_source, ] ); + + return 0; + } + + $id = filter_var( $data_source['post_id'], FILTER_SANITIZE_NUMBER_INT ); + $post_thumbnail = tec_sanitize_string( $data_source['post_thumbnail'] ); + + do_action( 'tribe_log', 'debug', $this->identifier, [ + 'status' => 'fetching thumbnail', + 'post_thumbnail' => $post_thumbnail, + 'post_id' => $id, + ] ); + + $thumbnail_id = tribe_upload_image( $post_thumbnail ); + + if ( false === $thumbnail_id ) { + do_action( + 'tribe_log', + 'error', + $this->identifier, + [ + 'action' => 'fetch', + 'post_thumbnail' => $post_thumbnail, + 'post_id' => $id, + 'status' => 'could not fetch', + ] + ); + + return 0; + } + + $set = true; + if ( (int) get_post_thumbnail_id( $id ) !== (int) $thumbnail_id ) { + $set = set_post_thumbnail( $id, $thumbnail_id ); + } + + if ( false === $set ) { + do_action( + 'tribe_log', + 'error', + $this->identifier, + [ + 'action' => 'set', + 'post_thumbnail' => $post_thumbnail, + 'attachment_id' => $thumbnail_id, + 'post_id' => $id, + 'status' => 'unable to set thumbnail', + ] + ); + + return $thumbnail_id; + } + + do_action( + 'tribe_log', + 'debug', + $this->identifier, + [ + 'action' => 'set', + 'post_thumbnail' => $post_thumbnail, + 'attachment_id' => $thumbnail_id, + 'post_id' => $id, + 'status' => 'completed - attachment created and linked to the post', + ] + ); + + return $thumbnail_id; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Process/Queue.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Process/Queue.php new file mode 100644 index 000000000..b1dc37fcc --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Process/Queue.php @@ -0,0 +1,1129 @@ +action = call_user_func( [ $class, 'action' ] ); + $this->feature_detection = tribe( 'feature-detection' ); + + parent::__construct(); + + $this->healthcheck_cron_hook_id = $this->identifier . '_cron'; + $this->healthcheck_cron_interval_id = $this->identifier . '_cron_interval'; + + add_action( $this->healthcheck_cron_hook_id, [ $this, 'handle_cron_healthcheck' ] ); + add_filter( 'cron_schedules', [ $this, 'schedule_cron_healthcheck' ] ); + + /* + * This object might have been built while processing crons so + * we hook on the the object cron identifier to handle the task + * if the cron-triggered action ever fires. + */ + add_action( $this->identifier, [ $this, 'maybe_handle' ] ); + } + + /** + * Stops a queue that might be running. + * + * The queue process results are not rolled back (e.g. 200 posts to create, stopped + * after 50, those 50 posts will persist). + * + * @since 4.7.12 + * + * @param string $queue_id The unique identifier of the queue that should be stopped. + * + * @see Tribe__Process__Queue::save() to get the queue unique id. + * + * @return bool Whether the queue was correctly stopped, and its information + * deleted, or not. + */ + public static function stop_queue( $queue_id ) { + $meta = (array) get_transient( $queue_id . '_meta' ); + delete_transient( $queue_id . '_meta' ); + + if ( ! empty( $meta['identifier'] ) ) { + delete_transient( $meta['identifier'] . '_process_lock' ); + } + + return delete_option( $queue_id ); + } + + /** + * Whether a queue process is stuck or not. + * + * A queue process that has not been doing anything for an amount + * of time is considered "stuck". + * + * @since 4.7.18 + * + * @param string $queue_id The queue process unique identifier. + * + * @return bool + */ + public static function is_stuck( $queue_id ) { + $queue_status = self::get_status_of( $queue_id ); + $is_stuck = false; + + /** + * Filters the maximum allowed time a queue process can go without updates + * before being considered stuck. + * + * @since 4.7.18 + * + * @param int $time_limit A value in seconds, defaults to 5'. + */ + $limit = (float) apply_filters( 'tribe_process_queue_time_limit', 300 ); + + if ( ! empty( $queue_status['last_update'] ) && is_numeric( $queue_status['last_update'] ) ) { + $is_stuck = time() - (int) $queue_status['last_update'] > $limit; + } else { + $queue_status['last_update'] = time(); + set_transient( $queue_id . '_meta', $queue_status->to_array(), DAY_IN_SECONDS ); + } + + /** + * Filters whether a queue is considered "stuck" or not. + * + * @since 4.7.18 + * + * @param bool $is_stuck + * @param string $queue_id + * @param Tribe__Data $queue_status + */ + return apply_filters( 'tribe_process_queue_is_stuck', $is_stuck, $queue_id, $queue_status ); + } + + /** + * Returns a queue status and information. + * + * @since 4.7.12 + * + * @param string $queue_id + * + * @return Tribe__Data An object containing information about the queue. + * + * @see Tribe__Process__Queue::save() to get the queue unique id. + */ + public static function get_status_of( $queue_id ) { + $meta = (array) get_transient( $queue_id . '_meta' ); + $data = [ + 'identifier' => $queue_id, + 'done' => (int) Tribe__Utils__Array::get( $meta, 'done', 0 ), + 'total' => (int) Tribe__Utils__Array::get( $meta, 'total', 0 ), + 'fragments' => (int) Tribe__Utils__Array::get( $meta, 'fragments', 0 ), + 'last_update' => (int) Tribe__Utils__Array::get( $meta, 'last_update', false ), + ]; + + return new Tribe__Data( $data, 0 ); + } + + /** + * Deletes all queues for a specific action. + * + * @since 4.7.19 + * + * @param string $action The action (prefix) of the queues to delete. + * + * @return int The number of delete queues. + */ + public static function delete_all_queues( $action ) { + global $wpdb; + + $action = $wpdb->esc_like( 'tribe_queue_' . $action ) . '%'; + + $queues = $wpdb->get_col( $wpdb->prepare( " + SELECT DISTINCT(option_name) + FROM {$wpdb->options} + WHERE option_name LIKE %s + ", $action ) ); + + if ( empty( $queues ) ) { + return 0; + } + + $deleted = 0; + + foreach ( $queues as $queue ) { + $deleted ++; + self::delete_queue( $queue ); + } + + return $deleted; + } + + /** + * {@inheritdoc} + */ + public function delete( $key ) { + self::delete_queue( $key ); + + return $this; + } + + /** + * Deletes a queue batch(es) and meta information. + * + * @since 4.7.18 + * + * @param string $key + */ + public static function delete_queue( $key ) { + global $wpdb; + + $meta_key = $key . '_meta'; + + $key = $wpdb->esc_like( $key ) . '%'; + + $wpdb->query( $wpdb->prepare( " + DELETE + FROM {$wpdb->options} + WHERE option_name LIKE %s + ", $key ) ); + + delete_transient( $meta_key ); + } + + /** + * Upates the queue and meta data for the process. + * + * @since 4.7.12 + * @since 4.9.5 Pulled method from the `WP_Background_Process` class. + * + * @param string $key The key of the data to save. + * @param array $data The data to save. + * + * @return $this This process instance. + */ + public function update( $key, $data ) { + $meta_key = $this->get_meta_key( $key ); + $meta = (array) get_transient( $meta_key ); + $done = $this->original_batch_count - count( $data ); + + $update_data = array_merge( $meta, [ + 'done' => $meta['done'] + $done, + 'last_update' => time(), + ] ); + + /** + * Filters the information that will be updated in the database for this queue type. + * + * @since 4.7.12 + * + * @param array $update_data + * @param self $this + */ + $update_data = apply_filters( "tribe_process_queue_{$this->identifier}_update_data", $update_data, $this ); + + set_transient( $meta_key, $update_data, DAY_IN_SECONDS ); + + if ( ! empty( $data ) ) { + update_option( $key, $data ); + } + + return $this; + } + + /** + * Returns the name of the transient that will store the queue meta information + * for the specific key. + * + * @since 4.7.12 + * + * @param string $key + * + * @return string + */ + public function get_meta_key( $key ) { + $key = preg_replace( '/^(.*)_\\d+$/', '$1', $key ); + + return $key . '_meta'; + } + + /** + * {@inheritdoc} + */ + public function save() { + $key = $this->generate_key(); + + $fragments_count = $this->save_split_data( $key, $this->data ); + + $save_data = [ + 'identifier' => $this->identifier, + 'done' => 0, + 'total' => count( $this->data ), + 'fragments' => $fragments_count, + 'last_update' => time(), + ]; + + /** + * Filters the information that will be saved to the database for this queue type. + * + * @since 4.7.12 + * + * @param array $save_data + * @param self $this + */ + $save_data = apply_filters( "tribe_process_queue_{$this->identifier}_save_data", $save_data, $this ); + + set_transient( $this->get_meta_key( $key ), $save_data ); + + $this->did_save = true; + $this->id = $key; + + return $this; + } + + /** + * Generates the unique key for the queue optionally using the client provided + * id. + * + * @since 4.7.12 + * + * @param int $length The lengthy of the key to generate, longer keys will + * add more entropy; default to 64. + * + * @return string The generated batch key. + */ + protected function generate_key( $length = 64 ) { + if ( empty( $this->id_base ) ) { + $this->id_base = md5( microtime() . mt_rand() ); + } + + $prepend = $this->identifier . '_batch_'; + + $this->batch_key = substr( $prepend . $this->id_base, 0, $length ); + + return $this->batch_key; + } + + /** + * Saves the queue data to the database taking max_packet_size into account. + * + * In some instances the serialized size of the data might be bigger than the + * database `max_packet_size`; trying to write all the data in one query would + * make the db "go away...". + * Here we try to read the database `max_packet_size` setting and use that information + * to avoid overloading the query. + * + * @param string $key + * @param array $data + * + * @return int The number of fragments the data was split and stored into. + */ + protected function save_split_data( $key, array $data ) { + if ( empty( $data ) ) { + return 0; + } + + $max_frag_size = $this->get_max_frag_size(); + // we add a 15% to the size to take the serialization and query overhead into account when fragmenting + $serialized_size = strlen( utf8_decode( maybe_serialize( $data ) ) ) * 1.15; + $frags_count = (int) ceil( $serialized_size / $max_frag_size ); + $per_frag = max( (int) floor( count( $data ) / $frags_count ), 1 ); + + $split_data = array_chunk( $data, $per_frag ); + + if ( empty( $split_data ) ) { + return 0; + } + + foreach ( $split_data as $i => $iValue ) { + $postfix = 0 === $i ? '' : "_{$i}"; + update_option( $key . $postfix, $split_data[ $i ], false ); + } + + return count( $split_data ); + } + + /** + * Returns the max frag size in bytes. + * + * The bottleneck here is the database `max_packet_size` so we try to read + * it from the database. + * + * @return int The max size, in bytes, of a data fragment. + */ + protected function get_max_frag_size() { + if ( ! empty( $this->max_frag_size ) ) { + return $this->max_frag_size; + } + + return tribe( 'db' )->get_max_allowed_packet_size(); + } + + /** + * Sets the maximum size, in bytes, of the queue fragments. + * + * This will prevent the class from trying to read the value from the database. + * + * @since 4.7.12 + * + * @param int $max_frag_size + */ + public function set_max_frag_size( $max_frag_size ) { + $this->max_frag_size = $max_frag_size; + } + + /** + * Returns the queue unique identifier. + * + * Mind that an id will only be available after saving a queue. + * + * @since 4.7.12 + * + * @return string + * @throws RuntimeException if trying to get the queue id before saving it. + */ + public function get_id() { + if ( null === $this->id ) { + // not localized as this is a developer-land error + throw new RuntimeException( 'Can only get the id of queue after saving it.' ); + } + + return $this->id; + } + + /** + * Sets the queue unique id. + * + * When using this method the client code takes charge of the queue id uniqueness; + * the class will not check it. + * + * @since 4.7.12 + * + * @param string $queue_id + * + * @throws RuntimeException If trying to set the queue id after saving it. + */ + public function set_id( $queue_id ) { + if ( $this->did_save ) { + throw new RuntimeException( 'The queue id can be set only before saving it.' ); + } + + $queue_id = preg_replace( '/^' . preg_quote( $this->identifier, '/' ) . '_batch_/', '', $queue_id ); + + $this->id_base = $queue_id; + } + + /** + * Overrides the base `dispatch` method to allow for constants and/or environment vars to run + * async requests in sync mode. + * + * @since 4.7.12 + * @since 4.9.5 Pulled method code from the `WP_Background_Process` class. + * + * @return mixed + */ + public function dispatch() { + if ( + ( defined( 'TRIBE_NO_ASYNC' ) && true === TRIBE_NO_ASYNC ) + || true === (bool) getenv( 'TRIBE_NO_ASYNC' ) + || (bool) tribe_get_request_var( 'tribe_queue_sync', false ) + || tribe_is_truthy( tribe_get_option( 'tribe_queue_sync', false ) ) + ) { + $result = $this->sync_process(); + $this->complete(); + + return $result; + } + + if ( $this->feature_detection->supports_async_process() ) { + // Schedule the cron health-check. + $this->schedule_event(); + + // Perform remote post. + return parent::dispatch(); + } + + /* + * If async AJAX-based processing is not available then we "dispatch" + * by scheduling a single cron event immediately (as soon as possible) + * for this handler cron identifier. + */ + if ( ! wp_next_scheduled( $this->identifier ) ) { + // Schedule the event to happen as soon as possible. + $scheduled = wp_schedule_single_event( time() - 1, $this->identifier ); + + if ( false === $scheduled ) { + /** @var Tribe__Log__Logger $logger */ + $logger = tribe( 'logger' ); + $class = get_class( $this ); + $src = call_user_func( [ $class, 'action' ] ); + $logger->log( 'Could not schedule event for cron-based processing', Tribe__Log::ERROR, $src ); + } + } + + return true; + } + + /** + * Handles the process immediately, not in an async manner. + * + * @since 4.7.12 + * + * @return array An array containing the result of each item handling. + */ + public function sync_process() { + $result = []; + $this->doing_sync = true; + + foreach ( $this->data as $item ) { + $result[] = $this->task( $item ); + } + + return $result; + } + + /** + * Returns the name of the option used by the queue to store its batch(es). + * + * Mind that this value will be set only when first saving the queue and it will not be set + * in following queue processing. + * + * @since 4.7.12 + * + * @param int $n The number of a specific batch option name to get; defaults to `0` to get the + * option name of the first one. + * + * @return string + * + * @throws RuntimeException If trying to get the value before saving the queue or during following + * processing. + */ + public function get_batch_key( $n = 0 ) { + if ( null === $this->batch_key || ! $this->did_save ) { + throw new RuntimeException( 'The batch key will only be set after the queue is first saved' ); + } + + return empty( $n ) ? $this->batch_key : $this->batch_key . '_' . (int) $n; + } + + /** + * Returns the queue action identifier. + * + * @since 4.9.5 Pulled from the `WP_Background_Process` class. + * + * @return string The queue action identifier. + */ + public function get_identifier() { + return $this->identifier; + } + + /** + * Returns a batch of items to process from the queue. + * + * @since 4.7.12 + * @since 4.9.5 Pulled method code from the `WP_Background_Process` class. + * + * @return stdClass The first batch of items from the queue. + */ + protected function get_batch() { + global $wpdb; + + $key = $wpdb->esc_like( $this->identifier . '_batch_' ) . '%'; + + $query = $wpdb->get_row( $wpdb->prepare( " + SELECT * + FROM {$wpdb->options} + WHERE option_name LIKE %s + ORDER BY option_id ASC + LIMIT 1 + ", $key ) ); + + $batch = new stdClass(); + $batch->key = $query->option_name; + $batch->data = maybe_unserialize( $query->option_value ); + + $this->original_batch_count = ! empty( $batch->data ) ? count( $batch->data ) : 0; + + return $batch; + } + + /** + * {@inheritdoc} + */ + protected function get_post_args() { + $post_args = parent::get_post_args(); + + /** + * While sending the data into the body makes sense for the async process it does + * not make sense when processing a queue since the data will be stored and read + * from the database; furthermore this could raise issues with the max POST size. + */ + $post_args['body'] = []; + + return $post_args; + } + + /** + * Maybe handle the process request in async or sync mode depending on the + * supported mode. + * + * @param array|null $data_source An optional data source. + * + * @since 4.9.5 + */ + public function maybe_handle( $data_source = null ) { + // Don't lock up other requests while processing + session_write_close(); + + if ( $this->feature_detection->supports_async_process() ) { + return $this->maybe_handle_async(); + } + + return $this->maybe_handle_sync(); + } + + /** + * Push an item to the process queue. + * + * @since 4.9.5 Pulled from the `WP_Background_Process` class. + * + * @param mixed $data An item to process. + * + * @return $this This process instance. + */ + public function push_to_queue( $data ) { + $this->data[] = $data; + + return $this; + } + + /** + * Maybe handle this process request in async mode. + * + * @since 4.9.5 + */ + protected function maybe_handle_async() { + if ( $this->is_process_running() ) { + // Background process already running. + wp_die(); + } + + if ( $this->is_queue_empty() ) { + // No data to process: we're done. + $this->complete(); + wp_die(); + } + + check_ajax_referer( $this->identifier, 'nonce' ); + + $this->handle(); + + wp_die(); + } + + /** + * Handle the process request in sync mode. + * + * @since 4.9.5 + */ + protected function maybe_handle_sync() { + if ( $this->is_process_running() ) { + // Background process already running. + return; + } + + if ( $this->is_queue_empty() ) { + // No data to process: we're done. + $this->complete(); + + return; + } + + $this->handle(); + } + + /** + * Checks whether the queue is empty or not. + * + * @since 4.9.5 Pulled from the `WP_Background_Process` class. + * + * @return bool Whether the queue is empty or not. + */ + protected function is_queue_empty() { + global $wpdb; + + $key = $wpdb->esc_like( $this->identifier . '_batch_' ) . '%'; + + $count = $wpdb->get_var( $wpdb->prepare( " + SELECT COUNT(*) + FROM {$wpdb->options} + WHERE option_name LIKE %s + ", $key ) ); + + return $count <= 0; + } + + /** + * Checks whether the process is currently running or not. + * + * @since 4.9.5 Pulled from the `WP_Background_Process` class. + */ + protected function is_process_running() { + if ( get_transient( $this->identifier . '_process_lock' ) ) { + return true; + } + + return false; + } + + /** + * Locks the process so that other instances cannot spawn and run. + * + * Lock the process so that multiple instances can't run simultaneously. + * Override if applicable, but the duration should be greater than that + * defined in the `time_exceeded()` method. + * + * @since 4.9.5 Pulled from the `WP_Background_Process` class. + */ + protected function lock_process() { + // Set start time of current process. + $this->start_time = time(); + + $lock_duration = $this->queue_lock_time; + + /** + * Filters the duration of the lock acquired by a process instance. + * + * The lock duration should be larger than the maximum time a process is allowed to run. + * + * @since 4.9.5 + * + * @param int $lock_duration The lock duration in seconds; defaults to one minute. + * @param static $this This process instance. + */ + $lock_duration = apply_filters( $this->identifier . '_queue_lock_time', $lock_duration, $this ); + + set_transient( $this->identifier . '_process_lock', microtime(), $lock_duration ); + } + + /** + * Releases the process lock so that other instances can spawn and run. + * + * @since 4.9.5 Pulled from the `WP_Background_Process` class. + * + * @return $this This process instance. + */ + protected function unlock_process() { + delete_transient( $this->identifier . '_process_lock' ); + + return $this; + } + + /** + * Handles the process request. + * + * Pass each queue item to the task handler, while remaining + * within server memory and time limit constraints. + * + * @since 4.9.5 Pulled from the `WP_Background_Process` class. + * + * @param array|null $data_source Unused and kept for compatibility with parent; the queue + * data is stored and read from the database. + */ + protected function handle( array $data_source = null ) { + $this->lock_process(); + + do { + $batch = $this->get_batch(); + + foreach ( $batch->data as $key => $value ) { + $task = $this->task( $value ); + + if ( false !== $task ) { + $batch->data[ $key ] = $task; + } else { + unset( $batch->data[ $key ] ); + } + + if ( $this->time_exceeded() || $this->memory_exceeded() ) { + // Batch limits reached. + break; + } + } + + // Update or delete current batch. + if ( ! empty( $batch->data ) ) { + $this->update( $batch->key, $batch->data ); + } else { + $this->delete( $batch->key ); + } + } while ( ! $this->time_exceeded() && ! $this->memory_exceeded() && ! $this->is_queue_empty() ); + + $this->unlock_process(); + + // Start next batch or complete process. + if ( ! $this->is_queue_empty() ) { + $this->dispatch(); + } else { + $this->complete(); + } + + if ( doing_action( $this->identifier ) ) { + /* + * We're probably acting in the context of a cron request or + * in the context of an explicitly triggered action: let's not + * die. + */ + return; + } + + wp_die(); + } + + /** + * Checks whether the memory limit was exceeded. + * + * Ensures the batch process never exceeds 90% + * of the maximum WordPress memory. + * + * @since 4.9.5 Pulled from the `WP_Background_Process` class. + * + * @return bool + */ + protected function memory_exceeded() { + $memory_limit = $this->get_memory_limit() * 0.9; // 90% of max memory + $current_memory = memory_get_usage( true ); + $return = false; + + if ( $current_memory >= $memory_limit ) { + $return = true; + } + + /** + * Filters whether the process did exceed the allowed memory limit or not. + * + * @since 4.9.5 + * + * @param bool $return Whether the process did exceed the allowed memory limit or not. + * @param static $this This process instance. + */ + return apply_filters( $this->identifier . '_memory_exceeded', $return, $this ); + } + + /** + * Returns the memory limit for this process. + * + * @since 4.9.5 Pulled from the `WP_Background_Process` class. + * + * @return int The memory limit in bytes. + */ + protected function get_memory_limit() { + if ( function_exists( 'ini_get' ) ) { + $memory_limit = ini_get( 'memory_limit' ); + } else { + // Sensible default. + $memory_limit = '128M'; + } + + if ( ! $memory_limit || -1 === (int) $memory_limit ) { + // Unlimited, set to 32GB. + $memory_limit = '32000M'; + } + + return (int) $memory_limit * 1024 * 1024; + } + + /** + * Checks whether the execution time was exceeded or not. + * + * Ensures the batch never exceeds a sensible time limit. + * A timeout limit of 30s is common on shared hosting. + * + * @since 4.9.5 Pulled from the `WP_Background_Process` class. + * + * @return bool Whether the execution time was exceeded or not. + */ + protected function time_exceeded() { + /** + * Filters the maximum time the process can operate before continuing in another + * request. + * We pick a safe default of 20 seconds but this value can be adjusted to suit the system + * timeout settings. + * + * @since 4.9.5 + * + * @param int $default_time_limit The time limit for the process. + * @param static $this This process instance. + */ + $time_limit = apply_filters( $this->identifier . '_default_time_limit', 20, $this ); + + $finish = $this->start_time + $time_limit; + $return = false; + + if ( time() >= $finish ) { + $return = true; + } + + /** + * Filters whether a process instance should be marked as having exceeded the time limit or not. + * + * @since 4.9.5 + * + * @param bool $return Whether the process did exceed the time limit or not. + * @param static $this This process instance. + */ + return apply_filters( $this->identifier . '_time_exceeded', $return ); + } + + /** + * Completes the processing, cleaning up after it. + * + * Override if applicable, but ensure that the below actions are + * performed, or, call parent::complete(). + * + * @since 4.9.5 Pulled from the `WP_Background_Process` class. + */ + protected function complete() { + // Unschedule the cron health-check. + $this->clear_scheduled_event(); + } + + /** + * Schedules a cron-based health-check to restart the queue if stuck. + * + * Filters the `cron_schedules` filter to add a check every 5 minutes. + * + * @since 4.9.5 Pulled from the `WP_Background_Process` class. + * + * @param mixed $schedules The cron schedules to check. + * + * @return mixed The updated cron schedules. + */ + public function schedule_cron_healthcheck( $schedules ) { + /** + * Filters the number of minutes to schedule the cron health-check. + * + * @since 4.9.5 + * + * @param int $interval The number of minutes to schedule the cron health-check; defaults to 5. + * @param static $this This process instance. + */ + $interval = apply_filters( $this->identifier . '_cron_interval', $this->healthcheck_cron_interval, $this ); + + // Adds every 5 minutes to the existing schedules. + $schedules[ $this->identifier . '_cron_interval' ] = [ + 'interval' => MINUTE_IN_SECONDS * $interval, + 'display' => sprintf( __( 'Every %d Minutes', 'tribe-common' ), $interval ), + ]; + + return $schedules; + } + + /** + * Handles the cron health-check. + * + * Restart the background process if not already running + * and data exists in the queue. + * + * @since 4.9.5 Pulled from the `WP_Background_Process` class. + */ + public function handle_cron_healthcheck() { + if ( $this->is_process_running() ) { + // Background process already running. + exit; + } + + if ( $this->is_queue_empty() ) { + // No data to process. + $this->clear_scheduled_event(); + exit; + } + + $this->handle(); + + exit; + } + + /** + * Schedules the cron health-check event. + * + * @since 4.9.5 Pulled from the `WP_Background_Process` class. + */ + protected function schedule_event() { + if ( ! wp_next_scheduled( $this->healthcheck_cron_hook_id ) ) { + wp_schedule_event( time(), $this->healthcheck_cron_interval_id, $this->healthcheck_cron_hook_id ); + } + } + + /** + * Clears the scheduled health-check cron event. + * + * @since 4.9.5 Pulled from the `WP_Background_Process` class. + */ + protected function clear_scheduled_event() { + $timestamp = wp_next_scheduled( $this->healthcheck_cron_hook_id ); + + if ( $timestamp ) { + wp_unschedule_event( $timestamp, $this->healthcheck_cron_hook_id ); + } + } + + /** + * Cancels the current process. + * + * Stops processing queue items and clean up. + * + * @since 4.9.5 Pulled from the `WP_Background_Process` class. + */ + public function cancel_process() { + if ( ! $this->is_queue_empty() ) { + $batch = $this->get_batch(); + + $this->delete( $batch->key ); + + wp_clear_scheduled_hook( $this->healthcheck_cron_hook_id ); + } + + } + + /** + * Executes the process task on a single item. + * + * Override this method to perform any actions required on each + * queue item. Return the modified item for further processing + * in the next pass through. Or, return false to remove the + * item from the queue. + * + * @since 4.9.5 Pulled from the `WP_Background_Process` class. + * + * @param mixed $item Queue item to iterate over. + * + * @return mixed + */ + abstract protected function task( $item ); + + /** + * Concrete implementation of the base handler method. + * + * Just a proxy to the `sync_process` method. + * + * @since 4.9.5 + * + * @param array|null $data_source If not provided the method will read the handler data from the + * request array. + * + * @return array|mixed|null The synchronous process result. + */ + public function sync_handle( array $data_source = null ) { + // In the base implementation the data source is unused and read from the database. + return $this->sync_process(); + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Process/Tester.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Process/Tester.php new file mode 100644 index 000000000..017f7e937 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Process/Tester.php @@ -0,0 +1,98 @@ +get_query_args(), $this->get_query_url() ); + $args = $this->get_post_args(); + + return wp_remote_post( esc_url_raw( $url ), $args ); + } + + /** + * An override of the method implemented by the base Tribe Handler + * class to make sure the processing is done in async mode. + * + * This is the same code as the base WP_Background_Process class. + * + * @since 4.7.23 + * + * @param null|array $data_source An optional data source. + */ + public function maybe_handle( $data_source = null ) { + // Don't lock up other requests while processing + session_write_close(); + + check_ajax_referer( $this->identifier, 'nonce' ); + + $this->handle(); + + wp_die(); + } + + /** + * The task this class will perform is just setting a transient. + * + * The transient existence will be used as a canary to detect if + * background processing is supported. + * + * @since 4.7.23 + * + * @param array|null $data_source Unused. + */ + protected function handle( array $data_source = null ) { + tec_timed_option()->set( static::TRANSIENT_NAME, 1, WEEK_IN_SECONDS ); + } + + /** + * Returns this handler action identifier. + * + * @since 4.7.23 + * + * @return string This handler action identifier. + */ + public static function action() { + return 'async_process_support_test'; + } +} \ No newline at end of file diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Promise.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Promise.php new file mode 100644 index 000000000..d3c1dc4b0 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Promise.php @@ -0,0 +1,309 @@ +save()->dispatch(); + * $promise_id = $promise->get_id(); + * + * The promise is really a background process in disguise and will work, for all + * intents and purposes, like one. + * + * @since 4.9.5 + */ + +class Tribe__Promise extends Tribe__Process__Queue { + + /** + * The action that will be done when the promise is done. + * + * @var string + */ + protected $resolved; + + /** + * An array of arguments that will be used to call a callback on completion. + * + * @var array + */ + protected $resolved_args; + + /** + * The action that will be done when the promise fails due to an error. + * + * @var string + */ + protected $rejected; + + /** + * An array of arguments that will be used to call a callback on failure. + * + * @var array + */ + protected $rejected_args; + + /** + * Whether this promise did resolve correctly or not. + * + * @var bool + */ + protected $resolved_correctly = true; + + /** + * Tribe__Promise constructor. + * + * @param string|array|Tribe__Utils__Callback $callback The callback that should run to perform the promise task. + * @param array $items The items to process, each item will be passed as first + * argument to the callback at run-time. + * @param array $extra_args An array of extra arguments that will be passed to the + * callback function. + */ + public function __construct( $callback = null, array $items = null, array $extra_args = [] ) { + parent::__construct(); + + if ( ! empty( $callback ) && ! empty( $items ) ) { + foreach ( $items as $target ) { + $item['callback'] = $callback; + $item['args'] = array_merge( [ $target ], $extra_args ); + $this->push_to_queue( $item ); + } + } + } + + /** + * {@inheritdoc} + */ + public static function action() { + return 'promise'; + } + + /** + * Sets a callback, and optional arguments, that will be called when the promise + * is resolved. + * + * The callback and arguments must be serializable and make sense in the context of, + * potentially, a different call from the one where this method is called. + * + * @since 4.9.5 + * + * @param callable|Tribe__Utils__Callback $resolved The callback to call on success. + * @param callable|Tribe__Utils__Callback $rejected The callback to call on failure. + * @param array $resolved_args The arguments that will be passed to the resolved + * callback. + * @param array $rejected_args The arguments that will be passed to the rejected + * callback. + * + * @return Tribe__Promise This promise. + * + * @throws LogicException If this method is called after saving the promise. + */ + public function then( $resolved, $rejected = null, array $resolved_args = null, array $rejected_args = null ) { + if ( $this->did_save ) { + throw new LogicException( 'The promise "then" method should be called before the "save" one' ); + } + + $this->resolved = $resolved; + $this->resolved_args = $resolved_args; + $this->rejected = $rejected; + $this->rejected_args = $rejected_args; + + foreach ( $this->data as &$item ) { + $item['resolved'] = $this->resolved; + $item['resolved_args'] = $this->resolved_args; + $item['rejected'] = $this->rejected; + $item['rejected_args'] = $this->rejected_args; + } + + return $this; + } + + /** + * Overrides the base method to save before dispatching. + * + * @since 4.9.5 + * + * @return mixed The dispatch return value. + */ + public function dispatch() { + if ( empty( $this->data ) ) { + $this->complete(); + + return null; + } + + if ( ! $this->did_save ) { + $this->save(); + } + + return parent::dispatch(); + } + + /** + * A custom error handler to log any error tha might happen while invoking a promise + * callback. + * + * @since 4.9.5 + * + * @param int $code The error code. + * @param string $error_message The error message. + * + * @see set_error_handler() + */ + public function error_handler( $code, $error_message ) { + $message = 'There was an error (' . $code . ') while invoking a promise callback:'; + $message .= "\n\t" . $error_message; + tribe( 'logger' )->log( $message, Tribe__Log::ERROR, __CLASS__ ); + } + + /** + * Performs the task associated with the promise. + * + * The promise is really just a flexible background process that + * + * @since 4.9.5 + * + * @param array $item The promise payload, keys: + * { + * @param callable|Tribe__Utils__Callback $callback The callback this promise will + * call to perform the task. + * @param array $args An array of arguments that will be passed to the callback. + * @param callable|Tribe__Utils__Callback $then The callback this promise will + * call when complete. + * @param array $then_args An array of arguments that will be passed to the then callback. + * } + * + * + * @return bool `true` if the task needs to run again, `false` if the task is complete. + */ + protected function task( $item ) { + if ( isset( $item['resolved'] ) ) { + $this->resolved = $item['resolved']; + if ( isset( $item['resolved_args'] ) ) { + $this->resolved_args = $item['resolved_args']; + } + } + + if ( isset( $item['rejected'] ) ) { + $this->rejected = $item['rejected']; + if ( isset( $item['rejected_args'] ) ) { + $this->rejected_args = $item['rejected_args']; + } + } + + $callback_args = isset( $item['args'] ) ? $item['args'] : null; + $done = $this->do_callback( $item['callback'], $callback_args ); + + // If we are done then return `false` to indicate "no need to run again". + return $done ? false : true; + } + + /** + * Overrides the base method to allow building promises on empty objects + * without actually writing to the database. + * + * A fake queue id is set for compatibility reasons. + * + * @since 4.9.5 + * + * @return Tribe__Process__Queue This object. + */ + public function save() { + if ( empty( $this->data ) ) { + $this->id = uniqid( 'promise_', true ); + + return $this; + } + + return parent::save(); + } + + /** + * Invokes a callback function with optional arguments. + * + * If the callback invocation results in an exception or error then the callback will return `true` + * and log. + * + * @since 4.9.5 + * + * @param callable|Tribe__Utils__Callback $callback The callback to call. + * @param array|null $callback_args An optional array of arguments to call the + * callback with. + * + * @return mixed The callback invocation return value. + */ + protected function do_callback( $callback, array $callback_args = null ) { + try { + set_error_handler( [ $this, 'error_handler' ] ); + + $callback = $this->unpack_callback( $callback ); + + if ( count( $callback_args ) ) { + $done = call_user_func_array( $callback, $callback_args ); + } else { + $done = call_user_func( $callback ); + } + + restore_error_handler(); + + return $done; + } catch ( Exception $e ) { + $message = 'Exception (' . get_class( $e ) . ') thrown while invoking a promise callback:'; + $message .= "\n\t" . $e->getMessage(); + tribe( 'logger' )->log( $message, Tribe__Log::ERROR, __CLASS__ ); + + $this->resolved_correctly = false; + $this->complete(); + + return true; + } + } + + /** + * Unpacks a callback returning a callable array for callbacks wrapped using the + * Tribe__Utils__Callback class. + * + * @since 4.9.5 + * + * @param string|array|Tribe__Utils__Callback $callback The callback to unpack. + * + * @return array|string A callable array of string. + */ + protected function unpack_callback( $callback ) { + if ( $callback instanceof Tribe__Utils__Callback ) { + $callback = [ tribe( $callback->get_slug() ), $callback->get_method() ]; + } + + return $callback; + } + + /** + * Overrides the base method to call the success callback on completion. + * + * @since 4.9.5 + */ + protected function complete() { + parent::complete(); + + if ( $this->resolved_correctly && null !== $this->resolved ) { + $callback_args = isset( $this->resolved_args ) ? $this->resolved_args : null; + $this->do_callback( $this->resolved, $callback_args ); + } elseif ( ! $this->resolved_correctly && null !== $this->rejected ) { + $callback_args = isset( $this->rejected_args ) ? $this->rejected_args : null; + $this->do_callback( $this->rejected, $callback_args ); + } + } + + /** + * An alias of the dispatch method to stick with the expected naming + * standard. + * + * @since 4.9.5 + * + * @return mixed The dispatch operation return value. + */ + public function resolve() { + return $this->dispatch(); + } +} \ No newline at end of file diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Promoter/Auth.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Promoter/Auth.php new file mode 100644 index 000000000..a7939e25a --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Promoter/Auth.php @@ -0,0 +1,112 @@ +connector = $connector; + } + + /** + * Register the promoter auth key as part of the settings in order to make it available into the REST API. + * + * @since 4.12.6 + * + * @return void + */ + public function register_setting() { + register_setting( + 'options', + 'tribe_promoter_auth_key', + [ + 'type' => 'string', + 'show_in_rest' => true, + 'description' => __( 'Promoter Key', 'tribe-common' ), + 'sanitize_callback' => 'sanitize_text_field', + ] + ); + } + + /** + * Add an update the KEY used for promoter during the connection. + * + * @since 4.9.12 + * + * @param $secret_key + * + * @return string + */ + public function filter_promoter_secret_key( $secret_key ) { + + _deprecated_function( __METHOD__, '4.12.6' ); + + return empty( $secret_key ) ? $this->generate_secret_key() : $secret_key; + } + + /** + * Authorize the request with the Promoter Connector. + * + * @since 4.9 + * + * @return bool Whether the request was authorized successfully. + */ + public function authorize_with_connector() { + $secret_key = $this->generate_secret_key(); + $promoter_key = tribe_get_request_var( 'promoter_key' ); + $license_key = tribe_get_request_var( 'license_key' ); + + // send request to auth connector + $result = $this->connector->authorize_with_connector( get_current_user_id(), $secret_key, $promoter_key, $license_key ); + + // If the secret was not stored correctly on Connector Application, remove it! + if ( ! $result ) { + delete_option( 'tribe_promoter_auth_key' ); + } + + return $result; + } + + /** + * Grab the WP constant and store it as the auth key, if none exists or is it empty + * it creates a dynamic one. + * + * @since 4.9.12 + * + * @since 4.9 + * + * @return string The secret key. + */ + public function generate_secret_key() { + + $salt = wp_generate_password( 6 ); + + if ( defined( 'AUTH_KEY' ) ) { + $key = AUTH_KEY; + } else { + $key = wp_generate_password( 25 ); + } + + $key = sha1( $salt . get_current_blog_id() . $key . get_bloginfo( 'url' ) ); + + update_option( 'tribe_promoter_auth_key', $key ); + + return $key; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Promoter/Connector.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Promoter/Connector.php new file mode 100644 index 000000000..e62d9fa7b --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Promoter/Connector.php @@ -0,0 +1,277 @@ +base_url() . 'connect'; + + $payload = [ + 'clientSecret' => $secret_key, + 'licenseKey' => $license_key, + 'userId' => $user_id, + ]; + + $token = TEC_JWT::encode( $payload, $promoter_key, 'HS256' ); + + $response = $this->make_call( $url, [ + 'body' => [ 'token' => $token ], + 'sslverify' => false, + ] ); + + return (bool) $response; + } + + /** + * Authenticate the current request user with the Auth Connector + * + * @param string $user_id User ID. + * + * @return bool|string User ID or if promoter is authorized then it return true like a valid user. + * + * @since 4.9 + */ + public function authenticate_user_with_connector( $user_id ) { + $this->authorized = false; + + // If user is already authenticated no need to move forward (wp-admin) and others. + if ( ! empty( $user_id ) ) { + $this->authorized = true; + return $user_id; + } + + $token = $this->get_token(); + + if ( empty( $token ) ) { + return $user_id; + } + + $url = $this->base_url() . 'connect/auth'; + + $response = $this->make_call( $url, [ + 'body' => [ 'token' => $token ], + 'sslverify' => false, + ] ); + + if ( ! $response ) { + return $user_id; + } + + $this->authorized = true; + + return $response; + } + + /** + * Get the token either from a request or a header + * + * @since 4.9.20 + * + * @return mixed + */ + protected function get_token() { + $request_token = $this->get_token_from_request(); + + return ( $request_token ) + ? sanitize_text_field( $request_token ) + : $this->get_token_from_headers(); + } + + /** + * Get the token from a Request variable if present, otherwise fallback to `null` + * + * @since 4.9.20 + * + * @return mixed + */ + protected function get_token_from_request() { + // Used in favor of tribe_get_request_var as at this point tribe_get_request_var is not defined. + return \Tribe__Utils__Array::get_in_any( + [ $_GET, $_POST, $_REQUEST ], + 'tribe_promoter_auth_token' + ); + } + + /** + * Get the token directly from a Bearer Authentication Header, for hosts that + * does not support large Query strings + * + * @since 4.9.20 + * + * @return mixed + */ + protected function get_token_from_headers() { + $headers = [ + 'HTTP_AUTHORIZATION', + 'REDIRECT_HTTP_AUTHORIZATION', + ]; + + foreach ( $headers as $header ) { + if ( empty( $_SERVER[ $header ] ) ) { + continue; + } + + list( $token ) = sscanf( $_SERVER[ $header ], 'Bearer %s' ); + + if ( $token ) { + return sanitize_text_field( $token ); + } + } + } + + /** + * Notify the Promoter app of changes within this system. + * + * @param int $post_id Post ID. + * + * @since 4.9 + */ + public function notify_promoter_of_changes( $post_id ) { + $post_type = get_post_type( $post_id ); + + if ( ! in_array( $post_type, [ 'tribe_events', 'tribe_tickets' ], true ) ) { + return; + } + + $secret_key = $this->get_secret_key(); + + if ( empty( $secret_key ) ) { + return; + } + + /** @var Tribe__Promoter__PUE $promoter_pue */ + $promoter_pue = tribe( 'promoter.pue' ); + $license_info = $promoter_pue->get_license_info(); + + if ( ! $license_info ) { + return; + } + + $license_key = $license_info['key']; + + $payload = [ + 'licenseKey' => $license_key, + 'sourceId' => $post_id instanceof WP_Post ? $post_id->ID : $post_id, + ]; + + $token = TEC_JWT::encode( $payload, $secret_key, 'HS256' ); + + $url = $this->base_url() . 'connect/notify'; + + $args = [ + 'body' => [ 'token' => $token ], + 'sslverify' => false, + ]; + + $this->make_call( $url, $args ); + } + + /** + * Get the value for the option `tribe_promoter_auth_key` + * + * @since 4.9.12 + * + * @return mixed + */ + public function get_secret_key() { + $secret_key = get_option( 'tribe_promoter_auth_key' ); + + /** + * @since 4.9.12 + * + * @param string $secret_key + */ + return apply_filters( 'tribe_promoter_secret_key', $secret_key ); + } + + /** + * Make the call to the remote endpoint. + * + * @since 4.9 + * + * @param array $args Data to send. + * + * @param string $url URL to send data to. + * + * @return string|false The response body or false if not successful. + * + */ + public function make_call( $url, $args ) { + $response = wp_remote_post( $url, wp_parse_args( $args, [ 'timeout' => 30 ] ) ); + $code = wp_remote_retrieve_response_code( $response ); + $body = wp_remote_retrieve_body( $response ); + + if ( $code > 299 || is_wp_error( $response ) ) { + do_action( + 'tribe_log', + 'debug', + __METHOD__, + [ + 'url' => $url, + 'args' => $args, + 'response' => $response, + 'response_code' => $code, + ] + ); + + return false; + } + + return $body; + } + + /** + * Check whether the user request is currently authorized by Promoter. + * + * @since 4.9.4 + * + * @return bool Whether the user request is currently authorized by Promoter. + */ + public function is_user_authorized() { + return $this->authorized; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Promoter/PUE.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Promoter/PUE.php new file mode 100644 index 000000000..0900c1d4e --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Promoter/PUE.php @@ -0,0 +1,96 @@ +pue_checker = new Tribe__PUE__Checker( 'http://tri.be/', $this->slug, [ + 'context' => 'service', + 'plugin_name' => __( 'Promoter', 'tribe-common' ), + ] ); + } + + /** + * Get whether service has a license and if the license is activated on network. + * + * @return array|false License information or false if not set. + * + * @since 4.9 + */ + public function get_license_info() { + $option_name = 'pue_install_key_' . $this->slug; + + $key = get_option( $option_name ); + + $is_network_key = false; + + if ( is_multisite() ) { + $network_key = get_network_option( null, $option_name ); + + if ( empty( $key ) ) { + $key = $network_key; + + $is_network_key = true; + } + } + + if ( empty( $key ) ) { + return false; + } + + return [ + 'key' => $key, + 'is_network_key' => $is_network_key, + ]; + } + + /** + * Check whether service has a license key set or not. + * + * @return bool Whether service has a license key set. + * + * @since 4.9 + */ + public function has_license_key() { + return ! empty( $this->get_license_info() ); + } + + /** + * Check whether service has a valid license key or not. + * + * @return bool Whether service has a valid license key. + * + * @since 4.9 + */ + public function has_valid_license() { + $license_info = $this->get_license_info(); + + if ( ! $license_info ) { + return false; + } + + $response = $this->pue_checker->validate_key( $license_info['key'], $license_info['is_network_key'] ); + + return isset( $response['status'] ) && 1 === (int) $response['status']; + } + +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Promoter/View.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Promoter/View.php new file mode 100644 index 000000000..f2fe93a82 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Promoter/View.php @@ -0,0 +1,96 @@ +set_template_origin( Tribe__Main::instance() ); + $this->set_template_folder( 'src/views/promoter' ); + $this->set_template_context_extract( true ); + $this->set_template_folder_lookup( true ); + } + + /** + * Add the rewrite rules and tags. + * + * @since 4.9 + */ + public function add_rewrites() { + add_rewrite_rule( 'tribe-promoter-auth/?$', 'index.php?tribe-promoter-auth-check=1', 'top' ); + add_rewrite_tag( '%tribe-promoter-auth-check%', '([^&]+)' ); + } + + /** + * Get the redirect URL for finishing onboarding + * + * @since 4.9.6 + * + * @return string Redirect URL for completing onboarding. + */ + public function authorized_redirect_url() { + $url = 'https://promoter.theeventscalendar.com/welcome/review'; + + if ( defined( 'TRIBE_PROMOTER_AUTHORIZED_REDIRECT_URL' ) ) { + $url = TRIBE_PROMOTER_AUTHORIZED_REDIRECT_URL; + } + + /** + * The url for redirecting in order to complete onboarding + * + * @since 4.9.6 + * + * @param string $url Redirect URL. + */ + return apply_filters( 'tribe_promoter_authorized_redirect_url', $url ); + } + + /** + * Display the auth check page when the correct permalink is loaded. + * + * @since 4.9 + */ + public function display_auth_check_view() { + global $wp_query; + + $promoter_key = tribe_get_request_var( 'promoter_key' ); + $license_key = tribe_get_request_var( 'license_key' ); + + if ( empty( $promoter_key ) || empty( $wp_query->query_vars['tribe-promoter-auth-check'] ) ) { + return; + } + + $is_admin = is_user_logged_in() && current_user_can( 'manage_options' ) && current_user_can( 'read_private_posts' ); + $authorized = false; + $auth_error = false; + + if ( $is_admin && ! empty( $_POST['promoter_authenticate'] ) ) { + /** @var Tribe__Promoter__Auth $promoter_auth */ + $promoter_auth = tribe( 'promoter.auth' ); + $authorized = $promoter_auth->authorize_with_connector(); + $auth_error = ! $authorized; + } + + if ( $authorized ) { + wp_redirect( esc_url_raw( $this->authorized_redirect_url() ) ); + } else { + $this->template( 'auth', [ + 'authorized' => $authorized, + 'auth_error' => $auth_error, + 'logged_in' => is_user_logged_in(), + 'admin' => $is_admin, + 'promoter_key' => $promoter_key, + 'license_key' => $license_key, + ], true ); + } + + tribe_exit(); + } + +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/REST/Endpoints/CREATE_Endpoint_Interface.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/REST/Endpoints/CREATE_Endpoint_Interface.php new file mode 100644 index 000000000..0b4b5eee2 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/REST/Endpoints/CREATE_Endpoint_Interface.php @@ -0,0 +1,29 @@ +message = $message; + $this->code = $code; + $this->status = $status; + } + + /** + * Return the error status. + * @return int + */ + public function getStatus() { + return $this->status; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/REST/Headers/Base_Header.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/REST/Headers/Base_Header.php new file mode 100644 index 000000000..a420ca07b --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/REST/Headers/Base_Header.php @@ -0,0 +1,19 @@ +base = $base; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/REST/Headers/Base_Interface.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/REST/Headers/Base_Interface.php new file mode 100644 index 000000000..4cf32f0ad --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/REST/Headers/Base_Interface.php @@ -0,0 +1,49 @@ +base->get_api_version_header() . ': disabled' ); + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/REST/Headers/Headers_Interface.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/REST/Headers/Headers_Interface.php new file mode 100644 index 000000000..554520867 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/REST/Headers/Headers_Interface.php @@ -0,0 +1,18 @@ +main = $main; + } + + /** + * Prints TEC REST API related meta on the site. + */ + public function add_header() { + $api_root = $this->base->get_rest_url(); + + if ( empty( $api_root ) ) { + return; + } + + printf( '', esc_attr( $this->base->get_api_version_meta_name() ), esc_attr( $this->main->get_version() ) ); + printf( '', esc_attr( $this->base->get_api_origin_meta_name() ), esc_url( $this->base->get_rest_origin_url() ) ); + printf( '', esc_attr( $this->main->get_reference_url() ), esc_url( $api_root ) ); + } + + /** + * Sends TEC REST API related headers. + */ + public function send_header() { + if ( headers_sent() ) { + return; + } + + $api_root = $this->base->get_rest_url(); + + if ( empty( $api_root ) ) { + return; + } + + header( $this->base->get_api_version_header() . ': ' . $this->main->get_version() ); + header( $this->base->get_api_root_header() . ': ' . esc_url_raw( $api_root ) ); + header( $this->base->get_api_origin_header() . ': ' . esc_url_raw( $this->base->get_rest_origin_url() ) ); + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/REST/Headers/Unsupported.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/REST/Headers/Unsupported.php new file mode 100644 index 000000000..af8e3b5a5 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/REST/Headers/Unsupported.php @@ -0,0 +1,39 @@ +main = $main; + } + + /** + * Prints TEC REST API related meta on the site. + */ + public function add_header() { + // no-op + } + + /** + * Sends TEC REST API related headers. + */ + public function send_header() { + if ( headers_sent() ) { + return; + } + + header( $this->base->get_api_version_header() . ': unsupported' ); + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/REST/Main.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/REST/Main.php new file mode 100644 index 000000000..5fcc28dcc --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/REST/Main.php @@ -0,0 +1,149 @@ +namespace; + } + + /** + * Returns the REST API URL prefix. + * + * @return string The REST API URL prefix. + */ + public function get_url_prefix() { + $use_builtin = $this->use_builtin(); + + if ( $use_builtin ) { + $prefix = rest_get_url_prefix(); + } else { + $prefix = apply_filters( 'rest_url_prefix', 'wp-json' ); + } + + $default_tec_prefix = $this->namespace . '/' . trim( $this->url_prefix(), '/' ); + $prefix = rtrim( $prefix, '/' ) . '/' . trim( $default_tec_prefix, '/' ); + + /** + * Filters the TEC REST API URL prefix + * + * @param string $prefix The complete URL prefix. + * @param string $default_tec_prefix The default URL prefix appended to the REST URL by The Events Calendar. + */ + return apply_filters( 'tribe_events_rest_url_prefix', $prefix, $default_tec_prefix ); + } + + /** + * Retrieves the URL to a TEC REST endpoint on a site. + * + * Note: The returned URL is NOT escaped. + * + * @global WP_Rewrite $wp_rewrite + * + * @param string $path Optional. TEC REST route. Default '/'. + * @param string $scheme Optional. Sanitization scheme. Default 'rest'. + * @param int $blog_id Optional. Blog ID. Default of null returns URL for current blog. + * + * @return string Full URL to the endpoint. + */ + public function get_url( $path = '/', $scheme = 'rest', $blog_id = null ) { + if ( empty( $path ) ) { + $path = '/'; + } + + $tec_path = '/' . trim( $this->namespace, '/' ) . $this->url_prefix() . '/' . ltrim( $path, '/' ); + + if ( $this->use_builtin() ) { + $url = get_rest_url( $blog_id, $tec_path, $scheme ); + } else { + if ( ( is_multisite() && get_blog_option( $blog_id, 'permalink_structure' ) ) || get_option( 'permalink_structure' ) ) { + global $wp_rewrite; + + if ( $wp_rewrite->using_index_permalinks() ) { + $url = get_home_url( $blog_id, $wp_rewrite->index . '/' . $this->get_url_prefix(), $scheme ); + } else { + $url = get_home_url( $blog_id, $this->get_url_prefix(), $scheme ); + } + + $url .= '/' . ltrim( $path, '/' ); + } else { + $url = get_home_url( $blog_id, 'index.php', $scheme ); + + $url = add_query_arg( 'rest_route', $tec_path, $url ); + } + + if ( is_ssl() ) { + // If the current host is the same as the REST URL host, force the REST URL scheme to HTTPS. + if ( $_SERVER['SERVER_NAME'] === parse_url( get_home_url( $blog_id ), PHP_URL_HOST ) ) { + $url = set_url_scheme( $url, 'https' ); + } + } + } + + /** + * Filters The Events Calendar REST URL. + * + * @param string $url TEC REST URL. + * @param string $path REST route. + * @param int $blog_id Blog ID. + * @param string $scheme Sanitization scheme. + */ + return apply_filters( 'tribe_rest_url', $url, $path, $blog_id, $scheme ); + } + + /** + * Whether built-in WP REST API functions and functionalities should/can be used or not. + * + * @return bool + */ + protected function use_builtin() { + /** + * Filters whether builtin WordPress REST API functions should be used or not if available. + */ + $use_builtin = apply_filters( 'tribe_events_rest_use_builtin', true ); + + return $use_builtin && function_exists( 'get_rest_url' ); + } + + /** + * Returns the REST API URL prefix that will be appended to the namespace. + * + * The prefix should be in the `/some/path` format. + * + * @return string + */ + abstract protected function url_prefix(); + + /** + * Returns the string indicating the REST API version. + * + * @return string + */ + abstract public function get_version(); + + /** + * Returns the URL where the API users will find the API documentation. + * + * @return string + */ + abstract public function get_reference_url(); +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/REST/Messages_Interface.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/REST/Messages_Interface.php new file mode 100644 index 000000000..13edb0360 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/REST/Messages_Interface.php @@ -0,0 +1,30 @@ + => ]` format. + */ + public function get_messages(); + + /** + * Prefixes a message slug with a common root. + * + * @param string $message_slug + * + * @return string The prefixed message slug. + */ + public function prefix_message_slug( $message_slug ); +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/REST/Post_Repository.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/REST/Post_Repository.php new file mode 100644 index 000000000..274e18327 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/REST/Post_Repository.php @@ -0,0 +1,107 @@ + $full_url, + 'id' => $image_id, + 'extension' => pathinfo( $file, PATHINFO_EXTENSION ), + ]; + + $metadata = wp_get_attachment_metadata( $image_id ); + + if ( + false !== $metadata + && isset( $metadata['image_meta'], $metadata['file'], $metadata['sizes'] ) + ) { + unset( $metadata['image_meta'], $metadata['file'] ); + + foreach ( $metadata['sizes'] as $size => &$meta ) { + $size_image_src = wp_get_attachment_image_src( $image_id, $size ); + $meta['url'] = ! empty( $size_image_src[0] ) ? $size_image_src[0] : ''; + unset( $meta['file'] ); + } + unset( $meta ); + + $data = array_filter( array_merge( $data, $metadata ) ); + } + + return $data; + } + + /** + * @param string $date A date string in a format `strtotime` can parse. + * + * @return array An array of date details for the end date; each entry will be + * empty if the date is empty. + */ + protected function get_date_details( $date ) { + if ( empty( $date ) ) { + return [ + 'year' => '', + 'month' => '', + 'day' => '', + 'hour' => '', + 'minutes' => '', + 'seconds' => '', + ]; + } + + $time = strtotime( $date ); + + return [ + 'year' => date( 'Y', $time ), + 'month' => date( 'm', $time ), + 'day' => date( 'd', $time ), + 'hour' => date( 'H', $time ), + 'minutes' => date( 'i', $time ), + 'seconds' => date( 's', $time ), + ]; + } + + /** + * Returns a localized and formatted list of cost values in ASC order. + * + * @since 4.7.19 + * + * @param array $cost_couples An array of cost couples in the [ => ] format. + * + * @return array + */ + protected function format_and_sort_cost_couples( array $cost_couples = [] ) { + global $wp_locale; + + $cost_values = []; + foreach ( $cost_couples as $key => $value ) { + $value = str_replace( + [ + $wp_locale->number_format['decimal_point'], + $wp_locale->number_format['thousands_sep'], + ], + [ '.', '' ], + '' . $value + ); + if ( is_numeric( $value ) ) { + $cost_values[] = $value; + } else { + $cost_values[] = $key; + } + } + + sort( $cost_values, SORT_NUMERIC ); + + return $cost_values; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/REST/Post_Repository_Interface.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/REST/Post_Repository_Interface.php new file mode 100644 index 000000000..096c8908f --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/REST/Post_Repository_Interface.php @@ -0,0 +1,14 @@ + 'post', + 'suppress_filters' => false, + 'posts_per_page' => -1, + ]; + + /** + * @var array A list of query modifiers that will trigger a overriding merge, thus + * replacing previous values, when set multiple times. + */ + protected static $replacing_modifiers = [ + 'p', + 'author', + 'author_name', + 'author__in', + 'author__not_in', + 'has_password', + 'post_password', + 'cat', + 'category__and', + 'category__in', + 'category__not_in', + 'category_name', + 'comment_count', + 'comment_status', + 'menu_order', + 'title', + 'title_like', + 'name', + 'post_name__in', + 'ping_status', + 'post__in', + 'post__not_in', + 'post_parent', + 'post_parent__in', + 'post_parent__not_in', + 'post_mime_type', + 's', + 'search', + 'tag', + 'tag__and', + 'tag__in', + 'tag__not_in', + 'tag_id', + 'tag_slug__and', + 'tag_slug__in', + 'ID', + 'id', + 'date', + 'after_date', + 'before_date', + 'date_gmt', + 'after_date_gmt', + 'before_date_gmt', + 'post_title', + 'post_content', + 'post_excerpt', + 'post_status', + 'to_ping', + 'post_modified', + 'post_modified_gmt', + 'post_content_filtered', + 'guid', + 'perm', + 'order', + ]; + + /** + * @var int + */ + protected static $meta_alias = 0; + + /** + * @var array A list of keys that denote the value to check should be cast to array. + */ + protected static $multi_value_keys = [ 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN' ]; + + /** + * @var array A map of SQL comparison operators to their human-readable counterpart. + */ + protected static $comparison_operators = [ + '=' => 'equals', + '!=' => 'not-equals', + '>' => 'gt', + '>=' => 'gte', + '<' => 'lt', + '<=' => 'lte', + 'LIKE' => 'like', + 'NOT LIKE' => 'not-like', + 'IN' => 'in', + 'NOT IN' => 'not-in', + 'BETWEEN' => 'between', + 'NOT BETWEEN' => 'not-between', + 'EXISTS' => 'exists', + 'NOT EXISTS' => 'not-exists', + 'REGEXP' => 'regexp', + 'NOT REGEXP' => 'not-regexp', + ]; + + /** + * A counter to keep track, on the class level, of the aliases generated for the terms table + * while building multi queries. + * + * @var int + */ + protected static $alias_counter = 1; + + /** + * @var string + */ + protected $filter_name = 'default'; + /** + * @var array The post IDs that will be updated. + */ + protected $ids = []; + /** + * @var bool Whether the post IDs to update have already been fetched or not. + */ + protected $has_ids = false; + /** + * @var array The updates that will be saved to the database. + */ + protected $updates = []; + + /** + * @var array A list of taxonomies this repository will recognize. + */ + protected $taxonomies = []; + + /** + * @var array A map detailing which fields should be converted from a + * GMT time and date to a local one. + */ + protected $to_local_time_map = [ + 'post_date_gmt' => 'post_date', + ]; + + /** + * @var array A map detailing which fields should be converted from a + * localized time and date to a GMT one. + */ + protected $to_gmt_map = [ + 'post_date' => 'post_date_gmt', + ]; + + /** + * @var array + */ + protected $default_args = [ 'post_type' => 'post' ]; + + /** + * @var array An array of query modifying callbacks populated while applying + * the filters. + */ + protected $query_modifiers = []; + + /** + * @var bool Whether the current query is void or not. + */ + protected $void_query = false; + + /** + * @var array An array of query arguments that will be populated while applying + * filters. + */ + protected $query_args = [ + 'meta_query' => [ 'relation' => 'AND' ], + 'tax_query' => [ 'relation' => 'AND' ], + 'date_query' => [ 'relation' => 'AND' ], + ]; + + /** + * @var array An array of query arguments that support 'relation'. + */ + protected $relation_query_args = [ + 'meta_query', + 'tax_query', + 'date_query', + ]; + + /** + * @var WP_Query The current query object built and modified by the instance. + */ + protected $current_query; + + /** + * @var array An associative array of the filters that will be applied and the used values. + */ + protected $current_filters = []; + + /** + * @var string|null The current filter being applied. + */ + protected $current_filter; + + /** + * @var Tribe__Repository__Query_Filters + */ + public $filter_query; + + /** + * @var string The filter that should be used to get a post by its primary key. + */ + protected $primary_key = 'p'; + + /** + * @var array A map of callbacks in the shape [ => ] + */ + protected $schema = []; + + /** + * @var array A map of schema slugs and their meta keys to be queried. + */ + protected $simple_meta_schema = []; + + /** + * @var array A map of schema slugs and their taxonomies to be queried. + */ + protected $simple_tax_schema = []; + + /** + * @var Tribe__Repository__Interface + */ + protected $main_repository; + + /** + * @var Tribe__Repository__Formatter_Interface + */ + protected $formatter; + + /** + * @var bool + */ + protected $skip_found_rows = true; + + /** + * @var Tribe__Repository__Interface + */ + protected $query_builder; + + /** + * A map relating aliases to their real update field name. + * + * E.g. the `title` alias might be an alias of `post_title` in update/save operations. + * This is done to allow using set-like methods with human-readable names. + * Extending classes should pre-fill this with default aliases. + * + * @var array + */ + protected $update_fields_aliases = [ + 'title' => 'post_title', + 'content' => 'post_content', + 'description' => 'post_content', + 'slug' => 'post_name', + 'excerpt' => 'post_excerpt', + 'status' => 'post_status', + 'parent' => 'post_parent', + 'author' => 'post_author', + 'date' => 'post_date', + 'date_gmt' => 'post_date_gmt', + 'date_utc' => 'post_date_gmt', + 'tag' => 'post_tag', + 'image' => '_thumbnail_id', + ]; + + /** + * The default create args that will be used by the repository + * to create posts of the managed type. + * + * @var + */ + protected $create_args; + + /** + * Indicates the current display context if any. + * Extending classes can support and use this property to know the + * display context. + * + * @var string + */ + protected $display_context = 'default'; + + /** + * Indicates the current render context if any. + * Extending classes can support and use this property to know the + * render context. + * + * @var string + */ + protected $render_context = 'default'; + + /** + * The query last built from the repository instance. + * + * @var WP_Query|null + */ + protected $last_built_query; + + /** + * The hash of the last built query. + * + * @var string + */ + protected $last_built_hash = ''; + + /** + * Tribe__Repository constructor. + * + * @since 4.7.19 + */ + public function __construct() { + $this->filter_query = new Tribe__Repository__Query_Filters(); + $this->default_args = array_merge( [ 'posts_per_page' => -1 ], $this->default_args ); + $post_types = (array) Tribe__Utils__Array::get( $this->default_args, 'post_type', [] ); + $this->taxonomies = get_taxonomies( [ 'object_type' => $post_types ], 'names' ); + + /** + * Allow plugins to init their classes and setup hooks at the initial setup of a repository. + * + * @param Tribe__Repository $this This repository instance + * + * @since 4.9.5 + */ + do_action( "tribe_repository_{$this->filter_name}_init", $this ); + } + + /** + * {@inheritdoc} + */ + public function get_default_args() { + return $this->default_args; + } + + /** + * {@inheritdoc} + */ + public function set_default_args( array $default_args ) { + $this->default_args = $default_args; + } + + /** + * Returns the value of a protected property. + * + * @since 4.7.19 + * + * @param string $name + * + * @return mixed|null + * @throws Tribe__Repository__Usage_Error If trying to access a non defined property. + */ + public function __get( $name ) { + if ( ! property_exists( $this, $name ) ) { + throw Tribe__Repository__Usage_Error::because_property_is_not_defined( $name, $this ); + } + + return $this->{$name}; + } + + /** + * Magic method to set protected properties. + * + * @since 4.7.19 + * + * @param string $name + * @param mixed $value + * + * @throws Tribe__Repository__Usage_Error As properties have to be set extending + * the class, using setter methods or via constructor injection + */ + public function __set( $name, $value ) { + throw Tribe__Repository__Usage_Error::because_properties_should_be_set_correctly( $name, $this ); + } + + /** + * Whether the class has a property with the specific name or not. + * + * @since 4.7.19 + * + * @param string $name + * + * @return bool + */ + public function __isset( $name ) { + return property_exists( $this, $name ) && isset( $this->{$name} ); + } + + /** + * {@inheritdoc} + */ + public function where( $key, $value = null ) { + $call_args = func_get_args(); + + return call_user_func_array( [ $this, 'by' ], $call_args ); + } + + /** + * {@inheritdoc} + */ + public function page( $page ) { + $this->query_args['paged'] = absint( $page ); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function per_page( $per_page ) { + // we allow for `-1` here + $this->query_args['posts_per_page'] = $per_page; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function count() { + if ( $this->void_query ) { + return 0; + } + + $query = $this->build_query(); + + // The request property will be set during the `get_posts` method and empty before it. + if ( ! empty( $query->request ) ) { + return (int) $query->post_count; + } + + $original_fields_value = $query->get( 'fields', '' ); + + $query->set( 'fields', 'ids' ); + + /** + * Filters the query object by reference before counting found posts in the current page. + * + * @since 4.7.19 + * + * @param WP_Query $query + */ + do_action( "tribe_repository_{$this->filter_name}_pre_count_posts", $query ); + + $ids = $query->get_posts(); + + $query->set( 'fields', $original_fields_value ); + + return is_array( $ids ) ? count( $ids ) : 0; + } + + /** + * {@inheritdoc} + */ + public function build_query( $use_query_builder = true ) { + $query = null; + + if ( array_key_exists( 'void_query', $this->query_args ) && false !== $this->query_args['void_query'] ) { + $this->void_query = true; + } + + // We'll let the query builder decide if the query has to be rebuilt or not. + if ( $use_query_builder && null !== $this->query_builder ) { + $query = $this->build_query_with_builder(); + } + + if ( null !== $this->last_built_query && $this->last_built_hash === $this->hash()) { + return $this->last_built_query; + } + + if ( null === $query ) { + $query = $this->build_query_internally(); + } + + /** + * Fires after the query has been built and before it's returned. + * + * @since 4.9.5 + * + * @param WP_Query $query The built query. + * @param array $query_args An array of query arguments used to build the query. + * @param Tribe__Repository $this This repository instance. + * @param bool $use_query_builder Whether a query builder was used to build this query or not. + * @param Tribe__Repository__Interface $query_builder The query builder in use, if any. + */ + do_action( "tribe_repository_{$this->filter_name}_query", + $query, + $this, + $use_query_builder, + $this->query_builder + ); + + $this->last_built_query = $query; + $this->last_built_hash = $this->hash(); + + return $query; + } + + /** + * {@inheritdoc} + */ + public function found() { + if ( $this->void_query ) { + return 0; + } + + $query = $this->build_query(); + + $original_no_found_rows_value = $query->get( 'no_found_rows' ); + + // The request property will be set during the `get_posts` method and empty before it. + if ( ! empty( $query->request ) && ( false === (boolean) $original_no_found_rows_value || ! $this->skip_found_rows ) ) { + return (int) $query->found_posts; + } + + $original_fields_value = $query->get( 'fields' ); + + $query->set( 'fields', 'ids' ); + $query->set( 'no_found_rows', false ); + + /** + * Filters the query object by reference before counting found posts. + * + * @since 4.7.19 + * + * @param WP_Query $query + */ + do_action( "tribe_repository_{$this->filter_name}_pre_found_posts", $query ); + + $query->get_posts(); + + $query->set( 'fields', $original_fields_value ); + $query->set( 'no_found_rows', $original_no_found_rows_value ); + + return (int) $query->found_posts; + } + + /** + * {@inheritdoc} + */ + public function all() { + if ( $this->void_query ) { + return []; + } + + $query = $this->build_query(); + + // The request property will be set during the `get_posts` method and empty before it. + if ( ! empty( $query->request ) ) { + return array_map( [ $this, 'format_item' ], $query->posts ); + } + + $original_fields_value = $query->get( 'fields', '' ); + + $return_ids = 'ids' === $original_fields_value; + + /** + * Do not skip counting the rows if we have some filtering to do on + * `found_posts`. + */ + $query->set( 'no_found_rows', $this->skip_found_rows ); + + // We'll let the class build the items later. + $query->set( 'fields', 'ids' ); + + /** + * Filters the query object by reference before getting the posts. + * + * @since 4.7.19 + * + * @param WP_Query $query + */ + do_action( "tribe_repository_{$this->filter_name}_pre_get_posts", $query ); + + $results = $query->get_posts(); + + /** + * Allow extending classes to customize the return value. + * Since we are filtering the array returning empty values while formatting + * the item will exclude it from the return values. + */ + $formatted = $return_ids + ? $results + : array_filter( array_map( [ $this, 'format_item' ], $results ) ); + + // Reset the fields if required. + $query->set( 'fields', $original_fields_value ); + + return $formatted; + } + + /** + * {@inheritdoc} + */ + public function offset( $offset, $increment = false ) { + /** + * The `offset` argument will only be used when `posts_per_page` is not -1 + * and will ignore pagination. + * So we filter to apply a real SQL OFFSET; we also leave in place the `offset` + * query var to have a fallback should the LIMIT cause proving difficult to filter. + */ + $this->query_args['offset'] = $increment + ? absint( $offset ) + (int) Tribe__Utils__Array::get( $this->query_args, 'offset', 0 ) + : absint( $offset ); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function order( $order = 'ASC' ) { + $order = strtoupper( $order ); + + if ( ! in_array( $order, [ 'ASC', 'DESC' ], true ) ) { + return $this; + } + + $this->query_args['order'] = $order; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function order_by( $order_by, $order = 'DESC' ) { + $this->query_args['orderby'] = $order_by; + + // Based on `WP_Query->parse_orderby` we should ignore the global order passed, and use the value on for each item in array. + if ( ! is_array( $order_by ) ) { + $this->query_args['order'] = $order; + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function fields( $fields ) { + $this->query_args['fields'] = $fields; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function permission( $permission ) { + if ( ! in_array( $permission, [ self::PERMISSION_READABLE, self::PERMISSION_EDITABLE ], true ) ) { + return $this; + } + + $this->query_args['perm'] = $permission; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function in( $post_ids ) { + $this->add_args( 'post__in', $post_ids ); + + return $this; + } + + /** + * Merges arguments into a query arg. + * + * @since 4.7.19 + * + * @param string $key + * @param array|int $value + */ + protected function add_args( $key, $value ) { + $this->query_args[ $key ] = (array) $value; + } + + /** + * {@inheritdoc} + */ + public function not_in( $post_ids ) { + $this->add_args( 'post__not_in', $post_ids ); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function parent( $post_id ) { + $this->add_args( 'post_parent__in', $post_id ); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function parent_in( $post_ids ) { + $this->add_args( 'post_parent__in', $post_ids ); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function parent_not_in( $post_ids ) { + $this->add_args( 'post_parent__not_in', $post_ids ); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function search( $search ) { + $this->query_args['s'] = $search; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function first() { + $query = $this->build_query(); + + $original_fields_value = $query->get( 'fields', '' ); + + $return_id = 'ids' === $original_fields_value; + + // The request property will be set during the `get_posts` method and empty before it. + if ( ! empty( $query->request ) ) { + $ids = $this->get_ids(); + + if ( empty( $ids ) ) { + return null; + } + + return $return_id ? reset( $ids ) : $this->format_item( reset( $ids ) ); + } + + $query->set( 'fields', 'ids' ); + + /** + * Filters the query object by reference before getting the first post from the query. + * + * @since 4.14.8 + * + * @param WP_Query $query The WP_Query object before get_posts() is called. + */ + do_action( "tribe_repository_{$this->filter_name}_pre_first_post", $query ); + + $ids = $query->get_posts(); + + $query->set( 'fields', $original_fields_value ); + + if ( empty( $ids ) ) { + return null; + } + + return $return_id ? reset( $ids ) : $this->format_item( reset( $ids ) ); + } + + /** + * Formats a post handled by the repository to the expected + * format. + * + * Extending classes should use this method to format return values to the expected format. + * + * @since 4.7.19 + * + * @param int|WP_Post $id + * + * @return WP_Post + */ + protected function format_item( $id ) { + $formatted = null === $this->formatter + ? get_post( $id ) + : $this->formatter->format_item( $id ); + + /** + * Filters a single formatted result. + * + * @since 4.9.11 + * + * @param mixed|WP_Post $formatted The formatted post result, usually a post object. + * @param int $id The formatted post ID. + * @param Tribe__Repository__Interface $this The current repository object. + */ + $formatted = apply_filters( "tribe_repository_{$this->filter_name}_format_item", $formatted, $id, $this ); + + return $formatted; + } + + /** + * {@inheritdoc} + */ + public function last() { + $query = $this->build_query(); + + $original_fields_value = $query->get('fields', ''); + + $return_id = 'ids' === $original_fields_value; + + // The request property will be set during the `get_posts` method and empty before it. + if ( ! empty( $query->request ) ) { + $ids = $this->get_ids(); + + if ( empty( $ids ) ) { + return null; + } + + return $return_id ? end( $ids ) : $this->format_item( end( $ids ) ); + } + + $query->set( 'fields', 'ids' ); + + /** + * Filters the query object by reference before getting the last post from the query. + * + * @since 4.14.8 + * + * @param WP_Query $query The WP_Query object before get_posts() is called. + */ + do_action( "tribe_repository_{$this->filter_name}_pre_last_post", $query ); + + $ids = $query->get_posts(); + + $query->set( 'fields', $original_fields_value ); + + if ( empty( $ids ) ) { + return null; + } + + return $return_id ? end( $ids ) : $this->format_item( end( $ids ) ); + } + + /** + * {@inheritdoc} + */ + public function nth( $n ) { + $per_page = (int) Tribe__Utils__Array::get_in_any( [ + $this->query_args, + $this->default_args, + ], 'posts_per_page', get_option( 'posts_per_page' ) ); + + if ( - 1 !== $per_page && $n > $per_page ) { + return null; + } + + $query = $this->build_query(); + + $return_ids = 'ids' === $query->get( 'fields', '' ); + + $i = absint( $n ) - 1; + + $ids = $this->get_ids(); + + if ( empty( $ids[ $i ] ) ) { + return null; + } + + return $return_ids ? $ids[ $i ] : $this->format_item( $ids[ $i ] ); + } + + /** + * Applies and returns a schema entry. + * + * @since 4.7.19 + * + * @param string $key + * @param mixed $value + * @param mixed ...$args Additional arguments for the application. + * + * @return mixed A scalar value or a callable. + */ + public function apply_modifier( $key, $value = null ) { + $call_args = func_get_args(); + + $application = Tribe__Utils__Array::get( $this->schema, $key, null ); + + /** + * Return primitives, including `null`, as they are. + */ + if ( ! is_callable( $application ) ) { + return $application; + } + + /** + * Allow for callbacks to fire immediately and return more complex values. + * This also means that callbacks meant to run on the next step, the one + * where args are applied, will need to be "wrapped" in callbacks themselves. + * The `$key` is removed from the args to get the value first and avoid + * unused args. + */ + $args_without_key = array_splice( $call_args, 1 ); + + $schema_entry = call_user_func_array( $application, $args_without_key ); + + /** + * Filters the applied modifier schema entry response. + * + * @param mixed $schema_entry A scalar value or a callable. + * @param Tribe__Repository $this This repository instance + * + * @since 4.9.5 + */ + return apply_filters( "tribe_repository_{$this->filter_name}_apply_modifier_schema_entry", $schema_entry, $this ); + } + + /** + * {@inheritdoc} + */ + public function take( $n ) { + $query = $this->build_query(); + + $return_ids = 'ids' === $query->get( 'fields', '' ); + + $matching_ids = $this->get_ids(); + + if ( empty( $matching_ids ) ) { + return []; + } + + $spliced = array_splice( $matching_ids, 0, $n ); + + return $return_ids ? $spliced : array_map( [ $this, 'format_item' ], $spliced ); + } + + /** + * Fetches a single instance of the post type handled by the repository. + * + * Similarly to the `get_post` function permissions are not taken into account when returning + * an instance by its primary key; extending classes can refine this behaviour to suit. + * + * @param mixed $primary_key + * + * @return WP_Post|null|mixed + */ + public function by_primary_key( $primary_key ) { + return $this->by( $this->primary_key, $primary_key )->first(); + } + + /** + * Filters posts by simple meta schema value. + * + * @since 4.9.5 + * + * @param mixed $value Meta value. + */ + public function filter_by_simple_meta_schema( $value ) { + $filter = $this->get_current_filter(); + + if ( ! array_key_exists( $filter, $this->simple_meta_schema ) ) { + return; + } + + $simple_meta = $this->simple_meta_schema[ $filter ]; + + $by = Tribe__Utils__Array::get( $simple_meta, 'by', 'meta_regexp_or_like' ); + + $this->by( $by, $simple_meta['meta_key'], $value ); + } + + /** + * Filters posts by simple tax schema value. + * + * @since 4.9.5 + * + * @param int|string|array $value Term value(s). + */ + public function filter_by_simple_tax_schema( $value ) { + $filter = $this->get_current_filter(); + + if ( ! array_key_exists( $filter, $this->simple_tax_schema ) ) { + return; + } + + $simple_tax = $this->simple_tax_schema[ $filter ]; + + $by = Tribe__Utils__Array::get( $simple_tax, 'by', 'term_in' ); + + $this->by( $by, $simple_tax['taxonomy'], $value ); + } + + /** + * {@inheritdoc} + */ + public function by( $key, $value = null ) { + if ( $this->void_query || ( 'void_query' === $key && false !== $value ) ) { + $this->void_query = true; + + // No point in doing more computations if the query is void. + return $this; + } + + $call_args = func_get_args(); + + $this->current_filters[ $key ] = array_slice( $call_args, 1 ); + + try { + // Set current filter as which one we are running. + $this->current_filter = $key; + + $query_modifier = $this->modify_query( $key, $call_args ); + + // Set current filter as no longer active, we aren't running it anymore. + $this->current_filter = null; + + /** + * Here we allow the repository to call one of its own methods and return `null`. + * A repository might have a `where` or `by` that is just building + * a more complex query using a base `where` or `by`. + */ + if ( null === $query_modifier ) { + return $this; + } + + /** + * Primitives are just merged in. + * Since we are using `array_merge_recursive` we expect them to be arrays. + */ + if ( ! ( is_object( $query_modifier ) || is_callable( $query_modifier ) ) ) { + + if ( ! is_array( $query_modifier ) ) { + throw new InvalidArgumentException( 'Query modifier should be an array!' ); + } + + $replace_modifiers = in_array( $key, $this->replacing_modifiers(), true ); + if ( $replace_modifiers ) { + /** + * We do a merge to make sure new values will override and replace the old + * ones. + */ + $this->query_args = array_merge( $this->query_args, $query_modifier ); + } else { + /** + * We do a recursive merge to allow "stacking" of same kind of queries; + * e.g. two or more `tax_query` or `meta_query` entries should merge into one. + */ + $this->query_args = Arr::merge_recursive_query_vars( $this->query_args, $query_modifier ); + } + } else { + /** + * If we get back something that is not an array then we add it to + * the stack of query modifying callbacks we'll call on the query + * after building it. + */ + $this->query_modifiers[] = $query_modifier; + } + } catch ( Exception $e ) { + /** + * We allow for the `apply` method to orderly fail to micro-optimize. + * If applying one parameter would yield no results then let's immediately bail. + * Schema should throw t + * his Exception if a light-weight on the filters would already + * deem a query as yielding nothing. + */ + $this->void_query = true; + + return $this; + } + + /** + * Catching other type of exceptions is something the client code should handle! + */ + + return $this; + } + + /** + * Returns the query modifier for a key. + * + * @since 4.7.19 + * + * @param string $key + * @param array $call_args + * + * @return mixed + * + * @throws Tribe__Repository__Usage_Error If the required filter is not defined by the class. + * @throws Tribe__Repository__Void_Query_Exception To signal the query would yield no results. + */ + protected function modify_query( $key, $call_args ) { + if ( ! $this->schema_has_modifier_for( $key ) ) { + if ( $this->has_default_modifier( $key ) ) { + // let's use the default filters normalizing the key first + $call_args[0] = $this->normalize_key( $key ); + $query_modifier = call_user_func_array( [ $this, 'apply_default_modifier' ], $call_args ); + } elseif ( 2 === count( $call_args ) ) { + // Pass query argument $key with the single value argument. + $query_modifier = [ + $key => $call_args[1], + ]; + } else { + // More than two $call_args were sent (key, value), assume it was meant for a filter that was not defined yet. + throw Tribe__Repository__Usage_Error::because_the_read_filter_is_not_defined( $key, $this ); + } + } else { + $query_modifier = call_user_func_array( [ $this, 'apply_modifier' ], $call_args ); + } + + return $query_modifier; + } + + /** + * Whether the current schema defines an application for the key or not. + * + * @since 4.7.19 + * + * @param $key + * + * @return bool + */ + protected function schema_has_modifier_for( $key ) { + return isset( $this->schema[ $key ] ); + } + + /** + * Whether a filter defined and handled by the repository exists or not. + * + * @since 4.7.19 + * + * @param string $key + * + * @return bool + */ + protected function has_default_modifier( $key ) { + $normalized_key = $this->normalize_key( $key ); + + return in_array( $normalized_key, self::$default_modifiers, true ); + } + + /** + * Normalizes the filter key to allow broad matching of the `by` filters. + * + * @since 4.7.19 + * + * E.g. `by( 'id', 23 )` is the same as `by( 'ID', 23 ). + * E.g. `by( 'parent', 23 )` is the same as `by( `post_parent`, 23 )` + * + * @param string $key + * + * @return string The normalized filter key + */ + protected function normalize_key( $key ) { + // `ID` to `id` + $normalized = strtolower( $key ); + + $post_prefixed = [ + 'password', + 'name__in', + '_in', + '_not_in', + 'parent', + 'parent__in', + 'parent__not_in', + 'mime_type', + 'content', + 'excerpt', + 'status', + 'modified', + 'modified_gmt', + 'content_filtered', + ]; + + if ( in_array( $key, $post_prefixed, true ) ) { + $normalized = 'post_' . $key; + } + + return $normalized; + } + + /** + * Returns a list of modifiers that, when applied multiple times, + * will replace the previous value. + * + * This behaviour is in opposition to "stackable" modifiers that will, + * instead, be composed and stacked. + * + * @since 4.7.19 + * + * @return array + */ + protected function replacing_modifiers() { + return self::$replacing_modifiers; + } + + /** + * Batch filter application method. + * + * This is the same as calling `where` multiple times with different arguments. + * + * @since 4.7.19 + * + * @param array $args An associative array of arguments to filter + * the posts by in the shape [ , ]. + * + * @return Tribe__Repository__Read_Interface|Tribe__Repository__Update_Interface + */ + public function where_args( array $args ) { + return $this->by_args( $args ); + } + + /** + * {@inheritdoc} + */ + public function by_args( array $args ) { + foreach ( $args as $key => $value ) { + $this->by( $key, $value ); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function save( $return_promise = false ) { + $to_update = $this->get_ids(); + + if ( empty( $to_update ) ) { + return $return_promise ? new Tribe__Promise() : []; + } + + $exit = []; + $postarrs = []; + + foreach ( $to_update as $id ) { + $postarrs[ $id ] = $this->filter_postarr_for_update( $this->build_postarr( $id ), $id ); + } + + // If any `filter_postarr_for_update` call returned a falsy value then drop it. + $postarrs = array_filter( $postarrs ); + + if ( + $this->is_background_update_active( $to_update ) + && count( $to_update ) > $this->get_background_update_threshold( $to_update ) + ) { + return $this->async_update( $postarrs, true ); + } + + $update_callback = $this->get_update_callback( $to_update, false ); + + foreach ( $postarrs as $id => $postarr ) { + $this_exit = $update_callback( $postarr ); + $exit[ $id ] = $id === $this_exit ? true : $this_exit; + } + + return $return_promise ? new Tribe__Promise : $exit; + } + + /** + * {@inheritdoc} + */ + public function get_ids() { + if ( $this->void_query ) { + return []; + } + + + try { + /** @var WP_Query $query */ + $query = $this->get_query(); + + // The request property will be set during the `get_posts` method and empty before it. + if ( empty( $query->request ) ) { + $query->set( 'fields', 'ids' ); + + /** + * Filters the query object by reference before getting the post IDs from the query. + * + * @since 4.14.8 + * + * @param WP_Query $query The WP_Query object before get_posts() is called. + */ + do_action( "tribe_repository_{$this->filter_name}_pre_get_ids_for_posts", $query ); + + return $query->get_posts(); + } + + return array_map( + static function ( $post ) { + if ( is_int( $post ) ) { + return $post; + } + $post_arr = (array) $post; + + return Arr::get( $post_arr, 'ID', Arr::get( $post_arr, 'id', 0 ) ); + }, + $query->posts + ); + + } catch ( Tribe__Repository__Void_Query_Exception $e ) { + /* + * Extending classes might use this method to run sub-queries + * and signal a void query; let's return an empty array. + */ + return []; + } + } + + /** + * {@inheritdoc} + */ + public function get_query() { + return $this->build_query(); + } + + /** + * Whether the current key can be updated by this repository or not. + * + * @since 4.7.19 + * + * @param string $key + * + * @return bool + */ + protected function can_be_updated( $key ) { + return ! in_array( $key, self::$blocked_keys, true ); + } + + /** + * Whether the current key is a date one requiring a converted key pair too or not. + * + * @param string $key + * + * @return bool + */ + protected function requires_converted_date( $key ) { + return array_key_exists( $key, $this->to_local_time_map ) || array_key_exists( $key, $this->to_gmt_map ); + } + + /** + * Updates the update post payload to add dates that should be provided in GMT + * and localized version. + * + * @since 4.7.19 + * + * @param string $key + * @param string|int $value + * @param array $postarr + */ + protected function update_postarr_dates( $key, $value, array &$postarr ) { + if ( array_key_exists( $key, $this->to_gmt_map ) ) { + $postarr[ $this->to_gmt_map[ $key ] ] = Tribe__Timezones::to_tz( $value, 'UTC' ); + } elseif ( array_key_exists( $key, $this->to_local_time_map ) ) { + $postarr[ $this->to_local_time_map[ $key ] ] = Tribe__Timezones::to_tz( $value, Tribe__Timezones::wp_timezone_string() ); + } + $postarr[ $key ] = $value; + } + + /** + * {@inheritdoc} + */ + public function set_args( array $update_map ) { + foreach ( $update_map as $key => $value ) { + $this->set( $key, $value ); + } + + return $this; + } + + /** + * Sets the args to be updated during save process. + * + * @param string $key Argument key. + * @param mixed $value Argument value. + * + * @throws Tribe__Repository__Usage_Error + * + * @return $this + */ + public function set( $key, $value ) { + if ( ! is_string( $key ) ) { + throw Tribe__Repository__Usage_Error::because_update_key_should_be_a_string( $this ); + } + + $this->updates[ $key ] = $value; + + return $this; + } + + /** + * Sets the create args the repository will use to create posts. + * + * @since 4.9.5 + * + * @param string|int $image The path to an image file, an image URL, or an attachment post ID. + * + * @return $this + */ + public function set_featured_image( $image ) { + if ( '' === $image || false === $image ) { + $thumbnail_id = false; + } elseif ( 0 === $image || null === $image ) { + $thumbnail_id = ''; + } else { + $thumbnail_id = tribe_upload_image( $image ); + } + + if ( false === $thumbnail_id ) { + return $this; + } + + return $this->set( '_thumbnail_id', $thumbnail_id ); + } + + /** + * {@inheritdoc} + */ + public function filter_name( $filter_name ) { + $this->filter_name = trim( $filter_name ); + + return $this; + + } + + /** + * {@inheritdoc} + */ + public function set_formatter( Tribe__Repository__Formatter_Interface $formatter ) { + $this->formatter = $formatter; + } + + /** + * Filters the query to only return posts that are related, via a meta key, to posts + * that satisfy a condition. + * + * @param string|array $meta_keys One or more `meta_keys` relating the queried post type(s) + * to another post type. + * @param string $compare The SQL comparison operator. + * @param string $field One (a column in the `posts` table) that should match + * the comparison criteria; required if the comparison operator is not `EXISTS` or + * `NOT EXISTS`. + * @param string|array $values One or more values the post field(s) should be compared to; + * required if the comparison operator is not `EXISTS` or `NOT EXISTS`. + * + * @return $this + * @throws Tribe__Repository__Usage_Error If the comparison operator requires + */ + public function where_meta_related_by( $meta_keys, $compare, $field = null, $values = null ) { + $meta_keys = Tribe__Utils__Array::list_to_array( $meta_keys ); + + if ( ! in_array( $compare, [ 'EXISTS', 'NOT EXISTS' ], true ) ) { + if ( empty( $field ) || empty( $values ) ) { + throw Tribe__Repository__Usage_Error::because_this_comparison_operator_requires_fields_and_values( $meta_keys, $compare, $this ); + } + } + + $field = esc_sql( $field ); + + /** @var wpdb $wpdb */ + global $wpdb; + $p = $this->sql_slug( 'meta_related_post', $compare, $meta_keys ); + $pm = $this->sql_slug( 'meta_related_post_meta', $compare, $meta_keys ); + + $this->filter_query->join( "LEFT JOIN {$wpdb->postmeta} {$pm} ON {$wpdb->posts}.ID = {$pm}.post_id" ); + $this->filter_query->join( "LEFT JOIN {$wpdb->posts} {$p} ON {$pm}.meta_value = {$p}.ID" ); + + $keys_in = $this->prepare_interval( $meta_keys ); + + if ( 'EXISTS' === $compare ) { + $this->filter_query->where( "{$pm}.meta_key IN {$keys_in} AND {$pm}.meta_id IS NOT NULL" ); + } elseif ( 'NOT EXISTS' === $compare ) { + $this->filter_query->where( "{$pm}.meta_id IS NULL" ); + } else { + if ( in_array( $compare, self::$multi_value_keys, true ) ) { + $values = $this->prepare_interval( $values ); + } else { + $values = $this->prepare_value( $values ); + } + $this->filter_query->where( "{$pm}.meta_key IN {$keys_in} AND {$p}.{$field} {$compare} {$values}" ); + } + + return $this; + } + + /** + * Filters the query to only return posts that are related, via a meta key, to posts + * that satisfy a condition. + * + * @since 4.10.3 + * + * @throws Tribe__Repository__Usage_Error If the comparison operator requires and no value provided. + * + * @param string|array $meta_keys One or more `meta_keys` relating the queried post type(s) + * to another post type. + * @param string $compare The SQL comparison operator. + * @param string $meta_field One (a column in the `postmeta` table) that should match + * the comparison criteria; required if the comparison operator is not `EXISTS` or + * `NOT EXISTS`. + * @param string|array $meta_values One or more values the post field(s) should be compared to; + * required if the comparison operator is not `EXISTS` or `NOT EXISTS`. + * @param boolean $or_not_exists Whether or not to also include a clause to check if value IS NULL. + * Example with this as true: `value = X OR value IS NULL`. + * + * @return $this + */ + public function where_meta_related_by_meta( $meta_keys, $compare, $meta_field = null, $meta_values = null, $or_not_exists = false ) { + $meta_keys = Tribe__Utils__Array::list_to_array( $meta_keys ); + + if ( ! in_array( $compare, [ 'EXISTS', 'NOT EXISTS' ], true ) ) { + if ( empty( $meta_field ) || empty( $meta_values ) ) { + throw Tribe__Repository__Usage_Error::because_this_comparison_operator_requires_fields_and_values( $meta_keys, $compare, $this ); + } + } + + $meta_field = esc_sql( $meta_field ); + + /** @var wpdb $wpdb */ + global $wpdb; + + $pm = $this->sql_slug( 'post_meta_related_post_meta', $compare, $meta_keys ); + $pmm = $this->sql_slug( 'meta_post_meta_related_post_meta', $compare, $meta_keys ); + + $this->filter_query->join( "LEFT JOIN {$wpdb->postmeta} {$pm} ON {$pm}.post_id = {$wpdb->posts}.ID" ); + $this->filter_query->join( " + LEFT JOIN {$wpdb->postmeta} {$pmm} + ON {$pmm}.post_id = {$pm}.meta_value + AND {$pmm}.meta_key = '{$meta_field}' + " ); + + $keys_in = $this->prepare_interval( $meta_keys ); + + if ( 'EXISTS' === $compare ) { + $this->filter_query->where( " + {$pm}.meta_key IN {$keys_in} + AND {$pmm}.meta_id IS NOT NULL + " ); + } elseif ( 'NOT EXISTS' === $compare ) { + $this->filter_query->where( " + {$pm}.meta_key IN {$keys_in} + AND {$pmm}.meta_id IS NULL + " ); + } else { + if ( in_array( $compare, static::$multi_value_keys, true ) ) { + $meta_values = $this->prepare_interval( $meta_values ); + } else { + $meta_values = $this->prepare_value( $meta_values ); + } + + $clause = "{$pmm}.meta_value {$compare} {$meta_values}"; + + if ( $or_not_exists ) { + $clause = " + ( + {$clause} + OR {$pmm}.meta_id IS NULL + ) + "; + } + + $this->filter_query->where( " + {$pm}.meta_key IN {$keys_in} + AND {$clause} + " ); + } + + return $this; + } + + /** + * Builds a fenced group of WHERE clauses that will be used with OR logic. + * + * Mind that this is a lower level implementation of WHERE logic that requires + * each callback method to add, at least, one WHERE clause using the repository + * own `where_clause` method. + * + * @param array $callbacks One or more WHERE callbacks that will be called + * this repository. The callbacks have the shape + * [ , <...args>] + * + * @return $this + * @throws Tribe__Repository__Usage_Error If one of the callback methods does + * not add any WHERE clause. + * + * @see Tribe__Repository::where_clause() + * @see Tribe__Repository__Query_Filters::where() + */ + public function where_or( $callbacks ) { + $all_callbacks = func_get_args(); + $buffered = $this->filter_query->get_buffered_where_clauses( true ); + + $this->filter_query->buffer_where_clauses( true ); + + $buffered_count = count( $buffered ); + + foreach ( $all_callbacks as $c ) { + call_user_func_array( [ $this, $c[0] ], array_slice( $c, 1 ) ); + + if ( $buffered_count === count( $this->filter_query->get_buffered_where_clauses() ) ) { + throw Tribe__Repository__Usage_Error::because_where_or_should_only_be_used_with_methods_that_add_where_clauses( $c, $this ); + } + + $buffered_count ++; + } + + $buffered = $this->filter_query->get_buffered_where_clauses( true ); + + $fenced = sprintf( '( %s )', implode( ' OR ', $buffered ) ); + + $this->where_clause( $fenced ); + + return $this; + } + + /** + * Adds an entry to the repository filter schema. + * + * @since 4.9.5 + * + * @param string $key The filter key, the one that will be used in `by` and `where` + * calls. + * @param callable $callback The function that should be called to apply this filter. + */ + public function add_schema_entry( $key, $callback ) { + $this->schema[ $key ] = $callback; + } + + /** + * Adds a simple meta entry to the repository filter schema. + * + * @since 4.9.5 + * + * @param string $key The filter key, the one that will be used in `by` and `where` calls. + * @param string|array $meta_key The meta key(s) to use for the meta lookup. + * @param string|null $by The ->by() lookup to use (defaults to meta_regexp_or_like). + */ + public function add_simple_meta_schema_entry( $key, $meta_key, $by = null ) { + $this->schema[ $key ] = [ $this, 'filter_by_simple_meta_schema' ]; + + $this->simple_meta_schema[ $key ] = [ + 'meta_key' => $meta_key, + 'by' => $by, + ]; + } + + /** + * Adds a simple taxonomy entry to the repository filter schema. + * + * @since 4.9.5 + * + * @param string $key The filter key, the one that will be used in `by` and `where` calls. + * @param string|array $taxonomy The taxonomy/taxonomies to use for the tax lookup. + * @param string|null $by The ->by() lookup to use (defaults to term_in). + */ + public function add_simple_tax_schema_entry( $key, $taxonomy, $by = null ) { + $this->schema[ $key ] = [ $this, 'filter_by_simple_tax_schema' ]; + + $this->simple_tax_schema[ $key ] = [ + 'taxonomy' => $taxonomy, + 'by' => $by, + ]; + } + + /** + * {@inheritdoc} + */ + public function add_update_field_alias( $alias, $field_name ) { + $this->update_fields_aliases[ $alias ] = $field_name; + } + + /** + * Returns modified query arguments after applying a default filter. + * + * @since 4.7.19 + * + * @param string $key + * @param mixed $value + * + * @return array + * @throws Tribe__Repository__Usage_Error If a filter is called with wrong arguments. + */ + protected function apply_default_modifier( $key, $value ) { + $args = []; + + $call_args = func_get_args(); + $arg_1 = isset( $call_args[2] ) ? $call_args[2] : null; + $arg_2 = isset( $call_args[3] ) ? $call_args[3] : null; + + /** @var wpdb $wpdb */ + global $wpdb; + + switch ( $key ) { + default: + // leverage built-in WP_Query filters + $args = [ $key => $value ]; + break; + case 'ID': + case 'id': + $args = [ 'p' => $value ]; + break; + case 'search': + if ( '' !== $value ) { + $args = [ 's' => $value ]; + } + break; + case 'post_status': + $this->query_args['post_status'] = (array) $value; + break; + case 'date': + case 'after_date': + $args = $this->get_posts_after( $value, 'post_date' ); + break; + case 'before_date': + $args = $this->get_posts_before( $value, 'post_date' ); + break; + case 'date_gmt': + case 'after_date_gmt': + $args = $this->get_posts_after( $value, 'post_date_gmt' ); + break; + case 'before_date_gmt': + $args = $this->get_posts_before( $value, 'post_date_gmt' ); + break; + case 'title_like': + $this->filter_query->to_get_posts_with_title_like( $value ); + break; + case 'post_content': + $this->filter_query->to_get_posts_with_content_like( $value ); + break; + case 'post_excerpt': + $this->filter_query->to_get_posts_with_excerpt_like( $value ); + break; + case 'to_ping': + $this->filter_query->to_get_posts_to_ping( $value ); + $args = [ 'to_ping' => $value ]; + break; + case 'post_modified': + $args = $this->get_posts_after( $value, 'post_modified' ); + break; + case 'post_modified_gmt': + $args = $this->get_posts_after( $value, 'post_modified_gmt' ); + break; + case 'post_content_filtered': + $this->filter_query->to_get_posts_with_filtered_content_like( $value ); + break; + case 'guid': + $this->filter_query->to_get_posts_with_guid_like( $value ); + break; + case 'menu_order': + $args = [ 'menu_order' => $value ]; + break; + case 'meta': + case 'meta_equals': + $args = $this->build_meta_query( $meta_key = $value, $meta_value = $arg_1, '=', $format = $arg_2 ); + break; + case 'meta_not_equals': + $args = $this->build_meta_query( $meta_key = $value, $meta_value = $arg_1, '!=', $format = $arg_2 ); + break; + case 'meta_gt': + case 'meta_greater_than': + $args = $this->build_meta_query( $meta_key = $value, $meta_value = $arg_1, '>', $format = $arg_2 ); + break; + case 'meta_gte': + case 'meta_greater_than_or_equal': + $args = $this->build_meta_query( $meta_key = $value, $meta_value = $arg_1, '>=', $format = $arg_2 ); + break; + case 'meta_like': + $args = $this->build_meta_query( $meta_key = $value, $meta_value = $arg_1, 'LIKE' ); + break; + case 'meta_not_like': + $args = $this->build_meta_query( $meta_key = $value, $meta_value = $arg_1, 'NOT LIKE' ); + break; + case 'meta_lt': + case 'meta_less_than': + $args = $this->build_meta_query( $meta_key = $value, $meta_value = $arg_1, '<', $format = $arg_2 ); + break; + case 'meta_lte': + case 'meta_less_than_or_equal': + $args = $this->build_meta_query( $meta_key = $value, $meta_value = $arg_1, '<=', $format = $arg_2 ); + break; + case 'meta_in': + $args = $this->build_meta_query( $meta_key = $value, $meta_value = $arg_1, 'IN', $format = $arg_2 ); + break; + case 'meta_not_in': + $args = $this->build_meta_query( $meta_key = $value, $meta_value = $arg_1, 'NOT IN', $format = $arg_2 ); + break; + case 'meta_between': + $args = $this->build_meta_query( $meta_key = $value, $meta_value = $arg_1, 'BETWEEN', $format = $arg_2 ); + break; + case 'meta_not_between': + $args = $this->build_meta_query( $meta_key = $value, $meta_value = $arg_1, 'NOT BETWEEN', $format = $arg_2 ); + break; + case 'meta_exists': + $args = $this->build_meta_query( $meta_key = $value, $meta_value = $arg_1, 'EXISTS' ); + break; + case 'meta_not_exists': + $args = $this->build_meta_query( $meta_key = $value, $meta_value = $arg_1, 'NOT EXISTS' ); + break; + case 'meta_regexp': + case 'meta_equals_regexp': + // Check if Regexp is fenced. + if ( tribe_is_regex( $arg_1 ) ) { + // Unfence the Regexp. + $arg_1 = tribe_unfenced_regex( $arg_1 ); + } + + $args = $this->build_meta_query( $meta_key = $value, $meta_value = $arg_1, 'REGEXP' ); + break; + case 'meta_not_regexp': + case 'meta_not_equals_regexp': + // Check if Regexp is fenced. + if ( tribe_is_regex( $arg_1 ) ) { + // Unfence the Regexp. + $arg_1 = tribe_unfenced_regex( $arg_1 ); + } + + $args = $this->build_meta_query( $meta_key = $value, $meta_value = $arg_1, 'NOT REGEXP' ); + break; + case 'meta_regexp_or_like': + case 'meta_equals_regexp_or_like': + $compare = 'LIKE'; + + // Check if Regexp is fenced (the only way for Regexp to be supported in this context). + if ( tribe_is_regex( $arg_1 ) ) { + $compare = 'REGEXP'; + + // Unfence the Regexp. + $arg_1 = tribe_unfenced_regex( $arg_1 ); + } + + $args = $this->build_meta_query( $meta_key = $value, $meta_value = $arg_1, $compare ); + break; + case 'meta_not_regexp_or_like': + case 'meta_not_equals_regexp_or_like': + $compare = 'NOT LIKE'; + + // Check if Regexp is fenced (the only way for Regexp to be supported in this context). + if ( tribe_is_regex( $arg_1 ) ) { + $compare = 'NOT REGEXP'; + + // Unfence the Regexp. + $arg_1 = tribe_unfenced_regex( $arg_1 ); + } + + $args = $this->build_meta_query( $meta_key = $value, $meta_value = $arg_1, $compare ); + break; + case 'taxonomy_exists': + $args = $this->build_tax_query( $taxonomy = $value, $terms = $arg_1, 'term_id', 'EXISTS' ); + break; + case 'taxonomy_not_exists': + $args = $this->build_tax_query( $taxonomy = $value, $terms = $arg_1, 'term_id', 'NOT EXISTS' ); + break; + case 'term_id_in': + $args = $this->build_tax_query( $taxonomy = $value, $terms = $arg_1, 'term_id', 'IN' ); + break; + case 'term_id_not_in': + $args = $this->build_tax_query( $taxonomy = $value, $terms = $arg_1, 'term_id', 'NOT IN' ); + break; + case 'term_id_and': + $args = $this->build_tax_query( $taxonomy = $value, $terms = $arg_1, 'term_id', 'AND' ); + break; + case 'term_name_in': + $args = $this->build_tax_query( $taxonomy = $value, $terms = $arg_1, 'name', 'IN' ); + break; + case 'term_name_not_in': + $args = $this->build_tax_query( $taxonomy = $value, $terms = $arg_1, 'name', 'NOT IN' ); + break; + case 'term_name_and': + $args = $this->build_tax_query( $taxonomy = $value, $terms = $arg_1, 'name', 'AND' ); + break; + case 'term_slug_in': + $args = $this->build_tax_query( $taxonomy = $value, $terms = $arg_1, 'slug', 'IN' ); + break; + case 'term_slug_not_in': + $args = $this->build_tax_query( $taxonomy = $value, $terms = $arg_1, 'slug', 'NOT IN' ); + break; + case 'term_slug_and': + $args = $this->build_tax_query( $taxonomy = $value, $terms = $arg_1, 'slug', 'AND' ); + break; + case 'term_in': + $arg_1 = Tribe__Terms::translate_terms_to_ids( $arg_1, $value, false ); + + $args = $this->build_tax_query( $taxonomy = $value, $terms = $arg_1, 'term_id', 'IN' ); + break; + case 'term_not_in': + $arg_1 = Tribe__Terms::translate_terms_to_ids( $arg_1, $value, false ); + + $args = $this->build_tax_query( $taxonomy = $value, $terms = $arg_1, 'term_id', 'NOT IN' ); + break; + case 'term_and': + $arg_1 = Tribe__Terms::translate_terms_to_ids( $arg_1, $value, false ); + + $args = $this->build_tax_query( $taxonomy = $value, $terms = $arg_1, 'term_id', 'AND' ); + break; + } + + + return $args; + } + + /** + * Builds a date query entry to get posts after a date. + * + * @since 4.7.19 + * + * @param string $value + * @param string $column + * + * @return array + */ + protected function get_posts_after( $value, $column = 'post_date' ) { + $timezone = in_array( $column, [ 'post_date_gmt', 'post_modified_gmt' ], true ) + ? 'UTC' + : Tribe__Timezones::generate_timezone_string_from_utc_offset( Tribe__Timezones::wp_timezone_string() ); + + if ( is_numeric( $value ) ) { + $value = "@{$value}"; + } + + $date = new DateTime( $value, new DateTimeZone( $timezone ) ); + + $array_key = sprintf( '%s-after', $column ); + + return [ + 'date_query' => [ + 'relation' => 'AND', + $array_key => [ + 'inclusive' => true, + 'column' => $column, + 'after' => $date->format( 'Y-m-d H:i:s' ), + ], + ], + ]; + } + + /** + * Builds a date query entry to get posts before a date. + * + * @since 4.7.19 + * + * @param string $value + * @param string $column + * + * @return array + */ + protected function get_posts_before( $value, $column = 'post_date' ) { + $timezone = in_array( $column, [ 'post_date_gmt', 'post_modified_gmt' ], true ) + ? 'UTC' + : Tribe__Timezones::generate_timezone_string_from_utc_offset( Tribe__Timezones::wp_timezone_string() ); + + if ( is_numeric( $value ) ) { + $value = "@{$value}"; + } + + $date = new DateTime( $value, new DateTimeZone( $timezone ) ); + + $array_key = sprintf( '%s-before', $column ); + + return [ + 'date_query' => [ + 'relation' => 'AND', + $array_key => [ + 'inclusive' => true, + 'column' => $column, + 'before' => $date->format( 'Y-m-d H:i:s' ), + ], + ], + ]; + } + + /** + * Builds a meta query entry. + * + * @since 4.7.19 + * + * @param string $meta_key + * @param string|array $meta_value + * @param string $compare + * @param string $type_or_format The type of value to compare + * + * @return array|null + * @throws Tribe__Repository__Usage_Error If trying to compare multiple values with a single + * comparison operator. + */ + protected function build_meta_query( $meta_key, $meta_value = 'value', $compare = '=', $type_or_format = null ) { + $meta_keys = Tribe__Utils__Array::list_to_array( $meta_key ); + + $postfix = Tribe__Utils__Array::get( self::$comparison_operators, $compare, '' ); + + if ( count( $meta_keys ) === 1 ) { + $array_key = $this->sql_slug( $meta_keys[0], $postfix ); + + $args = [ + 'meta_query' => [ + $array_key => [ + 'key' => $meta_keys[0], + 'compare' => strtoupper( $compare ), + ], + ], + ]; + + if ( ! in_array( $compare, [ 'EXISTS', 'NOT EXISTS' ], true ) ) { + $args['meta_query'][ $array_key ]['value'] = $meta_value; + } + + if ( is_string( $type_or_format ) && 0 === strpos( $type_or_format, '%' ) ) { + throw Tribe__Repository__Usage_Error::because_the_type_is_a_wpdb_prepare_format( $meta_key, $type_or_format, $this ); + } + + if ( null !== $type_or_format ) { + $args['meta_query'][ $array_key ]['type'] = $type_or_format; + } + + return $args; + } + + + if ( null === $type_or_format ) { + $type_or_format = '%s'; + } elseif ( 0 !== strpos( $type_or_format, '%' ) ) { + throw Tribe__Repository__Usage_Error::because_the_format_is_not_a_wpdb_prepare_one( $meta_key, $type_or_format, $this ); + } + + /** @var wpdb $wpdb */ + global $wpdb; + + // Build custom WHERE and JOINS to reduce the JOIN clauses + $pm_alias = $this->sql_slug( 'meta', $postfix, ++ self::$meta_alias ); + $meta_keys_in = sprintf( "('%s')", implode( "','", array_map( 'esc_sql', $meta_keys ) ) ); + + $this->validate_operator_and_values( $compare, $meta_keys, $meta_value ); + + if ( in_array( $compare, self::$multi_value_keys, true ) ) { + $meta_values = $this->prepare_interval( Tribe__Utils__Array::list_to_array( $meta_value ), $type_or_format ); + } else { + $meta_values = $this->prepare_value( $meta_value, $type_or_format ); + } + + $this->filter_query->join( "JOIN {$wpdb->postmeta} {$pm_alias} ON {$wpdb->posts}.ID = {$pm_alias}.post_id" ); + + if ( 'EXISTS' === $compare ) { + $this->filter_query->where( "{$pm_alias}.meta_key IN {$meta_keys_in} AND {$pm_alias}.meta_id IS NOT NULL" ); + } elseif ( 'NOT EXISTS' === $compare ) { + $this->filter_query->where( "{$pm_alias}.meta_key NOT IN {$meta_keys_in} AND {$pm_alias}.meta_id IS NOT NULL" ); + } else { + $this->filter_query->where( "{$pm_alias}.meta_key IN {$meta_keys_in} AND {$pm_alias}.meta_value {$compare} {$meta_values}" ); + } + } + + /** + * Generates a SQL friendly slug from the provided, variadic, fragments. + * + * @since 4.7.19 + * + * @param ...string $frag + * + * @return string + */ + protected function sql_slug( $frag ) { + $frags = func_get_args(); + + foreach ( $frags as &$frag ) { + if ( is_string( $frag ) ) { + Tribe__Utils__Array::get( self::$comparison_operators, $frag, $frag ); + } elseif ( is_array( $frag ) ) { + $frag = implode( '_', $frag ); + } + } + + + $frags = array_filter( $frags ); + + return strtolower( str_replace( '-', '_', sanitize_title( implode( '_', $frags ) ) ) ); + } + + /** + * Builds a taxonomy query entry. + * + * @since 4.7.19 + * + * @param string $taxonomy + * @param int|string|array $terms + * @param string $field + * @param string $operator + * + * @return array + */ + protected function build_tax_query( $taxonomy, $terms, $field, $operator ) { + if ( in_array( $operator, [ 'EXISTS', 'NOT EXISTS' ], true ) ) { + $array_key = $this->sql_slug( $taxonomy, $operator ); + } else { + $array_key = $this->sql_slug( $taxonomy, $field, $operator ); + } + + return [ + 'tax_query' => [ + $array_key => [ + 'taxonomy' => $taxonomy, + 'field' => $field, + 'terms' => $terms, + 'operator' => strtoupper( $operator ), + ], + ], + ]; + } + + /** + * {@inheritdoc} + */ + public function join_clause( $join ) { + $this->filter_query->join( $join ); + } + + /** + * {@inheritdoc} + */ + public function where_clause( $where ) { + $this->filter_query->where( $where ); + } + + /** + * {@inheritdoc} + */ + public function set_query_builder( $query_builder ) { + $this->query_builder = $query_builder; + } + + /** + * Builds and escapes an interval of strings. + * + * The return string includes opening and closing braces. + * + * @since 4.7.19 + * + * @param string|array $values One or more values to use to build + * the interval + * . + * @param string $format The format that should be used to escape + * the values; default to '%s'. + * @param string $operator The operator the interval is being prepared for; + * defaults to `IN`. + * + * @return string + */ + public function prepare_interval( $values, $format = '%s', $operator = 'IN' ) { + $values = Tribe__Utils__Array::list_to_array( $values ); + + $prepared = []; + foreach ( $values as $value ) { + $prepared[] = $this->prepare_value( $value, $format ); + } + + return in_array( $operator, [ 'BETWEEN', 'NOT BETWEEN' ] ) + ? sprintf( '%s AND %s', $prepared[0], $prepared[1] ) + : sprintf( '(%s)', implode( ',', $prepared ) ); + } + + /** + * Prepares a single value to be used in a SQL query. + * + * @since 4.7.19 + * + * @param mixed $value + * @param string $format + * + * @return string + */ + public function prepare_value( $value, $format = '%s' ) { + /** @var wpdb $wpdb */ + global $wpdb; + + return $wpdb->prepare( $format, $value ); + } + + /** + * Validates that a comparison operator is used with the correct type of values. + * + * This is just a wrap to signal this kind of code error not in bad SQL error but + * with a visible exception. + * + * @since 4.7.19 + * + * @param string $compare A SQL comparison operator + * @param string|array $meta_key + * @param mixed $meta_value + * + * @throws Tribe__Repository__Usage_Error + */ + protected function validate_operator_and_values( $compare, $meta_key, $meta_value ) { + if ( is_array( $meta_value ) && ! in_array( $compare, self::$multi_value_keys, true ) ) { + throw Tribe__Repository__Usage_Error::because_single_value_comparisons_should_be_used_with_one_value( + $meta_key, + $meta_value, + $compare, + $this + ); + } + } + + /** + * {@inheritdoc} + */ + public function by_related_to_min( $by_meta_keys, $min, $keys = null, $values = null ) { + $min = $this->prepare_value( $min, '%d' ); + + /** @var wpdb $wpdb */ + global $wpdb; + + $by_meta_keys = $this->prepare_interval( $by_meta_keys ); + + $join = ''; + $and_where = ''; + if ( ! empty( $keys ) || ! empty( $values ) ) { + $join = "\nJOIN {$wpdb->postmeta} pm2 ON pm1.post_id = pm2.post_id\n"; + } + if ( ! empty( $keys ) ) { + $keys = $this->prepare_interval( $keys ); + $and_where .= "\nAND pm2.meta_key IN {$keys}\n"; + } + if ( ! empty( $values ) ) { + $values = $this->prepare_interval( $values ); + $and_where .= "\nAND pm2.meta_value IN {$values}\n"; + } + + $this->where_clause( "{$wpdb->posts}.ID IN ( + SELECT pm1.meta_value + FROM {$wpdb->postmeta} pm1 {$join} + WHERE pm1.meta_key IN {$by_meta_keys} {$and_where} + GROUP BY( pm1.meta_value ) + HAVING COUNT(DISTINCT pm1.post_id) >= {$min} + )" ); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function by_related_to_max( $by_meta_keys, $max, $keys = null, $values = null ) { + $max = $this->prepare_value( $max, '%d' ); + + /** @var wpdb $wpdb */ + global $wpdb; + + $join = ''; + $and_where = ''; + if ( ! empty( $keys ) || ! empty( $values ) ) { + $join = "\nJOIN {$wpdb->postmeta} pm2 ON pm1.post_id = pm2.post_id\n"; + } + if ( ! empty( $keys ) ) { + $keys = $this->prepare_interval( $keys ); + $and_where .= "\nAND pm2.meta_key IN {$keys}\n"; + } + if ( ! empty( $values ) ) { + $values = $this->prepare_interval( $values ); + $and_where .= "\nAND pm2.meta_value IN {$values}\n"; + } + + $by_meta_keys = $this->prepare_interval( $by_meta_keys ); + + $this->where_clause( "{$wpdb->posts}.ID IN ( + SELECT pm1.meta_value + FROM {$wpdb->postmeta} pm1 {$join} + WHERE pm1.meta_key IN {$by_meta_keys} {$and_where} + GROUP BY( pm1.meta_value ) + HAVING COUNT(DISTINCT pm1.post_id) <= {$max} + )" ); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function by_related_to_between( $by_meta_keys, $min, $max, $keys = null, $values = null ) { + $min = $this->prepare_value( $min, '%d' ); + $max = $this->prepare_value( $max, '%d' ); + + /** @var wpdb $wpdb */ + global $wpdb; + + $by_meta_keys = $this->prepare_interval( $by_meta_keys ); + + $join = ''; + $and_where = ''; + if ( ! empty( $keys ) || ! empty( $values ) ) { + $join = "\nJOIN {$wpdb->postmeta} pm2 ON pm1.post_id = pm2.post_id\n"; + } + if ( ! empty( $keys ) ) { + $keys = $this->prepare_interval( $keys ); + $and_where .= "\nAND pm2.meta_key IN {$keys}\n"; + } + if ( ! empty( $values ) ) { + $values = $this->prepare_interval( $values ); + $and_where .= "\nAND pm2.meta_value IN {$values}\n"; + } + + $this->where_clause( "{$wpdb->posts}.ID IN ( + SELECT pm1.meta_value + FROM {$wpdb->postmeta} pm1 {$join} + WHERE pm1.meta_key IN {$by_meta_keys} {$and_where} + GROUP BY( pm1.meta_value ) + HAVING COUNT(DISTINCT pm1.post_id) BETWEEN {$min} AND {$max} + )" ); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function by_not_related_to( $by_meta_keys, $keys = null, $values = null ) { + + /** @var wpdb $wpdb */ + global $wpdb; + + $by_meta_keys = $this->prepare_interval( $by_meta_keys ); + + $join = ''; + $and_where = ''; + if ( ! empty( $keys ) || ! empty( $values ) ) { + $join = "\nJOIN {$wpdb->postmeta} pm2 ON pm1.post_id = pm2.post_id\n"; + } + if ( ! empty( $keys ) ) { + $keys = $this->prepare_interval( $keys ); + $and_where .= "\nAND pm2.meta_key IN {$keys}\n"; + } + if ( ! empty( $values ) ) { + $values = $this->prepare_interval( $values ); + $and_where .= "\nAND pm2.meta_value IN {$values}\n"; + } + + $this->where_clause( "{$wpdb->posts}.ID NOT IN ( + SELECT pm1.meta_value + FROM {$wpdb->postmeta} pm1 {$join} + WHERE pm1.meta_key IN {$by_meta_keys} {$and_where} + GROUP BY( pm1.meta_value ) + )" ); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function has_filter( $key, $value = null ) { + $args = func_get_args(); + $values = array_slice( $args, 1 ); + + if ( null === $value ) { + // We just want to check if a filter is applied. + return array_key_exists( $key, $this->current_filters ); + } + + // We check if the filter exists and the arguments match; inline to prevent "Undefined index" errors. + return array_key_exists( $key, $this->current_filters ) && array_slice( + $this->current_filters[ $key ], + 0, + min( count( $this->current_filters[ $key ] ), count( $values ) ) + ) === $values; + } + + /** + * {@inheritdoc} + */ + public function get_current_filter() { + return $this->current_filter; + } + + /** + * Returns a map relating comparison operators to their "pretty" name. + * + * @since 4.9.5 + * + * @return array + */ + public static function get_comparison_operators() { + return self::$comparison_operators; + } + + /** + * {@inheritdoc} + */ + public function delete( $return_promise = false ) { + $to_delete = $this->get_ids(); + + if ( empty( $to_delete ) ) { + return $return_promise ? new Tribe__Promise() : []; + } + + + /** + * Filters the post delete operation allowing third party code to bail out of + * the process completely. + * + * @since 4.9.5 + * + * @param array|null $deleted An array containing the the IDs of the deleted posts. + * @param self $this This repository instance. + */ + $deleted = apply_filters( "tribe_repository_{$this->filter_name}_delete", null, $to_delete ); + if ( null !== $deleted ) { + return $deleted; + } + + if ( + $this->is_background_delete_active( $to_delete ) + && count( $to_delete ) > $this->get_background_delete_threshold( $to_delete ) + ) { + return $this->async_delete( $to_delete, $return_promise ); + } + + $delete_callback = $this->get_delete_callback( $to_delete ); + + foreach ( $to_delete as $id ) { + $done = $delete_callback( $id ); + + if ( empty( $done ) ) { + tribe( 'logger' )->log( + __( 'Could not delete post with ID ' . $id, 'tribe-common' ), + Tribe__Log::WARNING, + $this->filter_name + ); + continue; + } + $deleted[] = $id; + } + + return $return_promise ? new Tribe__Promise() : $deleted; + } + + /** + * Whether background delete is activated for the repository or not. + * + * @since 4.9.5 + * + * @param array $to_delete An array of post IDs to delete. + * + * @return bool Whether background delete is activated for the repository or not. + */ + protected function is_background_delete_active( $to_delete ) { + /** + * Whether background, asynchronous, deletion of posts is active or not for all repositories. + * + * If active then if the number of posts to delete is over the threshold, defined + * by the `tribe_repository_delete_background_threshold` filter, then the deletion will happen + * in background in other requests. + * + * @since 4.9.5 + * + * @param bool $background_active Whether background deletion is active or not. + * @param array $to_delete The array of post IDs to delete. + */ + $background_active = (bool) apply_filters( 'tribe_repository_delete_background_activated', true, $to_delete ); + + /** + * Whether background, asynchronous, deletion of posts is active or not for this specific repository. + * + * If active then if the number of posts to delete is over the threshold, defined + * by the `tribe_repository_delete_background_threshold` filter, then the deletion will happen + * in background in other requests. + * + * @since 4.9.5 + * + * @param bool $background_active Whether background deletion is active or not. + * @param array $to_delete The array of post IDs to delete. + */ + $background_active = (bool) apply_filters( + "tribe_repository_{$this->filter_name}_delete_background_activated", + $background_active, + $to_delete + ); + + return $background_active; + } + + /** + * Returns the threshold above which posts will be deleted in background. + * + * @since 4.9.5 + * + * @param array $to_delete An array of post IDs to delete. + * + * @return int The threshold above which posts will be deleted in background. + */ + protected function get_background_delete_threshold( $to_delete ) { + /** + * The number of posts above which the deletion will happen in background. + * + * This filter will be ignored if background delete is deactivated with the `tribe_repository_delete_background_activated` + * or `tribe_repository_{$this->filter_name}_delete_background_activated` filter. + * + * @since 4.9.5 + * + * @param int The threshold over which posts will be deleted in background. + * @param array $to_delete The post IDs to delete. + */ + $background_threshold = (int) apply_filters( 'tribe_repository_delete_background_threshold', 20, $to_delete ); + + /** + * The number of posts above which the deletion will happen in background. + * + * This filter will be ignored if background delete is deactivated with the `tribe_repository_delete_background_activated` + * or `tribe_repository_{$this->filter_name}_delete_background_activated` filter. + * + * @since 4.9.5 + * + * @param int The threshold over which posts will be deleted in background. + * @param array $to_delete The post IDs to delete. + */ + $background_threshold = (int) apply_filters( + "tribe_repository_{$this->filter_name}_delete_background_threshold", + $background_threshold, + $to_delete + ); + + return $background_threshold; + } + + /** + * Whether background update is activated for the repository or not. + * + * @since 4.9.5 + * + * @param array $to_update An array of post IDs to update. + * + * @return bool Whether background update is activated for the repository or not. + */ + protected function is_background_update_active( $to_update ) { + /** + * Whether background, asynchronous, update of posts is active or not for all repositories. + * + * If active then if the number of posts to update is over the threshold, defined + * by the `tribe_repository_update_background_threshold` filter, then the update will happen + * in background in other requests. + * + * @since 4.9.5 + * + * @param bool $background_active Whether background update is active or not. + * @param array $to_update The array of post IDs to update. + */ + $background_active = (bool) apply_filters( 'tribe_repository_update_background_activated', true, $to_update ); + + /** + * Whether background, asynchronous, update of posts is active or not for this specific repository. + * + * If active then if the number of posts to update is over the threshold, defined + * by the `tribe_repository_update_background_threshold` filter, then the update will happen + * in background in other requests. + * + * @since 4.9.5 + * + * @param bool $background_active Whether background update is active or not. + * @param array $to_update The array of post IDs to update. + */ + $background_active = (bool) apply_filters( + "tribe_repository_{$this->filter_name}_update_background_activated", + $background_active, + $to_update + ); + + return $background_active; + } + + /** + * Returns the threshold above which posts will be updated in background. + * + * @since 4.9.5 + * + * @param array $to_update An array of post IDs to update. + * + * @return int The threshold above which posts will be updated in background. + */ + protected function get_background_update_threshold( $to_update ) { + /** + * The number of posts above which the update will happen in background. + * + * This filter will be ignored if background update is deactivated with the `tribe_repository_update_background_activated` + * or `tribe_repository_{$this->filter_name}_update_background_activated` filter. + * + * @since 4.9.5 + * + * @param int The threshold over which posts will be updated in background. + * @param array $to_update The post IDs to update. + */ + $background_threshold = (int) apply_filters( 'tribe_repository_update_background_threshold', 20, $to_update ); + + /** + * The number of posts above which the update will happen in background. + * + * This filter will be ignored if background update is deactivated with the `tribe_repository_update_background_activated` + * or `tribe_repository_{$this->filter_name}_update_background_activated` filter. + * + * @since 4.9.5 + * + * @param int The threshold over which posts will be updated in background. + * @param array $to_update The post IDs to update. + */ + $background_threshold = (int) apply_filters( + "tribe_repository_{$this->filter_name}_update_background_threshold", + $background_threshold, + $to_update + ); + + return $background_threshold; + } + + + /** + * {@inheritdoc} + */ + public function async_delete( array $to_delete, $return_promise = true ) { + $promise = new Tribe__Promise( $this->get_delete_callback( $to_delete, true ), $to_delete ); + if ( ! $return_promise ) { + // Dispatch it immediately and return the IDs that will be deleted. + $promise->save()->dispatch(); + + return $to_delete; + } + + // Return the promise and let the client do the dispatching. + return $promise; + } + + /** + * Returns the delete callback function or method to use to delete posts. + * + * @since 4.9.5 + * + * @param int|array $to_delete The post ID to delete or an array of post IDs to delete. + * @param bool $background Whether the callback will be used in background delete operations or not. + * + * @return callable The callback to use. + */ + protected function get_delete_callback( $to_delete, $background = false ) { + /** + * Filters the callback that all repositories should use to delete posts. + * + * @since 4.9.5 + * + * @param callable $callback The callback that should be used to delete each post; defaults + * to `wp_delete_post`; falsy return values will be interpreted as + * failures to delete. + * @param array|int $to_delete An array of post IDs to delete. + * @param bool $background Whether the delete operation will happen in background or not. + */ + $callback = apply_filters( 'tribe_repository_delete_callback', 'wp_delete_post', (array) $to_delete, (bool) $background ); + + /** + * Filters the callback that all repositories should use to delete posts. + * + * @since 4.9.5 + * + * @param callable $callback The callback that should be used to delete each post; defaults + * to `wp_delete_post`; falsy return values will be interpreted as + * failures to delete. + * @param array|int $to_delete An array of post IDs to delete. + * @param bool $background Whether the delete operation will happen in background or not. + */ + $callback = apply_filters( + "tribe_repository_{$this->filter_name}_delete_callback", + $callback, + (array) $to_delete, + (bool) $background + ); + + return $callback; + } + + /** + * {@inheritdoc} + */ + public function get_filter_name() { + return $this->filter_name; + } + + /** + * Returns the update callback function or method to use to update posts. + * + * @since 4.9.5 + * + * @param int|array $to_update The post ID to update or an array of post IDs to update. + * @param bool $background Whether the callback will be used in background update operations or not. + * + * @return callable The callback to use. + */ + protected function get_update_callback( $to_update, $background = false ) { + /** + * Filters the callback that all repositories should use to update posts. + * + * @since 4.9.5 + * + * @param callable $callback The callback that should be used to update each post; defaults + * to `wp_update_post`; falsy return values will be interpreted as + * failures to update. + * @param array|int $to_update An array of post IDs to update. + * @param bool $background Whether the update operation will happen in background or not. + */ + $callback = apply_filters( 'tribe_repository_update_callback', 'wp_update_post', (array) $to_update, (bool) $background ); + + /** + * Filters the callback that all repositories should use to update posts. + * + * @since 4.9.5 + * + * @param callable $callback The callback that should be used to update each post; defaults + * to `wp_update_post`; falsy return values will be interpreted as + * failures to update. + * @param array|int $to_update An array of post IDs to update. + * @param bool $background Whether the update operation will happen in background or not. + */ + $callback = apply_filters( + "tribe_repository_{$this->filter_name}_update_callback", + $callback, + (array) $to_update, + (bool) $background + ); + + return $callback; + } + + /** + * {@inheritdoc} + */ + public function async_update( array $to_update, $return_promise = true ) { + $promise = new Tribe__Promise( $this->get_update_callback( $to_update, true ), $to_update ); + if ( ! $return_promise ) { + // Dispatch it immediately and return the IDs that will be deleted. + $promise->save()->dispatch(); + + return $to_update; + } + + // Return the promise and let the client do the dispatching. + return $promise; + } + + /** + * {@inheritdoc} + */ + public function get_update_fields_aliases() { + return $this->update_fields_aliases; + } + + /** + * {@inheritdoc} + */ + public function set_update_fields_aliases( array $update_fields_aliases ) { + $this->update_fields_aliases = $update_fields_aliases; + } + + /** + * {@inheritdoc} + */ + public function filter_postarr_for_update( array $postarr, $post_id ) { + /** + * Filters the post array that will be used for an update. + * + * @since 4.9.5 + * + * @param array $postarr The post array that will be sent to the update callback. + * @param int The post ID if set. + */ + return apply_filters( "tribe_repository_{$this->filter_name}_update_postarr", $postarr, $post_id ); + } + + /** + * A utility method to cast any PHP error into an exception proper. + * + * Usage: `set_error_handler( array( $repository, 'cast_error_to_exception' ) ); + * + * @since 4.9.5 + * + * @param int $code The error code. + * @param string $message The error message. + */ + public function cast_error_to_exception( $code, $message ) { + throw new RuntimeException( $message, $code ); + } + + /** + * {@inheritdoc} + */ + public function create() { + $postarr = $this->filter_postarr_for_create( array_merge( $this->build_postarr(), $this->create_args ) ); + + // During the filtering allow extending classes or filters to prevent the create completely. + if ( false === ( bool ) $postarr ) { + return false; + } + + $created = call_user_func( $this->get_create_callback( $postarr ), $postarr ); + + $post = $this->format_item( $created ); + + return $post instanceof WP_Post && $post->ID === $created ? $post : false; + } + + /** + * {@inheritdoc} + */ + public function filter_postarr_for_create( array $postarr ) { + /** + * Filters the post array that will be used for the creation of a post + * of the type managed by the repository. + * + * @since 4.9.5 + * + * @param array $postarr The post array that will be sent to the create callback. + */ + return apply_filters( "tribe_repository_{$this->filter_name}_update_postarr", $postarr ); + } + + /** + * {@inheritdoc} + */ + public function build_postarr( $id = null ) { + $postarr = [ + 'tax_input' => [], + 'meta_input' => [], + ]; + + /* + * The check is lax here by design: we leave space for the client code + * to use this method to build post arrays; when this is used by the + * repository the integrity of `$id` is granted. + */ + $is_update = null !== $id && is_numeric( $id ); + + // But still let's provide values that make sense. + if ( $is_update ) { + $postarr['ID'] = (int) $id; + } + + foreach ( $this->updates as $key => $value ) { + if ( + $value instanceof Closure || + ( is_array( $value ) && is_callable( $value ) ) + ) { + $value = $value( $id, $key, $this ); + } + + // Allow fields to be aliased + $key = Tribe__Utils__Array::get( $this->update_fields_aliases, $key, $key ); + + if ( ! $this->can_be_updated( $key ) ) { + throw Tribe__Repository__Usage_Error::because_this_field_cannot_be_updated( $key, $this ); + } + + if ( $this->is_a_post_field( $key ) ) { + if ( $this->requires_converted_date( $key ) ) { + $this->update_postarr_dates( $key, $value, $postarr ); + } else { + $postarr[ $key ] = $value; + } + } elseif ( $this->is_a_taxonomy( $key ) ) { + $taxonomy = get_taxonomy( $key ); + if ( $taxonomy instanceof WP_Taxonomy ) { + $postarr['tax_input'][ $key ] = Tribe__Utils__Array::list_to_array( $value ); + } + } else { + // it's a custom field + $postarr['meta_input'][ $key ] = $value; + } + } + + return $postarr; + } + + /** + * Returns the create callback function or method to use to create posts. + * + * @since 4.9.5 + * + * @param array $postarr The post array that will be used for the creation. + * + * @return callable The callback to use. + */ + protected function get_create_callback( array $postarr ) { + /** + * Filters the callback that all repositories should use to create posts. + * + * @since 4.9.5 + * + * @param callable $callback The callback that should be used to create posts; defaults + * to `wp_insert_post`; non numeric and existing post ID return + * values will be interpreted as failures to create the post. + * @param array $postarr The post array that will be used for the creation. + */ + $callback = apply_filters( 'tribe_repository_create_callback', 'wp_insert_post', $postarr ); + + /** + * Filters the callback that all repositories should use to create posts. + * + * @since 4.9.5 + * + * @param callable $callback The callback that should be used to create posts; defaults + * to `wp_insert_post`; non numeric and existing post ID return + * values will be interpreted as failures to create the post. + * @param array $postarr The post array that will be used for the creation. + */ + $callback = apply_filters( + "tribe_repository_{$this->filter_name}_create_callback", + $callback, + $postarr + ); + + return $callback; + } + + /** + * Returns the create args the repository will use to create posts. + * + * @since 4.9.5 + * + * @return array The create args the repository will use to create posts. + */ + public function get_create_args() { + return $this->create_args; + } + + /** + * Sets the create args the repository will use to create posts. + * + * @since 4.9.5 + * + * @param array $create_args The create args the repository will use to create posts. + */ + public function set_create_args( array $create_args ) { + $this->create_args = $create_args; + } + + /** + * Returns a value trying to fetch it from an array first and then + * reading it from the meta. + * + * @since 4.9.5 + * + * @param array $postarr The array to look into. + * @param string $key The key to retrieve. + * @param int|null $post_id The post ID to fetch the value for. + * @param mixed $default The default value to return if nothing was found. + * + * @return mixed The found value if any. + */ + protected function get_from_postarr_or_meta( array $postarr, $key, $post_id = null, $default = null ) { + $default_value = get_post_meta( $post_id, $key, true ); + if ( '' === $default_value || null === $post_id ) { + $default_value = $default; + } + + return Tribe__Utils__Array::get( $postarr['meta_input'], $key, $default_value ); + } + + /** + * {@inheritdoc} + */ + public function set_display_context( $context = 'default' ) { + $this->display_context = $context; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function set_render_context( $context = 'default' ) { + $this->render_context = $context; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function get_query_for_posts( array $posts ) { + $posts = array_filter( array_map( 'get_post', $posts ) ); + $query = new \WP_Query(); + // Let's make it look like the posts are the result of a query using `post__in`. + $query->set( 'post__in', wp_list_pluck( $posts, 'ID' ) ); + $query->found_posts = count( $posts ); + $query->posts = $posts; + $query->post_count = count( $posts ); + $query->current_post = - 1; + + return $query; + } + + /** + * {@inheritdoc} + */ + public function pluck( $field ) { + $list = new WP_List_Util( $this->all() ); + + return $list->pluck( $field ); + } + + /** + * {@inheritdoc} + */ + public function filter( $args = [], $operator = 'AND' ) { + $list = new WP_List_Util( $this->all() ); + + return $list->filter( $args, $operator ); + } + + /** + * {@inheritdoc} + */ + public function sort( $orderby = [], $order = 'ASC', $preserve_keys = false ) { + $list = new WP_List_Util( $this->all() ); + + return $list->sort( $orderby, $order, $preserve_keys ); + } + + /** + * {@inheritdoc} + */ + public function collect() { + return new Tribe__Utils__Post_Collection( $this->all() ); + } + + /** + * Builds the ORM query with the query builder. + * + * Allow classes extending or decorating the repository to act before + * the query is built or replace its building completely. + * + * @since 4.9.5 + * + * @return WP_Query|null A built query object or `null` if the builder failed or bailed. + */ + protected function build_query_with_builder() { + $built = $this->query_builder->build_query(); + + $built->builder = $this->query_builder; + + if ( null !== $built ) { + $query = $built; + } + + return $query; + } + + /** + * Builds the ORM query internally, without a query builder. + * + * @since 4.9.5 + * + * @return WP_Query The built query object. + */ + protected function build_query_internally() { + $query = new WP_Query(); + + $query->builder = $this; + + $this->filter_query->set_query( $query ); + + /** + * Here we merge, not recursively, to allow user-set query arguments + * to override the default ones. + */ + $query_args = array_merge( $this->default_args, $this->query_args ); + + $default_post_status = [ 'publish' ]; + if ( current_user_can( 'read_private_posts' ) ) { + $default_post_status[] = 'private'; + } + + $query_args['post_status'] = Tribe__Utils__Array::get( $query_args, 'post_status', $default_post_status ); + + /** + * Filters the query arguments that will be used to fetch the posts. + * + * @param array $query_args An array of the query arguments the query will be + * initialized with. + * @param WP_Query $query The query object, the query arguments have not been parsed yet. + * @param $this $this This repository instance + */ + $query_args = apply_filters( "tribe_repository_{$this->filter_name}_query_args", $query_args, $query, $this ); + + /** + * Provides a last-ditch effort to override the filtered offset. + * + * This should only be used if doing creating pagination for performance purposes. + * + * @since 4.11.0 + * + * @param null|int $filtered_offset Offset parameter setting. + * @param array $query_args List of query arguments. + */ + $filtered_offset = apply_filters( 'tribe_repository_query_arg_offset_override', null, $query_args ); + + if ( $filtered_offset || isset( $query_args['offset'] ) ) { + $per_page = (int) Tribe__Utils__Array::get( $query_args, 'posts_per_page', get_option( 'posts_per_page' ) ); + + if ( $filtered_offset ) { + $query_args['offset'] = $filtered_offset; + } elseif ( isset( $query_args['offset'] ) ) { + $offset = absint( $query_args['offset'] ); + $page = (int) Tribe__Utils__Array::get( $query_args, 'paged', 1 ); + + $real_offset = $per_page === -1 ? $offset : ( $per_page * ( $page - 1 ) ) + $offset; + $query_args['offset'] = $real_offset; + + /** + * Unset the `offset` query argument to avoid applying it multiple times when this method + * is used, on the same repository, more than once. + */ + unset( $this->query_args['offset'] ); + } + + $query_args['posts_per_page'] = $per_page === -1 ? self::MAX_NUMBER_OF_POSTS_PER_PAGE : $per_page; + } + + foreach ( $query_args as $key => $value ) { + $query->set( $key, $value ); + } + + /** + * Here process the previously set query modifiers passing them the + * query object before it executes. + * The query modifiers should modify the query by reference. + */ + foreach ( $this->query_modifiers as $arg ) { + if ( is_object( $arg ) && method_exists( $arg, '__invoke' ) ) { + // __invoke, assume changes are made by reference + $arg( $query ); + } elseif ( is_callable( $arg ) ) { + // assume changes are made by reference + $arg( $query ); + } + } + + return $query; + } + + /** + * {@inheritDoc} + */ + public function hash( array $settings = [], WP_Query $query = null ) { + return md5( json_encode( $this->get_hash_data( $settings, $query ) ) ); + } + + /** + * {@inheritDoc} + */ + public function get_hash_data( array $settings, WP_Query $query = null ) { + $filters = $this->current_filters; + $query_vars = null !== $query + ? $query->query + : array_merge( $this->default_args, $this->query_args ); + + if ( isset( $settings['exclude'] ) ) { + $filters = array_diff_key( + $filters, + array_combine( $settings['exclude'], $settings['exclude'] ) + ); + $query_vars = array_diff_key( + $query_vars, + array_combine( $settings['exclude'], $settings['exclude'] ) + ); + } + + if ( isset( $settings['include'] ) ) { + $filters = array_intersect_key( + $filters, + array_combine( $settings['include'], $settings['include'] ) + ); + $query_vars = array_intersect_key( + $query_vars, + array_combine( $settings['include'], $settings['include'] ) + ); + } + + Tribe__Utils__Array::recursive_ksort( $filters ); + Tribe__Utils__Array::recursive_ksort( $query_vars ); + + return [ 'filters' => $filters, 'query_vars' => $query_vars ]; + } + + /** + * {@inheritDoc} + */ + public function get_last_built_query() { + return $this->last_built_query; + } + + /** + * Checks a SQL relation is valid. + * + * Allowed values are 'OR' and 'AND'. + * + * @since 4.9.6 + * + * @param string $relation The relation to check. + * + * @throws \Tribe__Repository__Usage_Error If the relation is not a valid one. + */ + protected function validate_relation( $relation ) { + if ( ! in_array( $relation, [ 'OR', 'AND' ], true ) ) { + throw Tribe__Repository__Usage_Error::because_this_relation_is_not_valid( $relation ); + } + } + + /** + * Sanitizes and prepares string to be used in a LIKE comparison. + * + * If no leading and trailing `%` was found it will be added at the start and end of the string. + * + * @since 4.9.6 + * + * @param string|array $value The string to prepare or an array of strings to prepare. + * + * @return string|array The sanitized string, or strings. + */ + protected function prepare_like_string( $value ) { + $original_value = $value; + $values = (array) $value; + $prepared = []; + $pattern = '/^(?
    %{0,1})(?.*?)(?%{0,1})$/u';
    +
    +		global $wpdb;
    +
    +		foreach ( $values as $v ) {
    +			preg_match( $pattern, $v, $matches );
    +			$pre = $matches['pre'] ?: '';
    +			$post = $matches['post'] ?: '';
    +			$string = $wpdb->esc_like( $matches['string'] );
    +
    +			if ( '' === $pre && '' === $post ) {
    +				// If the string does not contain any starting and ending placeholder we'll add all combinations.
    +				$prepared[] = '%' . $string;
    +				$prepared[] = $string . '%';
    +				$prepared[] = $string;
    +				$pre = $post = '%';
    +			}
    +
    +			$prepared[] = $pre . $string . $post;
    +		}
    +
    +		return is_array( $original_value ) ? $prepared : reset( $prepared );
    +	}
    +
    +	/**
    +	 * Builds the WHERE clause for a set of fields.
    +	 *
    +	 * This method is table-agnostic. While flexible it will also require some care to be used.
    +	 *
    +	 * @since 4.9.6
    +	 *
    +	 * @param string|array $fields  One or more fields to build the clause for.
    +	 * @param string       $compare The comparison operator to use to build the
    +	 * @param string|array $values One or more values to build the WHERE clause for.
    +	 * @param string       $value_format The format, a `$wpdb::prepare()` compatible one, to use to format the values.
    +	 * @param string       $where_relation The relation to apply between each WHERE fragment.
    +	 * @param string       $value_relation The relation to apply between each value fragment.
    +	 *
    +	 * @return string The built WHERE clause.
    +	 *
    +	 * @throws \Tribe__Repository__Usage_Error If the relations are not valid or another WHERE building issue happens.
    +	 */
    +	protected function build_fields_where_clause(
    +		$fields,
    +		$compare,
    +		$values,
    +		$value_format = '%s',
    +		$where_relation = 'OR',
    +		$value_relation = 'OR'
    +	) {
    +		$this->validate_relation( $where_relation );
    +		$this->validate_relation( $value_relation );
    +		global $wpdb;
    +		$fields_where_clauses = [];
    +		$fields = (array) $fields;
    +		$values = (array) $values;
    +		foreach ( $fields as $field ) {
    +			$value_clauses = [];
    +			foreach ( $values as $compare_value ) {
    +				if ( ! is_array( $compare_value ) || count( $compare_value ) === 1 ) {
    +					$value_clauses[] = $wpdb->prepare(
    +						"({$field} {$compare} {$value_format})",
    +						$compare_value
    +					);
    +				} else {
    +					$value_format = implode(
    +						',',
    +						array_fill( 0, count( $compare_value ), $value_format )
    +					);
    +					$value_clauses[] = $wpdb->prepare(
    +						"({$field} {$compare} ({$value_format}))",
    +						$compare_value
    +					);
    +				}
    +			}
    +			$fields_where_clauses[] = '(' . implode( " {$value_relation} ", $value_clauses ) . ')';
    +		}
    +
    +		$fields_where = $wpdb->remove_placeholder_escape(
    +			implode( " {$where_relation} ", $fields_where_clauses )
    +		);
    +
    +		return $fields_where;
    +	}
    +
    +	/**
    +	 * Returns the term IDs of terms matching a criteria, the match is made on the terms slug and name.
    +	 *
    +	 * This should be used to break-down a query and fetch term IDs, to then use in a "lighter" join, later.
    +	 *
    +	 * @since 4.9.6
    +	 *
    +	 * @param string|array $taxonomy The taxonomy, or taxonomies, to fetch the terms for.
    +	 * @param string $compare The comparison operator to use, e.g. 'LIKE' or '=>'.
    +	 * @param string|array $value An array of values to compare the terms slug or names with.
    +	 * @param string $relation The relation, either 'OR' or 'AND', to apply to the matching.
    +	 * @param string $format The format, a `$wpdb::prepare()` supported one, to use to format the values for the query.
    +	 *
    +	 * @return array An array of term IDs matching the query, if any.
    +	 */
    +	protected function fetch_taxonomy_terms_matches( $taxonomy, $compare, $value, $relation = 'OR', $format = '%s' ) {
    +		global $wpdb;
    +		$taxonomies = (array) $taxonomy;
    +		$values = (array) $value;
    +
    +		$compare_target = count( $values ) > 1
    +			? '(' . $this->filter_query->create_interval_of_strings( $values ) . ')'
    +			: $wpdb->prepare( $format, reset( $values ) );
    +
    +		$taxonomies_interval = $this->filter_query->create_interval_of_strings( $taxonomies );
    +
    +		$query = "SELECT  tt.term_taxonomy_id FROM {$wpdb->terms} AS t
    +			INNER JOIN {$wpdb->term_taxonomy} AS tt ON t.term_id = tt.term_id
    +			WHERE tt.taxonomy IN ({$taxonomies_interval}) AND
    +			( t.slug {$compare} {$compare_target} {$relation} t.name {$compare} {$compare_target} )";
    +
    +		return $wpdb->get_col( $wpdb->remove_placeholder_escape( $query ) );
    +	}
    +
    +	/**
    +	 * {@inheritDoc}
    +	 */
    +	public function where_multi( array $fields, $compare, $value, $where_relation = 'OR', $value_relation = 'OR' ) {
    +		$compare = strtoupper( trim( $compare ) );
    +
    +		// Check each value is compatible with the comparison operator.
    +		$values = (array) $value;
    +		foreach ( $values as $v ) {
    +			$this->validate_operator_and_values( $compare, 'where_multi', $v );
    +		}
    +
    +		global $wpdb;
    +
    +		if ( in_array( $compare, [ 'LIKE', 'NOT LIKE' ], true ) ) {
    +			$values = $this->prepare_like_string( $values );
    +		}
    +
    +		$where_relation = strtoupper( trim( $where_relation ) );
    +		$this->validate_relation( $where_relation );
    +		$value_relation = strtoupper( trim( $value_relation ) );
    +		$this->validate_relation( $value_relation );
    +
    +		$post_fields = [];
    +		$taxonomies = [];
    +
    +		foreach ( $fields as $field ) {
    +			if ( $this->is_a_post_field( $field ) ) {
    +				$post_fields[] = $field;
    +			} elseif ( array_key_exists( $field, $this->simple_tax_schema ) ) {
    +				// Handle simple tax schema aliases.
    +				$schema = $this->simple_tax_schema[ $field ]['taxonomy'];
    +
    +				if ( ! is_array( $schema ) ) {
    +					$taxonomies[] = $schema;
    +
    +					continue;
    +				}
    +
    +				// If doing an AND where relation, pass all taxonomies in to be grouped with OR.
    +				if ( 'AND' === $where_relation ) {
    +					$this->where_multi( $schema, $compare, $value, 'OR', $value_relation );
    +
    +					continue;
    +				}
    +
    +				foreach ( $schema as $taxonomy ) {
    +					$taxonomies[] = $taxonomy;
    +				}
    +			} elseif ( array_key_exists( $field, $this->simple_meta_schema ) ) {
    +				// Handle simple meta schema aliases.
    +				$schema = $this->simple_meta_schema[ $field ]['meta_key'];
    +
    +				if ( ! is_array( $schema ) ) {
    +					$custom_fields[] = $schema;
    +
    +					continue;
    +				}
    +
    +				// If doing an AND where relation, pass all meta keys in to be grouped with OR.
    +				if ( 'AND' === $where_relation ) {
    +					$this->where_multi( $schema, $compare, $value, 'OR', $value_relation );
    +
    +					continue;
    +				}
    +
    +				foreach ( $schema as $meta_key ) {
    +					$custom_fields[] = $meta_key;
    +				}
    +			} elseif ( $this->is_a_taxonomy( $field ) ) {
    +				$taxonomies[] = $field;
    +			} else {
    +				$custom_fields[] = $field;
    +			}
    +		}
    +
    +		$value_formats = [];
    +
    +		foreach ( $values as $v ) {
    +			$value_format = '%d';
    +			if ( is_string( $v ) ) {
    +				$value_format = '%s';
    +			} elseif ( (int) $v !== (float) $v ) {
    +				$value_format = '%f';
    +			}
    +			$value_formats[] = $value_format;
    +		}
    +
    +		// If the value formats differ then treat all of them as strings.
    +		if ( count( array_unique( $value_formats ) ) > 1 ) {
    +			$value_format = '%s';
    +		} else {
    +			$value_format = reset( $value_formats );
    +		}
    +
    +		$where = [];
    +
    +		if ( ! empty( $post_fields ) ) {
    +			$post_fields = array_map( static function ( $post_field ) use ( $wpdb ) {
    +				return "{$wpdb->posts}.$post_field";
    +			}, $post_fields );
    +
    +			$post_fields_where = $this->build_fields_where_clause(
    +				$post_fields,
    +				$compare,
    +				$values,
    +				$value_format,
    +				$where_relation,
    +				$value_relation
    +			);
    +
    +			$wheres[] = $post_fields_where;
    +		}
    +
    +		if ( ! empty( $taxonomies ) ) {
    +			$all_matching_term_ids = [];
    +			$taxonomy_values = $values;
    +
    +			if ( in_array( $compare, [ 'IN', 'NOT IN', 'BETWEEN', 'NOT BETWEEN' ], true ) ) {
    +				// We can use multiple values in the same query.
    +				$taxonomy_values = [ $values ];
    +			}
    +
    +			foreach ( $taxonomy_values as $taxonomy_value ){
    +				$matching_term_ids = $this->fetch_taxonomy_terms_matches(
    +					$taxonomies,
    +					$compare,
    +					$taxonomy_value,
    +					$where_relation,
    +					$value_format
    +				);
    +
    +				if ( empty( $matching_term_ids ) ) {
    +					if ( 'AND' === $value_relation ) {
    +						// No reason to waste any more time.
    +						$this->void_query = true;
    +
    +						return $this;
    +					}
    +
    +					continue;
    +				}
    +
    +				$all_matching_term_ids[] = $matching_term_ids;
    +			}
    +
    +			$intersection = count( $all_matching_term_ids ) > 1
    +				? array_intersect( ...$all_matching_term_ids )
    +				: reset( $all_matching_term_ids );
    +
    +			if ( 'AND' === $where_relation && 0 === count( $intersection ) ) {
    +				// Let's not waste any more time.
    +				$this->void_query = true;
    +
    +				return $this;
    +			}
    +
    +			$merge = count( $all_matching_term_ids ) > 1
    +				? array_unique( array_merge( ...$all_matching_term_ids ) )
    +				: (array) reset( $all_matching_term_ids );
    +			$matching_term_ids = $where_relation === 'OR' ? array_filter( $merge ) : array_filter( $intersection );
    +
    +			if ( 'AND' === $where_relation || ! empty( $matching_term_ids ) ) {
    +				// Let's not add WHERE and JOIN clauses if there is nothing to add.
    +				$tt_alias = 'tribe_tt_' . self::$alias_counter ++;
    +				$this->filter_query->join(
    +					"JOIN {$wpdb->term_relationships} {$tt_alias} ON {$wpdb->posts}.ID = {$tt_alias}.object_id"
    +				);
    +				$matching_term_ids_interval = implode( ',', $matching_term_ids );
    +				$wheres[] = "{$tt_alias}.term_taxonomy_id IN ({$matching_term_ids_interval})";
    +			}
    +		}
    +
    +		if ( ! empty( $custom_fields ) ) {
    +			$meta_alias = 'tribe_meta_' . self::$alias_counter ++;
    +
    +			$custom_fields = array_map( static function ( $custom_field ) use ( $wpdb, $meta_alias ) {
    +				return $wpdb->prepare(
    +					"{$meta_alias}.meta_key = %s AND {$meta_alias}.meta_value",
    +					$custom_field
    +				);
    +			}, $custom_fields );
    +
    +			$meta_where = $this->build_fields_where_clause(
    +				$custom_fields,
    +				$compare,
    +				$values,
    +				$value_format,
    +				$where_relation,
    +				$value_relation
    +			);
    +
    +			$this->filter_query->join(
    +				"JOIN {$wpdb->postmeta} {$meta_alias} ON {$wpdb->posts}.ID = {$meta_alias}.post_id"
    +			);
    +
    +			$wheres[] = $meta_where;
    +		}
    +
    +		$this->filter_query->where( implode( " {$where_relation} ", $wheres ) );
    +
    +		return $this;
    +	}
    +
    +	/**
    +	 * {@inheritDoc}
    +	 */
    +	public function set_query( WP_Query $query ) {
    +		if (
    +			$this->last_built_query instanceof WP_Query
    +			&& !empty($this->last_built_query->request)
    +		){
    +			throw Tribe__Repository__Usage_Error::because_query_cannot_be_set_after_it_ran();
    +		}
    +		$this->last_built_query = $query;
    +		$this->last_built_hash  = $this->hash();
    +
    +		return $this;
    +	}
    +
    +	/**
    +	 * {@inheritDoc}
    +	 */
    +	public function set_found_rows( $found_rows ) {
    +		$this->skip_found_rows = ! $found_rows;
    +
    +		return $this;
    +	}
    +
    +	/**
    +	 * Flush current filters and query information.
    +	 *
    +	 * @since 4.9.10
    +	 *
    +	 * @return self
    +	 */
    +	public function flush() {
    +		$this->current_query    = null;
    +		$this->current_filters  = [];
    +		$this->current_filter   = null;
    +		$this->last_built_query = null;
    +		$this->last_built_hash  = '';
    +
    +		return $this;
    +	}
    +
    +	/**
    +	 * {@inheritDoc}
    +	 */
    +	public function next() {
    +		$next         = clone $this;
    +		$current_page = isset( $this->query_args['paged'] )
    +			? (int) $this->query_args['paged']
    +			: 1;
    +		$next->page( $current_page + 1 );
    +
    +		// Let's try to avoid running a query if we already know if a next page will yield any result or not.
    +		$query_ran = ! empty( $this->last_built_query ) && ! empty( $this->last_built_query->request );
    +		if ( $query_ran && ( false === (bool) $this->last_built_query->get( 'no_found_rows' ) ) ) {
    +			$found             = $this->last_built_query->found_posts;
    +			$posts_per_page    = $this->last_built_query->get( 'posts_per_page' );
    +			$this_is_last_page = ( $current_page * $posts_per_page ) >= $found;
    +			if ( $this_is_last_page ) {
    +				$next->void_query = true;
    +			}
    +		}
    +
    +		$next->last_built_query = null;
    +
    +		return $next;
    +	}
    +
    +	/**
    +	 * {@inheritDoc}
    +	 */
    +	public function prev() {
    +		$prev         = clone $this;
    +		$current_page = isset( $this->query_args['paged'] )
    +			? (int) $this->query_args['paged']
    +			: 1;
    +
    +		if ( $current_page === 1 ) {
    +			$prev->void_query = true;
    +
    +			return $prev;
    +		}
    +
    +		// If we're on page 1 we know there will be previous posts.
    +		$prev->page( $current_page - 1 );
    +		$prev->last_built_query = null;
    +
    +		return $prev;
    +	}
    +
    +	/**
    +	 * {@inheritDoc}
    +	 */
    +	public function void_query( $void_query = true ) {
    +		$this->void_query = (bool) $void_query;
    +
    +		return $this;
    +	}
    +
    +	/**
    +	 * {@inheritDoc}
    +	 */
    +	public function get_last_sql(): ?string {
    +		return $this->last_built_query instanceof WP_Query ?
    +			$this->last_built_query->request
    +			: null;
    +	}
    +}
    diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Repository/Core_Read_Interface.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Repository/Core_Read_Interface.php
    new file mode 100644
    index 000000000..ca777fb70
    --- /dev/null
    +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Repository/Core_Read_Interface.php
    @@ -0,0 +1,402 @@
    +,  ]. * * @return Tribe__Repository__Read_Interface
    +	 */
    +	public function by_args( array $args );
    +
    +	/**
    +	 * Applies a filter to the query.
    +	 *
    +	 * While the signature only shows 2 arguments additional arguments will be passed
    +	 * to the schema filters.
    +	 *
    +	 * @since 4.7.19
    +	 *
    +	 * @param string $key
    +	 * @param mixed  $value
    +	 * @param mixed  ...$args Additional, optional, call arguments that will be passed to
    +	 *                        the schema.
    +	 *
    +	 * @return Tribe__Repository__Read_Interface
    +	 */
    +	public function by( $key, $value = null );
    +
    +	/**
    +	 * Just an alias of the `by` method to allow for easier reading.
    +	 *
    +	 * @since 4.7.19
    +	 *
    +	 * @param string $key
    +	 * @param mixed  $value
    +	 *
    +	 * @return Tribe__Repository__Read_Interface
    +	 */
    +	public function where( $key, $value = null );
    +
    +	/**
    +	 * Sets the page of posts to fetch.
    +	 *
    +	 * Mind that this implementation does not support a `by( 'page', 2 )`
    +	 * filter to force more readable code.
    +	 *
    +	 * @since 4.7.19
    +	 *
    +	 * @param int $page
    +	 *
    +	 * @return Tribe__Repository__Read_Interface
    +	 */
    +	public function page( $page );
    +
    +	/**
    +	 * Sets the number of posts to retrieve per page.
    +	 *
    +	 * Mind that this implementation does not support a `by( 'per_page', 5 )`
    +	 * filter to force more readable code; by default posts per page is set to
    +	 * the pagination defaults for the post type.
    +	 *
    +	 * @param int $per_page
    +	 *
    +	 * @return Tribe__Repository__Read_Interface
    +	 */
    +	public function per_page( $per_page );
    +
    +	/**
    +	 * Returns the number of posts found matching the query.
    +	 *
    +	 * Mind that this value ignores the offset returning the
    +	 * number of results if limits where not applied.
    +	 *
    +	 * @since 4.7.19
    +	 *
    +	 * @return int
    +	 */
    +	public function found();
    +
    +	/**
    +	 * Returns all posts matching the query.
    +	 *
    +	 * Mind that "all" means "all the posts matching all the filters" so pagination applies.
    +	 *
    +	 * @return array
    +	 */
    +	public function all();
    +
    +	/**
    +	 * Sets the offset on the query.
    +	 *
    +	 * Mind that this implementation does not support a `by( 'offset', 2 )`
    +	 * filter to force more readable code.
    +	 *
    +	 * @since 4.7.19
    +	 *
    +	 * @param int  $offset
    +	 * @param bool $increment Whether to increment the offset by the value
    +	 *                        or replace it.
    +	 *
    +	 * @return Tribe__Repository__Read_Interface
    +	 */
    +	public function offset( $offset, $increment = false );
    +
    +	/**
    +	 * Sets the order on the query.
    +	 *
    +	 * Mind that this implementation does not support a `by( 'order', 2 )`
    +	 * filter to force more readable code.
    +	 *
    +	 * @since 4.7.19
    +	 *
    +	 * @param string $order
    +	 *
    +	 * @return Tribe__Repository__Read_Interface
    +	 */
    +	public function order( $order = 'ASC' );
    +
    +	/**
    +	 * Sets the order criteria results should be fetched by.
    +	 *
    +	 * Mind that this implementation does not support a `by( 'order_by', 'title' )`
    +	 * filter to force more readable code.
    +	 *
    +	 * @since 4.7.19
    +	 *
    +	 * @param string|array $order_by The post field, custom field or alias key to order posts by.
    +	 * @param string                      $order    The order direction; optional; shortcut for the `order` method; defaults
    +	 *                                              to `DESC`.
    +	 *
    +	 * @return Tribe__Repository__Read_Interface
    +	 */
    +	public function order_by( $order_by, $order = 'DESC' );
    +
    +	/**
    +	 * Sets the fields that should be returned by the query.
    +	 *
    +	 * Mind that this implementation does not support a `by( 'fields', 'ids' )`
    +	 * filter to force more readable code.
    +	 *
    +	 * @since 4.7.19
    +	 *
    +	 * @param string $fields
    +	 *
    +	 * @return Tribe__Repository__Read_Interface
    +	 */
    +	public function fields( $fields );
    +
    +	/**
    +	 * Sugar method to set the `post__in` argument.
    +	 *
    +	 * Successive calls will stack, not replace each one.
    +	 *
    +	 * @since 4.7.19
    +	 *
    +	 * @param array|int $post_ids
    +	 *
    +	 * @return Tribe__Repository__Read_Interface
    +	 */
    +	public function in( $post_ids );
    +
    +	/**
    +	 * Sugar method to set the `post__not_in` argument.
    +	 *
    +	 * Successive calls will stack, not replace each one.
    +	 *
    +	 * @since 4.7.19
    +	 *
    +	 * @param array|int $post_ids
    +	 *
    +	 * @return Tribe__Repository__Read_Interface
    +	 */
    +	public function not_in( $post_ids );
    +
    +	/**
    +	 * Sugar method to set the `post_parent__in` argument.
    +	 *
    +	 * Successive calls will stack, not replace each one.
    +	 *
    +	 * @since 4.7.19
    +	 *
    +	 * @param array|int $post_id
    +	 *
    +	 * @return Tribe__Repository__Read_Interface
    +	 */
    +	public function parent( $post_id );
    +
    +	/**
    +	 * Sugar method to set the `post_parent__in` argument.
    +	 *
    +	 * Successive calls will stack, not replace each one.
    +	 *
    +	 * @since 4.7.19
    +	 *
    +	 * @param array $post_ids
    +	 *
    +	 * @return Tribe__Repository__Read_Interface
    +	 */
    +	public function parent_in( $post_ids );
    +
    +	/**
    +	 * Sugar method to set the `post_parent__not_in` argument.
    +	 *
    +	 * Successive calls will stack, not replace each one.
    +	 *
    +	 * @since 4.7.19
    +	 *
    +	 * @param array $post_ids
    +	 *
    +	 * @return Tribe__Repository__Read_Interface
    +	 */
    +	public function parent_not_in( $post_ids );
    +
    +	/**
    +	 * Sugar method to set the `s` argument.
    +	 *
    +	 * Successive calls will replace the search string.
    +	 * This is the default WordPress search, to search by title,
    +	 * content or excerpt only use the `title`, `content`, `excerpt` filters.
    +	 *
    +	 * @param $search
    +	 *
    +	 * @return Tribe__Repository__Read_Interface
    +	 */
    +	public function search( $search );
    +
    +	/**
    +	 * Returns the number of posts found matching the query in the current page.
    +	 *
    +	 * While the `found` method will return the number of posts found
    +	 * across all pages this method will only return the number of
    +	 * posts found in the current page.
    +	 * Differently from the `found` method this method will apply the
    +	 * offset if set.
    +	 *
    +	 * @since 4.7.19
    +	 *
    +	 * @return int
    +	 */
    +	public function count();
    +
    +	/**
    +	 * Returns the first post of the page matching the current query.
    +	 *
    +	 * If, by default or because set with the `per_page` method, all
    +	 * posts matching the query should be returned then this will be
    +	 * the first post of all those matching the query.
    +	 *
    +	 * @since 4.7.19
    +	 *
    +	 * @return WP_Post|mixed|null
    +	 *
    +	 * @see   Tribe__Repository__Read_Interface::per_page()
    +	 */
    +	public function first();
    +
    +	/**
    +	 * Returns the last post of the page matching the current query.
    +	 *
    +	 * If, by default or because set with the `per_page` method, all
    +	 * posts matching the query should be returned then this will be
    +	 * the last post of all those matching the query.
    +	 *
    +	 * @since 4.7.19
    +	 *
    +	 * @return WP_Post|mixed|null
    +	 *
    +	 * @see   Tribe__Repository__Read_Interface::per_page()
    +	 */
    +	public function last();
    +
    +	/**
    +	 * Returns the nth post (1-based) of the page matching the current query.
    +	 *
    +	 * Being 1-based the second post can be fetched using `nth( 2 )`.
    +	 * If, by default or because set with the `per_page` method, all
    +	 * posts matching the query should be returned then this will be
    +	 * the nth post of all those matching the query.
    +	 *
    +	 * @since 4.7.19
    +	 *
    +	 * @param int $n
    +	 *
    +	 * @return WP_Post|mixed|null
    +	 *
    +	 * @see   Tribe__Repository__Read_Interface::per_page()
    +	 */
    +	public function nth( $n );
    +
    +	/**
    +	 * Returns the first n posts of the page matching the current query.
    +	 *
    +	 * If, by default or because set with the `per_page` method, all
    +	 * posts matching the query should be returned then this method will
    +	 * return the first n posts of all those matching the query.
    +	 *
    +	 * @since 4.7.19
    +	 *
    +	 * @return array An array of posts matching the query.
    +	 *
    +	 * @see   Tribe__Repository__Read_Interface::per_page()
    +	 */
    +	public function take( $n );
    +
    +	/**
    +	 * Plucks a field from all results and returns it.
    +	 *
    +	 * This method will implicitly build and use a `WP_List_Util` instance on the return
    +	 * value of a call to the `all` method.
    +	 *
    +	 * @since 4.9.5
    +	 *
    +	 * @param string $field The field to pluck from each result.
    +	 *
    +	 * @return array An array of the plucked results.
    +	 *
    +	 * @see   \wp_list_pluck()
    +	 */
    +	public function pluck( $field );
    +
    +	/**
    +	 * Filters the results according to the specified criteria.
    +	 *
    +	 * This method will implicitly build and use a `WP_List_Util` instance on the return
    +	 * value of a call to the `all` method.
    +	 *
    +	 * @since 4.9.5
    +	 *
    +	 * @param array  $args     Optional. An array of key => value arguments to match
    +	 *                         against each object. Default empty array.
    +	 * @param string $operator Optional. The logical operation to perform. 'AND' means
    +	 *                         all elements from the array must match. 'OR' means only
    +	 *                         one element needs to match. 'NOT' means no elements may
    +	 *                         match. Default 'AND'.
    +	 *
    +	 * @return array An array of the filtered results.
    +	 *
    +	 * @see   \wp_list_filter()
    +	 */
    +	public function filter( $args = [], $operator = 'AND' );
    +
    +	/**
    +	 * Sorts the results according to the specified criteria.
    +	 *
    +	 * This method will implicitly build and use a `WP_List_Util` instance on the return
    +	 * value of a call to the `all` method.
    +	 *
    +	 * @since 4.9.5
    +	 *
    +	 * @param string|array $orderby       Optional. Either the field name to order by or an array
    +	 *                                    of multiple orderby fields as $orderby => $order.
    +	 * @param string       $order         Optional. Either 'ASC' or 'DESC'. Only used if $orderby
    +	 *                                    is a string.
    +	 * @param bool         $preserve_keys Optional. Whether to preserve keys. Default false.
    +	 *
    +	 * @return array An array of the sorted results.
    +	 *
    +	 * @see   \wp_list_sort()
    +	 */
    +	public function sort( $orderby = [], $order = 'ASC', $preserve_keys = false );
    +
    +	/**
    +	 * Builds a collection on the result of the `all()` method call.
    +	 *
    +	 * @since 4.9.5
    +	 *
    +	 * @return \Tribe__Utils__Post_Collection
    +	 */
    +	public function collect();
    +
    +	/**
    +	 * Gets the ids of the posts matching the query.
    +	 *
    +	 * @return array An array containing the post IDs to update.
    +	 */
    +	public function get_ids();
    +}
    diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Repository/Decorator.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Repository/Decorator.php
    new file mode 100644
    index 000000000..6947f609d
    --- /dev/null
    +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Repository/Decorator.php
    @@ -0,0 +1,704 @@
    +decorated->get_default_args();
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function set_default_args( array $default_args ) {
    +		return $this->decorated->set_default_args( $default_args );
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function filter_name( $filter_name ) {
    +		$this->decorated->filter_name( $filter_name );
    +
    +		return $this;
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function by_args( array $args ) {
    +		$this->decorated->by_args( $args );
    +
    +		return $this;
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function by( $key, $value = null ) {
    +		$call_args = func_get_args();
    +		call_user_func_array( [ $this->decorated, 'by' ], $call_args );
    +
    +		return $this;
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function where( $key, $value = null ) {
    +		$call_args = func_get_args();
    +		call_user_func_array( [ $this->decorated, 'where' ], $call_args );
    +
    +		return $this;
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function page( $page ) {
    +		$this->decorated->page( $page );
    +
    +		return $this;
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function per_page( $per_page ) {
    +		$this->decorated->per_page( $per_page );
    +
    +		return $this;
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function found() {
    +		return $this->decorated->found();
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function all() {
    +		return $this->decorated->all();
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function offset( $offset, $increment = false ) {
    +		$this->decorated->offset( $offset );
    +
    +		return $this;
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function order( $order = 'ASC' ) {
    +		$this->decorated->order( $order );
    +
    +		return $this;
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function order_by( $order_by, $order = 'DESC' ) {
    +		$this->decorated->order_by( $order_by, $order );
    +
    +		return $this;
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function fields( $fields ) {
    +		$this->decorated->fields( $fields );
    +
    +		return $this;
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function permission( $permission ) {
    +		$this->decorated->permission( $permission );
    +
    +		return $this;
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function in( $post_ids ) {
    +		$this->decorated->in( $post_ids );
    +
    +		return $this;
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function not_in( $post_ids ) {
    +		$this->decorated->not_in( $post_ids );
    +
    +		return $this;
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function parent( $post_id ) {
    +		$this->decorated->parent( $post_id );
    +
    +		return $this;
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function parent_in( $post_ids ) {
    +		$this->decorated->parent_in( $post_ids );
    +
    +		return $this;
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function parent_not_in( $post_ids ) {
    +		$this->decorated->parent_not_in( $post_ids );
    +
    +		return $this;
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function search( $search ) {
    +		$this->decorated->search( $search );
    +
    +		return $this;
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function count() {
    +		return $this->decorated->count();
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function first() {
    +		return $this->decorated->first();
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function last() {
    +		return $this->decorated->last();
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function nth( $n ) {
    +		return $this->decorated->nth( $n );
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function take( $n ) {
    +		return $this->decorated->take( $n );
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function by_primary_key( $primary_key ) {
    +		return $this->decorated->by_primary_key( $primary_key );
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function set( $key, $value ) {
    +		$this->decorated->set( $key, $value );
    +
    +		return $this;
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function get_query() {
    +		return $this->decorated->get_query();
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function set_args( array $update_map ) {
    +		$this->decorated->set_args( $update_map );
    +
    +		return $this;
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function save( $return_promise = true ) {
    +		$this->decorated->save( $return_promise );
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function set_formatter( Tribe__Repository__Formatter_Interface $formatter ) {
    +		$this->decorated->set_formatter( $formatter );
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function join_clause( $join ) {
    +		$this->decorated->join_clause( $join );
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function where_clause( $where ) {
    +		$this->decorated->where_clause( $where );
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function set_query_builder( $query_builder ) {
    +		$this->decorated->set_query_builder( $query_builder );
    +	}
    +
    +	/**
    +	 * Sets the repository to be decorated.
    +	 *
    +	 * @since 4.7.19
    +	 *
    +	 * @param Tribe__Repository__Interface $decorated
    +	 */
    +	protected function set_decorated_repository( Tribe__Repository__Interface $decorated ) {
    +		$this->decorated = $decorated;
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function build_query( $use_query_builder = true ) {
    +		return $this->decorated->build_query( $use_query_builder );
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function where_or( $callbacks ) {
    +		$call_args = func_get_args();
    +		call_user_func_array( [ $this->decorated, 'where_or' ], $call_args );
    +
    +		return $this;
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function by_related_to_min( $by_meta_keys, $min, $keys = null, $values = null ) {
    +		$this->decorated->by_related_to_min( $by_meta_keys, $min, $keys, $values );
    +
    +		return $this;
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function by_related_to_max( $by_meta_keys, $max, $keys = null, $values = null ) {
    +		$this->decorated->by_related_to_max( $by_meta_keys, $max, $keys, $values );
    +
    +		return $this;
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function by_related_to_between( $by_meta_keys, $min, $max, $keys = null, $values = null ) {
    +		$this->decorated->by_related_to_between( $by_meta_keys, $min, $max, $keys, $values );
    +
    +		return $this;
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function by_not_related_to( $by_meta_keys, $keys = null, $values = null ) {
    +		$this->decorated->by_not_related_to( $by_meta_keys, $keys, $values );
    +
    +		return $this;
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function has_filter( $key, $value = null ) {
    +		return $this->decorated->has_filter( $key, $value );
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function get_current_filter() {
    +		return $this->decorated->get_current_filter();
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function get_ids() {
    +		return $this->decorated->get_ids();
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function add_schema_entry( $key, $callback ) {
    +		$this->decorated->add_schema_entry( $key, $callback );
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function prepare_interval( $values, $format = '%s' ) {
    +		return $this->decorated->prepare_interval( $values, $format );
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function delete( $return_promise = false ) {
    +		return $this->decorated->delete( $return_promise );
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function async_delete( array $to_delete, $return_promise = true ) {
    +		return $this->decorated->async_delete( $to_delete, $return_promise );
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function add_update_field_alias( $alias, $field_name ) {
    +		$this->decorated->add_update_field_alias( $alias, $field_name );
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function async_update( array $to_update, $return_promise = true ) {
    +		return $this->decorated->async_update( $to_update, $return_promise );
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function get_update_fields_aliases() {
    +		return $this->decorated->get_update_fields_aliases();
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function set_update_fields_aliases( array $update_fields_aliases ) {
    +		$this->decorated->set_update_fields_aliases( $update_fields_aliases );
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function get_filter_name() {
    +		return $this->decorated->get_filter_name();
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function filter_postarr_for_update( array $postarr, $post_id ) {
    +		return $this->decorated->filter_postarr_for_update( $postarr, $post_id );
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function build_postarr( $id = null ) {
    +		return $this->decorated->build_postarr();
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function create() {
    +		return $this->decorated->create();
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function filter_postarr_for_create( array $postarr ) {
    +		return $this->decorated->filter_postarr_for_create( $postarr, $post_id );
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function set_create_args( array $create_args ) {
    +		$this->decorated->set_create_args( $create_args );
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function get_create_args() {
    +		return $this->decorated->get_create_args();
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function set_display_context( $context = 'default' ) {
    +		$this->decorated->set_display_context( $context );
    +
    +		return $this;
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function set_render_context( $context = 'default' ) {
    +		$this->decorated->set_render_context( $context );
    +
    +		return $this;
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function get_query_for_posts( array $posts ) {
    +		return $this->decorated->get_query_for_posts( $posts );
    +	}
    +
    +	/**
    +	 * Whether the decorator is decorating an instance of a specific repository class or not.
    +	 *
    +	 * The check is made recursively for decorators to get to the first repository implementation.
    +	 *
    +	 * @since 4.9.5
    +	 *
    +	 * @param string $class The class to check for.
    +	 *
    +	 * @return bool Whether the decorator is decorating an instance of a specific repository class or not.
    +	 */
    +	public function decorates_an_instance_of( $class ) {
    +		return $this->decorated instanceof Tribe__Repository__Decorator
    +			? $this->decorated->decorates_an_instance_of( $class )
    +			: $this->decorated instanceof $class;
    +	}
    +
    +	/**
    +	 * Returns the concrete repository implementation that's "hidden" under the decorator(s).
    +	 *
    +	 * @since 4.9.5
    +	 *
    +	 * @return \Tribe__Repository__Interface The concrete repository instance.
    +	 */
    +	public function get_decorated_repository() {
    +		return $this->decorated instanceof Tribe__Repository__Decorator
    +			? $this->decorated->get_decorated_repository()
    +			: $this->decorated;
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function pluck( $field ) {
    +		return $this->decorated->pluck( $field );
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function filter( $orderby = [], $order = 'ASC', $preserve_keys = false ) {
    +		return $this->decorated->filter( $orderby, $order, $preserve_keys );
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function sort( $orderby = [], $order = 'ASC', $preserve_keys = false ) {
    +		return $this->decorated->sort( $orderby, $order, $preserve_keys );
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function collect() {
    +		return $this->decorated->collect();
    +	}
    +
    +	/**
    +	 * {@inheritdoc}
    +	 */
    +	public function hash( array $settings = [], WP_Query $query = null ) {
    +		return $this->decorated->hash( $settings );
    +	}
    +
    +	/**
    +	 * {@inheritDoc}
    +	 */
    +	public function get_hash_data( array $settings, WP_Query $query = null ) {
    +		return $this->decorated->get_hash_data( $settings, $query );
    +	}
    +
    +	/**
    +	 * {@inheritDoc}
    +	 */
    +	public function get_last_built_query() {
    +		return $this->decorated->last_built_query;
    +	}
    +
    +	/**
    +	 * {@inheritDoc}
    +	 */
    +	public function where_multi( array $fields, $compare, $value, $where_relation = 'OR', $value_relation = 'OR' ) {
    +		$this->decorated->where_multi( $fields, $compare, $value, $where_relation, $value_relation );
    +
    +		return $this;
    +	}
    +
    +	/**
    +	 * Handle getting additional property from decorated object.
    +	 *
    +	 * @since 4.9.6.1
    +	 *
    +	 * @param string $name Property name.
    +	 *
    +	 * @return mixed
    +	 */
    +	public function __get( $name ) {
    +		return $this->decorated->{$name};
    +	}
    +
    +	/**
    +	 * Handle setting additional property on decorated object.
    +	 *
    +	 * @since 4.9.6.1
    +	 *
    +	 * @param string $name  Property name.
    +	 * @param mixed  $value Property value.
    +	 */
    +	public function __set( $name, $value ) {
    +		$this->decorated->{$name} = $value;
    +	}
    +
    +	/**
    +	 * Check if additional property on decorated object exists.
    +	 *
    +	 * @since 4.9.6.1
    +	 *
    +	 * @param string $name Property name.
    +	 *
    +	 * @return bool
    +	 */
    +	public function __isset( $name ) {
    +		return isset( $this->decorated->{$name} );
    +	}
    +
    +	/**
    +	 * Call methods on decorated object.
    +	 *
    +	 * @since 4.9.6.1
    +	 *
    +	 * @param string $name      Method name.
    +	 * @param array  $arguments Method arguments.
    +	 *
    +	 * @return mixed
    +	 */
    +	public function __call( $name, $arguments ) {
    +		return call_user_func_array( [ $this->decorated, $name ], $arguments );
    +	}
    +
    +	/**
    +	 * {@inheritDoc}
    +	 */
    +	public function set_query( WP_Query $query ) {
    +		$this->decorated->set_query( $query );
    +
    +		return $this;
    +	}
    +
    +	/**
    +	 * {@inheritDoc}
    +	 */
    +	public function next() {
    +		return $this->decorated->next();
    +	}
    +
    +	/**
    +	 * {@inheritDoc}
    +	 */
    +	public function prev() {
    +		return $this->decorated->prev();
    +	}
    +
    +	/**
    +	 * {@inheritDoc}
    +	 */
    +	public function set_found_rows( $found_rows ) {
    +		$this->decorated->set_found_rows( $found_rows );
    +
    +		return $this;
    +	}
    +
    +	/**
    +	 * {@inheritDoc}
    +	 */
    +	public function void_query( $void_query = true ) {
    +		$this->decorated->void_query( $void_query );
    +
    +		return $this;
    +	}
    +
    +	/**
    +	 * {@inheritDoc}
    +	 */
    +	public function get_last_sql(): ?string {
    +		return $this->decorated->get_last_sql();
    +	}
    +}
    diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Repository/Filter_Validation.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Repository/Filter_Validation.php
    new file mode 100644
    index 000000000..11573f1e5
    --- /dev/null
    +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Repository/Filter_Validation.php
    @@ -0,0 +1,61 @@
    + => [  =>  ]
    +	 *  ]
    +	 * ```
    +	 *
    +	 * @since 4.10.2
    +	 *
    +	 * @param       string $filter The name of the filter currently validating.
    +	 * @param array        $call_args The current filter call args, usually `func_get_args()`.
    +	 *
    +	 * @throws Usage_Error If there is a definition for the filter and the argument count or nature is not correct.
    +	 */
    +	protected function ensure_args_for_filter( $filter, array $call_args ) {
    +		$map = isset( static::$filter_args_map ) ? static::$filter_args_map : false;
    +
    +		if ( empty( $map ) ) {
    +			return;
    +		}
    +
    +		$required_args = Arr::get( $filter, $map, false );
    +
    +		if ( false === $required_args ) {
    +			return;
    +		}
    +
    +		if ( count( $required_args ) !== count( $call_args ) ) {
    +			throw Usage_Error::because_filter_requires_args( $filter, array_keys( $required_args ) );
    +		}
    +
    +		$iterator = new \MultipleIterator();
    +		$iterator->attachIterator( new \ArrayIterator( array_keys( $required_args ) ) );
    +		$iterator->attachIterator( new \ArrayIterator( array_values( $required_args ) ) );
    +		$iterator->attachIterator( new \ArrayIterator( $call_args ) );
    +
    +		foreach ( $required_args as list( $arg_name, $validator, $input ) ) {
    +			if ( empty( $validator( $input ) ) ) {
    +				throw Usage_Error::because_filter_arg_is_not_valid( $filter, $arg_name );
    +			}
    +		}
    +	}
    +}
    diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Repository/Formatter_Interface.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Repository/Formatter_Interface.php
    new file mode 100644
    index 000000000..4e484a769
    --- /dev/null
    +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Repository/Formatter_Interface.php
    @@ -0,0 +1,24 @@
    +, <...args>]
    +	 *
    +	 * @return $this
    +	 * @throws Tribe__Repository__Usage_Error If one of the callback methods does
    +	 *                                        not add any WHERE clause.
    +	 *
    +	 * @see Tribe__Repository::where_clause()
    +	 * @see Tribe__Repository__Query_Filters::where()
    +	 */
    +	public function where_or( $callbacks );
    +
    +	/**
    +	 * Filters the query to return posts that have got a number or posts
    +	 * related to them by meta at least equal to a value.
    +	 *
    +	 * @since 4.7.19
    +	 *
    +	 * @param string|array $by_meta_keys One or more `meta_keys` relating
    +	 *                                   another post TO this post type.
    +	 * @param int          $min          The minimum number of posts of another type that should
    +	 *                                   be related to the queries post type(s).
    +	 * @param string|array $keys         One or more meta_keys to check on the post type in relation
    +	 *                                   with the query post type(s); if the `$values` parameter is
    +	 *                                   not provided then this will trigger an EXISTS check.
    +	 * @param string|array $values       One or more value the meta_key specified with `$keys` should
    +	 *                                   match.
    +	 *
    +	 * @return $this
    +	 */
    +	public function by_related_to_min( $by_meta_keys, $min, $keys = null, $values = null );
    +
    +	/**
    +	 * Filters the query to return posts that have got a number or posts
    +	 * related to them by meta at most equal to a value.
    +	 *
    +	 * @since 4.7.19
    +	 *
    +	 * @param string|array $by_meta_keys One or more `meta_keys` relating
    +	 *                                   another post TO this post type.
    +	 *                                   be related to the queries post type(s).
    +	 * @param int          $max          The maximum number of posts of another type that should
    +	 *                                   be related to the queries post type(s).
    +	 * @param string|array $keys         One or more meta_keys to check on the post type in relation
    +	 *                                   with the query post type(s); if the `$values` parameter is
    +	 *                                   not provided then this will trigger an EXISTS check.
    +	 * @param string|array $values       One or more value the meta_key specified with `$keys` should
    +	 *                                   match.
    +	 *
    +	 * @return $this
    +	 */
    +	public function by_related_to_max( $by_meta_keys, $max, $keys = null, $values = null );
    +
    +	/**
    +	 * Filters the query to return posts that have got a number or posts
    +	 * related to them by meta between two values.
    +	 *
    +	 * @since 4.7.19
    +	 *
    +	 * @param string|array $by_meta_keys One or more `meta_keys` relating
    +	 *                                   another post TO this post type.
    +	 * @param int          $min          The minimum number of posts of another type that should
    +	 *                                   be related to the queries post type(s).
    +	 * @param int          $max          The maximum number of posts of another type that should
    +	 *                                   be related to the queries post type(s).
    +	 *
    +	 * @param string|array $keys         One or more meta_keys to check on the post type in relation
    +	 *                                   with the query post type(s); if the `$values` parameter is
    +	 *                                   not provided then this will trigger an EXISTS check.
    +	 * @param string|array $values       One or more value the meta_key specified with `$keys` should
    +	 *                                   match.
    +	 *
    +	 * @return $this
    +	 */
    +	public function by_related_to_between( $by_meta_keys, $min, $max, $keys = null, $values = null );
    +
    +	/**
    +	 * Filters the query to return posts that are not related to posts that have a specific meta value.
    +	 *
    +	 * @since 5.0.2.1
    +	 *
    +	 * @param string|array $by_meta_keys One or more `meta_keys` relating
    +	 *                                   another post TO this post type.
    +	 *
    +	 * @param string|array $keys         One or more meta_keys to check on the post type in relation
    +	 *                                   with the query post type(s); if the `$values` parameter is
    +	 *                                   not provided then this will trigger an EXISTS check.
    +	 * @param string|array $values       One or more value the meta_key specified with `$keys` should
    +	 *                                   match.
    +	 *
    +	 * @return $this
    +	 */
    +	public function by_not_related_to( $by_meta_keys, $keys = null, $values = null );
    +
    +	/**
    +	 * Adds an entry to the repository filter schema.
    +	 *
    +	 * @since 4.9.5
    +	 *
    +	 * @param string   $key      The filter key, the one that will be used in `by` and `where`
    +	 *                           calls.
    +	 * @param callable $callback The function that should be called to apply this filter.
    +	 */
    +	public function add_schema_entry( $key, $callback );
    +
    +	/**
    +	 * Returns an hash string for this repository instance filters and, optionally, a generated query.
    +	 *
    +	 * By default all applied filters, and query vars, will be included but specific filters can
    +	 * be excluded, or included, from the hash generation.
    +	 * The possibility to include the query in the hash generation is required as the query vars could
    +	 * be further modified after the repository filters are applied and the query is built.
    +	 *
    +	 * @since 4.9.5
    +	 *
    +	 * @param array          $settings An array of settings to define how the hash should be produced in the shape
    +	 *                                 `[ 'exclude' => [ 'ex_1', ... ], 'include' => [ 'inc_1', ... ] ]`. This array
    +	 *                                 will apply both to the Repository filters and the query vars.
    +	 * @param WP_Query|null $query An optional query object to include in the hashing.
    +	 *
    +	 * @return string The generated hash string.
    +	 *
    +	 */
    +	public function hash( array $settings = [], WP_Query $query = null );
    +
    +	/**
    +	 * Returns the data the repository would use to build the hash.
    +	 *
    +	 * @since 4.9.5
    +	 *
    +	 * @param array          $settings An array of settings to define how the hash should be produced in the shape
    +	 *                                 `[ 'exclude' => [ 'ex_1', ... ], 'include' => [ 'inc_1', ... ] ]`. This array
    +	 *                                 will apply both to the Repository filters and the query vars.
    +	 * @param WP_Query|null $query An optional query object to include in the hashing.
    +	 *
    +	 * @return array An array of hash data components.
    +	 */
    +	public function get_hash_data( array $settings, WP_Query $query = null );
    +
    +	/**
    +	 * Returns the last built query from the repository instance.
    +	 *
    +	 * @since 4.9.6
    +	 *
    +	 * @return WP_Query|null The last built query instance if any.
    +	 */
    +	public function get_last_built_query();
    +
    +	/**
    +	 * Builds, and adds to the query, a WHERE clause to the query on multiple fields.
    +	 *
    +	 * @since 4.9.6
    +	 *
    +	 * @param array  $fields         The fields to add WHERE clauses for. The fields can be post fields, custom fields or
    +	 *                               taxonomy terms.
    +	 * @param string $compare        The comparison operator to use, e.g. 'LIKE' or '>'.
    +	 * @param mixed  $value          The value, or values, to compare with; the format will be set depending on the type of
    +	 *                               each value.
    +	 * @param string $where_relation The relation to join the WHERE clauses with, either 'OR' or 'AND'; default to 'OR'.
    +	 * @param string $value_relation The relation to join the value clauses in case the value is an array, either 'OR'
    +	 *                               or 'AND'; defaults to 'OR'.
    +	 *
    +	 * @return $this This repository instance to allow chain calls.
    +	 *
    +	 * @throws \Tribe__Repository__Usage_Error If the comparison operator or the relation are not valid.
    +	 */
    +	public function where_multi( array $fields, $compare, $value, $where_relation = 'OR', $value_relation = 'OR' );
    +
    +	/**
    +	 * Sets the query instance the repository will use.
    +	 *
    +	 * Setting a query explicitly
    +	 *
    +	 * @since 4.9.9
    +	 *
    +	 * @param  \WP_Query  $query An query instance.
    +	 *
    +	 * @return \Tribe__Repository__Interface The repository instance, for chaining.
    +	 * @throws \Tribe__Repository__Usage_Error If trying to set the query after a fetching operation is done.
    +	 */
    +	public function set_query( WP_Query $query );
    +
    +	/**
    +	 * Returns a cloned instance of the repository that will yield the next page results.
    +	 *
    +	 * Mind that this method will always return a Repository instance, no matter if a next page exists or not.
    +	 * If a next page does not exist then the instance returned by this method will yield no posts and a count of `0`.
    +	 *
    +	 * @since 4.9.11
    +	 *
    +	 * @return \Tribe__Repository__Interface The repository instance that will yield the next page results.
    +	 */
    +	public function next();
    +
    +	/**
    +	 * Returns a cloned instance of the repository that will yield the previous page results.
    +	 *
    +	 * Mind that this method will always return a Repository instance, no matter if a previous page exists or not.
    +	 * If a previous page does not exist then the instance returned by this method will yield no posts and a count
    +	 * of `0`.
    +	 *
    +	 * @since 4.9.11
    +	 *
    +	 * @return \Tribe__Repository__Interface The repository instance that will yield the previous page results.
    +	 */
    +	public function prev();
    +
    +	/**
    +	 * Sets the found rows calculation to be enabled for queries.
    +	 *
    +	 * @since 4.9.10
    +	 *
    +	 * @param bool $found_rows Whether found rows calculation should be enabled.
    +	 *
    +	 * @return \Tribe__Repository__Interface The repository instance, for chaining.
    +	 */
    +	public function set_found_rows( $found_rows );
    +
    +	/**
    +	 * Voids the repositories queries preventing the repository from running any query.
    +	 *
    +	 * @since 4.9.14
    +	 *
    +	 * @param bool $void Whether to void the repository queries or not.
    +	 *
    +	 * @return Tribe__Repository__Interface $this The repository instance.
    +	 */
    +	public function void_query( $void_query = true );
    +
    +	/**
    +	 * Returns the SQL code for the last query built and ran by the repository, if any.
    +	 *
    +	 * @since 5.0.1
    +	 *
    +	 * @return string|null The SQL code for the last query built and ran by the repository, if any.
    +	 */
    +	public function get_last_sql(): ?string;
    +}
    diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Repository/Query_Filters.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Repository/Query_Filters.php
    new file mode 100644
    index 000000000..8f5ff8ad7
    --- /dev/null
    +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Repository/Query_Filters.php
    @@ -0,0 +1,1129 @@
    + [
    +			'post_title'   => [],
    +			'post_content' => [],
    +			'post_excerpt' => [],
    +		],
    +		'status' => [],
    +		'join'   => [],
    +		'where'  => [],
    +	];
    +
    +	/**
    +	 * An array of the filters that can be set and unset by id.
    +	 *
    +	 * @since 4.9.14
    +	 *
    +	 * @var array
    +	 */
    +	protected static $identifiable_filters = [ 'fields', 'join', 'where', 'orderby' ];
    +
    +	/**
    +	 * @var array
    +	 */
    +	protected $query_vars;
    +
    +	/**
    +	 * @var WP_Query
    +	 */
    +	protected $current_query;
    +
    +	/**
    +	 * @var int A reasonably large number for the LIMIT clause.
    +	 */
    +	protected $really_large_number = 99999999;
    +
    +	/**
    +	 * @var array A list of the filters this class has added.
    +	 */
    +	protected $active_filters = [];
    +
    +	/**
    +	 * @var bool
    +	 */
    +	protected $buffer_where_clauses = false;
    +
    +	/**
    +	 * @var array
    +	 */
    +	protected $buffered_where_clauses = [];
    +
    +	/**
    +	 * Stores the last request run by the current query.
    +	 *
    +	 * @var string
    +	 */
    +	protected $last_request;
    +
    +	/**
    +	 * Tribe__Repository__Query_Filters constructor.
    +	 *
    +	 * @since 4.7.19
    +	 */
    +	public function __construct() {
    +		$this->query_vars = self::$initial_query_vars;
    +	}
    +
    +	/**
    +	 * Builds an "not exists or is not in" media query.
    +	 *
    +	 * @since 4.7.19
    +	 *
    +	 * @param array|string     $meta_keys On what meta_keys the check should be made.
    +	 * @param int|string|array $values    A single value, an array of values or a CSV list of values.
    +	 * @param string           $query_slug
    +	 *
    +	 * @return array
    +	 */
    +	public static function meta_not_in( $meta_keys, $values, $query_slug ) {
    +		$meta_keys = Tribe__Utils__Array::list_to_array( $meta_keys );
    +		$values    = Tribe__Utils__Array::list_to_array( $values );
    +
    +		if ( empty( $meta_keys ) || count( $values ) === 0 ) {
    +			return [];
    +		}
    +
    +		$args = [
    +			'meta_query' => [
    +				$query_slug => [
    +					'relation' => 'AND',
    +				],
    +			],
    +		];
    +
    +		foreach ( $meta_keys as $key ) {
    +			$args['meta_query'][ $query_slug ][ $key ] = [
    +				'not-exists' => [
    +					'key'     => $key,
    +					'compare' => 'NOT EXISTS',
    +				],
    +				'relation'   => 'OR',
    +			];
    +
    +			if ( count( $values ) > 1 ) {
    +				$args['meta_query'][ $query_slug ][ $key ]['not-in'] = [
    +					'key'     => $key,
    +					'compare' => 'NOT IN',
    +					'value'   => $values,
    +				];
    +			} else {
    +				$args['meta_query'][ $query_slug ][ $key ]['not-equals'] = [
    +					'key'     => $key,
    +					'value'   => $values[0],
    +					'compare' => '!=',
    +				];
    +			}
    +		}
    +
    +		return $args;
    +	}
    +
    +	/**
    +	 * Builds an "exists and is in" media query.
    +	 *
    +	 * @since 4.7.19
    +	 *
    +	 * @param array|string     $meta_keys On what meta_keys the check should be made.
    +	 * @param int|string|array $values    A single value, an array of values or a CSV list of values.
    +	 * @param string           $query_slug
    +	 *
    +	 * @return array
    +	 */
    +	public static function meta_in( $meta_keys, $values, $query_slug ) {
    +		$meta_keys = Tribe__Utils__Array::list_to_array( $meta_keys );
    +		$values    = Tribe__Utils__Array::list_to_array( $values );
    +
    +		if ( empty( $meta_keys ) || count( $values ) === 0 ) {
    +			return [];
    +		}
    +
    +		$args = [
    +			'meta_query' => [
    +				$query_slug => [
    +					'relation' => 'OR',
    +				],
    +			],
    +		];
    +
    +		foreach ( $meta_keys as $meta_key ) {
    +			if ( count( $values ) > 1 ) {
    +				$args['meta_query'][ $query_slug ][ $meta_key ] = [
    +					'key'     => $meta_key,
    +					'compare' => 'IN',
    +					'value'   => $values,
    +				];
    +			} else {
    +				$args['meta_query'][ $query_slug ][ $meta_key ] = [
    +					'key'     => $meta_key,
    +					'compare' => '=',
    +					'value'   => $values[0],
    +				];
    +			}
    +		}
    +
    +		return $args;
    +	}
    +
    +	/**
    +	 * Builds a meta query to check that at least of the meta key exists.
    +	 *
    +	 * @since 4.7.19
    +	 *
    +	 * @param array|string $meta_keys
    +	 * @param string       $query_slug
    +	 *
    +	 * @return array
    +	 */
    +	public static function meta_exists( $meta_keys, $query_slug ) {
    +		$meta_keys = Tribe__Utils__Array::list_to_array( $meta_keys );
    +
    +		if ( empty( $meta_keys ) ) {
    +			return [];
    +		}
    +
    +		$args = [
    +			'meta_query' => [
    +				$query_slug => [
    +					'relation' => 'OR',
    +				],
    +			],
    +		];
    +
    +		foreach ( $meta_keys as $meta_key ) {
    +			$args['meta_query'][ $query_slug ][ $meta_key ] = [
    +				'key'     => $meta_key,
    +				'compare' => 'EXISTS',
    +			];
    +		}
    +
    +		return $args;
    +	}
    +
    +	/**
    +	 * Builds a meta query to check that a meta is either equal to a value or
    +	 * not exists.
    +	 *
    +	 * @since 4.7.19
    +	 *
    +	 * @param array|string $meta_keys
    +	 * @param array|string $values
    +	 * @param string       $query_slug
    +	 *
    +	 * @return array
    +	 */
    +	public static function meta_in_or_not_exists( $meta_keys, $values, $query_slug ) {
    +		$meta_keys = Tribe__Utils__Array::list_to_array( $meta_keys );
    +		$values    = Tribe__Utils__Array::list_to_array( $values );
    +
    +		if ( empty( $meta_keys ) || count( $values ) === 0 ) {
    +			return [];
    +		}
    +
    +		$args = [
    +			'meta_query' => [
    +				$query_slug => [
    +					'relation' => 'AND',
    +				],
    +			],
    +		];
    +
    +		foreach ( $meta_keys as $meta_key ) {
    +			$args['meta_query'][ $query_slug ][ $meta_key ]['does-not-exist'] = [
    +				'key'     => $meta_key,
    +				'compare' => 'NOT EXISTS',
    +			];
    +			$args['meta_query'][ $query_slug ][ $meta_key ]['relation']       = 'OR';
    +			if ( count( $values ) > 1 ) {
    +				$args['meta_query'][ $query_slug ][ $meta_key ]['in'] = [
    +					'key'     => $meta_key,
    +					'compare' => 'IN',
    +					'value'   => $values,
    +				];
    +			} else {
    +				$args['meta_query'][ $query_slug ][ $meta_key ]['equals'] = [
    +					'key'     => $meta_key,
    +					'compare' => '=',
    +					'value'   => $values[0],
    +				];
    +			}
    +		}
    +
    +		return $args;
    +	}
    +
    +	/**
    +	 * Builds a meta query to check that a meta is either not equal to a value or
    +	 * not exists.
    +	 *
    +	 * @since 4.7.19
    +	 *
    +	 * @param array|string $meta_keys
    +	 * @param array|string $values
    +	 * @param string       $query_slug
    +	 *
    +	 * @return array
    +	 */
    +	public static function meta_not_in_or_not_exists( $meta_keys, $values, $query_slug ) {
    +		$meta_keys = Tribe__Utils__Array::list_to_array( $meta_keys );
    +		$values    = Tribe__Utils__Array::list_to_array( $values );
    +
    +		if ( empty( $meta_keys ) || count( $values ) === 0 ) {
    +			return [];
    +		}
    +
    +		$args = [
    +			'meta_query' => [
    +				$query_slug => [
    +					'relation' => 'AND',
    +				],
    +			],
    +		];
    +
    +		foreach ( $meta_keys as $meta_key ) {
    +			$args['meta_query'][ $query_slug ][ $meta_key ]['does-not-exist'] = [
    +				'key'     => $meta_key,
    +				'compare' => 'NOT EXISTS',
    +			];
    +			$args['meta_query'][ $query_slug ][ $meta_key ]['relation']       = 'OR';
    +
    +			if ( count( $values ) > 1 ) {
    +				$args['meta_query'][ $query_slug ][ $meta_key ]['not-in'] = [
    +					'key'     => $meta_key,
    +					'compare' => 'NOT IN',
    +					'value'   => $values,
    +				];
    +			} else {
    +				$args['meta_query'][ $query_slug ][ $meta_key ]['not-equals'] = [
    +					'key'     => $meta_key,
    +					'compare' => '!=',
    +					'value'   => $values[0],
    +				];
    +			}
    +		}
    +
    +		return $args;
    +	}
    +
    +	/**
    +	 * Filters the WHERE clause of the query to match posts with a field like.
    +	 *
    +	 * @since 4.7.19
    +	 *
    +	 * @param string   $where
    +	 * @param WP_Query $query
    +	 *
    +	 * @return string
    +	 */
    +	public function filter_by_like( $where, WP_Query $query ) {
    +		if ( $query !== $this->current_query ) {
    +			return $where;
    +		}
    +
    +		if ( empty( $this->query_vars['like'] ) ) {
    +			return $where;
    +		}
    +
    +		foreach ( $this->query_vars['like'] as $field => $entries ) {
    +			foreach ( $entries as $entry ) {
    +				$where .= $this->and_field_like( $field, $entry );
    +			}
    +		}
    +
    +		return $where;
    +	}
    +
    +	/**
    +	 * Builds the escaped WHERE entry to match a field like the entry.
    +	 *
    +	 * @since 4.7.19
    +	 *
    +	 * @param string $field
    +	 * @param string $entry
    +	 *
    +	 * @return string
    +	 */
    +	protected function and_field_like( $field, $entry ) {
    +		/** @var wpdb $wpdb */
    +		global $wpdb;
    +
    +		$like       = $wpdb->esc_like( $entry );
    +		$variations = [
    +			$wpdb->prepare( "{$wpdb->posts}.{$field} LIKE %s ", "{$like}%" ),
    +			$wpdb->prepare( "{$wpdb->posts}.{$field} LIKE %s ", "%{$like}%" ),
    +			$wpdb->prepare( "{$wpdb->posts}.{$field} LIKE %s ", "%{$like}" ),
    +		];
    +
    +		return ' AND (' . implode( ' OR ', $variations ) . ')';
    +	}
    +
    +	/**
    +	 * Filters the found posts value to apply filtering and selections on the PHP
    +	 * side of things.
    +	 *
    +	 * Here we perform, after the query did run, further filtering operations that would
    +	 * result in more JOIN and/or sub-SELECT clauses being added to the query.
    +	 *
    +	 * @since 4.7.19
    +	 *
    +	 * @param int      $found_posts The number of found posts.
    +	 * @param WP_Query $query       The current query object.
    +	 *
    +	 * @return string
    +	 */
    +	public function filter_found_posts( $found_posts, WP_Query $query ) {
    +		if ( $query !== $this->current_query ) {
    +			return $found_posts;
    +		}
    +
    +		if ( empty( $this->query_vars['found_posts_filters'] ) ) {
    +			return $found_posts;
    +		}
    +
    +		$filtered_found_posts = $found_posts;
    +		$ids_only             = $query->get( 'fields' ) === 'ids';
    +
    +		/** @var wpdb $wpdb */
    +		global $wpdb;
    +
    +		/**
    +		 * Handles meta-based relations between posts.
    +		 */
    +		foreach ( $this->query_vars['found_posts_filters']['meta_related'] as $info ) {
    +			list( $meta_keys, $field, $field_values, $compare ) = $info;
    +			$post_ids          = $ids_only ? $query->posts : wp_list_pluck( $query->posts, 'ID' );
    +			$post_ids_interval = '(' . implode( ',', $post_ids ) . ')';
    +			$meta_keys         = "('" . implode( "','", array_map( 'esc_sql', $meta_keys ) ) . "')";
    +			$field             = esc_sql( $field );
    +			$field_values      = is_array( $field_values )
    +				? "('" . implode( "','", array_map( 'esc_sql', $field_values ) ) . "')"
    +				: $wpdb->prepare( '%s', $field_values );
    +
    +			$relation_query = "
    +				SELECT DISTINCT( pm.post_id )
    +				FROM {$wpdb->posts} p
    +				JOIN {$wpdb->postmeta} pm
    +				ON pm.meta_value = p.ID
    +				WHERE pm.post_id IN {$post_ids_interval}
    +				AND pm.meta_key IN {$meta_keys}
    +				AND p.{$field} {$compare} {$field_values}
    +				";
    +
    +			$matching_ids = $wpdb->get_col( $relation_query );
    +
    +			if ( empty( $matching_ids ) ) {
    +				$query->posts         = [];
    +				$filtered_found_posts = 0;
    +				break;
    +			}
    +
    +			if ( $ids_only ) {
    +				$query->posts = array_intersect( $query->posts, $matching_ids );
    +			} else {
    +				$updated_query_posts = [];
    +				foreach ( $query->posts as $this_post ) {
    +					if ( in_array( $this_post->ID, $matching_ids ) ) {
    +						$updated_query_posts[] = $this_post;
    +					}
    +				}
    +				$query->posts = $updated_query_posts;
    +			}
    +			$filtered_found_posts = count( $query->posts );
    +		}
    +
    +		$query->post_count = $filtered_found_posts;
    +
    +		return $filtered_found_posts;
    +	}
    +
    +	/**
    +	 * Sets the current query object.
    +	 *
    +	 * @since 4.7.19
    +	 *
    +	 * @param WP_Query $query
    +	 */
    +	public function set_query( WP_Query $query ) {
    +		$this->current_query = $query;
    +	}
    +
    +	/**
    +	 * Sets up `posts_where` filtering to get posts with a title like the value.
    +	 *
    +	 * @since 4.7.19
    +	 *
    +	 * @param string $value
    +	 */
    +	public function to_get_posts_with_title_like( $value ) {
    +		$this->query_vars['like']['post_title'][] = $value;
    +
    +		if ( ! has_filter( 'posts_where', [ $this, 'filter_by_like' ] ) ) {
    +			$this->add_filter( 'posts_where', [ $this, 'filter_by_like' ], 10, 2 );
    +		}
    +	}
    +
    +	/**
    +	 * Proxy method to add a  filter calling the WordPress `add_filter` function
    +	 * and keep track of it.
    +	 *
    +	 * @since 4.7.19
    +	 *
    +	 * @param string   $tag
    +	 * @param callable $function_to_add
    +	 * @param int      $priority
    +	 * @param int      $accepted_args
    +	 */
    +	protected function add_filter( $tag, $function_to_add, $priority = 10, $accepted_args = 1 ) {
    +		$this->active_filters[] = [ $tag, $function_to_add, $priority ];
    +		add_filter( $tag, $function_to_add, $priority, $accepted_args );
    +	}
    +
    +	/**
    +	 * Sets up `posts_where` filtering to get posts with a content like the value.
    +	 *
    +	 * @since 4.7.19
    +	 *
    +	 * @param string $value
    +	 */
    +	public function to_get_posts_with_content_like( $value ) {
    +		$this->query_vars['like']['post_content'][] = $value;
    +
    +		if ( ! has_filter( 'posts_where', [ $this, 'filter_by_like' ] ) ) {
    +			$this->add_filter( 'posts_where', [ $this, 'filter_by_like' ], 10, 2 );
    +		}
    +	}
    +
    +	/**
    +	 * Sets up `posts_where` filtering to get posts with an excerpt like the value.
    +	 *
    +	 * @since 4.7.19
    +	 *
    +	 * @param string $value
    +	 */
    +	public function to_get_posts_with_excerpt_like( $value ) {
    +		$this->query_vars['like']['post_excerpt'] = $value;
    +
    +		if ( ! has_filter( 'posts_where', [ $this, 'filter_by_like' ] ) ) {
    +			add_filter( 'posts_where', [ $this, 'filter_by_like' ], 10, 2 );
    +		}
    +	}
    +
    +	/**
    +	 * Sets up `posts_where` filtering to get posts with a filtered content like the value.
    +	 *
    +	 * @since 4.7.19
    +	 *
    +	 * @param string $value
    +	 */
    +	public function to_get_posts_with_filtered_content_like( $value ) {
    +		$this->query_vars['like']['post_content_filtered'][] = $value;
    +
    +		if ( ! has_filter( 'posts_where', [ $this, 'filter_by_like' ] ) ) {
    +			add_filter( 'posts_where', [ $this, 'filter_by_like' ], 10, 2 );
    +		}
    +	}
    +
    +	/**
    +	 * Sets up `posts_where` filtering to get posts with a guid that equals the value.
    +	 *
    +	 * @since 4.7.19
    +	 *
    +	 * @param string $value
    +	 */
    +	public function to_get_posts_with_guid_like( $value ) {
    +		$this->query_vars['like']['guid'][] = $value;
    +
    +		if ( ! has_filter( 'posts_where', [ $this, 'filter_by_like' ] ) ) {
    +			add_filter( 'posts_where', [ $this, 'filter_by_like' ], 10, 2 );
    +		}
    +	}
    +
    +	/**
    +	 * Sets up `posts_where` filtering to get posts with a `to_ping` field equal to the value.
    +	 *
    +	 * @since 4.7.19
    +	 *
    +	 * @param string $value
    +	 */
    +	public function to_get_posts_to_ping( $value ) {
    +		$this->query_vars['to_ping'] = $value;
    +
    +		if ( ! has_filter( 'posts_where', [ $this, 'filter_by_to_ping' ] ) ) {
    +			add_filter( 'posts_where', [ $this, 'filter_by_to_ping' ], 10, 2 );
    +		}
    +	}
    +
    +	/**
    +	 * Filters the WHERE clause of the query to match posts with a specific `to_ping`
    +	 * entry.
    +	 *
    +	 * @since 4.7.19
    +	 *
    +	 * @param string   $where
    +	 * @param WP_Query $query
    +	 *
    +	 * @return string
    +	 */
    +	public function filter_by_to_ping( $where, WP_Query $query ) {
    +		return $this->where_field_is( $where, $query, 'ping_status' );
    +	}
    +
    +	/**
    +	 * Builds the escaped WHERE entry to match a field that equals the entry.
    +	 *
    +	 * @since 4.7.19
    +	 *
    +	 * @param string   $where
    +	 * @param WP_Query $query
    +	 * @param string   $field
    +	 * @param string   $prepare
    +	 *
    +	 * @return string
    +	 */
    +	protected function where_field_is( $where, WP_Query $query, $field, $prepare = '%s' ) {
    +		if ( $query !== $this->current_query ) {
    +			return $where;
    +		}
    +
    +
    +		if ( empty( $this->query_vars[ $field ] ) ) {
    +			return $where;
    +		}
    +
    +		/** @var wpdb $wpdb */
    +		global $wpdb;
    +
    +		$where .= $wpdb->prepare( " AND {$wpdb->posts}.{$field} = {$prepare} ", $this->query_vars[ $field ] );
    +
    +		return $where;
    +	}
    +
    +	/**
    +	 * Removes all the filters this class applied.
    +	 *
    +	 * @since 4.7.19
    +	 */
    +	public function remove_filters() {
    +		foreach ( $this->active_filters as $filters ) {
    +			list( $tag, $function_to_add, $priority ) = $filters;
    +			remove_filter( $tag, $function_to_add, $priority );
    +		}
    +	}
    +
    +	/**
    +	 * Add a custom WHERE clause to the query.
    +	 *
    +	 * @since 4.7.19
    +	 * @since 4.9.14 Added the `$id` and `$override` parameters.
    +	 *
    +	 * @param string $where_clause
    +	 * @param null|string $id          Optional WHERE ID to prevent duplicating clauses.
    +	 * @param boolean     $override    Whether to override the clause if a WHERE by the same ID exists or not.
    +	 */
    +	public function where( $where_clause, $id = null, $override =false  ) {
    +		if ( $this->buffer_where_clauses ) {
    +			if ( $id ) {
    +				if ( $override || ! isset( $this->buffered_where_clauses[ $id ] ) ) {
    +					$this->buffered_where_clauses[ $id ] = $where_clause;
    +				}
    +			} else {
    +				$this->buffered_where_clauses[] = '(' . $where_clause . ')';
    +			}
    +		} else {
    +			if ( $id ) {
    +				if ( $override || ! isset( $this->query_vars['where'][ $id ] ) ) {
    +					$this->query_vars['where'][ $id ] = '(' . $where_clause . ')';
    +				}
    +			} else {
    +				$this->query_vars['where'][] = '(' . $where_clause . ')';
    +			}
    +
    +			if ( ! has_filter( 'posts_where', [ $this, 'filter_posts_where' ] ) ) {
    +				add_filter( 'posts_where', [ $this, 'filter_posts_where' ], 10, 2 );
    +			}
    +		}
    +	}
    +
    +	/**
    +	 * Add a custom JOIN clause to the query.
    +	 *
    +	 * @since 4.7.19
    +	 *
    +	 * @param string      $join_clause JOIN clause.
    +	 * @param null|string $id          Optional JOIN ID to prevent duplicating joins.
    +	 * @param boolean     $override    Whether to override the clause if a JOIN by the same ID exists.
    +	 */
    +	public function join( $join_clause, $id = null, $override = false ) {
    +		if ( $id ) {
    +			if ( $override || ! isset( $this->query_vars['join'][ $id ] ) ) {
    +				$this->query_vars['join'][ $id ] = $join_clause;
    +			}
    +		} else {
    +			$this->query_vars['join'][] = $join_clause;
    +		}
    +
    +		if ( ! has_filter( 'posts_join', [ $this, 'filter_posts_join' ] ) ) {
    +			add_filter( 'posts_join', [ $this, 'filter_posts_join' ], 10, 2 );
    +		}
    +	}
    +
    +	/**
    +	 * Add a custom ORDER BY to the query.
    +	 *
    +	 * @since 4.9.5
    +	 * @since 4.9.14 Added the `$id` and `$override` parameters.
    +	 * @since 4.9.21 Added the `$order` and `$after` parameters.
    +	 *
    +	 * @param string|array $orderby       The order by criteria; this argument can be specified in array form to specify
    +	 *                                    multiple order by clauses and orders associated to each,
    +	 *                                    e.g. `[ '_meta_1' => 'ASC', '_meta_2' => 'DESC' ]`. If a simple array is
    +	 *                                    passed, then the order will be set to the default one for each entry.
    +	 *                                    This arguments supports the same formats of the `WP_Query` `orderby` argument.
    +	 * @param null|string  $id            Optional ORDER ID to prevent duplicating order-by clauses.
    +	 * @param boolean      $override      Whether to override the clause if another by the same ID exists.
    +	 * @param bool         $after         Whether to append the order by clause to the ones managed by WordPress or not.
    +	 *                                    Defaults to `false`,to prepend them to the ones managed by WordPress.
    +	 */
    +	public function orderby( $orderby, $id = null, $override = false, $after = false ) {
    +		$orderby_key = $after ? static::AFTER . 'orderby' : 'orderby';
    +		$entries = [];
    +
    +		foreach ( (array) $orderby as $key => $value ) {
    +			/*
    +			 * As WordPress does, we support "simple" entries, like `[ 'menu_order', 'post_date' ]` and entries in the
    +			 * shape `[ 'menu_order' => 'ASC', 'post_date' => 'DESC' ]`.
    +			 */
    +			$the_orderby = is_numeric( $key ) ? $value : $key;
    +			$the_order   = is_numeric( $key ) ? 'DESC' : $value;
    +
    +			$entries[] = [ $the_orderby, $the_order ];
    +		}
    +
    +		$id = $id ?: 'default';
    +
    +		// Use the `$id` parameter to allow later method calls to replace values set in previous calls.
    +		if ( $id ) {
    +			if ( $override || ! isset( $this->query_vars[ $orderby_key ][ $id ] ) ) {
    +				$this->query_vars[ $orderby_key ][ $id ] = $entries;
    +			}
    +		} else {
    +			$this->query_vars[ $orderby_key ][ $id ] = array_merge( $this->query_vars[ $orderby_key ][ $id ], $entries );
    +		}
    +
    +		if ( ! has_filter( 'posts_orderby', [ $this, 'filter_posts_orderby' ] ) ) {
    +			add_filter( 'posts_orderby', [ $this, 'filter_posts_orderby' ], 10, 2 );
    +		}
    +	}
    +
    +	/**
    +	 * Add custom select fields to the query.
    +	 *
    +	 * @since 4.9.5
    +	 * @since 4.9.14 Added the `$id` and `$override` parameters.
    +	 *
    +	 * @param string $field The field to add to the result.
    +	 * @param null|string $id       Optional ORDER ID to prevent duplicating order-by clauses..
    +	 * @param boolean     $override Whether to override the clause if another by the same ID exists.
    +	 */
    +	public function fields( $field, $id = null, $override = false ) {
    +		if ( $id ) {
    +			if ( $override || ! isset( $this->query_vars['fields'][ $id ] ) ) {
    +				$this->query_vars['fields'][ $id ] = $field;
    +			}
    +		} else {
    +			$this->query_vars['fields'][] = $field;
    +		}
    +
    +		if ( ! has_filter( 'posts_fields', [ $this, 'filter_posts_fields' ] ) ) {
    +			add_filter( 'posts_fields', [ $this, 'filter_posts_fields' ], 10, 2 );
    +		}
    +	}
    +
    +	/**
    +	 * Whether WHERE clauses should be buffered or not.
    +	 *
    +	 * @since 4.7.19
    +	 *
    +	 * @param bool $buffer_clauses
    +	 */
    +	public function buffer_where_clauses( $buffer_clauses ) {
    +		$this->buffer_where_clauses = (bool) $buffer_clauses;
    +	}
    +
    +	/**
    +	 * Returns the buffered WHERE clause and, optionally, cleans
    +	 * and deactivates buffering.
    +	 *
    +	 * @since 4.7.19
    +	 *
    +	 * @param bool $get_clean Whether  to clean the buffered WHERE
    +	 *                        clauses and deactivate buffering before
    +	 *                        returning them or not.
    +	 *
    +	 * @return array
    +	 */
    +	public function get_buffered_where_clauses( $get_clean = false ) {
    +		$clauses = $this->buffered_where_clauses;
    +
    +		if ( $get_clean ) {
    +			$this->buffer_where_clauses   = false;
    +			$this->buffered_where_clauses = [];
    +		}
    +
    +		return $clauses;
    +	}
    +
    +	/**
    +	 * Builds the escaped WHERE entry to match a field not in the entry.
    +	 *
    +	 * @since 4.7.19
    +	 *
    +	 * @param string   $where
    +	 * @param WP_Query $query
    +	 * @param string   $field
    +	 *
    +	 * @return string
    +	 */
    +	protected function where_field_not_in( $where, WP_Query $query, $field ) {
    +		if ( $query !== $this->current_query ) {
    +			return $where;
    +		}
    +
    +		if ( empty( $this->query_vars[ $field ] ) ) {
    +			return $where;
    +		}
    +
    +		$input = $this->query_vars[ $field ];
    +
    +		$stati_interval = $this->create_interval_of_strings( $input );
    +
    +		$where .= $this->and_field_not_in_interval( $field, $stati_interval );
    +
    +		return $where;
    +	}
    +
    +	/**
    +	 * Creates a SQL interval of strings.
    +	 *
    +	 * @since 4.7.19
    +	 *
    +	 * @param string|array $input
    +	 *
    +	 * @return string
    +	 */
    +	public function create_interval_of_strings( $input ) {
    +		$buffer = [];
    +
    +		/** @var wpdb $wpdb */
    +		global $wpdb;
    +
    +		foreach ( $input as $string ) {
    +			$buffer[] = is_array( $string ) ? $string : [ $string ];
    +		}
    +
    +		$buffer = array_unique( call_user_func_array( 'array_merge', $buffer ) );
    +
    +		$safe_strings = [];
    +		foreach ( $buffer as $raw_status ) {
    +			$safe_strings[] = $wpdb->prepare( '%s', $raw_status );
    +		}
    +
    +		return implode( ',', $safe_strings );
    +	}
    +
    +	/**
    +	 * Builds a WHERE clause where field is not in interval.
    +	 *
    +	 * @since 4.7.19
    +	 *
    +	 * @param string $field
    +	 * @param string $interval
    +	 *
    +	 * @return string
    +	 */
    +	protected function and_field_not_in_interval( $field, $interval ) {
    +		/** @var wpdb $wpdb */
    +		global $wpdb;
    +
    +		return " AND {$wpdb->posts}.{$field} NOT IN ('{$interval}') ";
    +	}
    +
    +	/**
    +	 * Builds the escaped WHERE entry to match a field in the entry.
    +	 *
    +	 * @since 4.7.19
    +	 *
    +	 * @param string   $where
    +	 * @param WP_Query $query
    +	 * @param string   $field
    +	 *
    +	 * @return string
    +	 */
    +	protected function where_field_in( $where, WP_Query $query, $field ) {
    +		if ( $query !== $this->current_query ) {
    +			return $where;
    +		}
    +
    +		if ( empty( $this->query_vars[ $field ] ) ) {
    +			return $where;
    +		}
    +
    +		$interval = $this->create_interval_of_strings( $this->query_vars[ $field ] );
    +
    +		$where .= $this->and_field_in_interval( $field, $interval );
    +
    +		return $where;
    +	}
    +
    +	/**
    +	 * Builds a AND WHERE clause.
    +	 *
    +	 * @since 4.7.19
    +	 *
    +	 * @param string $field
    +	 * @param string $interval
    +	 *
    +	 * @return string
    +	 */
    +	protected function and_field_in_interval( $field, $interval ) {
    +		/** @var wpdb $wpdb */
    +		global $wpdb;
    +
    +		return " AND {$wpdb->posts}.{$field} IN ('{$interval}') ";
    +	}
    +
    +	/**
    +	 * Filter the `posts_where` filter to add custom WHERE clauses.
    +	 *
    +	 * @since 4.7.19
    +	 *
    +	 * @param string   $where
    +	 * @param WP_Query $query
    +	 *
    +	 * @return string
    +	 */
    +	public function filter_posts_where( $where, WP_Query $query ) {
    +		if ( $query !== $this->current_query ) {
    +			return $where;
    +		}
    +
    +		if ( empty( $this->query_vars['where'] ) ) {
    +			return $where;
    +		}
    +
    +		$where .= ' AND ' . implode( "\nAND ", $this->query_vars['where'] ) . ' ';
    +
    +		return $where;
    +	}
    +
    +	/**
    +	 * Filter the `posts_join` filter to add custom JOIN clauses.
    +	 *
    +	 * @since 4.7.19
    +	 *
    +	 * @param string   $join
    +	 * @param WP_Query $query
    +	 *
    +	 * @return string
    +	 */
    +	public function filter_posts_join( $join, WP_Query $query ) {
    +		if ( $query !== $this->current_query ) {
    +			return $join;
    +		}
    +
    +		if ( empty( $this->query_vars['join'] ) ) {
    +			return $join;
    +		}
    +
    +		$join .= "\n" . implode( "\n ", $this->query_vars['join'] ) . ' ';
    +
    +		return $join;
    +	}
    +
    +	/**
    +	 * Filter the `posts_orderby` filter to add custom JOIN clauses.
    +	 *
    +	 * @since 4.9.5
    +	 *
    +	 * @param string   $orderby The `ORDER BY` clause of the query being filtered.
    +	 * @param WP_Query $query   The query object currently being filtered.
    +	 *
    +	 * @return string The filtered `ORDER BY` clause.
    +	 */
    +	public function filter_posts_orderby( $orderby, WP_Query $query ) {
    +		if ( $query !== $this->current_query ) {
    +			return $orderby;
    +		}
    +
    +		if ( empty( $this->query_vars['orderby'] ) && empty( $this->query_vars[ static::AFTER . 'orderby' ] ) ) {
    +			return $orderby;
    +		}
    +
    +		$frags = [ $orderby ];
    +
    +		/*
    +		 * Entries will be set, from the `orderby` method, to the `[ [ ,  ], [ ,  ] ]`
    +		 * format.
    +		 */
    +		$build_entry = static function ( $entries ) {
    +			$buffer  = [];
    +
    +			foreach ( $entries as list( $orderby, $order ) ) {
    +				$buffer[] = sprintf( '%s %s', $orderby, $order );
    +			}
    +
    +			return implode( ', ', $buffer );
    +		};
    +
    +		if ( ! empty( $this->query_vars['orderby'] ) ) {
    +			$before = implode( ', ', array_map( $build_entry, $this->query_vars['orderby'] ) );
    +			$frags  = [ $before, $orderby ];
    +		}
    +
    +		if ( ! empty( $this->query_vars[ static::AFTER . 'orderby' ] ) ) {
    +			$frags[] = implode( ', ', array_map( $build_entry, $this->query_vars[ static::AFTER . 'orderby' ] ) );
    +		}
    +
    +		return implode( ', ', array_filter( $frags ) );
    +	}
    +
    +	/**
    +	 * Filter the `posts_fields` filter to amend fields to be selected.
    +	 *
    +	 * @since 4.9.5
    +	 *
    +	 * @param array    $fields
    +	 * @param WP_Query $query
    +	 *
    +	 * @return string
    +	 */
    +	public function filter_posts_fields( $fields, WP_Query $query ) {
    +		if ( $query !== $this->current_query ) {
    +			return $fields;
    +		}
    +
    +		if ( empty( $this->query_vars['fields'] ) ) {
    +			return $fields;
    +		}
    +
    +		$fields .= ', ' . implode( ', ', $this->query_vars['fields'] );
    +
    +		return $fields;
    +	}
    +
    +	/**
    +	 * Captures the request SQL as built from the query class.
    +	 *
    +	 * This happens on the `posts_pre_query` filter and
    +	 *
    +	 * @since 4.9.5
    +	 *
    +	 * @param null|array $posts A pre-filled array of post results.
    +	 * @param \WP_Query  $query The current query object; this is used by the
    +	 *                          method to intercept only the request generated by
    +	 *                          its attached query.
    +	 *
    +	 * @return array|null An empty array to short-circuit the `get_posts` request; the input
    +	 *                    value, if the query is not the one attached to this filter or the method
    +	 *                    is called not in the context of the `posts_pre_query` filter;
    +	 */
    +	public function capture_request( $posts = null, WP_Query $query = null ) {
    +		if ( ! doing_filter( 'posts_pre_query' ) ) {
    +			// Let's make sure nothing bad happens if this runs outside of its natural context.
    +			return null;
    +		}
    +
    +		if ( $query !== $this->current_query ) {
    +			return $posts;
    +		}
    +
    +		$this->last_request = $query->request;
    +
    +		remove_filter( 'posts_pre_query', [ $this, 'capture_request' ] );
    +
    +		// This will short-circuit the query not running it.
    +		return [];
    +	}
    +
    +	/**
    +	 * Returns the controlled query request SQL.
    +	 *
    +	 * It's not possible to build the SQL for a query outside of a request to `get_posts`
    +	 * so what this class does is fire such a request intercepting it before it actually
    +	 * runs and returning an empty post array.
    +	 * To really run the query it's sufficien to run `get_posts` again on it.
    +	 *
    +	 * @since 4.9.5
    +	 *
    +	 * @return string The request SQL, as built from the `WP_Query` class including all the
    +	 *                possible filtering applied by this class and other classes.
    +	 */
    +	public function get_request() {
    +		add_filter( 'posts_pre_query', [ $this, 'capture_request' ], 10, 2 );
    +
    +		$this->current_query->get_posts();
    +
    +		return $this->last_request;
    +	}
    +
    +	/**
    +	 * Returns the fields, join, where and orderby clauses for an id.
    +	 *
    +	 * @since 4.9.14
    +	 *
    +	 * @param string $id The identifier of the group to remove.
    +	 *
    +	 * @return array An associative array of identifiable filters and their values, if any.
    +	 *
    +	 * @see Tribe__Repository__Query_Filters::$identifiable_filters
    +	 */
    +	public function get_filters_by_id( $id ) {
    +		$entries = [];
    +
    +		foreach ( static::$identifiable_filters as $key ) {
    +			if ( empty( $this->query_vars[ $key ][ $id ] ) ) {
    +				continue;
    +			}
    +			$entries[ $key ] = $this->query_vars[ $key ][ $id ];
    +		}
    +
    +		return $entries;
    +	}
    +
    +	/**
    +	 * Removes fields, join, where and orderby clauses for an id.
    +	 *
    +	 * @since 4.9.14
    +	 *
    +	 * @param string $id The identifier of the group to remove.
    +	 */
    +	public function remove_filters_by_id( $id ) {
    +		array_walk(
    +			$this->query_vars,
    +			static function ( array &$filters, $key ) use ( $id ) {
    +				if ( ! in_array( $key, static::$identifiable_filters, true ) ) {
    +					return;
    +				}
    +				unset( $filters[ $id ] );
    +			}
    +		);
    +	}
    +}
    diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Repository/Read_Interface.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Repository/Read_Interface.php
    new file mode 100644
    index 000000000..57dc581cd
    --- /dev/null
    +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Repository/Read_Interface.php
    @@ -0,0 +1,173 @@
    +where( 'post__in' , $ids )->get_query()` is
    +	 * to avoid all the overhead of a query that, probably did run already.
    +	 *
    +	 * @since 4.9.5
    +	 *
    +	 * @param array $posts An array of post objects or post IDs the query should return as if fetched.
    +	 *
    +	 * @return WP_Query A query object ready to return, and operate, on the posts.
    +	 */
    +	public function get_query_for_posts( array $posts );
    +}
    diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Repository/Setter_Interface.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Repository/Setter_Interface.php
    new file mode 100644
    index 000000000..c57ca92d0
    --- /dev/null
    +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Repository/Setter_Interface.php
    @@ -0,0 +1,46 @@
    +`.
    +	 *
    +	 * @since 4.11.5
    +	 *
    +	 * @var string
    +	 */
    +	public static $localized_matcher_delimiter = '~';
    +
    +	/**
    +	 * WP_Rewrite Instance
    +	 *
    +	 * @var WP_Rewrite
    +	 */
    +	public $rewrite;
    +
    +	/**
    +	 * Rewrite rules Holder
    +	 *
    +	 * @var array
    +	 */
    +	public $rules = [];
    +
    +	/**
    +	 * Base slugs for rewrite urls
    +	 *
    +	 * @var array
    +	 */
    +	public $bases = [];
    +
    +	/**
    +	 * After creating the Hooks on WordPress we lock the usage of the function.
    +	 *
    +	 * @var boolean
    +	 */
    +	protected $hook_lock = false;
    +
    +	/**
    +	 * An array cache of resolved canonical URLs in the shape `[  =>  ]`.
    +	 *
    +	 * @since 4.9.11
    +	 *
    +	 * @var array
    +	 */
    +	protected $canonical_url_cache = null;
    +
    +	/**
    +	 * An array cache of parsed URLs in the shape `[  =>  ]`.
    +	 *
    +	 * @since 4.9.11
    +	 *
    +	 * @var array
    +	 */
    +	protected $parse_request_cache = null;
    +
    +	/**
    +	 * And array cache of cleaned URLs.
    +	 *
    +	 * @since 4.9.11
    +	 *
    +	 * @var array
    +	 */
    +	protected $clean_url_cache = null;
    +	/**
    +	 * A reference to the Locale Switcher instance.
    +	 *
    +	 * @since 5.0.8
    +	 *
    +	 * @var Translations_Loader
    +	 */
    +	protected $translations_loader;
    +
    +	/**
    +	 * Static Singleton Factory Method
    +	 *
    +	 * @return self
    +	 */
    +	public static function instance() {
    +		if ( ! static::$instance ) {
    +			static::$instance = new static;
    +		}
    +
    +		return static::$instance;
    +	}
    +
    +	/**
    +	 * Tribe__Rewrite constructor.
    +	 *
    +	 * @param WP_Rewrite|null $wp_rewrite An instance of the `WP_Rewrite` class.
    +	 * @param Translations_Loader|null $translations_loader An instance of the translations loader.
    +	 */
    +	public function __construct( WP_Rewrite $wp_rewrite = null, Translations_Loader $translations_loader = null ) {
    +		$this->rewrite             = $wp_rewrite;
    +		$this->translations_loader = $translations_loader ?? tribe( Translations_Loader::class );
    +	}
    +
    +	/**
    +	 * When you are going to use any of the functions to create new rewrite rules you need to setup first
    +	 *
    +	 * @param  WP_Rewrite|null $wp_rewrite Pass the WP_Rewrite if you have it
    +	 *
    +	 * @return Tribe__Rewrite       The modified version of the class with the required variables in place
    +	 */
    +	public function setup( $wp_rewrite = null ) {
    +		if ( ! $wp_rewrite instanceof WP_Rewrite ) {
    +			global $wp_rewrite;
    +		}
    +
    +		$this->rewrite = $wp_rewrite;
    +		$this->bases   = $this->get_bases( 'regex' );
    +
    +		return $this;
    +	}
    +
    +	/**
    +	 * Generate the Rewrite Rules
    +	 *
    +	 * @param  WP_Rewrite $wp_rewrite WordPress Rewrite that will be modified, pass it by reference (&$wp_rewrite)
    +	 */
    +	public function filter_generate( WP_Rewrite $wp_rewrite ) {
    +		// Gets the rewrite bases and completes any other required setup work
    +		$this->setup( $wp_rewrite );
    +
    +		/**
    +		 * Use this to change the Tribe__Rewrite instance before new rules
    +		 * are committed.
    +		 *
    +		 * Should be used when you want to add more rewrite rules without having to
    +		 * deal with the array merge, noting that rules for The Events Calendar are
    +		 * themselves added via this hook (default priority).
    +		 *
    +		 * @var Tribe__Rewrite $rewrite
    +		 */
    +		do_action( 'tribe_pre_rewrite', $this );
    +	}
    +
    +
    +	/**
    +	 * Do not allow people to Hook methods twice by mistake
    +	 */
    +	public function hooks( $remove = false ) {
    +		if ( false === $this->hook_lock ) {
    +			// Don't allow people do Double the hooks
    +			$this->hook_lock = true;
    +
    +			$this->add_hooks();
    +		} elseif ( true === $remove ) {
    +			$this->remove_hooks();
    +		}
    +	}
    +
    +	/**
    +	 * Converts any percentage placeholders in the array keys back to % symbols.
    +	 *
    +	 * @param  array $rules
    +	 *
    +	 * @return array
    +	 */
    +	public function remove_percent_placeholders( array $rules ) {
    +		foreach ( $rules as $key => $value ) {
    +			$this->replace_array_key( $rules, $key, str_replace( self::PERCENT_PLACEHOLDER, '%', $key ) );
    +		}
    +
    +		return $rules;
    +	}
    +
    +	/**
    +	 * Filter for the `rewrite_rules_array` hook.
    +	 *
    +	 * @since 5.0.10
    +	 *
    +	 * @param array|mixed $rules The rules to be filtered.
    +	 *
    +	 * @return array|mixed Rules after filtering.
    +	 */
    +	public function filter_rewrite_rules_array( $rules ) {
    +		if ( ! is_array( $rules ) ) {
    +			return $rules;
    +		}
    +
    +		return $this->remove_percent_placeholders( $rules );
    +	}
    +
    +	protected function add_hooks() {
    +		add_filter( 'generate_rewrite_rules', [ $this, 'filter_generate' ] );
    +
    +		// Remove percent Placeholders on all items
    +		add_filter( 'rewrite_rules_array', [ $this, 'filter_rewrite_rules_array' ], 25 );
    +
    +		add_action( 'shutdown', [ $this, 'dump_cache' ] );
    +	}
    +
    +	protected function remove_hooks() {
    +		remove_filter( 'generate_rewrite_rules', [ $this, 'filter_generate' ] );
    +		remove_filter( 'rewrite_rules_array', [ $this, 'filter_rewrite_rules_array' ], 25 );
    +
    +		remove_action( 'shutdown', [ $this, 'dump_cache' ] );
    +	}
    +
    +	/**
    +	 * Determines if we have plain permalink.
    +	 *
    +	 * @since 4.11.2
    +	 *
    +	 * @return bool If we use plain permalink or not.
    +	 */
    +	public static function is_plain_permalink() {
    +		return tribe_context()->is( 'plain_permalink' );
    +	}
    +
    +	/**
    +	 * Get the base slugs for the rewrite rules.
    +	 *
    +	 * WARNING: Don't mess with the filters below if you don't know what you are doing
    +	 *
    +	 * @param  string $method Use "regex" to return a Regular Expression with the possible Base Slugs using l10n
    +	 *
    +	 * @return object         Return Base Slugs with l10n variations
    +	 */
    +	public function get_bases( $method = 'regex' ) {
    +		return new stdClass();
    +	}
    +
    +	/**
    +	 * The base method for creating a new Rewrite rule
    +	 *
    +	 * @param array|string $regex The regular expression to catch the URL
    +	 * @param array        $args  The arguments in which the regular expression "alias" to
    +	 *
    +	 * @return Tribe__Events__Rewrite
    +	 */
    +	public function add( $regex, $args = [] ) {
    +		$regex = (array) $regex;
    +
    +		$default = [];
    +		$args    = array_filter( wp_parse_args( $args, $default ) );
    +
    +		$url = add_query_arg( $args, 'index.php' );
    +
    +		// Optional Trailing Slash
    +		$regex[] = '?$';
    +
    +		// Glue the pieces with slashes
    +		$regex = implode( '/', array_filter( $regex ) );
    +
    +		// Add the Bases to the regex
    +		foreach ( $this->bases as $key => $value ) {
    +			$regex = str_replace( [ '{{ ' . $key . ' }}', '{{' . $key . '}}' ], $value, $regex );
    +		}
    +
    +		// Apply the Preg Indexes to the URL
    +		preg_match_all( '/%([0-9])/', $url, $matches );
    +		foreach ( end( $matches ) as $index ) {
    +			$url = str_replace( '%' . $index, $this->rewrite->preg_index( $index ), $url );
    +		}
    +
    +		// Add the rule
    +		$this->rules[ $regex ] = $url;
    +
    +		return $this;
    +	}
    +
    +	/**
    +	 * Returns a sanitized version of $slug that can be used in rewrite rules.
    +	 *
    +	 * This is ideal for those times where we wish to support internationalized
    +	 * URLs (ie, where "venue" in "venue/some-slug" may be rendered in non-ascii
    +	 * characters).
    +	 *
    +	 * In the case of registering new post types, $permastruct_name should
    +	 * generally match the CPT name itself.
    +	 *
    +	 * @param  string $slug
    +	 * @param  string $permastruct_name
    +	 * @param  string $is_regular_exp
    +	 *
    +	 * @return string
    +	 */
    +	public function prepare_slug( $slug, $permastruct_name, $is_regular_exp = true ) {
    +		$needs_handling = false;
    +		$sanitized_slug = sanitize_title( $slug );
    +
    +		// Was UTF8 encoding required for the slug? %a0 type entities are a tell-tale of this
    +		if ( preg_match( '/(%[0-9a-f]{2})+/', $sanitized_slug ) ) {
    +			/**
    +			 * Controls whether special UTF8 URL handling is setup for the set of
    +			 * rules described by $permastruct_name.
    +			 *
    +			 * This only fires if Tribe__Events__Rewrite::prepare_slug() believes
    +			 * handling is required.
    +			 *
    +			 * @var string $permastruct_name
    +			 * @var string $slug
    +			 */
    +			$needs_handling = apply_filters(
    +				'tribe_events_rewrite_utf8_handling', true, $permastruct_name, $slug
    +			);
    +		}
    +
    +		if ( $needs_handling ) {
    +			// User agents encode things the same way but in uppercase
    +			$sanitized_slug = strtoupper( $sanitized_slug );
    +
    +			// UTF8 encoding results in lots of "%" chars in our string which play havoc
    +			// with WP_Rewrite::generate_rewrite_rules(), so we swap them out temporarily
    +			$sanitized_slug = str_replace( '%', Tribe__Rewrite::PERCENT_PLACEHOLDER, $sanitized_slug );
    +		}
    +
    +		$prepared_slug = $is_regular_exp ? preg_quote( $sanitized_slug ) : $sanitized_slug;
    +
    +		/**
    +		 * Provides an opportunity to modify the sanitized slug which will be used
    +		 * in rewrite rules relating to $permastruct_name.
    +		 *
    +		 * @var string $prepared_slug
    +		 * @var string $permastruct_name
    +		 * @var string $original_slug
    +		 */
    +		return apply_filters( 'tribe_rewrite_prepared_slug', $prepared_slug, $permastruct_name, $slug );
    +	}
    +
    +	/**
    +	 * A way to replace an Array key without destroying the array ordering
    +	 *
    +	 * @since  4.0.6
    +	 *
    +	 * @param  array  &$array  The Rules Array should be used here
    +	 * @param  string $search  Search for this Key
    +	 * @param  string $replace Replace with this key]
    +	 *
    +	 * @return bool            Did we replace anything?
    +	 */
    +	protected function replace_array_key( &$array, $search, $replace ) {
    +		$keys  = array_keys( $array );
    +		$index = array_search( $search, $keys );
    +
    +		if ( false !== $index ) {
    +			$keys[ $index ] = $replace;
    +			$array          = array_combine( $keys, $array );
    +
    +			return true;
    +		}
    +
    +		return false;
    +	}
    +
    +	/**
    +	 * Returns the canonical URLs associated with a ugly link.
    +	 *
    +	 * This method will handle "our" URLs to go from their ugly form, filled with query vars, to the "pretty" one, if
    +	 * possible.
    +	 *
    +	 * @since 4.9.11
    +	 *
    +	 * @param string $url The URL to try and translate into its canonical form.
    +	 * @param bool   $force Whether to try and use the cache or force a new canonical URL conversion.
    +	 *
    +	 * @return string The canonical URL, or the input URL if it could not be resolved to a canonical one.
    +	 */
    +	public function get_canonical_url( $url, $force = false ) {
    +		if ( get_class( $this ) === Tribe__Rewrite::class ) {
    +			throw new BadMethodCallException(
    +				'Method get_canonical_url should only be called on extending classes.'
    +			);
    +		}
    +
    +		if ( null === $this->rewrite ) {
    +			// We re-do this check here as the object might have been initialized before the global rewrite was set.
    +			$this->setup();
    +		}
    +
    +		/**
    +		 * Filters the canonical URL for an input URL before any kind of logic runs.
    +		 *
    +		 * @since 4.9.11
    +		 *
    +		 * @param string|null    $canonical_url The canonical URL, defaults to `null`; returning a non `null` value will
    +		 *                                      make the logic bail and return the value.
    +		 * @param string         $url           The input URL to resolve to a canonical one.
    +		 * @param Tribe__Rewrite $this          This rewrite object.
    +		 */
    +		$canonical_url = apply_filters( 'tribe_rewrite_pre_canonical_url', null, $url );
    +		if ( null !== $canonical_url ) {
    +			return $canonical_url;
    +		}
    +
    +		$home_url = home_url();
    +
    +		// It's not a path we, or WP, could possibly handle.
    +		$has_http_scheme = (bool) parse_url( $url, PHP_URL_SCHEME );
    +		if (
    +			$home_url === $url
    +			|| ( $has_http_scheme && false === strpos( $url, $home_url ) )
    +		) {
    +			return $url;
    +		}
    +
    +		$canonical_url = $url;
    +		// To avoid issues with missing `path` component let's always add a trailing '/'.
    +		if ( false !== strpos( $url, '?' ) ) {
    +			$canonical_url = preg_replace( '~(\\/)*\\?~', '/?', $canonical_url );
    +		} elseif ( false !== strpos( $url, '#' ) ) {
    +			$canonical_url = preg_replace( '~(\\/)*#~', '/#', $canonical_url );
    +		}
    +
    +		// Canonical URLs are supposed to contain the home URL.
    +		if ( false === strpos( $canonical_url, $home_url ) ) {
    +			$canonical_url = home_url( $canonical_url );
    +		}
    +
    +		if ( empty( $canonical_url ) ) {
    +			return $home_url;
    +		}
    +
    +		// Passthru vars are additional salts for the cache that would render it useless: parse them here.
    +		$query = (string) parse_url( $url, PHP_URL_QUERY );
    +		wp_parse_str( $query, $query_vars );
    +		// Non-scalar value query vars should not be handled, but they should survive the resolution and not be cached.
    +		$scalar_query_vars = array_filter( $query_vars, 'is_scalar' );
    +		$passthru_vars     = array_diff_key( $query_vars, $scalar_query_vars );
    +		// Remove the passthru query vars from the URL to match the correct cache.
    +		$url = remove_query_arg( array_keys( $passthru_vars ), $url );
    +		// Normalize the URL to make sure there's a trailing slash at the end of the path, before the query or fragment.
    +		$url = preg_replace( '~(?warmup_cache(
    +				'canonical_url',
    +				WEEK_IN_SECONDS,
    +				Listener::TRIGGER_GENERATE_REWRITE_RULES
    +			);
    +			if ( isset( $this->canonical_url_cache[ $url ] ) ) {
    +				// Re-apply passthru vars now, if any.
    +				return add_query_arg( $passthru_vars, $this->canonical_url_cache[ $url ] );
    +			}
    +		}
    +
    +		$query_vars = array_intersect_key( $query_vars, $scalar_query_vars );
    +
    +		if ( isset( $query_vars['paged'] ) && 1 === (int) $query_vars['paged'] ) {
    +			// Remove the `paged` query var if it's 1.
    +			unset( $query_vars['paged'] );
    +		}
    +
    +		ksort( $query_vars );
    +
    +		$our_rules          = $this->get_handled_rewrite_rules();
    +		$handled_query_vars = $this->get_rules_query_vars( $our_rules );
    +		$handled_post_types = $this->get_post_types();
    +
    +		if (
    +			// The rules we handle should not be empty.
    +			empty( $our_rules )
    +			|| ! (
    +				// Supported post types should be either keys or values, of the `post_type` argument, in the query vars.
    +				count( array_intersect_key( array_flip( $handled_post_types ), $query_vars ) )
    +				|| in_array( Arr::get( $query_vars, 'post_type', 'post' ), $handled_post_types, true )
    +			)
    +		) {
    +			$wp_canonical = redirect_canonical( $canonical_url, false );
    +			if ( empty( $wp_canonical ) ) {
    +				$wp_canonical = $canonical_url;
    +			}
    +
    +			$this->canonical_url_cache[ $url ] = $wp_canonical;
    +
    +			return $wp_canonical;
    +		}
    +
    +		$bases = (array) $this->get_bases();
    +		ksort( $bases );
    +
    +		$localized_matchers = $this->get_localized_matchers();
    +		$dynamic_matchers   = $this->get_dynamic_matchers( $query_vars );
    +
    +		// Try to match only on the query vars we're actually handling.
    +		$matched_vars   = array_intersect_key( $query_vars, array_combine( $handled_query_vars, $handled_query_vars ) );
    +		$unmatched_vars = array_diff_key( $query_vars, array_combine( $handled_query_vars, $handled_query_vars ) );
    +
    +		if ( empty( $matched_vars ) ) {
    +			// The URL does contain query vars, but none we handle.
    +			$wp_canonical = trailingslashit( redirect_canonical( $url, false ) );
    +			$this->canonical_url_cache[ $url ] = $wp_canonical;
    +
    +			return $wp_canonical;
    +		}
    +
    +		$found = false;
    +
    +		foreach ( $our_rules as $link_template => $index_path ) {
    +			wp_parse_str( (string) parse_url( $index_path, PHP_URL_QUERY ), $link_vars );
    +			ksort( $link_vars );
    +
    +			if ( array_keys( $link_vars ) !== array_keys( $matched_vars ) ) {
    +				continue;
    +			}
    +
    +			if ( ! (
    +				Arr::get( $matched_vars, 'post_type', '' ) === Arr::get( $link_vars, 'post_type', '' )
    +				&& Arr::get( $matched_vars, 'eventDisplay', '' ) === Arr::get( $link_vars, 'eventDisplay', '' )
    +			) ) {
    +				continue;
    +			}
    +
    +			$replace = array_map( function ( $localized_matcher ) use ( $matched_vars ) {
    +				if ( ! is_array( $localized_matcher ) ) {
    +					// For the dates.
    +					return isset( $matched_vars[ $localized_matcher ] )
    +						? $matched_vars[ $localized_matcher ]
    +						: '';
    +				}
    +
    +				$query_var  = $localized_matcher['query_var'];
    +				$query_vars = [ $query_var ];
    +
    +				if ( $query_var === 'name' ) {
    +					$query_vars = array_merge( $query_vars, $this->get_post_types() );
    +				}
    +
    +				if ( ! array_intersect( array_keys( $matched_vars ), $query_vars ) ) {
    +					return '';
    +				}
    +
    +				if ( isset( $localized_matcher['localized_slug'] ) ) {
    +					// If available, then return the localized slug instead of inferring it as we do below.
    +					return $localized_matcher['localized_slug'];
    +				}
    +
    +				/*
    +				 * We use `end` as, by default, the localized version of the slug in the current language will be at the
    +				 * end of the array.
    +				 */
    +				return end( $localized_matcher['localized_slugs'] );
    +			}, $localized_matchers );
    +
    +			// Include dynamic matchers now.
    +			$replace = array_merge( $dynamic_matchers, $replace );
    +
    +			/*
    +			 * Prune from the replacements the empty values. This will resolve conflicts (e.g. single and archive w/
    +			 * same slug) as no two can be true at the same time.
    +			 * Remove the `` prefix added to localized matchers, if any.
    +			 */
    +			$replace = array_filter( $replace );
    +			$replace = array_combine(
    +				array_map( static function ( $key ) {
    +					return preg_replace(
    +						'/' . preg_quote( Tribe__Rewrite::$localized_matcher_delimiter, '/' ) . '\\w*$/',
    +						'',
    +						$key
    +					);
    +				}, array_keys( $replace ) ),
    +				$replace
    +			);
    +
    +			// Use case-insensitive replace to make sure to work with some decoding using uppercase escaped chars.
    +			$replaced = str_ireplace( array_keys( $replace ), $replace, $link_template );
    +
    +			// Remove trailing chars.
    +			$path     = rtrim( $replaced, '?$' );
    +			$resolved = trailingslashit( home_url( $path ) );
    +			$found = true;
    +
    +			break;
    +		}
    +
    +		if ( empty( $resolved ) ) {
    +			$wp_canonical = redirect_canonical( $canonical_url, false );
    +			$resolved     = empty( $wp_canonical ) ? $canonical_url : $wp_canonical;
    +		}
    +
    +		if ( $canonical_url !== $resolved ) {
    +			// Be sure to add a trailing slash to the URL; before `?` or `#`.
    +			$resolved = preg_replace( '/(?canonical_url_cache[ $url ] = $resolved;
    +		}
    +
    +		// Re-apply passthru vars now, if any. After the caching to allow salting the cache key too much.
    +		$resolved = add_query_arg( $passthru_vars, $resolved );
    +
    +		return $resolved;
    +	}
    +
    +	/**
    +	 * Returns an array of rewrite rules handled by the implementation.
    +	 *
    +	 * @since 4.9.11
    +	 *
    +	 * @return array An array of rewrite rules handled by the implementation in the shape `[  =>  ]`.
    +	 */
    +	protected function get_handled_rewrite_rules() {
    +		// Try and pull it from memoized values.
    +		$cache     = tribe_cache();
    +		$our_rules = $cache['handled_rewrite_rules'] ?? null;
    +
    +		// We need to make sure we are have WP_Rewrite setup
    +		if ( ! $this->rewrite || empty( $this->rewrite->rules ) ) {
    +			$this->setup();
    +		}
    +
    +		$all_rules = isset( $this->rewrite->rules ) ? (array) $this->rewrite->rules : [];
    +
    +		if ( ! is_array( $our_rules ) ) {
    +			// While this is specific to The Events Calendar we're handling a small enough post type base to keep it here.
    +			$pattern = '/post_type=tribe_(events|venue|organizer)/';
    +			// Reverse the rules to try and match the most complex first.
    +			$our_rules = array_filter( $all_rules,
    +				static function ( $rule_query_string ) use ( $pattern ) {
    +					return is_string( $rule_query_string ) && preg_match( $pattern, $rule_query_string );
    +				}
    +			);
    +
    +			$cache['handled_rewrite_rules'] = $our_rules;
    +		}
    +
    +		/**
    +		 * Filters the list of rewrite rules handled by our code to add or remove some as required.
    +		 *
    +		 * @since  4.9.18
    +		 *
    +		 * @param array                $our_rules An array of rewrite rules handled by our code, in the shape
    +		 *                                        `[  =>  ]`.
    +		 *                                        E.g. `[ '(?:events)/(?:list)/?$' => 'index.php?post_type=tribe_events&eventDisplay=list' ]`.
    +		 * @param array $all_rules All the current rewrite rules, before any filtering is applied; these
    +		 *                                        have the same ` rewrite >` format as the previous argument,
    +		 *                                        which is the format used by WordPress rewrite rules.
    +		 */
    +		$our_rules = apply_filters( 'tribe_rewrite_handled_rewrite_rules', $our_rules, $all_rules );
    +
    +		return $our_rules;
    +	}
    +
    +	/**
    +	 * Returns a map relating localized regex matchers to query vars.
    +	 *
    +	 * @since 4.9.11
    +	 *
    +	 * @return array A map of localized regex matchers in the shape `[  =>  ]`.
    +	 */
    +	protected function get_localized_matchers() {
    +		$cache_key = __METHOD__;
    +		$cache = tribe_cache();
    +
    +		$localized_matchers = $cache[ $cache_key ];
    +
    +		if ( ! empty( $localized_matchers ) ) {
    +			return $localized_matchers;
    +		}
    +
    +		$bases         = (array) $this->get_bases();
    +		$query_var_map = $this->get_matcher_to_query_var_map();
    +		$localized_matchers = [];
    +
    +		foreach ( $bases as $base => $localized_matcher ) {
    +			// Use the base too to allow possible conflicts if the slugs are the same for single and archive.
    +			$localized_matcher_key = $localized_matcher . static::$localized_matcher_delimiter . $base;
    +
    +			if ( isset( $localized_matchers[ $localized_matcher_key ] ) ) {
    +				continue;
    +			}
    +
    +			if ( isset( $query_var_map[ $base ] ) ) {
    +				$localized_matchers[ $localized_matcher_key ] = [
    +					'base'            => $base,
    +					'query_var'       => $query_var_map[ $base ],
    +					'en_slug'         => $base,
    +					'localized_slugs' => [ $base ],
    +				];
    +				// If we have the localized slug version then let's parse it.
    +				preg_match( '/^\\(\\?:(?[^\\)]+)\\)$/u', $localized_matcher, $buffer );
    +				if ( ! empty( $buffer['slugs'] ) ) {
    +					$slugs = explode( '|', $buffer['slugs'] );
    +
    +					$localized_matchers[ $localized_matcher_key ]['localized_slugs'] = array_map(
    +						static function ( $localized_slug ) {
    +							return str_replace( '\-', '-', $localized_slug );
    +						},
    +						$slugs
    +					);
    +
    +					// The English version is the first.
    +					$localized_matchers[ $localized_matcher_key ]['en_slug'] = reset( $slugs );
    +
    +					$localized_slug = $this->filter_matcher( null, $base );
    +
    +					if ( $localized_slug ) {
    +						$localized_matchers[ $localized_matcher_key ]['localized_slug'] = $localized_slug;
    +					}
    +				}
    +			}
    +		}
    +
    +		$cache[ $cache_key ] = $localized_matchers;
    +
    +		return $localized_matchers;
    +	}
    +
    +	/**
    +	 * Returns a map relating localized matcher slugs to the corresponding query var.
    +	 *
    +	 * @since 4.9.11
    +	 *
    +	 * @return array A map relating localized matcher slugs to the corresponding query var.
    +	 */
    +	protected function get_matcher_to_query_var_map() {
    +		throw new BadMethodCallException(
    +			'This method should not be called on the base class (' . __CLASS__ . '); only on extending classes.'
    +		);
    +	}
    +
    +	/**
    +	 * Return a list of the query vars handled in the input rewrite rules.
    +	 *
    +	 * @since 4.9.11
    +	 *
    +	 * @param array $rules A set of rewrite rules in the shape `[  =>  ]`.
    +	 *
    +	 * @return array A list of all the query vars handled in the rules.
    +	 */
    +	protected function get_rules_query_vars( array $rules ) {
    +		static $cache_var_name = __METHOD__;
    +
    +		$cached_rules = tribe_get_var( $cache_var_name, [] );
    +		$cache_key = md5( json_encode( $rules ) );
    +
    +		if ( ! isset( $cached_rules[ $cache_key ] ) ) {
    +			$cached_rules[ $cache_key ] = array_unique(
    +				array_filter(
    +					array_merge(
    +						[],
    +						...array_values(
    +							array_map(
    +								static function ( $rule_string ) {
    +									wp_parse_str( parse_url( $rule_string, PHP_URL_QUERY ), $vars );
    +
    +									return array_keys( $vars );
    +								},
    +								array_filter( $rules, 'is_string' )
    +							)
    +						)
    +					)
    +				)
    +			);
    +
    +			tribe_set_var( $cache_var_name, $cached_rules );
    +		}
    +
    +		return $cached_rules[ $cache_key ];
    +	}
    +
    +	/**
    +	 * Sets up the dynamic matchers based on the link query vars.
    +	 *
    +	 * @since 4.9.11
    +	 *
    +	 * @param array $query_vars An map of query vars and their values.
    +	 *
    +	 * @return array A map of dynamic matchers in the shape `[  =>  ]`.
    +	 */
    +	protected function get_dynamic_matchers( array $query_vars ) {
    +		$bases            = (array) $this->get_bases();
    +		$dynamic_matchers = [];
    +
    +		/*
    +		 * In some instance we use the `page` (w/o `d`) to paginate a dynamic archive.
    +		 * Let's support that too.
    +		 * It's important to add `page` after `paged` to try and match the longest (`paged`) first.
    +		 */
    +		foreach ( [ 'paged', 'page' ] as $page_var ) {
    +			if ( isset( $query_vars[ $page_var ] ) ) {
    +				$page_regex = $bases['page'];
    +				preg_match( '/^\(\?:(?[^\\)]+)\)/', $page_regex, $matches );
    +				if ( isset( $matches['slugs'] ) ) {
    +					$slugs = explode( '|', $matches['slugs'] );
    +					// The localized version is the last, by default.
    +					$en_slug        = end( $slugs );
    +					$localized_slug = $this->filter_matcher( null, 'page' );
    +
    +					// We use two different regular expressions to read pages, let's add both.
    +					if ( $localized_slug ) {
    +						$dynamic_matchers["{$page_regex}/(\d+)"]       = "{$localized_slug}/{$query_vars[$page_var]}";
    +						$dynamic_matchers["{$page_regex}/([0-9]{1,})"] = "{$localized_slug}/{$query_vars[$page_var]}";
    +					} else {
    +						$dynamic_matchers["{$page_regex}/(\d+)"]       = "{$en_slug}/{$query_vars[$page_var]}";
    +						$dynamic_matchers["{$page_regex}/([0-9]{1,})"] = "{$en_slug}/{$query_vars[$page_var]}";
    +					}
    +				}
    +			}
    +		}
    +
    +		if ( isset( $query_vars['tag'] ) ) {
    +			$tag      = $query_vars['tag'];
    +			$tag_term = get_term_by( 'slug', $tag, 'post_tag' );
    +
    +			if ( $tag_term instanceof WP_Term ) {
    +				// Let's actually add the matcher only if the tag exists.
    +				$tag_regex = $bases['tag'];
    +				preg_match( '/^\(\?:(?[^\\)]+)\)/', $tag_regex, $matches );
    +				if ( isset( $matches['slugs'] ) ) {
    +					$slugs = explode( '|', $matches['slugs'] );
    +					// The localized version is the last, by default.
    +					$en_slug        = end( $slugs );
    +					$localized_slug = $this->filter_matcher( null, 'tag' );
    +
    +					if ( $localized_slug ) {
    +						$dynamic_matchers["{$tag_regex}/([^/]+)"] = "{$localized_slug}/{$tag}";
    +					} else {
    +						$dynamic_matchers["{$tag_regex}/([^/]+)"] = "{$en_slug}/{$tag}";
    +					}
    +				}
    +			}
    +		}
    +
    +		if ( isset( $query_vars['feed'] ) ) {
    +			$feed_regex                      = 'feed/(feed|rdf|rss|rss2|atom)';
    +			$dynamic_matchers[ $feed_regex ] = "feed/{$query_vars['feed']}";
    +		}
    +
    +		return $dynamic_matchers;
    +	}
    +
    +	/**
    +	 * Returns a list of post types supported by the implementation.
    +	 *
    +	 * @since 4.9.11
    +	 *
    +	 * @return array An array of post types supported and handled by the rewrite implementation.
    +	 */
    +	protected function get_post_types() {
    +		throw new BadMethodCallException( 'Method get_post_types should be implemented by extending classes.' );
    +	}
    +
    +	/**
    +	 * Parses a URL to produce an array of query variables.
    +	 *
    +	 * Most of this functionality was copied from `WP::parse_request()` method
    +	 * with some changes to avoid conflicts and removing non-required behaviors.
    +	 *
    +	 * @since  4.9.11
    +	 *
    +	 * @param string $url              The URLto parse.
    +	 * @param array  $extra_query_vars An associative array of extra query vars to use for the parsing. These vars will
    +	 *                                 be read before the WordPress defined ones overriding them.
    +	 * @param bool   $force Whether to try and use the cache or force a new canonical URL conversion.
    +	 *
    +	 * @return array An array of query vars, as parsed from the input URL.
    +	 */
    +	public function parse_request( $url, array $extra_query_vars = [], $force = false ) {
    +		if ( null === $this->rewrite ) {
    +			// We re-do this check here as the object might have been initialized before the global rewrite was set.
    +			$this->setup();
    +		}
    +
    +		/**
    +		 * Allows short-circuiting the URL parsing.
    +		 *
    +		 * This filter will run before any logic runs, its result will not be cached and this filter will be called on
    +		 * each call to this method.
    +		 * Returning a non `null` value here will short-circuit this logic.
    +		 *
    +		 * @since 4.9.11
    +		 *
    +		 * @param array  $query_vars       The parsed query vars array.
    +		 * @param array  $extra_query_vars An associative array of extra query vars that will be processed before the
    +		 *                                 WordPress defined ones.
    +		 * @param string $url              The URL to parse.
    +		 */
    +		$parsed = apply_filters( 'tribe_rewrite_pre_parse_query_vars', null, $extra_query_vars, $url );
    +		if ( null !== $parsed ) {
    +			return $parsed;
    +		}
    +
    +		if ( ! $force ) {
    +			$this->warmup_cache(
    +				'parse_request',
    +				WEEK_IN_SECONDS,
    +				Listener::TRIGGER_GENERATE_REWRITE_RULES
    +			);
    +			if ( isset( $this->parse_request_cache[ $url ] ) ) {
    +				return $this->parse_request_cache[ $url ];
    +			}
    +		}
    +
    +		$query_vars           = [];
    +		$post_type_query_vars = [];
    +		$perma_query_vars     = [];
    +		$url_components = parse_url($url);
    +		$url_path = Arr::get( $url_components, 'path', '/' );
    +		$site_path = parse_url( home_url(), PHP_URL_PATH );
    +		if ( ! empty( $site_path ) && '/' !== $site_path ) {
    +			// The current site is in a sub-directory: the site path should be dropped from the request path.
    +			$url_path = str_replace( $site_path, '', $url_path );
    +		}
    +		$url_query = Arr::get( $url_components, 'query', '' );
    +		parse_str( $url_query, $url_query_vars );
    +		// Look for matches, removing leading `/` char.
    +		$request_match         = ltrim( $url_path, '/' );
    +		$decoded_request_match = urldecode( $request_match );
    +
    +		// Fetch the rewrite rules.
    +		$rewrite_rules = $this->rewrite->wp_rewrite_rules();
    +		$matched_rule = false;
    +
    +		if ( ! empty( $rewrite_rules ) ) {
    +			foreach ( (array) $rewrite_rules as $match => $query ) {
    +				$matches_regex = preg_match( "#^$match#", $request_match, $matches )
    +				                 || preg_match( "#^$match#", $decoded_request_match, $matches );
    +
    +				if ( ! $matches_regex ) {
    +					continue;
    +				}
    +
    +				if (
    +					$this->rewrite->use_verbose_page_rules
    +					&& preg_match( '/pagename=\$matches\[([0-9]+)\]/', $query, $varmatch )
    +				) {
    +					// This is a verbose page match, let's check to be sure about it.
    +					$page = get_page_by_path( $matches[ $varmatch[1] ] );
    +					if ( ! $page ) {
    +						continue;
    +					}
    +					$post_status_obj = get_post_status_object( $page->post_status );
    +					if (
    +						! $post_status_obj->public
    +						&& ! $post_status_obj->protected
    +						&& ! $post_status_obj->private
    +						&& $post_status_obj->exclude_from_search
    +					) {
    +						continue;
    +					}
    +				}
    +
    +				// Got a match.
    +				$matched_rule = $match;
    +				break;
    +			}
    +
    +			if ( false !== $matched_rule ) {
    +				// Trim the query of everything up to the '?'.
    +				$query = preg_replace( '!^.+\?!', '', $query );
    +				// Substitute the substring matches into the query.
    +				$query = addslashes( WP_MatchesMapRegex::apply( $query, $matches ) );
    +				// Parse the query.
    +				parse_str( $query, $perma_query_vars );
    +			}
    +		}
    +
    +		foreach ( get_post_types( [], 'objects' ) as $post_type => $t ) {
    +			if (
    +				is_post_type_viewable( $t )
    +				&& $t->query_var
    +			) {
    +				$post_type_query_vars[ $t->query_var ] = $post_type;
    +			}
    +		}
    +
    +		global $wp;
    +
    +		/*
    +		 * WordPress would apply this filter in the `parse_request` method to allow the registration of additional query
    +		 * vars. They might not have been registered at this point so we do this again making sure to avoid duplicates.
    +		 */
    +		$public_query_vars = array_unique( apply_filters( 'query_vars', $wp->public_query_vars ) );
    +
    +		foreach ( $public_query_vars as $wpvar ) {
    +			if ( isset( $extra_query_vars[ $wpvar ] ) ) {
    +				$query_vars[ $wpvar ] = $extra_query_vars[ $wpvar ];
    +			} elseif ( isset( $perma_query_vars[ $wpvar ] ) ) {
    +				$query_vars[ $wpvar ] = $perma_query_vars[ $wpvar ];
    +			}
    +			if ( ! empty( $query_vars[ $wpvar ] ) ) {
    +				if ( ! is_array( $query_vars[ $wpvar ] ) ) {
    +					$query_vars[ $wpvar ] = (string) $query_vars[ $wpvar ];
    +				} else {
    +					foreach ( $query_vars[ $wpvar ] as $vkey => $v ) {
    +						if ( is_scalar( $v ) ) {
    +							$query_vars[ $wpvar ][ $vkey ] = (string) $v;
    +						}
    +					}
    +				}
    +				if ( isset( $post_type_query_vars[ $wpvar ] ) ) {
    +					$query_vars['post_type'] = $post_type_query_vars[ $wpvar ];
    +					$query_vars['name']      = $query_vars[ $wpvar ];
    +				}
    +			}
    +		}
    +
    +		// Convert urldecoded spaces back into `+`.
    +		foreach ( get_taxonomies( [], 'objects' ) as $taxonomy => $t ) {
    +			if ( $t->query_var && isset( $query_vars[ $t->query_var ] ) ) {
    +				$query_vars[ $t->query_var ] = str_replace( ' ', '+', $query_vars[ $t->query_var ] );
    +			}
    +		}
    +
    +		// Don't allow non-publicly queryable taxonomies to be queried from the front end.
    +		if ( ! is_admin() ) {
    +			foreach ( get_taxonomies( [ 'publicly_queryable' => false ], 'objects' ) as $taxonomy => $t ) {
    +				/*
    +				 * Disallow when set to the 'taxonomy' query var.
    +				 * Non-publicly queryable taxonomies cannot register custom query vars. See register_taxonomy().
    +				 */
    +				if ( isset( $query_vars['taxonomy'] ) && $taxonomy === $query_vars['taxonomy'] ) {
    +					unset( $query_vars['taxonomy'], $query_vars['term'] );
    +				}
    +			}
    +		}
    +
    +		// Limit publicly queried post_types to those that are publicly_queryable
    +		if ( isset( $query_vars['post_type'] ) ) {
    +			$queryable_post_types = get_post_types( [ 'publicly_queryable' => true ] );
    +			if ( ! is_array( $query_vars['post_type'] ) ) {
    +				if ( ! in_array( $query_vars['post_type'], $queryable_post_types ) ) {
    +					unset( $query_vars['post_type'] );
    +				}
    +			} else {
    +				$query_vars['post_type'] = array_intersect( $query_vars['post_type'], $queryable_post_types );
    +			}
    +		}
    +
    +		// Resolve conflicts between posts with numeric slugs and date archive queries.
    +		$query_vars = wp_resolve_numeric_slug_conflicts( $query_vars );
    +
    +		foreach ( (array) $wp->private_query_vars as $var ) {
    +			if ( isset( $extra_query_vars[ $var ] ) ) {
    +				$query_vars[ $var ] = $extra_query_vars[ $var ];
    +			}
    +		}
    +
    +		/*
    +		 * If we have both the `name` query var and the post type one, then let's remove the `name` one.
    +		 */
    +		if ( array_intersect( array_keys( $query_vars ), $this->get_post_types() ) ) {
    +			unset( $query_vars['name'] );
    +		}
    +
    +		if ( ! empty( $url_query_vars ) ) {
    +			// If the URL did have query vars keep them if not overridden by our resolution.
    +			$query_vars = array_merge( $url_query_vars, $query_vars );
    +		}
    +
    +		// Prune the query vars to drop the empty `page` or `paged` ones.
    +		$query_vars = array_filter( $query_vars, static function ( $value, $key ) {
    +			return ! in_array( $key, [ 'paged', 'page' ] ) || (int) $value !== 0;
    +		}, ARRAY_FILTER_USE_BOTH );
    +
    +		/**
    +		 * Filters the array of parsed query variables after the class logic has been applied to it.
    +		 *
    +		 * Due to the costly nature of this operation the results will be cached. The logic, and this filter, will
    +		 * not run a second time for the same URL in the context of the same request.
    +		 *
    +		 * @since 4.9.11
    +		 *
    +		 * @param array  $query_vars       The parsed query vars array.
    +		 * @param array  $extra_query_vars An associative array of extra query vars that will be processed before the
    +		 *                                 WordPress defined ones.
    +		 * @param string $url              The URL to parse.
    +		 */
    +		$query_vars = apply_filters( 'tribe_rewrite_parse_query_vars', $query_vars, $extra_query_vars, $url );
    +
    +		if ( $matched_rule ) {
    +			// Since we're caching let's not cache unmatchec URLs to allow for their later, valid matching.
    +			$this->parse_request_cache[ $url ] = $query_vars;
    +		}
    +
    +		return $query_vars;
    +	}
    +
    +	/**
    +	 * Returns the "clean" version of a URL.
    +	 *
    +	 * The URL is first parsed then resolved to a canonical URL.
    +	 * As an example the URL `/events/list/?post_type=tribe_events` is "dirty" in that the `post_type` query variable
    +	 * is redundant. The clean version of the URL is `/events/list/`, where the query variable is removed.
    +	 *
    +	 * @since 4.9.11
    +	 *
    +	 * @param string $url The URL to clean.
    +	 * @param bool   $force Whether to try and use the cache or force a new URL cleaning run.
    +	 *
    +	 * @return string The cleaned URL, or the input URL if it could not be resolved to a clean one.
    +	 */
    +	public function get_clean_url( $url, $force = false ) {
    +		if ( ! $force ) {
    +			$this->warmup_cache(
    +				'clean_url',
    +				WEEK_IN_SECONDS,
    +				Listener::TRIGGER_GENERATE_REWRITE_RULES
    +			);
    +			if ( isset( $this->clean_url_cache[ $url ] ) ) {
    +				return $this->clean_url_cache[ $url ];
    +			}
    +		}
    +
    +		$parsed_vars = $this->parse_request( $url );
    +
    +		if ( empty( $parsed_vars ) ) {
    +			return home_url();
    +		}
    +
    +		$clean = $this->get_canonical_url( add_query_arg( $parsed_vars, home_url( '/' ) ), $force );
    +
    +		$this->clean_url_cache[ $url ] = $clean;
    +
    +		return $clean;
    +	}
    +
    +	/**
    +	 * Filters the localized matcher to allow integrations to provider contextual translations of the matcher.
    +	 *
    +	 * @since 5.0.17
    +	 *
    +	 * @param string|null $localized_matcher The localized matcher.
    +	 * @param string      $base              The base the localized matcher is for.
    +	 *
    +	 * @return string The localized matcher.
    +	 */
    +	protected function filter_matcher( ?string $localized_matcher, string $base ): string {
    +		return (string) apply_filters( 'tec_common_rewrite_localize_matcher', $localized_matcher, $base );
    +	}
    +}
    diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/Body_Classes.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/Body_Classes.php
    new file mode 100644
    index 000000000..52c3b496f
    --- /dev/null
    +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/Body_Classes.php
    @@ -0,0 +1,88 @@
    +hooks();
    +	}
    +
    +	/**
    +	 * Set up hooks for classes.
    +	 *
    +	 * @since 4.12.6
    +	 */
    +	private function hooks() {
    +		add_filter( 'body_class', [ $this, 'add_body_classes' ] );
    +		add_filter( 'admin_body_class', [ $this, 'add_admin_body_classes' ] );
    +
    +		/**
    +		 * Allows plugins to hook into the hooks action to register their own hooks.
    +		 *
    +		 * @since 4.12.6
    +		 *
    +		 * @param Tribe\Service_Providers\Dialog $dialog
    +		 */
    +		do_action( 'tribe_body_classes_hooks', $this );
    +	}
    +
    +	/**
    +	 * Hook in and add FE body classes.
    +	 *
    +	 * @since 4.12.6
    +	 *
    +	 * @param array $classes An array of body class names.
    +	 * @return array The modified array of body class names.
    +	 */
    +	public function add_body_classes( $classes = [] ) {
    +		/** @var Body_Class_Object $body_classes */
    +		$body_classes = tribe( Body_Class_Object::class );
    +
    +		return $body_classes->add_body_classes( $classes );
    +	}
    +
    +	/**
    +	 * Hook in and add admin body classes.
    +	 *
    +	 * @since 4.12.6
    +	 *
    +	 * @param array $classes An array of body class names.
    +	 * @return array The modified array of body class names.
    +	 */
    +	public function add_admin_body_classes( $classes = [] ) {
    +		/** @var Body_Class_Object $body_classes */
    +		$body_classes = tribe( Body_Class_Object::class );
    +
    +		return $body_classes->add_admin_body_classes( $classes );
    +	}
    +
    +}
    diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/Crons.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/Crons.php
    new file mode 100644
    index 000000000..1055ca954
    --- /dev/null
    +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/Crons.php
    @@ -0,0 +1,39 @@
    + 0 ) {
    +			$panels = array_merge( $panels, $tribe_panels );
    +		}
    +
    +		return $panels;
    +	}
    +}
    diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/Dialog.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/Dialog.php
    new file mode 100644
    index 000000000..df6391c8f
    --- /dev/null
    +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/Dialog.php
    @@ -0,0 +1,113 @@
    +hooks();
    +	}
    +
    +	/**
    +	 * Set up hooks for classes.
    +	 *
    +	 * @since 4.10.0
    +	 */
    +	private function hooks() {
    +		add_action( 'tribe_common_loaded', [ $this, 'register_dialog_assets' ] );
    +		add_filter( 'tribe_template_public_namespace', [ $this, 'template_public_namespace' ], 10, 2 );
    +
    +		/**
    +		 * Allows plugins to hook into the hooks action to register their own hooks
    +		 *
    +		 * @since 4.10.0
    +		 *
    +		 * @param Tribe\Service_Providers\Dialog $dialog
    +		 */
    +		do_action( 'tribe_dialog_hooks', $this );
    +	}
    +
    +	/**
    +	  * {@inheritdoc}
    +	 *
    +	 * @since  4.10.0
    +	 */
    +	public function template_public_namespace( $namespace, $obj ) {
    +		if ( ! empty( $obj->template_namespace ) && 'dialog' === $obj->template_namespace ) {
    +			array_push( $namespace, 'dialog' );
    +		}
    +
    +		return $namespace;
    +	}
    +
    +	/**
    +	 * Register assets associated with dialog
    +	 *
    +	 * @since 4.10.0
    +	 */
    +	public function register_dialog_assets() {
    +		$main = \Tribe__Main::instance();
    +
    +		tribe_asset(
    +			$main,
    +			'tribe-dialog',
    +			'dialog.css',
    +			[ 'tec-variables-full' ],
    +			[],
    +			[ 'groups' => 'tribe-dialog' ]
    +		);
    +
    +		tribe_asset(
    +			$main,
    +			'tec-a11y-dialog',
    +			'tec-a11y-dialog.js',
    +			[ 'underscore', 'tribe-common' ],
    +			[],
    +			[ 'groups' => 'tribe-dialog' ]
    +		);
    +
    +		tribe_asset(
    +			$main,
    +			'tribe-dialog-js',
    +			'dialog.js',
    +			[ 'tec-a11y-dialog' ],
    +			[],
    +			[ 'groups' => 'tribe-dialog' ]
    +		);
    +
    +		/**
    +		 * Allows plugins to hook into the assets action to register their own assets
    +		 *
    +		 * @since 4.10.0
    +		 *
    +		 * @param Tribe\Service_Providers\Dialog $dialog
    +		 */
    +		do_action( 'tribe_dialog_assets_registered', $this );
    +	}
    +}
    diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/Onboarding.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/Onboarding.php
    new file mode 100644
    index 000000000..b26b29fa7
    --- /dev/null
    +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/Onboarding.php
    @@ -0,0 +1,149 @@
    +is_enabled() ) {
    +			return;
    +		}
    +
    +		$this->container->singleton( Onboarding_Main::class, Onboarding_Main::class );
    +		$this->container->singleton( static::class, static::class );
    +
    +		$this->hooks();
    +	}
    +
    +	/**
    +	 * Set up hooks for classes.
    +	 *
    +	 * @since 4.14.9
    +	 */
    +	protected function hooks() {
    +		add_action( 'tribe_common_loaded', [ $this, 'register_assets' ] );
    +
    +		add_action( 'admin_enqueue_scripts', tribe_callback( Onboarding_Main::class, 'localize_tour' ) );
    +		add_action( 'admin_enqueue_scripts', tribe_callback( Onboarding_Main::class, 'localize_hints' ) );
    +	}
    +
    +	/**
    +	 * Register assets associated with onboarding.
    +	 *
    +	 * @since 4.14.9
    +	 */
    +	public function register_assets() {
    +		$main = \Tribe__Main::instance();
    +
    +		tribe_asset(
    +			$main,
    +			'tec-intro-js',
    +			'node_modules/intro.js/intro.js',
    +			[],
    +			[ 'admin_enqueue_scripts' ],
    +			[
    +				'groups'       => self::$group_key,
    +				'conditionals' => [ $this, 'should_enqueue_assets' ],
    +			]
    +		);
    +
    +		tribe_asset(
    +			$main,
    +			'tec-intro-styles',
    +			'node_modules/intro.js/introjs.css',
    +			[],
    +			[ 'admin_enqueue_scripts' ],
    +			[
    +				'groups'       => self::$group_key,
    +				'conditionals' => [ $this, 'should_enqueue_assets' ],
    +			]
    +		);
    +
    +		tribe_asset(
    +			$main,
    +			'tec-onboarding-styles',
    +			'onboarding.css',
    +			[ 'tec-intro-styles', 'tec-variables-skeleton', 'tec-variables-full' ],
    +			[ 'admin_enqueue_scripts' ],
    +			[
    +				'groups'       => self::$group_key,
    +				'conditionals' => [ $this, 'should_enqueue_assets' ],
    +			]
    +		);
    +
    +		tribe_asset(
    +			$main,
    +			'tec-onboarding-js',
    +			'onboarding.js',
    +			[
    +				'tribe-common',
    +				'tec-intro-js',
    +			],
    +			[ 'admin_enqueue_scripts' ],
    +			[
    +				'groups'       => self::$group_key,
    +				'in_footer'    => false,
    +				'localize'     => [
    +					'name' => 'TribeOnboarding',
    +					'data' => [
    +						'hintButtonLabel' => __( 'Got it', 'tribe-common' ),
    +					],
    +				],
    +				'conditionals' => [ $this, 'should_enqueue_assets' ],
    +			]
    +		);
    +	}
    +
    +	/**
    +	 * Define if the assets for `Onboarding` should be enqueued or not.
    +	 *
    +	 * @since 4.14.9
    +	 *
    +	 * @return bool If the Onboarding assets should be enqueued or not.
    +	 */
    +	public function should_enqueue_assets() {
    +		return $this->is_enabled();
    +	}
    +
    +	/**
    +	 * Check if the onboarding is enabled or not.
    +	 *
    +	 * @since 4.14.9
    +	 *
    +	 * @return bool
    +	 */
    +	public function is_enabled() {
    +		/**
    +		 * Filter to disable tribe onboarding
    +		 *
    +		 * @since 4.14.9
    +		 *
    +		 * @param bool $disabled If we want to disable the on boarding.
    +		 */
    +		$is_enabled = (bool) apply_filters( 'tec_onboarding_enabled', false );
    +
    +		return $is_enabled && is_admin();
    +	}
    +}
    diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/PUE.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/PUE.php
    new file mode 100644
    index 000000000..25eb50b14
    --- /dev/null
    +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/PUE.php
    @@ -0,0 +1,52 @@
    +container->singleton( Update_Prevention::class, Update_Prevention::class );
    +
    +		// Setup all of WP hooks associated with PUE.
    +		$this->register_hooks();
    +	}
    +
    +	/**
    +	 * Registers the provider handling all the 1st level filters and actions for PUE.
    +	 *
    +	 * @since 4.9.2
    +	 */
    +	protected function register_hooks() {
    +		add_filter( 'upgrader_source_selection', [ $this, 'filter_upgrader_source_selection' ], 15, 4 );
    +	}
    +
    +	/**
    +	 * Filters the source file location for the upgrade package for the PUE Update_Prevention engine.
    +	 *
    +	 * @since  4.9.12
    +	 *
    +	 * @param string      $source        File source location.
    +	 * @param string      $remote_source Remote file source location.
    +	 * @param WP_Upgrader $upgrader      WP_Upgrader instance.
    +	 * @param array       $extra         Extra arguments passed to hooked filters.
    +	 */
    +	public function filter_upgrader_source_selection( $source, $remote_source, $upgrader, $extras ) {
    +		return $this->container->make( Update_Prevention::class )->filter_upgrader_source_selection( $source, $remote_source, $upgrader, $extras );
    +	}
    +}
    diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/Processes.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/Processes.php
    new file mode 100644
    index 000000000..f7d929a48
    --- /dev/null
    +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/Processes.php
    @@ -0,0 +1,281 @@
    +context = tribe( 'context' );
    +
    +		// If the context of this request is neither AJAX or Cron bail.
    +		if ( ! ( $this->context->doing_ajax() || $this->context->doing_cron() ) ) {
    +			return;
    +		}
    +
    +		/** @var Tribe__Feature_Detection $feature_detection */
    +		$feature_detection         = tribe( 'feature-detection' );
    +		$action                    = tribe_get_request_var( 'action', false );
    +		$testing_for_async_support = $action === $this->get_handler_action( 'Tribe__Process__Tester' );
    +
    +		 // Dispatch in async mode if testing for it (w/o re-checking) or if async processes are supported.
    +		if ( $testing_for_async_support || $feature_detection->supports_async_process() ) {
    +			$this->dispatch_async();
    +
    +			return;
    +		}
    +
    +		$this->dispatch_cron();
    +	}
    +
    +	/**
    +	 * Hooks the correct handler for the action.
    +	 *
    +	 * @since 4.7.12
    +	 *
    +	 * @param string $action
    +	 */
    +	protected function hook_handler_for( $action ) {
    +		if ( null === $this->handler_actions ) {
    +			$handlers = [
    +				'Tribe__Process__Tester',
    +				'Tribe__Process__Post_Thumbnail_Setter',
    +			];
    +
    +			/**
    +			 * Filters the process handler classes the Service Provider should handle.
    +			 *
    +			 * All handlers should extend the `Tribe__Process__Handler` base class.
    +			 *
    +			 * @since 4.7.12
    +			 *
    +			 * @param array $handlers
    +			 */
    +			$handlers = array_unique( apply_filters( 'tribe_process_handlers', $handlers ) );
    +
    +			$this->handler_actions = array_combine(
    +				$handlers,
    +				array_map( [ $this, 'get_handler_action' ], $handlers )
    +			);
    +		}
    +
    +		$array_search = array_search( $action, $this->handler_actions, true );
    +
    +		if ( false === $handler_class = $array_search ) {
    +			return;
    +		}
    +
    +		// the handler will handle the hooking
    +		$this->container->make( $handler_class );
    +	}
    +
    +	/**
    +	 * Hooks the correct queue for the action.
    +	 *
    +	 * @since 4.7.12
    +	 *
    +	 * @param string $action
    +	 */
    +	protected function hook_queue_for( $action ) {
    +		if ( null === $this->queue_actions ) {
    +			$queues = [
    +				'Tribe__Promise',
    +			];
    +
    +			/**
    +			 * Filters the queue processing classes the Service Provider should handle.
    +			 *
    +			 * All queues should extend the `Tribe__Process__Queue` base class.
    +			 *
    +			 * @since 4.7.12
    +			 *
    +			 * @param array $queues An array of class names, each extending the `Tribe__Process__Queue` base class.
    +			 */
    +			$queues = array_unique( apply_filters( 'tribe_process_queues', $queues ) );
    +
    +			$all_queues_actions = array_combine(
    +				$queues,
    +				array_map( [ $this, 'get_queue_action' ], $queues )
    +			);
    +		}
    +
    +		$array_search = array_search( $action, $all_queues_actions, true );
    +
    +		if ( false === $queue_class = $array_search ) {
    +			return;
    +		}
    +
    +		// the queue will handle the hooking
    +		$this->container->make( $queue_class );
    +	}
    +
    +	/**
    +	 * Returns the action for the handler.
    +	 *
    +	 * @since 4.7.12
    +	 *
    +	 * @param string $handler_class
    +	 *
    +	 * @return string
    +	 */
    +	protected function get_handler_action( $handler_class ) {
    +		/** @var Tribe__Process__Handler handler_class */
    +		return 'tribe_process_' . call_user_func( [ $handler_class, 'action' ] );
    +	}
    +
    +	/**
    +	 * Returns the action for the queue.
    +	 *
    +	 * @since 4.7.12
    +	 *
    +	 * @param string $queue_class
    +	 *
    +	 * @return string
    +	 */
    +	protected function get_queue_action( $queue_class ) {
    +		/** @var Tribe__Process__Queue queue_class */
    +		return 'tribe_queue_' . call_user_func( [ $queue_class, 'action' ] );
    +	}
    +
    +	/**
    +	 * Dispatches the request, if in AJAX context of a valid queue processing request,
    +	 *  to the correct handler.
    +	 *
    +	 * @since 4.7.23
    +	 */
    +	protected function dispatch_async() {
    +		if ( ! (
    +			$this->context->doing_ajax()
    +			&& false !== $action = tribe_get_request_var( 'action', false )
    +		) ) {
    +			return;
    +		}
    +
    +		$this->hook_handler_for_action( $action );
    +	}
    +
    +	/**
    +	 * Start the process handlers if in the context of a cron process and
    +	 * if any is registered.
    +	 *
    +	 * @since 4.7.23
    +	 */
    +	protected function dispatch_cron() {
    +		if ( ! $this->context->doing_cron() ) {
    +			return;
    +		}
    +
    +		/*
    +		 * Here we parse the scheduled cron events to get those scheduled by a queue
    +		 * or process handler.
    +		 */
    +		$hooks = $this->get_scheduled_like( [ 'tribe_process_', 'tribe_queue_' ] );
    +
    +		if ( empty( $hooks ) ) {
    +			return;
    +		}
    +
    +		foreach ( $hooks as $action ) {
    +			/*
    +			 * Building the queue or process handler for an action will make it
    +			 * so the handler, in its `__construct` method, will hook on the action
    +			 * triggered by its cron event.
    +			 */
    +			$this->hook_handler_for_action( $action );
    +		}
    +	}
    +
    +	/**
    +	 * Hooks the correct queue or process handler for an action if any.
    +	 *
    +	 * @since 4.7.23
    +	 *
    +	 * @param string $action The action to hook the handler, or queue, for.
    +	 */
    +	protected function hook_handler_for_action( $action ) {
    +		if (
    +			0 !== strpos( $action, 'tribe_process_' )
    +			&& 0 !== strpos( $action, 'tribe_queue_' )
    +		) {
    +			return;
    +		}
    +
    +		if ( 0 === strpos( $action, 'tribe_process_' ) ) {
    +			$this->hook_handler_for( $action );
    +		} else {
    +			$this->hook_queue_for( $action );
    +		}
    +	}
    +
    +	/**
    +	 * Parses the `cron` array to return the hook names starting with a pattern.
    +	 *
    +	 * @since 4.7.23
    +	 *
    +	 * @param string|array $needles A pattern to look for or an array of patterns; if
    +	 *                              this is an array then a match will be an hook that
    +	 *                              matches at least one pattern.
    +	 *
    +	 * @return array An array of hook names matching the pattern.
    +	 */
    +	protected function get_scheduled_like( $needles ) {
    +		$cron = get_option( 'cron', false );
    +
    +		if ( empty( $cron ) ) {
    +			return [];
    +		}
    +
    +		$needles  = (array) $needles;
    +		$matching = [];
    +
    +		foreach ( $cron as $time ) {
    +			if ( ! is_array( $time ) ) {
    +				continue;
    +			}
    +			foreach ( $time as $hook => $entry ) {
    +				foreach ( $needles as $needle ) {
    +					if ( false !== strpos( $hook, $needle ) ) {
    +						$matching[] = $hook;
    +					}
    +				}
    +			}
    +		}
    +
    +		return $matching;
    +	}
    +}
    diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/Promoter.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/Promoter.php
    new file mode 100644
    index 000000000..f170958df
    --- /dev/null
    +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/Promoter.php
    @@ -0,0 +1,119 @@
    +hook();
    +	}
    +
    +	/**
    +	 * Setup hooks for classes.
    +	 */
    +	private function hook() {
    +		add_action( 'template_redirect', tribe_callback( 'promoter.view', 'display_auth_check_view' ), 10, 0 );
    +		add_action( 'init', tribe_callback( 'promoter.view', 'add_rewrites' ) );
    +
    +		/** @var Tribe__Promoter__PUE $pue */
    +		$pue = tribe( 'promoter.pue' );
    +
    +		// Only add the setting if a promoter key is present.
    +		if ( $pue->has_license_key() ) {
    +			add_action(
    +				'init',
    +				tribe_callback( 'promoter.auth', 'register_setting' )
    +			);
    +		}
    +
    +		// The usage of a high priority so we can push the icon to the end
    +		add_action( 'admin_bar_menu', [ $this, 'add_promoter_logo_on_admin_bar' ], 1000 );
    +		add_action( 'tribe_common_loaded', [ $this, 'add_promoter_assets' ] );
    +	}
    +
    +	/**
    +	 * Add Admin Bar link to the promoter website
    +	 *
    +	 * @since 4.9.2
    +	 * @param $wp_admin_bar
    +	 */
    +	public function add_promoter_logo_on_admin_bar( $wp_admin_bar ) {
    +		/** @var Tribe__Promoter__PUE $pue */
    +		$pue = tribe( 'promoter.pue' );
    +		if ( ! $pue->has_license_key() ) {
    +			return;
    +		}
    +
    +		/**
    +		 * It uses and inline SVG as will provider more flexibility for styling so we can change
    +		 * the fill of the path property of the SVG so we can match the WP installations.
    +		 */
    +		$args = [
    +			'id'    => 'promoter-admin-bar',
    +			'title' => sprintf(
    +				"%s%s",
    +				'',
    +				'Promoter'
    +			),
    +			'href'  => 'https://promoter.theeventscalendar.com/',
    +			'meta'  => [
    +				'target' => '_blank',
    +				'class'  => 'promoter-admin-bar-link',
    +			],
    +		];
    +		$wp_admin_bar->add_node( $args );
    +	}
    +
    +	/**
    +	 * Register assets associated with promoter
    +	 *
    +	 * @since 4.9.2
    +	 */
    +	public function add_promoter_assets() {
    +		tribe_asset(
    +			Tribe__Main::instance(),
    +			'promoter',
    +			'promoter.css',
    +			[],
    +			[ 'wp_enqueue_scripts', 'admin_enqueue_scripts' ],
    +			[
    +				'conditionals' => [ $this, 'should_load_promoter_styles' ],
    +			]
    +		);
    +	}
    +
    +	/**
    +	 * Only load the styles related to promoter if user is logged in and there's a valid license
    +	 * for promoter
    +	 *
    +	 * @since 4.9.2
    +	 *
    +	 * @return bool
    +	 */
    +	public function should_load_promoter_styles() {
    +		return is_user_logged_in() && tribe( 'promoter.pue' )->has_license_key();
    +	}
    +}
    diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/Shortcodes.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/Shortcodes.php
    new file mode 100644
    index 000000000..b952ddaa0
    --- /dev/null
    +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/Shortcodes.php
    @@ -0,0 +1,123 @@
    +container->singleton( Manager::class, Manager::class );
    +		$this->container->singleton(
    +			'shortcode.manager',
    +			function() {
    +				return $this->container->make( Manager::class );
    +			}
    +		);
    +
    +		$this->register_hooks();
    +		$this->register_assets();
    +
    +		$this->container->singleton( static::class, $this );
    +
    +	}
    +
    +	/**
    +	 * Static method wrapper around a filter to allow full deactivation of this provider
    +	 *
    +	 * @since 4.12.0
    +	 *
    +	 * @return boolean If this service provider is active.
    +	 */
    +	public static function is_active() {
    +		/**
    +		 * Allows filtering to deactivate all shortcodes loading.
    +		 *
    +		 * @since 4.12.0
    +		 *
    +		 * @param boolean $is_active If shortcodes should be loaded or not.
    +		 */
    +		return apply_filters( 'tribe_shortcodes_is_active', true );
    +	}
    +
    +	/**
    +	 * Register all the assets associated with this service provider.
    +	 *
    +	 * @since 4.12.0
    +	 */
    +	protected function register_assets() {
    +
    +	}
    +
    +	/**
    +	 * Registers the provider handling all the 1st level filters and actions for this service provider.
    +	 *
    +	 * @since 4.12.0
    +	 */
    +	protected function register_hooks() {
    +		add_action( 'init', [ $this, 'action_add_shortcodes' ], 20 );
    +		add_filter( 'pre_do_shortcode_tag', [ $this, 'filter_pre_do_shortcode_tag' ], 10, 4 );
    +		add_filter( 'do_shortcode_tag', [ $this, 'filter_do_shortcode_tag' ], 10, 4 );
    +	}
    +
    +	/**
    +	 * Adds the new shortcodes, this normally will trigger on `init@P20` due to how we the
    +	 * v1 is added on `init@P10` and we remove them on `init@P15`.
    +	 *
    +	 * It's important to leave gaps on priority for better injection.
    +	 *
    +	 * @since 4.12.0
    +	 */
    +	public function action_add_shortcodes() {
    +		$this->container->make( Manager::class )->add_shortcodes();
    +	}
    +
    +	/**
    +	 * Filters `pre_do_shortcode_tag` to mark that a tribe shortcode is currently being done.
    +	 *
    +	 * @since 4.12.9
    +	 *
    +	 * @param bool|string $return      Short-circuit return value. Either false or the value to replace the shortcode with.
    +	 * @param string      $tag         Shortcode name.
    +	 * @param array       $attr        Shortcode attributes array,
    +	 * @param array       $m           Regular expression match array.
    +	 *
    +	 * @return bool|string Short-circuit return value.
    +	 */
    +	public function filter_pre_do_shortcode_tag( $false, $tag, $attr, $m ) {
    +		return $this->container->make( Manager::class )->filter_pre_do_shortcode_tag( $false, $tag, $attr, $m );
    +	}
    +
    +	/**
    +	 * * Filters `do_shortcode_tag` to mark that a tribe shortcode is complete, and remove it from the current list.
    +	 *
    +	 * @since 4.12.9
    +	 *
    +	 * @param string       $output Shortcode output.
    +	 * @param string       $tag    Shortcode name.
    +	 * @param array|string $attr   Shortcode attributes array or empty string.
    +	 * @param array        $m      Regular expression match array.
    +	 *
    +	 * @return string Shortcode output.
    +	 */
    +	public function filter_do_shortcode_tag( $output, $tag, $attr, $m ) {
    +		return $this->container->make( Manager::class )->filter_do_shortcode_tag( $output, $tag, $attr, $m );
    +	}
    +}
    diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/Tooltip.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/Tooltip.php
    new file mode 100644
    index 000000000..dd5ef6d19
    --- /dev/null
    +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/Tooltip.php
    @@ -0,0 +1,67 @@
    +hook();
    +	}
    +
    +	/**
    +	 * Setup hooks for classes.
    +	 *
    +	 * @since 4.9.8
    +	 */
    +	private function hook() {
    +		add_action( 'tribe_common_loaded', [ $this, 'add_tooltip_assets' ] );
    +	}
    +
    +	/**
    +	 * Register assets associated with tooltip
    +	 *
    +	 * @since 4.9.8
    +	 */
    +	public function add_tooltip_assets() {
    +		$main = \Tribe__Main::instance();
    +
    +		tribe_asset(
    +			$main,
    +			'tribe-tooltip',
    +			'tooltip.css',
    +			[ 'tribe-common-skeleton-style' ],
    +			null,
    +			[
    +				'groups' => 'tribe-tooltip',
    +			]
    +		);
    +
    +		tribe_asset(
    +			$main,
    +			'tribe-tooltip-js',
    +			'tooltip.js',
    +			[ 'jquery', 'tribe-common' ],
    +			null,
    +			[
    +				'groups' => 'tribe-tooltip'
    +			]
    +		);
    +	}
    +}
    diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/Widgets.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/Widgets.php
    new file mode 100644
    index 000000000..2a36d8f63
    --- /dev/null
    +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Service_Providers/Widgets.php
    @@ -0,0 +1,79 @@
    +container->singleton( Manager::class, Manager::class );
    +		$this->container->singleton(
    +			'widget.manager',
    +			function() {
    +				return $this->container->make( Manager::class );
    +			}
    +		);
    +
    +		$this->register_hooks();
    +
    +		$this->container->singleton( static::class, $this );
    +		$this->container->singleton( 'widgets', $this );
    +	}
    +
    +	/**
    +	 * Static method wrapper around a filter to allow full deactivation of this provider.
    +	 *
    +	 * @since 4.12.12
    +	 *
    +	 * @return boolean If this service provider is active.
    +	 */
    +	public static function is_active() {
    +		/**
    +		 * Allows filtering to prevent all Tribe widgets from loading.
    +		 *
    +		 * @since 4.12.12
    +		 *
    +		 * @param boolean $is_active If widgets should be loaded or not.
    +		 */
    +		return apply_filters( 'tribe_widgets_is_active', true );
    +	}
    +
    +	/**
    +	 * Registers the provider handling all the 1st level filters and actions for this service provider.
    +	 *
    +	 * @since 4.12.12
    +	 */
    +	protected function register_hooks() {
    +		add_action( 'widgets_init', [ $this, 'register_widgets_with_wp' ], 20 );
    +	}
    +
    +	/**
    +	 * Adds the new widgets.
    +	 *
    +	 * This triggers on `init@P20` due to how v1 is added on `init@P10` and removed on `init@P15`,
    +	 * as it's important to leave gaps on priority for future flexibility.
    +	 *
    +	 * @since 4.12.12
    +	 */
    +	public function register_widgets_with_wp() {
    +		$this->container->make( Manager::class )->register_widgets_with_wp();
    +	}
    +}
    diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Settings.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Settings.php
    new file mode 100644
    index 000000000..d5880cfd1
    --- /dev/null
    +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Settings.php
    @@ -0,0 +1,819 @@
    + => array(...) ]
    +		 *
    +		 * @var array
    +		 */
    +		protected $fields_for_save = [];
    +
    +		/**
    +		 * An array that contains the fields that are currently being validated.
    +		 *
    +		 * @var array
    +		 */
    +		protected $current_fields = [];
    +
    +		/**
    +		 * Static Singleton Factory Method.
    +		 *
    +		 * @return Tribe__Settings
    +		 */
    +		public static function instance() {
    +			return tribe( 'settings' );
    +		}
    +
    +		/**
    +		 * Class constructor.
    +		 *
    +		 * @return void
    +		 */
    +		public function __construct() {
    +
    +			// Set instance variables.
    +			$this->menuName    = apply_filters( 'tribe_settings_menu_name', esc_html__( 'Events', 'tribe-common' ) );
    +			$this->requiredCap = apply_filters( 'tribe_settings_req_cap', 'manage_options' );
    +			$this->adminSlug   = apply_filters( 'tribe_settings_admin_slug', 'tribe-common' );
    +			$this->help_slug   = apply_filters( 'tribe_settings_help_slug', 'tribe-common-help' );
    +			$this->errors      = get_option( 'tribe_settings_errors', [] );
    +			$this->major_error = get_option( 'tribe_settings_major_error', false );
    +			$this->sent_data   = get_option( 'tribe_settings_sent_data', [] );
    +			$this->validated   = [];
    +			$this->defaultTab  = null;
    +			$this->currentTab  = null;
    +
    +			$this->hook();
    +		}
    +
    +		/**
    +		 * Hooks the actions and filters required for the class to work.
    +		 */
    +		public function hook() {
    +			// Run actions & filters.
    +			add_action( 'admin_init', [ $this, 'initTabs' ] );
    +			add_action( 'tribe_settings_below_tabs', [ $this, 'displayErrors' ] );
    +			add_action( 'tribe_settings_below_tabs', [ $this, 'displaySuccess' ] );
    +		}
    +
    +		/**
    +		 * Determines whether or not the full admin pages should be initialized.
    +		 *
    +		 * @return boolean
    +		 */
    +		public function should_setup_pages() {
    +			// @todo: Deprecate this and update where needed.
    +			return true;
    +		}
    +
    +		/**
    +		 * create the main option page
    +		 *
    +		 * @return void
    +		 */
    +		public function addPage() {
    +			_deprecated_function( __METHOD__, '4.15.0' );
    +		}
    +
    +		/**
    +		 * create the network options page
    +		 *
    +		 * @return void
    +		 */
    +		public function addNetworkPage() {
    +			_deprecated_function( __METHOD__, '4.15.0' );
    +		}
    +
    +		/**
    +		 * Init all the tabs.
    +		 *
    +		 * @return void
    +		 */
    +		public function initTabs() {
    +			$admin_pages = tribe( 'admin.pages' );
    +			$admin_page  = $admin_pages->get_current_page();
    +
    +			if ( empty( $admin_pages->has_tabs( $admin_page ) ) ) {
    +				return;
    +			}
    +
    +			// Load settings tab-specific helpers and enhancements.
    +			Tribe__Admin__Live_Date_Preview::instance();
    +
    +			do_action( 'tribe_settings_do_tabs', $admin_page ); // This is the hook to use to add new tabs.
    +
    +			$this->tabs       = (array) apply_filters( 'tribe_settings_tabs', [], $admin_page );
    +			$this->allTabs    = (array) apply_filters( 'tribe_settings_all_tabs', [], $admin_page );
    +			$this->noSaveTabs = (array) apply_filters( 'tribe_settings_no_save_tabs', [], $admin_page );
    +
    +			if ( is_network_admin() ) {
    +				$this->defaultTab = apply_filters( 'tribe_settings_default_tab_network', 'network', $admin_page );
    +				$current_tab      = ( isset( $_GET['tab'] ) && $_GET['tab'] ) ? esc_attr( $_GET['tab'] ) : $this->defaultTab;
    +				$this->currentTab = apply_filters( 'tribe_settings_current_tab', $current_tab, $admin_page );
    +				$this->url        = $this->get_tab_url( $this->currentTab );
    +			} else {
    +				$tabs_keys        = array_keys( $this->tabs );
    +				$default_tab      = apply_filters( 'tribe_settings_default_tab', 'general', $admin_page );
    +				$this->defaultTab = in_array( $default_tab, $tabs_keys ) ? $default_tab : $tabs_keys[0];
    +				$this->currentTab = apply_filters( 'tribe_settings_current_tab', ( isset( $_GET['tab'] ) && $_GET['tab'] ) ? esc_attr( $_GET['tab'] ) : $this->defaultTab );
    +				$this->url        = $this->get_tab_url( $this->currentTab );
    +			}
    +
    +			$this->fields_for_save = (array) apply_filters( 'tribe_settings_fields', [], $admin_page );
    +			do_action( 'tribe_settings_after_do_tabs', $admin_page );
    +			$this->fields = (array) apply_filters( 'tribe_settings_fields', [], $admin_page );
    +			$this->validate();
    +		}
    +
    +		/**
    +		 * Get the current settings page URL
    +		 *
    +		 * @since 4.15.0
    +		 *
    +		 * @return string The current settings page URL.
    +		 */
    +		public function get_settings_page_url( array $args = [] ) {
    +			$admin_pages = tribe( 'admin.pages' );
    +			$page        = $admin_pages->get_current_page();
    +			$tab         = tribe_get_request_var( 'tab', $this->defaultTab );
    +			$defaults    = [
    +				'page' => $page,
    +				'tab'  => $tab,
    +			];
    +
    +			// Allow the link to be "changed" on the fly.
    +			$args = wp_parse_args( $args, $defaults );
    +
    +			$url = add_query_arg(
    +				$args,
    +				is_network_admin() ? network_admin_url( 'settings.php' ) : admin_url( 'admin.php' )
    +			);
    +
    +			return apply_filters( 'tribe_settings_page_url', $url, $page, $tab );
    +		}
    +
    +		/**
    +		 * Get the settings page title.
    +		 *
    +		 * @since 4.15.0
    +		 *
    +		 * @param string $admin_page The admin page ID.
    +		 * @return string The settings page title.
    +		 */
    +		public function get_page_title( $admin_page ) {
    +			$page_title = sprintf(
    +				// Translators: %s is the name of the menu item.
    +				__( '%s Settings', 'tribe-common' ),
    +				$this->menuName
    +			);
    +
    +			/**
    +			 * Filter the tribe settings page title.
    +			 *
    +			 * @since 4.15.0
    +			 *
    +			 * @param string $page_title The settings page title.
    +			 * @param string $admin_page The admin page ID.
    +			 */
    +			return apply_filters( 'tribe_settings_page_title', $page_title, $admin_page );
    +		}
    +
    +		/**
    +		 * Generate the main option page.
    +		 * includes the view file.
    +		 *
    +		 * @since 4.15.0 Add the current page as parameter for the actions.
    +		 *
    +		 * @return void
    +		 */
    +		public function generatePage() {
    +			$admin_pages = tribe( 'admin.pages' );
    +			$admin_page  = $admin_pages->get_current_page();
    +
    +			do_action( 'tribe_settings_top', $admin_page );
    +			echo '
    '; + echo '

    '; + echo esc_html( $this->get_page_title( $admin_page ) ); + echo '

    '; + do_action( 'tribe_settings_above_tabs' ); + $this->generateTabs( $this->currentTab, $admin_page ); + do_action( 'tribe_settings_below_tabs' ); + do_action( 'tribe_settings_below_tabs_tab_' . $this->currentTab, $admin_page ); + echo '
    '; + do_action( 'tribe_settings_above_form_element' ); + do_action( 'tribe_settings_above_form_element_tab_' . $this->currentTab, $admin_page ); + echo apply_filters( 'tribe_settings_form_element_tab_' . $this->currentTab, '
    ' ); + do_action( 'tribe_settings_before_content' ); + do_action( 'tribe_settings_before_content_tab_' . $this->currentTab ); + do_action( 'tribe_settings_content_tab_' . $this->currentTab ); + if ( ! has_action( 'tribe_settings_content_tab_' . $this->currentTab ) ) { + echo '

    ' . esc_html__( "You've requested a non-existent tab.", 'tribe-common' ) . '

    '; + } + do_action( 'tribe_settings_after_content_tab_' . $this->currentTab ); + do_action( 'tribe_settings_after_content', $this->currentTab ); + if ( has_action( 'tribe_settings_content_tab_' . $this->currentTab ) && ! in_array( $this->currentTab, $this->noSaveTabs ) ) { + wp_nonce_field( 'saving', 'tribe-save-settings' ); + echo '
    '; + echo ''; + echo ''; + } + echo apply_filters( 'tribe_settings_closing_form_element', '
    ' ); + do_action( 'tribe_settings_after_form_element' ); + do_action( 'tribe_settings_after_form_element_tab_' . $this->currentTab, $admin_page ); + echo '
    '; + do_action( 'tribe_settings_after_form_div' ); + echo '
    '; + do_action( 'tribe_settings_bottom' ); + } + + /** + * Generate the tabs in the settings screen. + * + * @return void + */ + public function generateTabs() { + if ( is_array( $this->tabs ) && ! empty( $this->tabs ) ) { + echo ''; + } + } + + /** + * Generate the URL for a tab. + * + * @since 4.15.0 + * + * @param string $tab The tab slug. + * + * @return string $url The URL. + */ + public function get_tab_url( $tab ) { + $admin_pages = tribe( 'admin.pages' ); + $admin_page = $admin_pages->get_current_page(); + $wp_page = is_network_admin() ? network_admin_url( 'settings.php' ) : admin_url( 'admin.php' ); + $url = add_query_arg( + [ + 'page' => $admin_page, + 'tab' => $tab, + ], + $wp_page + ); + + $url = apply_filters( 'tec_settings_tab_url', $url, $admin_page, $tab ); + + return $url; + } + + /** + * validate the settings + * + * @return void + */ + public function validate() { + $admin_pages = tribe( 'admin.pages' ); + $admin_page = $admin_pages->get_current_page(); + + do_action( 'tribe_settings_validate_before_checks', $admin_page ); + + // Check that the right POST && variables are set. + if ( isset( $_POST['tribeSaveSettings'] ) && isset( $_POST['current-settings-tab'] ) ) { + // check permissions + if ( ! current_user_can( AdminPages::get_capability() ) ) { + $this->errors[] = esc_html__( "You don't have permission to do that.", 'tribe-common' ); + $this->major_error = true; + } + + // Check the nonce. + if ( ! wp_verify_nonce( $_POST['tribe-save-settings'], 'saving' ) ) { + $this->errors[] = esc_html__( 'The request was sent insecurely.', 'tribe-common' ); + $this->major_error = true; + } + + // check that the request originated from the current tab. + if ( $_POST['current-settings-tab'] != $this->currentTab ) { + $this->errors[] = esc_html__( "The request wasn't sent from this tab.", 'tribe-common' ); + $this->major_error = true; + } + + // Bail if we have errors. + if ( count( $this->errors ) ) { + remove_action( 'shutdown', [ $this, 'deleteOptions' ] ); + add_option( 'tribe_settings_errors', $this->errors ); + add_option( 'tribe_settings_major_error', $this->major_error ); + wp_redirect( $this->get_settings_page_url() ); + exit; + } + + // Some hooks. + do_action( 'tribe_settings_validate', $admin_page ); + do_action( 'tribe_settings_validate_tab_' . $this->currentTab, $admin_page ); + + // Set the current tab and current fields. + $tab = $this->currentTab; + $fields = $this->current_fields = $this->fields_for_save[ $tab ]; + + if ( is_array( $fields ) ) { + // Loop through the fields and validate them. + foreach ( $fields as $field_id => $field ) { + // Get the value. + $value = ( isset( $_POST[ $field_id ] ) ) ? $_POST[ $field_id ] : null; + $value = apply_filters( 'tribe_settings_validate_field_value', $value, $field_id, $field ); + + // Make sure it has validation set up for it, else do nothing. + if ( + ( ! isset( $field['conditional'] ) || $field['conditional'] ) + && ( ! empty( $field['validation_type'] ) || ! empty( $field['validation_callback'] ) ) + ) { + // Some hooks. + do_action( 'tribe_settings_validate_field', $field_id, $value, $field ); + do_action( 'tribe_settings_validate_field_' . $field_id, $value, $field ); + + // Validate this field. + $validate = new Tribe__Validate( $field_id, $field, $value ); + + if ( isset( $validate->result->error ) ) { + // Uh oh; validation failed. + $this->errors[ $field_id ] = $validate->result->error; + } elseif ( $validate->result->valid ) { + // Validation passed. + $this->validated[ $field_id ] = new stdClass; + $this->validated[ $field_id ]->field = $validate->field; + $this->validated[ $field_id ]->value = $validate->value; + } + } + } + + // Do not generate errors for dependent fields that should not show. + if ( ! empty( $this->errors ) ) { + $keep = array_filter( array_keys( $this->errors ), [ $this, 'dependency_checks' ] ); + $compare = empty( $keep ) ? [] : array_combine( $keep, $keep ); + $this->errors = array_intersect_key( $this->errors, $compare ); + } + + // Run the saving method. + $this->save(); + } + } + } + + /** + * Save the settings. + * + * @since 4.15.0 Add the current page as parameter for the actions. + * + * @return void + */ + public function save() { + $admin_pages = tribe( 'admin.pages' ); + $admin_page = $admin_pages->get_current_page(); + + // Some hooks. + do_action( 'tribe_settings_save', $admin_page ); + do_action( 'tribe_settings_save_tab_' . $this->currentTab, $admin_page ); + + // We'll need this later. + $parent_options = []; + + /** + * loop through each validated option and either + * save it as is or figure out its parent option ID + * (in that case, it's a serialized option array and + * will be saved in the next loop) + */ + if ( ! empty( $this->validated ) ) { + foreach ( $this->validated as $field_id => $validated_field ) { + // Get the value and filter it. + $value = $validated_field->value; + $value = apply_filters( 'tribe_settings_save_field_value', $value, $field_id, $validated_field ); + + // Figure out the parent option [could be set to false] and filter it. + if ( is_network_admin() ) { + $parent_option = ( isset( $validated_field->field['parent_option'] ) ) ? $validated_field->field['parent_option'] : Tribe__Main::OPTIONNAMENETWORK; + } + if ( ! is_network_admin() ) { + $parent_option = ( isset( $validated_field->field['parent_option'] ) ) ? $validated_field->field['parent_option'] : Tribe__Main::OPTIONNAME; + } + + $parent_option = apply_filters( 'tribe_settings_save_field_parent_option', $parent_option, $field_id ); + $network_option = isset( $validated_field->field['network_option'] ) ? (bool) $validated_field->field['network_option'] : false; + + // Some hooks. + do_action( 'tribe_settings_save_field', $field_id, $value, $validated_field ); + do_action( 'tribe_settings_save_field_' . $field_id, $value, $validated_field ); + + if ( ! $parent_option ) { + if ( $network_option || is_network_admin() ) { + update_site_option( $field_id, $value ); + } else { + update_option( $field_id, $value ); + } + } else { + // Set the parent option. + $parent_options[ $parent_option ][ $field_id ] = $value; + } + } + } + + /** + * loop through parent option arrays + * and save them + * NOTE: in the case of the main option Tribe Options, + * this will save using the Tribe__Settings_Manager::set_options method. + */ + foreach ( $parent_options as $option_id => $new_options ) { + // Get the old options. + if ( is_network_admin() ) { + $old_options = (array) get_site_option( $option_id ); + } else { + $old_options = (array) get_option( $option_id ); + } + + // Set the options by parsing old + new and filter that. + $options = apply_filters( 'tribe_settings_save_option_array', wp_parse_args( $new_options, $old_options ), $option_id ); + + if ( $option_id == Tribe__Main::OPTIONNAME ) { + // Save using the Tribe__Settings_Manager method. + Tribe__Settings_Manager::set_options( $options ); + } elseif ( $option_id == Tribe__Main::OPTIONNAMENETWORK ) { + Tribe__Settings_Manager::set_network_options( $options ); + } else { + // Save using regular WP method. + if ( is_network_admin() ) { + update_site_option( $option_id, $options ); + } else { + update_option( $option_id, $options ); + } + } + } + + do_action( 'tribe_settings_after_save', $admin_page ); + do_action( 'tribe_settings_after_save_' . $this->currentTab, $admin_page ); + remove_action( 'shutdown', [ $this, 'deleteOptions' ] ); + add_option( 'tribe_settings_sent_data', $_POST ); + add_option( 'tribe_settings_errors', $this->errors ); + add_option( 'tribe_settings_major_error', $this->major_error ); + wp_redirect( esc_url_raw( add_query_arg( [ 'saved' => true ], $this->get_settings_page_url() ) ) ); + exit; + } + + /** + * Display errors, if any, after saving. + * + * @return void + */ + public function displayErrors() { + // Fetch the errors and filter them. + $errors = (array) apply_filters( 'tribe_settings_display_errors', $this->errors ); + $count = apply_filters( 'tribe_settings_count_errors', count( $errors ) ); + + // Bail if we don't have errors. + if ( ! apply_filters( 'tribe_settings_display_errors_or_not', ( $count > 0 ) ) ) { + return; + } + + $output = '

    '; + $output .= esc_html__( 'Your form had the following errors:', 'tribe-common' ); + $output .= '

      '; + + // Loop through each error. + foreach ( $errors as $error ) { + $output .= '
    • ' . (string) $error . '
    • '; + } + + if ( count( $errors ) ) { + $message = ( isset( $this->major_error ) && $this->major_error ) + ? esc_html__( 'None of your settings were saved. Please try again.' ) + : esc_html( _n( 'The above setting was not saved. Other settings were successfully saved.', 'The above settings were not saved. Other settings were successfully saved.', $count, 'tribe-common' ) ); + } + + $output .= '

    ' . $message . '

    '; + + // Final output, filtered of course. + echo apply_filters( 'tribe_settings_error_message', $output ); + } + + /** + * Display success message after saving. + * + * @return void + */ + public function displaySuccess() { + $errors = (array) apply_filters( 'tribe_settings_display_errors', $this->errors ); + $count = apply_filters( 'tribe_settings_count_errors', count( $errors ) ); + + // Are we coming from the saving place? + if ( isset( $_GET['saved'] ) && ! apply_filters( 'tribe_settings_display_errors_or_not', ( $count > 0 ) ) ) { + // output the filtered message + $message = esc_html__( 'Settings saved.', 'tribe-common' ); + $output = '

    ' . $message . '

    '; + echo apply_filters( 'tribe_settings_success_message', $output, $this->currentTab ); + } + + // Delete Temporary Options After Display Errors and Success. + $this->deleteOptions(); + } + + /** + * Delete temporary options. + * + * @return void + */ + public function deleteOptions() { + delete_option( 'tribe_settings_errors' ); + delete_option( 'tribe_settings_major_error' ); + delete_option( 'tribe_settings_sent_data' ); + } + + /** + * Returns the main admin settings URL. + * + * @return string + */ + public function get_url( array $args = [] ) { + $defaults = [ + 'page' => $this->adminSlug, + 'parent' => self::$parent_page, + ]; + + // Allow the link to be "changed" on the fly + $args = wp_parse_args( $args, $defaults ); + + $url = admin_url( $args['parent'] ); + + // keep the resulting URL args clean + unset( $args['parent'] ); + + return apply_filters( 'tribe_settings_url', add_query_arg( $args, $url ), $args, $url ); + } + + /** + * The "slug" used for adding submenu pages + * + * @return string + */ + public function get_parent_slug() { + $slug = self::$parent_page; + + // If we don't have an event post type, then we can just use the tribe-common slug. + if ( 'edit.php' === $slug || 'admin.php?page=tribe-common' === $slug ) { + $slug = self::$parent_slug; + } + + return $slug; + } + + /** + * @return string + */ + public function get_help_slug() { + return $this->help_slug; + } + + /** + * Determines whether or not the network admin pages should be initialized. + * + * When running in parallel with TEC 3.12.4, TEC should be relied on to handle the admin screens + * that version of TEC (and lower) is tribe-common ignorant. Therefore, tribe-common has to be + * the smarter, more lenient codebase. + * Beyond this at least one of the two "root" plugins (The Events Calendar and Event Tickets) + * should be network activated to add the page. + * + * @return boolean + */ + public function should_setup_network_pages() { + $root_plugin_is_mu_activated = array_sum( array_map( 'is_plugin_active_for_network', $this->root_plugins ) ) >= 1; + + if ( ! $root_plugin_is_mu_activated ) { + return false; + } + + if ( ! class_exists( 'Tribe__Events__Main' ) ) { + return true; + } + + if ( version_compare( Tribe__Events__Main::VERSION, '4.0beta', '>=' ) ) { + return true; + } + + return false; + + } + + /** + * Sets what `common` should consider root plugins. + * + * @param array $root_plugins An array of plugins in the `/` format. + */ + public function set_root_plugins( array $root_plugins ) { + $this->root_plugins = $root_plugins; + } + + /** + * Whether the specified field dependency condition is valid or not depending on + * its parent field value. + * + * @since 4.7.7 + * + * @param string $field_id The id of the field that might be removed. + * + * @return bool `true` if the field dependency condition is valid, `false` if the field + * dependency condition is not valid. + */ + protected function dependency_checks( $field_id ) { + $does_not_exist = ! array_key_exists( $field_id, $this->current_fields ); + + if ( $does_not_exist ) { + return false; + } + + $has_no_dependency = ! isset( $this->current_fields[ $field_id ]['validate_if'] ); + + if ( $has_no_dependency ) { + return true; + } + + $condition = $this->current_fields[ $field_id ]['validate_if']; + + if ( $condition instanceof Tribe__Field_Conditional ) { + $parent_field = Tribe__Utils__Array::get( $this->validated, $condition->depends_on(), null ); + + return $condition->check( $parent_field->value, $this->current_fields ); + } + + return is_callable( $condition ) + ? call_user_func( $condition, $this->current_fields ) + : true == $condition; + } + } // end class +} // endif class_exists diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Settings_Manager.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Settings_Manager.php new file mode 100644 index 000000000..74f166cf3 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Settings_Manager.php @@ -0,0 +1,355 @@ +add_hooks(); + + // Load multisite defaults. + if ( is_multisite() ) { + $tribe_events_mu_defaults = []; + if ( file_exists( WP_CONTENT_DIR . '/tribe-events-mu-defaults.php' ) ) { + require_once WP_CONTENT_DIR . '/tribe-events-mu-defaults.php'; + } + self::$tribe_events_mu_defaults = apply_filters( 'tribe_events_mu_defaults', $tribe_events_mu_defaults ); + } + } + + public function add_hooks() { + // option pages + add_action( '_network_admin_menu', [ $this, 'init_options' ] ); + add_action( '_admin_menu', [ $this, 'init_options' ] ); + + add_action( 'tribe_settings_do_tabs', [ $this, 'do_setting_tabs' ] ); + add_action( 'tribe_settings_validate_tab_network', [ $this, 'save_all_tabs_hidden' ] ); + add_action( 'updated_option', [ $this, 'update_options_cache' ], 10, 3 ); + } + + /** + * For performance reasons our options are saved in memory, but we need to make sure we update it when WordPress + * updates the variable directly. + * + * @since 4.11.0 + * + * @param string $option Name of the updated option. + * @param mixed $old_value The old option value. + * @param mixed $value The new option value. + * + * @return void + */ + public function update_options_cache( $option, $old_value, $value ) { + // Bail when not our option. + if ( Tribe__Main::OPTIONNAME !== $option ) { + return; + } + + tribe_set_var( self::OPTION_CACHE_VAR_NAME, $value ); + } + + /** + * Init the settings API and add a hook to add your own setting tabs + * + * @return void + */ + public function init_options() { + Tribe__Settings::instance(); + } + + /** + * Create setting tabs + * + * @return void + */ + public function do_setting_tabs() { + // Make sure Thickbox is available regardless of which admin page we're on + add_thickbox(); + + $this->do_licenses_tab(); + } + + /** + * Get all options for the Events Calendar + * + * @return array of options + */ + public static function get_options() { + $options = tribe_get_var( self::OPTION_CACHE_VAR_NAME, [] ); + + if ( empty( $options ) ) { + $options = (array) get_option( Tribe__Main::OPTIONNAME, [] ); + + tribe_set_var( self::OPTION_CACHE_VAR_NAME, $options ); + } + + return $options; + } + + /** + * Get value for a specific option + * + * @param string $option_name name of option + * @param string $default default value + * + * @return mixed results of option query + */ + public static function get_option( $option_name, $default = '' ) { + if ( ! $option_name ) { + return null; + } + $options = static::get_options(); + + $option = $default; + if ( array_key_exists( $option_name, $options ) ) { + $option = $options[ $option_name ]; + } elseif ( is_multisite() && isset( self::$tribe_events_mu_defaults ) && is_array( self::$tribe_events_mu_defaults ) && in_array( $option_name, array_keys( self::$tribe_events_mu_defaults ) ) ) { + $option = self::$tribe_events_mu_defaults[ $option_name ]; + } + + return apply_filters( 'tribe_get_single_option', $option, $default, $option_name ); + } + + /** + * Saves the options for the plugin + * + * @param array $options formatted the same as from get_options() + * @param bool $apply_filters + * + * @return bool + */ + public static function set_options( $options, $apply_filters = true ) { + if ( ! is_array( $options ) ) { + return false; + } + if ( true === $apply_filters ) { + $options = apply_filters( 'tribe-events-save-options', $options ); + } + $updated = update_option( Tribe__Main::OPTIONNAME, $options ); + + if ( $updated ) { + tribe_set_var( self::OPTION_CACHE_VAR_NAME, $options ); + } + + return $updated; + } + + /** + * Set an option + * + * @param string $name The option key or 'name'. + * @param mixed $value The value we want to set. + * + * @return bool + */ + public static function set_option( $name, $value ) { + $options = self::get_options(); + $options[ $name ] = $value; + + return static::set_options( $options ); + } + + /** + * Remove an option. Actually remove (unset), as opposed to setting to null/empty string/etc. + * + * @since 4.14.13 + * + * @param string $name The option key or 'name'. + * + * @return bool + */ + public static function remove_option( $name ) { + $options = self::get_options(); + unset( $options[ $name ] ); + + return static::set_options( $options ); + } + + /** + * Get all network options for the Events Calendar + * + * @return array of options + * @TODO add force option, implement in setNetworkOptions + */ + public static function get_network_options() { + if ( ! isset( self::$network_options ) ) { + $options = get_site_option( Tribe__Main::OPTIONNAMENETWORK, [] ); + self::$network_options = apply_filters( 'tribe_get_network_options', $options ); + } + + return self::$network_options; + } + + /** + * Get value for a specific network option + * + * @param string $option_name name of option + * @param string $default default value + * + * @return mixed results of option query + */ + public static function get_network_option( $option_name, $default = '' ) { + if ( ! $option_name ) { + return null; + } + + if ( ! isset( self::$network_options ) ) { + self::get_network_options(); + } + + if ( isset( self::$network_options[ $option_name ] ) ) { + $option = self::$network_options[ $option_name ]; + } else { + $option = $default; + } + + return apply_filters( 'tribe_get_single_network_option', $option, $default ); + } + + /** + * Saves the network options for the plugin + * + * @param array $options formatted the same as from get_options() + * @param bool $apply_filters + * + * @return void + */ + public static function set_network_options( $options, $apply_filters = true ) { + if ( ! is_array( $options ) ) { + return; + } + + if ( + isset( $_POST['tribeSaveSettings'] ) + && isset( $_POST['current-settings-tab'] ) + ) { + $options['hideSettingsTabs'] = tribe_get_request_var( 'hideSettingsTabs', [] ); + } + + $admin_pages = tribe( 'admin.pages' ); + $admin_page = $admin_pages->get_current_page(); + + if ( true === $apply_filters ) { + $options = apply_filters( 'tribe-events-save-network-options', $options, $admin_page ); + } + + if ( update_site_option( Tribe__Main::OPTIONNAMENETWORK, $options ) ) { + self::$network_options = apply_filters( 'tribe_get_network_options', $options ); + } else { + self::$network_options = self::get_network_options(); + } + } + + /** + * Add the network admin options page + * + * @return void + */ + public static function add_network_options_page() { + _deprecated_function( __METHOD__, '4.15.0' ); + } + + /** + * Render network admin options view + * + * @return void + */ + public static function do_network_settings_tab() { + _deprecated_function( __METHOD__, '4.15.0' ); + } + + /** + * Registers the license key management tab in the Events > Settings screen, + * only if premium addons are detected. + */ + protected function do_licenses_tab() { + $show_tab = ( current_user_can( 'activate_plugins' ) && $this->have_addons() ); + + /** + * Provides an oppotunity to override the decision to show or hide the licenses tab + * + * Normally it will only show if the current user has the "activate_plugins" capability + * and there are some currently-activated premium plugins. + * + * @var bool + */ + if ( ! apply_filters( 'tribe_events_show_licenses_tab', $show_tab ) ) { + return; + } + + /** + * @var $licenses_tab + */ + include Tribe__Main::instance()->plugin_path . 'src/admin-views/tribe-options-licenses.php'; + + /** + * Allows the fields displayed in the licenses tab to be modified. + * + * @var array + */ + $license_fields = apply_filters( 'tribe_license_fields', $licenses_tab ); + + new Tribe__Settings_Tab( 'licenses', esc_html__( 'Licenses', 'tribe-common' ), [ + 'priority' => '40', + 'fields' => $license_fields, + 'network_admin' => is_network_admin() ? true : false, + ] ); + } + + /** + * Create the help tab + */ + public function do_help_tab() { + /** + * Include Help tab Assets here + */ + + include_once Tribe__Main::instance()->plugin_path . 'src/admin-views/help.php'; + } + + /** + * Add help menu item to the admin (unless blocked via network admin settings). + * + * @deprecated 5.0.2 + */ + public function add_help_admin_menu_item() { + _deprecated_function( __METHOD__, '5.0.2', 'Now handled by Tribe\Events\Admin\Settings::add_admin_pages()' ); + } + + /** + * Tries to discover if licensable addons are activated on the same site. + * + * @return bool + */ + protected function have_addons() { + $addons = apply_filters( 'tribe_licensable_addons', [] ); + + return ! empty( $addons ); + } + + /** + * Save hidden tabs + * + * @return void + */ + public function save_all_tabs_hidden() { + $all_tabs_keys = array_keys( apply_filters( 'tribe_settings_all_tabs', [] ) ); + + $network_options = (array) get_site_option( Tribe__Main::OPTIONNAMENETWORK ); + + $this->set_network_options( $network_options ); + } + + /** + * Static Singleton Factory Method + * + * @return Tribe__Settings_Manager + */ + public static function instance() { + return tribe( 'settings.manager' ); + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Settings_Tab.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Settings_Tab.php new file mode 100644 index 000000000..efbd0bc4d --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Settings_Tab.php @@ -0,0 +1,230 @@ +defaults = [ + 'fields' => [], + 'priority' => 50, + 'show_save' => true, + 'display_callback' => false, + 'network_admin' => false, + ]; + + // parse args with defaults + $this->args = wp_parse_args( $args, $this->defaults ); + + // set each instance variable and filter + $this->id = apply_filters( 'tribe_settings_tab_id', $id ); + $this->name = apply_filters( 'tribe_settings_tab_name', $name ); + foreach ( $this->defaults as $key => $value ) { + $this->{$key} = apply_filters( 'tribe_settings_tab_' . $key, $this->args[ $key ], $id ); + } + + // run actions & filters + if ( ! $this->network_admin ) { + add_filter( 'tribe_settings_all_tabs', [ $this, 'addAllTabs' ] ); + } + add_filter( 'tribe_settings_tabs', [ $this, 'addTab' ], $this->priority ); + } + + /** + * filters the tabs array from Tribe__Settings + * and adds the current tab to it + * does not add a tab if it's empty + * + * @param array $tabs the $tabs from Tribe__Settings + * + * @return array $tabs the filtered tabs + */ + public function addTab( $tabs ) { + $hideSettingsTabs = Tribe__Settings_Manager::get_network_option( 'hideSettingsTabs', [] ); + if ( ( isset( $this->fields ) || has_action( 'tribe_settings_content_tab_' . $this->id ) ) && ( empty( $hideSettingsTabs ) || ! in_array( $this->id, $hideSettingsTabs ) ) ) { + if ( ( is_network_admin() && $this->args['network_admin'] ) || ( ! is_network_admin() && ! $this->args['network_admin'] ) ) { + $tabs[ $this->id ] = $this->name; + add_filter( 'tribe_settings_fields', [ $this, 'addFields' ] ); + add_filter( 'tribe_settings_no_save_tabs', [ $this, 'showSaveTab' ] ); + add_filter( 'tribe_settings_content_tab_' . $this->id, [ $this, 'doContent' ] ); + } + } + + return $tabs; + } + + /** + * Adds this tab to the list of total tabs, even if it is not displayed. + * + * @param array $allTabs All the tabs from Tribe__Settings. + * + * @return array $allTabs All the tabs. + */ + public function addAllTabs( $allTabs ) { + $allTabs[ $this->id ] = $this->name; + + return $allTabs; + } + + + /** + * filters the fields array from Tribe__Settings + * and adds the current tab's fields to it + * + * @param array $field the $fields from Tribe__Settings + * + * @return array $fields the filtered fields + */ + public function addFields( $fields ) { + if ( ! empty ( $this->fields ) ) { + $fields[ $this->id ] = $this->fields; + } elseif ( has_action( 'tribe_settings_content_tab_' . $this->id ) ) { + $fields[ $this->id ] = $this->fields = [ 0 => null ]; // just to trick it + } + + return $fields; + } + + /** + * sets whether the current tab should show the save + * button or not + * + * @param array $noSaveTabs the $noSaveTabs from Tribe__Settings + * + * @return array $noSaveTabs the filtered non saving tabs + */ + public function showSaveTab( $noSaveTabs ) { + if ( ! $this->show_save || empty( $this->fields ) ) { + $noSaveTabs[ $this->id ] = $this->id; + } + + return $noSaveTabs; + } + + /** + * Displays the content for the tab. + * + * @return void + */ + public function doContent() { + if ( $this->display_callback && is_callable( $this->display_callback ) ) { + call_user_func( $this->display_callback ); + + return; + } + + $sent_data = get_option( 'tribe_settings_sent_data', [] ); + + if ( is_array( $this->fields ) && ! empty( $this->fields ) ) { + foreach ( $this->fields as $key => $field ) { + if ( isset( $sent_data[ $key ] ) ) { + // If we just saved [or attempted to], get the value that was input. + $value = $sent_data[ $key ]; + } else { + // Some options should always be stored at network level + $network_option = isset( $field['network_option'] ) ? (bool) $field['network_option'] : false; + + if ( is_network_admin() ) { + $parent_option = ( isset( $field['parent_option'] ) ) ? $field['parent_option'] : Tribe__Main::OPTIONNAMENETWORK; + } + if ( ! is_network_admin() ) { + $parent_option = ( isset( $field['parent_option'] ) ) ? $field['parent_option'] : Tribe__Main::OPTIONNAME; + } + // get the field's parent_option in order to later get the field's value + $parent_option = apply_filters( 'tribe_settings_do_content_parent_option', $parent_option, $key ); + $default = ( isset( $field['default'] ) ) ? $field['default'] : null; + $default = apply_filters( 'tribe_settings_field_default', $default, $field ); + + if ( ! $parent_option ) { + // no parent option, get the straight up value + if ( $network_option || is_network_admin() ) { + $value = get_site_option( $key, $default ); + } else { + $value = get_option( $key, $default ); + } + } else { + // there's a parent option + if ( $parent_option == Tribe__Main::OPTIONNAME ) { + // get the options from Tribe__Settings_Manager if we're getting the main array + $value = Tribe__Settings_Manager::get_option( $key, $default ); + } elseif ( $parent_option == Tribe__Main::OPTIONNAMENETWORK ) { + $value = Tribe__Settings_Manager::get_network_option( $key, $default ); + } else { + // else, get the parent option normally + if ( is_network_admin() ) { + $options = (array) get_site_option( $parent_option ); + } else { + $options = (array) get_option( $parent_option ); + } + $value = ( isset( $options[ $key ] ) ) ? $options[ $key ] : $default; + } + } + } + + // escape the value for display + if ( ! empty( $field['esc_display'] ) && function_exists( $field['esc_display'] ) ) { + $value = $field['esc_display']( $value ); + } elseif ( is_string( $value ) ) { + $value = esc_attr( stripslashes( $value ) ); + } + + // filter the value + $value = apply_filters( 'tribe_settings_get_option_value_pre_display', $value, $key, $field ); + + // create the field + new Tribe__Field( $key, $field, $value ); + } + } else { + // no fields setup for this tab yet + echo '

    ' . esc_html__( 'There are no fields set up for this tab yet.', 'tribe-common' ) . '

    '; + } + } + + } // end class +} // endif class_exists diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Shortcode/Manager.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Shortcode/Manager.php new file mode 100644 index 000000000..0dabc730b --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Shortcode/Manager.php @@ -0,0 +1,182 @@ + => ]` + */ + public function get_registered_shortcodes() { + $shortcodes = []; + + /** + * Allow the registering of shortcodes into the our Tribe plugins. + * + * @since 4.12.0 + * + * @var array An associative array of shortcodes in the shape `[ => ]`. + */ + $shortcodes = apply_filters( 'tribe_shortcodes', $shortcodes ); + + return $shortcodes; + } + + /** + * Verifies if a given shortcode slug is registered for handling. + * + * @since 4.12.0 + * + * @param string $slug Which slug we are checking if is registered. + * + * @return bool Whether a shortcode is registered or not. + */ + public function is_shortcode_registered( $slug ) { + $registered_shortcodes = $this->get_registered_shortcodes(); + return isset( $registered_shortcodes[ $slug ] ); + } + + /** + * Verifies if a given shortcode class name is registered for handling. + * + * @since 4.12.0 + * + * @param string $class_name Which class name we are checking if is registered. + * + * @return bool Whether a shortcode is registered, by class. + */ + public function is_shortcode_registered_by_class( $class_name ) { + $registered_shortcodes = $this->get_registered_shortcodes(); + return in_array( $class_name, $registered_shortcodes ); + } + + /** + * Add new shortcodes handler to catch the correct strings. + * + * @since 4.12.0 + */ + public function add_shortcodes() { + $registered_shortcodes = $this->get_registered_shortcodes(); + + // Add to WordPress all of the registered Shortcodes + foreach ( $registered_shortcodes as $shortcode => $class_name ) { + add_shortcode( $shortcode, [ $this, 'render_shortcode' ] ); + } + } + + /** + * Makes sure we are correctly handling the Shortcodes we manage. + * + * @since 4.12.0 + * + * @param array $arguments Set of arguments passed to the Shortcode at hand. + * @param string $content Contents passed to the shortcode, inside of the open and close brackets. + * @param string $shortcode Which shortcode tag are we handling here. + * + * @return string The rendered shortcode HTML. + */ + public function render_shortcode( $arguments, $content, $shortcode ) { + $registered_shortcodes = $this->get_registered_shortcodes(); + + // Bail when we try to handle an unregistered shortcode (shouldn't happen) + if ( ! $this->is_shortcode_registered( $shortcode ) ) { + return false; + } + + /** @var Shortcode_Interface $instance */ + $instance = new $registered_shortcodes[ $shortcode ]; + $instance->setup( $arguments, $content ); + + return $instance->get_html(); + } + + /** + * Filter `pre_do_shortcode_tag` to add the current shortcode. + * + * @since 4.12.9 + * + * @param bool|string $return Short-circuit return value. Either false or the value to replace the shortcode with. + * @param string $tag Shortcode name. + * @param array $attr Shortcode attributes array, + * @param array $m Regular expression match array. + * + * @return bool|string Short-circuit return value. + */ + public function filter_pre_do_shortcode_tag( $return, $tag, $attr, $m ) { + if ( ! $this->is_shortcode_registered( $tag ) ) { + return $return; + } + + // Add to the doing shortcode. + $this->current_shortcode[] = $tag; + + return $return; + } + + /** + * Filter `do_shortcode_tag` to remove the shortcode from the `$tribe_current_shortcode` list. + * + * @since 4.12.9 + * + * @param string $output Shortcode output. + * @param string $tag Shortcode name. + * @param array|string $attr Shortcode attributes array or empty string. + * @param array $m Regular expression match array. + * + * @return string Shortcode output. + */ + public function filter_do_shortcode_tag( $output, $tag, $attr, $m ) { + if ( ! $this->is_shortcode_registered( $tag ) ) { + return $output; + } + + if ( isset( $this->current_shortcode[ $tag ] ) ) { + unset( $this->current_shortcode[ $tag ] ); + } + + return $output; + } + + /** + * Check if a shortcode is being done. + * + * @since 4.12.9 + * + * @param null|string $tag The shortcode tag name, or null to check if doing any shortcode. + * + * @return bool If the shortcode is being done or not. + */ + public function is_doing_shortcode( $tag = null ) { + if ( null === $tag ) { + return ! empty( $this->current_shortcode ); + } + + return in_array( $tag, $this->current_shortcode, true ); + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Shortcode/Shortcode_Abstract.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Shortcode/Shortcode_Abstract.php new file mode 100644 index 000000000..d855760fb --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Shortcode/Shortcode_Abstract.php @@ -0,0 +1,254 @@ + 'canonical', 'from' => 'to', 'that' => 'becomes_this' ] + * Example shortcode usage: [some_tag alias=17 to='Fred'] will be parsed as [some_tag canonical=17 to='Fred'] + * + * @since 4.12.2 + * + * @var array + */ + protected $aliased_arguments = []; + + /** + * Array of callbacks for arguments validation. + * + * @since 4.12.0 + * + * @var array + */ + protected $validate_arguments_map = []; + + /** + * Arguments of the current shortcode. + * + * @since 4.12.0 + * + * @var array + */ + protected $arguments; + + /** + * Content of the current shortcode. + * + * @since 4.12.0 + * + * @var string + */ + protected $content; + + /** + * {@inheritDoc} + */ + public function setup( $arguments, $content ) { + $this->arguments = $this->parse_arguments( (array) $arguments ); + $this->content = $content; + } + + /** + * {@inheritDoc} + */ + public function set_aliased_arguments( array $alias_map ) { + $this->aliased_arguments = Arr::filter_to_flat_scalar_associative_array( (array) $alias_map ); + } + + /** + * {@inheritDoc} + */ + public function get_aliased_arguments() { + return $this->aliased_arguments; + } + + /** + * {@inheritDoc} + */ + public function parse_arguments( array $arguments ) { + $arguments = Arr::parse_associative_array_alias( (array) $arguments, (array) $this->get_aliased_arguments() ); + $arguments = shortcode_atts( $this->get_default_arguments(), $arguments, $this->slug ); + + return $this->validate_arguments( $arguments ); + } + + /** + * {@inheritDoc} + */ + public function validate_arguments( array $arguments ) { + $validate_arguments_map = $this->filter_validated_arguments_map( $this->get_validated_arguments_map() ); + foreach ( $validate_arguments_map as $key => $callback ) { + $arguments[ $key ] = $callback( isset( $arguments[ $key ] ) ? $arguments[ $key ] : null ); + } + + return $arguments; + } + + /** + * {@inheritDoc} + */ + public function get_registration_slug() { + return $this->slug; + } + + /** + * {@inheritDoc} + */ + public function get_validated_arguments_map() { + return $this->validate_arguments_map; + } + + /** + * {@inheritDoc} + */ + public function filter_validated_arguments_map( $validate_arguments_map ) { + /** + * Applies a filter to instance arguments validation callbacks. + * + * @since 4.12.0 + * + * @param array $validate_arguments_map Current set of callbacks for arguments. + * @param static $instance Which instance of shortcode we are dealing with. + */ + $validate_arguments_map = apply_filters( 'tribe_shortcode_validate_arguments_map', $validate_arguments_map, $this ); + + $registration_slug = $this->get_registration_slug(); + + /** + * Applies a filter to instance arguments validation callbacks based on the registration slug of the shortcode. + * + * @since 4.12.0 + * + * @param array $validate_arguments_map Current set of callbacks for arguments. + * @param static $instance Which instance of shortcode we are dealing with. + */ + $validate_arguments_map = apply_filters( "tribe__shortcode_{$registration_slug}_validate_arguments_map", $validate_arguments_map, $this ); + + return $validate_arguments_map; + } + + /** + * {@inheritDoc} + */ + public function get_arguments() { + /** + * Applies a filter to instance arguments. + * + * @since 4.12.0 + * + * @param array $arguments Current set of arguments. + * @param static $instance Which instance of shortcode we are dealing with. + */ + $arguments = apply_filters( 'tribe_shortcode_arguments', $this->arguments, $this ); + + $registration_slug = $this->get_registration_slug(); + + /** + * Applies a filter to instance arguments based on the registration slug of the shortcode. + * + * @since 4.12.0 + * + * @param array $arguments Current set of arguments. + * @param static $instance Which instance of shortcode we are dealing with. + */ + $arguments = apply_filters( "tribe_shortcode_{$registration_slug}_arguments", $arguments, $this ); + + return $arguments; + } + + /** + * {@inheritDoc} + */ + public function get_argument( $index, $default = null ) { + $arguments = $this->get_arguments(); + $argument = Arr::get( $arguments, $index, $default ); + + /** + * Applies a filter to a specific shortcode argument, catch all for all shortcodes. + * + * @since 4.12.0 + * + * @param mixed $argument The argument. + * @param array $index Which index we indent to fetch from the arguments. + * @param array $default Default value if it doesn't exist. + * @param static $instance Which instance of shortcode we are dealing with. + */ + $argument = apply_filters( 'tribe_shortcode_argument', $argument, $index, $default, $this ); + + $registration_slug = $this->get_registration_slug(); + + /** + * Applies a filter to a specific shortcode argument, to a particular registration slug. + * + * @since 4.12.0 + * + * @param mixed $argument The argument value. + * @param array $index Which index we indent to fetch from the arguments. + * @param array $default Default value if it doesn't exist. + * @param static $instance Which instance of shortcode we are dealing with. + */ + $argument = apply_filters( "tribe_shortcode_{$registration_slug}_argument", $argument, $index, $default, $this ); + + return $argument; + } + + /** + * {@inheritDoc} + */ + public function get_default_arguments() { + /** + * Applies a filter to instance default arguments. + * + * @since 4.12.0 + * + * @param array $default_arguments Current set of default arguments. + * @param static $instance Which instance of shortcode we are dealing with. + */ + $default_arguments = apply_filters( 'tribe_shortcode_default_arguments', $this->default_arguments, $this ); + + $registration_slug = $this->get_registration_slug(); + + /** + * Applies a filter to instance default arguments based on the registration slug of the shortcode. + * + * @since 4.12.0 + * + * @param array $default_arguments Current set of default arguments. + * @param static $instance Which instance of shortcode we are dealing with. + */ + $default_arguments = apply_filters( "tribe_shortcode_{$registration_slug}_default_arguments", $default_arguments, $this ); + + return $default_arguments; + } + +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Shortcode/Shortcode_Interface.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Shortcode/Shortcode_Interface.php new file mode 100644 index 000000000..ca3e77a7f --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Shortcode/Shortcode_Interface.php @@ -0,0 +1,125 @@ + 'canonical', 'from' => 'to', 'that' => 'becomes_this' ] + */ + public function set_aliased_arguments( array $alias_map ); + + /** + * Gets the aliased arguments array. + * + * @since 4.12.2 + * + * @return array The associative array map of aliases and their canonical arguments. + */ + public function get_aliased_arguments(); + + /** + * Returns the arguments for the shortcode parsed correctly with defaults applied. + * + * @since 4.12.0 + * + * @param array $arguments Set of arguments passed to the Shortcode at hand. + * + * @return array The parsed shortcode arguments map. + */ + public function parse_arguments( array $arguments ); + + /** + * Returns the array of arguments for this shortcode after applying the validation callbacks. + * + * @since 4.12.0 + * + * @param array $arguments Set of arguments passed to the Shortcode at hand. + * + * @return array The validated shortcode arguments map. + */ + public function validate_arguments( array $arguments ); + + /** + * Returns the array of callbacks for this shortcode's arguments. + * + * @since 4.12.0 + * + * @return array A map of the shortcode arguments that have survived validation. + */ + public function get_validated_arguments_map(); + + /** + * Returns a shortcode default arguments. + * + * @since 4.12.0 + * + * @return array The shortcode default arguments map. + */ + public function get_default_arguments(); + + /** + * Returns a shortcode arguments after been parsed. + * + * @since 4.12.0 + * + * @return array The shortcode arguments, as set by the user in the shortcode string. + */ + public function get_arguments(); + + /** + * Returns a shortcode argument after it has been parsed. + * + * @since 4.12.0 + * + * @param array|string $index Which index we indent to fetch from the arguments. + * @param array $default Default value if it doesn't exist. + * + * @uses Tribe__Utils__Array::get For index fetching and Default. + * + * @return mixed Value for the Index passed as the first argument. + */ + public function get_argument( $index, $default = null ); + + /** + * Returns a shortcode's HTML. + * + * @since 4.12.0 + * + * @return string The shortcode rendered HTML code. + */ + public function get_html(); +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Shortcode/Utils.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Shortcode/Utils.php new file mode 100644 index 000000000..9584f53df --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Shortcode/Utils.php @@ -0,0 +1,45 @@ + $settings Widget settings. + * @param array $allowed Allowed settings for shortcode. + * + * @return string Shortcode attributes string. + */ + public static function get_attributes_string( $settings, $allowed = [] ) { + $settings_string = ''; + + $allowed = array_flip( $allowed ); + + foreach ( $settings as $key => $value ) { + if ( ! empty( $allowed ) && ! isset( $allowed[ $key ] ) ) { + continue; + } + + $key = esc_attr( $key ); + + if ( is_array( $value ) ) { + $value = implode( ', ', $value ); + } + + $value = esc_attr( $value ); + + $settings_string .= " {$key}=\"{$value}\""; + } + + return $settings_string; + } +} \ No newline at end of file diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Simple_Table.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Simple_Table.php new file mode 100644 index 000000000..4c3e2ec08 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Simple_Table.php @@ -0,0 +1,166 @@ + 'tribe_table', 'border' => '0' ); + */ + public $table_attributes; + public $tr_attributes; + public $th_attributes; + public $td_attributes; + + /** + * Will HTML escape all table cells + * + * @var bool + */ + public $html_escape_td_values = true; + + /** + * Tribe__Simple_Table constructor. + * + * @param array $tbody Multidimension array containing table rows/columns + * @param array $thead Single dimension array containing table headings + */ + public function __construct( $tbody, $thead = [] ) { + $this->thead = $thead; + $this->tbody = $tbody; + } + + /** + * @param bool $vertical Whether heading appears vertically (above) data or horizontally (to the side) + * + * @return string HTML table + */ + public function output_table( $vertical = true ) { + + if ( $vertical ) { + return $this->output_table_vertical(); + } else { + return $this->output_table_horizontal(); + } + } + + /** + * Outputs table with heading above data + * + * @return string HTML table + */ + private function output_table_vertical() { + + $table_contents = ''; + + // Create thead + if ( ! empty( $this->thead ) ) { + foreach ( $this->thead as $th ) { + $table_contents .= $this->output_element( 'th', $th, $this->th_attributes ); + } + $table_contents = $this->output_element( 'tr', $table_contents, $this->tr_attributes ); + } + + // Create tbody + foreach ( $this->tbody as $tr ) { + $tr_str = ''; + + foreach ( $tr as $td ) { + $tr_str .= $this->output_element( 'td', $td, $this->td_attributes ); + } + + $table_contents .= $this->output_element( 'tr', $tr_str, $this->tr_attributes ); + } + + // Wrap it all up in a table + $output = $this->output_element( 'table', $table_contents, $this->table_attributes ); + + return $output; + } + + /** + * Outputs table with heading to the left of the data + * + * @return string HTML table + */ + private function output_table_horizontal() { + + $table_contents = ''; + + // Finds the table row with the most columns + $max_col = isset( $this->thead ) ? count( $this->thead ) : 1; + foreach ( $this->tbody as $table_item ) { + if ( $max_col < count( $table_item ) ) { + $max_col = count( $table_item ); + } + } + + // Create table rows + for ( $i = 0; $i < $max_col; $i++ ) { + $tr_contents = ''; + + // row heading + if ( isset( $this->thead[ $i ] ) ) { + $tr_contents .= $this->output_element( 'th', $this->thead[ $i ], $this->th_attributes ); + } + + // columns + foreach ( $this->tbody as $table_item ) { + if ( ! isset( $table_item[ $i ] ) ) { + continue; + } + $tr_contents .= $this->output_element( 'td', $table_item[ $i ], $this->td_attributes ); + } + + $table_contents .= $this->output_element( 'tr', $tr_contents, $this->tr_attributes ); + } + + // Wrap it all up in a table + $output = $this->output_element( 'table', $table_contents, $this->table_attributes ); + + return $output; + } + + /** + * Outputs an HTML element, mostly useful for elements that have attributes + * + * @param string $html_tag HTML element name. Example: 'table' + * @param string $data Text/HTML contained inside of the element + * @param array $attributes HTML attributes for element + * + * @return string HTML element + */ + private function output_element( $html_tag, $data = null, $attributes = [] ) { + $output = '<' . tag_escape( $html_tag ); + + if ( ! empty( $attributes ) ) { + foreach ( $attributes as $att => $val ) { + $output .= ' ' . $att . '="' . esc_attr( $val ) . '"'; + } + } + + if ( is_string( $data ) ) { + $output .= '>'; + $output .= ( 'td' === $html_tag && $this->html_escape_td_values ) ? esc_html( $data ) : $data; + $output .= ''; + } else { + $output .= ' />'; + } + + return $output; + } + +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Support.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Support.php new file mode 100644 index 000000000..1c7be122f --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Support.php @@ -0,0 +1,478 @@ +must_escape = (array) apply_filters( 'tribe_help_must_escape_fields', $this->must_escape ); + + add_action( 'tribe_help_pre_get_sections', [ $this, 'append_system_info' ], 10 ); + add_action( 'delete_option_rewrite_rules', [ $this, 'log_rewrite_rule_purge' ] ); + + add_action( 'rest_api_init', [ __CLASS__, 'create_sysinfo_endpoint' ] ); + add_action( 'wp_ajax_tribe_toggle_sysinfo_optin', [ __CLASS__, 'ajax_sysinfo_optin' ] ); + } + + /** + * Display help tab info in events settings + * + * @param Tribe__Admin__Help_Page $help The Help Page Instance + */ + public function append_system_info( Tribe__Admin__Help_Page $help ) { + $help->add_section_content( 'system-info', $this->formattedSupportStats(), 10 ); + } + + /** + * Collect system information for support + * + * @return array of system data for support + */ + public function getSupportStats() { + global $wpdb; + $user = wp_get_current_user(); + + $plugins = []; + if ( function_exists( 'get_plugin_data' ) ) { + $plugins_raw = wp_get_active_and_valid_plugins(); + foreach ( $plugins_raw as $k => $v ) { + $plugin_details = get_plugin_data( $v ); + $plugin = $plugin_details['Name']; + if ( ! empty( $plugin_details['Version'] ) ) { + $plugin .= sprintf( ' version %s', $plugin_details['Version'] ); + } + if ( ! empty( $plugin_details['Author'] ) ) { + $plugin .= sprintf( ' by %s', $plugin_details['Author'] ); + } + if ( ! empty( $plugin_details['AuthorURI'] ) ) { + $plugin .= sprintf( ' (%s)', $plugin_details['AuthorURI'] ); + } + $plugins[] = $plugin; + } + } + + $network_plugins = []; + if ( is_multisite() && function_exists( 'get_plugin_data' ) ) { + $plugins_raw = wp_get_active_network_plugins(); + foreach ( $plugins_raw as $k => $v ) { + $plugin_details = get_plugin_data( $v ); + $plugin = $plugin_details['Name']; + if ( ! empty( $plugin_details['Version'] ) ) { + $plugin .= sprintf( ' version %s', $plugin_details['Version'] ); + } + if ( ! empty( $plugin_details['Author'] ) ) { + $plugin .= sprintf( ' by %s', $plugin_details['Author'] ); + } + if ( ! empty( $plugin_details['AuthorURI'] ) ) { + $plugin .= sprintf( ' (%s)', $plugin_details['AuthorURI'] ); + } + $network_plugins[] = $plugin; + } + } + + $mu_plugins = []; + if ( function_exists( 'get_mu_plugins' ) ) { + $mu_plugins_raw = get_mu_plugins(); + foreach ( $mu_plugins_raw as $k => $v ) { + $plugin = $v['Name']; + if ( ! empty( $v['Version'] ) ) { + $plugin .= sprintf( ' version %s', $v['Version'] ); + } + if ( ! empty( $v['Author'] ) ) { + $plugin .= sprintf( ' by %s', $v['Author'] ); + } + if ( ! empty( $v['AuthorURI'] ) ) { + $plugin .= sprintf( ' (%s)', $v['AuthorURI'] ); + } + $mu_plugins[] = $plugin; + } + } + + $keys = apply_filters( 'tribe-pue-install-keys', [] ); + //Obfuscate the License Keys for Security + if ( is_array( $keys ) && ! empty( $keys ) ) { + $secure_keys = []; + foreach ( $keys as $plugin => $license ) { + $secure_keys[ $plugin ] = preg_replace( '/^(.{4}).*(.{4})$/', '$1' . str_repeat( '#', 32 ) . '$2', $license ); + } + $keys = $secure_keys; + } + + //Server + $server = explode( ' ', $_SERVER['SERVER_SOFTWARE'] ); + $server = explode( '/', reset( $server ) ); + + //PHP Information + $php_info = []; + $php_vars = [ + 'max_execution_time', + 'memory_limit', + 'upload_max_filesize', + 'post_max_size', + 'display_errors', + 'log_errors', + ]; + + foreach ( $php_vars as $php_var ) { + if ( isset( $wpdb->qm_php_vars ) && isset( $wpdb->qm_php_vars[ $php_var ] ) ) { + $val = $wpdb->qm_php_vars[ $php_var ]; + } else { + $val = ini_get( $php_var ); + } + $php_info[ $php_var ] = $val; + } + + $homepage = get_option( 'show_on_front' ); + $homepage_page_id = get_option( 'page_on_front' ); + + if ( 'page' === $homepage ) { + if ( -10 === (int) $homepage_page_id ) { + $homepage_page_id .= ' (Main Events Page)'; + } else { + $homepage_page_id .= ' (' . esc_html( get_the_title( $homepage_page_id ) ) . ')'; + } + } + + $site_url = get_site_url(); + $systeminfo = [ + 'Home URL' => get_home_url(), + 'Site URL' => $site_url, + 'Site Language' => get_option( 'WPLANG' ) ? get_option( 'WPLANG' ) : esc_html__( 'English', 'tribe-common' ), + 'Character Set' => get_option( 'blog_charset' ), + 'Name' => $user->display_name, + 'Email' => $user->user_email, + 'Install keys' => $keys, + 'WordPress version' => get_bloginfo( 'version' ), + 'Permalink Structure' => $site_url . get_option( 'permalink_structure' ), + 'Your homepage displays' => $homepage, + 'Homepage page ID' => $homepage_page_id, + 'PHP version' => phpversion(), + 'PHP' => $php_info, + 'Server' => $server[0], + 'SAPI' => php_sapi_name(), + 'Plugins' => $plugins, + 'Network Plugins' => $network_plugins, + 'MU Plugins' => $mu_plugins, + 'Theme' => wp_get_theme()->get( 'Name' ), + 'Multisite' => is_multisite(), + 'Settings' => Tribe__Settings_Manager::get_options(), + 'WP Timezone' => get_option( 'timezone_string' ) ? get_option( 'timezone_string' ) : esc_html__( 'Unknown or not set', 'tribe-common' ), + 'WP GMT Offset' => get_option( 'gmt_offset' ) ? ' ' . get_option( 'gmt_offset' ) : esc_html__( 'Unknown or not set', 'tribe-common' ), + 'Default PHP Timezone' => date_default_timezone_get(), + 'WP Date Format' => get_option( 'date_format' ), + 'WP Time Format' => get_option( 'time_format' ), + 'Week Starts On' => get_option( 'start_of_week' ), + 'Common Library Dir' => $GLOBALS['tribe-common-info']['dir'], + 'Common Library Version' => $GLOBALS['tribe-common-info']['version'], + ]; + + if ( $this->rewrite_rules_purged ) { + $systeminfo['rewrite rules purged'] = esc_html__( 'Rewrite rules were purged on load of this help page. Chances are there is a rewrite rule flush occurring in a plugin or theme!', 'tribe-common' ); + } + + /** + * Allow for customization of the array of information that's turned into the "System Information" screen in the "Help" admin page. + * + * @param array $systeminfo The array of information turned into the "System Information" screen. + * + * @deprecated 4.14.13 Using a newer format of filter. + */ + $systeminfo = apply_filters( 'tribe-events-pro-support', $systeminfo ); + + /** + * Allow for customization of the array of information that's turned into the "System Information" screen in the "Help" admin page. + * + * @since 4.14.13 + * + * @param array $systeminfo The array of information turned into the "System Information" screen. + * + */ + $systeminfo = apply_filters( 'tec_system_information', $systeminfo ); + + return $systeminfo; + } + + /** + * Render system information into a pretty output + * + * @return string pretty HTML + */ + public function formattedSupportStats() { + $systeminfo = $this->getSupportStats(); + $output = ''; + $output .= '
    '; + + foreach ( $systeminfo as $k => $v ) { + + switch ( $k ) { + case 'name' : + case 'email' : + continue 2; + break; + case 'url' : + $v = sprintf( '%s', $v, $v ); + break; + } + + if ( is_array( $v ) ) { + $keys = array_keys( $v ); + $key = array_shift( $keys ); + $is_numeric_array = is_numeric( $key ); + unset( $keys ); + unset( $key ); + } + + $output .= sprintf( '
    %s
    ', $k ); + if ( empty( $v ) ) { + $output .= '
    -
    '; + } elseif ( is_bool( $v ) ) { + $output .= sprintf( '
    %s
    ', esc_html( $v ) ); + } elseif ( is_string( $v ) ) { + $output .= sprintf( '
    %s
    ', esc_html( $v ) ); + } elseif ( is_array( $v ) && $is_numeric_array ) { + $output .= sprintf( '
    • %s
    ', join( '
  • ', $v ) ); + } else { + $formatted_v = []; + foreach ( $v as $obj_key => $obj_val ) { + $obj_val = $this->obfuscator->obfuscate( $obj_key, $obj_val ); + + if ( is_array( $obj_val ) ) { + $formatted_v[] = sprintf( '
  • %s =
    %s
  • ', $obj_key, esc_html( print_r( $obj_val, true ) ) ); + } else { + $obj_val = esc_html( $obj_val ); + $formatted_v[] = sprintf( '
  • %s = %s
  • ', $obj_key, $obj_val ); + } + } + $v = join( "\n", $formatted_v ); + $output .= sprintf( '
      %s
    ', wp_kses_post( print_r( $v, true ) ) ); + } + } + + $output .= '
    '; + + return $output; + } + + /** + * Logs the occurrence of rewrite rule purging + */ + public function log_rewrite_rule_purge() { + $this->rewrite_rules_purged = true; + }//end log_rewrite_rule_purge + + /** + * Sets the obfuscator to be used. + * + * @param Tribe__Support__Obfuscator $obfuscator + */ + public function set_obfuscator( Tribe__Support__Obfuscator $obfuscator ) { + $this->obfuscator = $obfuscator; + } + + /** + * Creates Fields in Help Tab to Opt In to System Info + * + * @return string + */ + public static function opt_in() { + + + $optin_key = ! empty( get_option( self::$option_key ) ); + + $opt_in = '

    '; + $opt_in .= '

    ' . esc_html__( 'Your system information will only be used by The Events Calendar\'s support team. All information is stored securely. We do not share this information with any third parties.', 'tribe-common' ) . '

    '; + $opt_in .= '

    '; + + return $opt_in; + } + + /** + * Method to send back sysinfo + * + * @param $query + * + * @return string|void + * + */ + public static function sysinfo_query( $query ) { + + $optin_key = get_option( self::$option_key ); + + if ( ! $optin_key ) { + wp_send_json_error( __( 'Invalid Key', 'tribe-common' ) ); + } + + $key = $query['key']; + if ( $key != $optin_key ) { + wp_send_json_error( __( 'Invalid Key', 'tribe-common' ) ); + } + + $support = Tribe__Support::getInstance(); + $systeminfo = $support->formattedSupportStats(); + + return $systeminfo; + } + + /* + * Create Unique Endpoint Per Site + */ + public static function create_sysinfo_endpoint() { + $optin_key = get_option( self::$option_key ); + if ( $optin_key ) { + register_rest_route( + 'tribe_events/v2', + '/(?P[a-z0-9\-]+)/sysinfo/', + [ + 'methods' => 'GET', + 'callback' => [ 'Tribe__Support', 'sysinfo_query' ], + 'permission_callback' => '__return_true', + ] + ); + } + } + + /** + * Ajax Method to Create Unique Key and send to tec.com + */ + public static function ajax_sysinfo_optin() { + + if ( ! isset( $_POST['confirm'] ) || ! wp_verify_nonce( $_POST['confirm'], 'sysinfo_optin_nonce' ) ) { + wp_send_json_error( __( 'Permission Error', 'tribe-common' ) ); + } + + if ( 'generate' == $_POST['generate_key'] ) { + $random = base_convert( rand( 0, getrandmax() ), 10, 36 ); + $optin_key = hash( 'sha1', $random ); + + update_option( self::$option_key, $optin_key ); + + //Only Connect If a License Exists + $keys = apply_filters( 'tribe-pue-install-keys', [] ); + if ( is_array( $keys ) && ! empty( $keys ) ) { + self::send_sysinfo_key( $optin_key ); + } else { + wp_send_json_success( __( 'Unique System Info Key Generated', 'tribe-common' ) ); + } + + } elseif ( 'remove' == $_POST['generate_key'] ) { + $optin_key = get_option( self::$option_key ); + + delete_option( self::$option_key ); + + self::send_sysinfo_key( $optin_key, null, 'remove' ); + + } + + wp_send_json_error( __( 'Permission Error', 'tribe-common' ) ); + } + + /** + * Contact Tribe Website to Add SysInfo Key + * + * @param null $optin_key provide key for system info + * @param null $url domain of current site + * @param null $remove string used if removing $optin_key from tec.com + * @param null $pueadd boolean to disable messaging when coming from pue script + */ + public static function send_sysinfo_key( $optin_key = null, $url = null, $remove = null, $pueadd = false ) { + $url = $url ? $url : urlencode( str_replace( [ 'http://', 'https://' ], '', get_site_url() ) ); + $teccom_url = defined( 'TEC_URL' ) ? TEC_URL : 'https://theeventscalendar.com'; + $query = trailingslashit( $teccom_url ) . 'wp-json/tribe_system/v2/customer-info/' . $optin_key . '/' . $url; + + if ( $remove ) { + $query .= '?status=remove'; + } + + $response = wp_remote_get( esc_url( $query ) ); + + $response = json_decode( wp_remote_retrieve_body( $response ) ); + + if ( ! $pueadd ) { + // make sure the response came back okay + if ( ! isset( $response->success ) ) { + //on error delete the key + delete_option( self::$option_key ); + + //send error response + wp_send_json_error( $response ); + } + + wp_send_json_success( $response->data ); + } + } + + + /****************** SINGLETON GUTS ******************/ + + /** + * Enforce Singleton Pattern + */ + private static $instance; + + + public static function getInstance() { + if ( null == self::$instance ) { + $instance = new self; + $instance->set_obfuscator( new Tribe__Support__Obfuscator( $instance->must_obfuscate_prefixes ) ); + self::$instance = $instance; + } + + return self::$instance; + } + } + +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Support/Obfuscator.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Support/Obfuscator.php new file mode 100644 index 000000000..af60e6a59 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Support/Obfuscator.php @@ -0,0 +1,68 @@ +prefixes = $prefixes; + } + + /** + * Whether a value should be obfuscated or not. + * + * @param string $key + * + * @return bool + */ + public function should_obfuscate( $key ) { + foreach ( $this->prefixes as $prefix ) { + if ( strpos( $key, $prefix ) === 0 ) { + return true; + } + } + + return false; + } + + /** + * Conditionally obfuscates a string value. + * + * @param string $key + * @param mixed $string_value + * + * @return mixed Either the obfuscated string or the original value if not a string. + */ + public function obfuscate( $key, $string_value ) { + if ( ! is_string( $string_value ) ) { + return $string_value; + } + if ( ! $this->should_obfuscate( $key ) ) { + return $string_value; + } + + $length = strlen( $string_value ); + if ( $length <= 3 ) { + return preg_replace( "/./", "#", $string_value ); + } elseif ( $length > 3 && $length <= 5 ) { + return preg_replace( '/^(.{1}).*$/', '$1' . str_repeat( '#', $length - 1 ) . '$2', $string_value ); + } elseif ( $length > 5 && $length <= 9 ) { + return preg_replace( '/^(.{1}).*(.{1})$/', '$1' . str_repeat( '#', $length - 2 ) . '$2', $string_value ); + } elseif ( $length > 9 && $length <= 19 ) { + return preg_replace( '/^(.{2}).*(.{2})$/', '$1' . str_repeat( '#', $length - 4 ) . '$2', $string_value ); + } elseif ( $length > 19 && $length <= 31 ) { + return preg_replace( '/^(.{3}).*(.{3})$/', '$1' . str_repeat( '#', $length - 6 ) . '$2', $string_value ); + } + + return preg_replace( '/^(.{4}).*(.{4})$/', '$1' . str_repeat( '#', $length - 8 ) . '$2', $string_value ); + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Support/Template_Checker.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Support/Template_Checker.php new file mode 100644 index 000000000..4bbad8789 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Support/Template_Checker.php @@ -0,0 +1,275 @@ +plugin_version = $this->base_version_number( $plugin_version ); + $this->plugin_views_dir = $plugin_views_dir; + $this->theme_views_dir = $theme_views_dir; + + $this->scan_view_directory(); + $this->scan_for_overrides(); + } + + /** + * Given a version number with an alpha/beta type suffix, strips that suffix and + * returns the "base" version number. + * + * For example, given "9.8.2beta1" this method will return "9.8.2". + * + * The utility of this is that if the author of a template change sets the + * version tag in the template header to 9.8.2 (to continue the same example) we + * don't need to worry about updating that for each alpha, beta or RC we put out. + * + * @param string $version_number + * + * @return string + */ + protected function base_version_number( $version_number ) { + return preg_replace( '/[a-z]+[a-z0-9]*$/i', '', $version_number ); + } + + /** + * Recursively scans the plugin's view directory and examines the template headers + * of each file it finds within. + */ + protected function scan_view_directory() { + // If the provided directory is invalid flag the problem and go no further + if ( $this->bad_directory( $this->plugin_views_dir ) ) { + return; + } + + $view_directory = new RecursiveDirectoryIterator( $this->plugin_views_dir ); + $directory_list = new RecursiveIteratorIterator( $view_directory ); + + foreach ( $directory_list as $file ) { + $this->scan_view( $file ); + } + } + + /** + * Scans an individual view file, adding it's version number (if found) to the + * $this->views array. + * + * @param SplFileInfo $file + */ + protected function scan_view( SplFileInfo $file ) { + if ( ! $file->isFile() || ! $file->isReadable() ) { + return; + } + + $version = $this->get_template_version( $file->getPathname() ); + $this->originals[ $this->short_name( $file->getPathname() ) ] = $version; + } + + protected function scan_for_overrides() { + // If the provided directory is invalid flag the problem and go no further + if ( $this->bad_directory( $this->theme_views_dir ) ) { + return; + } + + foreach ( $this->originals as $view_file => $current_version ) { + $override_path = trailingslashit( $this->theme_views_dir ) . $view_file; + + if ( ! is_file( $override_path ) || ! is_readable( $override_path ) ) { + continue; + } + + $this->overrides[ $view_file ] = $this->get_template_version( $override_path ); + } + } + + /** + * Tests to ensure the provided view directory path is invalid or unreadable. + * + * @param string $directory + * @return bool + */ + protected function bad_directory( $directory ) { + if ( is_dir( $directory ) && is_readable( $directory ) ) { + return false; + } + + return true; + } + + /** + * Inspects the template header block within the specified file and extracts the + * version number, if one can be found. + * + * @param string $template_filepath + * @return string + */ + protected function get_template_version( $template_filepath ) { + if ( ! is_file( $template_filepath ) || ! is_readable( $template_filepath ) ) { + return ''; + } + + $view_content = file_get_contents( $template_filepath ); + + if ( ! preg_match( '/^\s*\*\s*@version\s*([0-9\.]+)/mi', $view_content, $matches ) ) { + return ''; + } + + return $matches[1]; + } + + /** + * Given a full filepath (ie, to a view file), chops off the base path found + * in $this->plugin_views_dir. + * + * For example, given: + * + * $this->plugin_views_dir = '/srv/project/wp-content/plugins/my-plugin/views' + * $full_filepath = '/srv/project/wp-content/plugins/my-plugin/views/modules/icon.php' + * + * Returns: + * + * 'modules/icon.php' + * + * @param string $full_filepath + * @return string + */ + protected function short_name( $full_filepath ) { + if ( 0 === strpos( $full_filepath, $this->plugin_views_dir ) ) { + return trim( substr( $full_filepath, strlen( $this->plugin_views_dir ) ), DIRECTORY_SEPARATOR ); + } + + return $full_filepath; + } + + /** + * Returns an array of the plugin's shipped view files, where each key is the + * view filename and the value is the version it was last updated. + * + * @return array + */ + public function get_views() { + return $this->originals; + } + + /** + * Returns an array of any or all of the plugin's shipped view files that contain + * a version field in their header blocks. + * + * @see $this->get_views() for format of returned array + * + * @return array + */ + public function get_versioned_views() { + $versioned_views = []; + + foreach ( $this->originals as $key => $version ) { + if ( ! empty( $version ) ) { + $versioned_views[ $key ] = $version; + } + } + + return $versioned_views; + } + + /** + * Returns an array of any shipped plugin views that were updated or introduced + * with the current release (as specified by $this->plugin_version). + * + * @see $this->get_views() for format of returned array + * + * @return array + */ + public function get_views_tagged_this_release() { + $currently_tagged_views = []; + + foreach ( $this->get_versioned_views() as $key => $version ) { + if ( $version === $this->plugin_version ) { + $currently_tagged_views[ $key ] = $version; + } + } + + return $currently_tagged_views; + } + + /** + * Returns an array of theme overrides, where each key is the view filename and the + * value is the version it was last updated (may be empty). + * + * @return array + */ + public function get_overrides() { + return $this->overrides; + } + + /** + * Returns an array of any or all theme overrides that contain a version field in their + * header blocks. + * + * @see $this->get_overrides() for format of returned array + * + * @return array + */ + public function get_versioned_overrides() { + $versioned_views = []; + + foreach ( $this->overrides as $key => $version ) { + if ( ! empty( $version ) ) { + $versioned_views[ $key ] = $version; + } + } + + return $versioned_views; + } + + /** + * Returns an array of any or all theme overrides that seem to be based on an earlier + * version than that which currently ships with the plugin. + * + * If optional param $include_unknown is set to true, the list will include theme + * overrides where the version could not be determined (for instance, this might result + * in theme overrides where the template header - or version tag - was removed being + * included). + * + * @see $this->get_overrides() for format of returned array + * + * @param bool $include_unknown = false + * @return array + */ + public function get_outdated_overrides( $include_unknown = false ) { + $outdated = []; + $originals = $this->get_versioned_views(); + + $overrides = $include_unknown + ? $this->get_overrides() + : $this->get_versioned_overrides(); + + foreach ( $overrides as $view => $override_version ) { + if ( empty( $originals[ $view ] ) ) { + continue; + } + + $shipped_version = $originals[ $view ]; + + if ( version_compare( $shipped_version, $override_version, '>' ) ) { + $outdated[ $view ] = $override_version; + } + } + + return $outdated; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Support/Template_Checker_Report.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Support/Template_Checker_Report.php new file mode 100644 index 000000000..87545f41b --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Support/Template_Checker_Report.php @@ -0,0 +1,119 @@ + $plugin_template_system ) { + self::generate_for( $plugin_name, $plugin_template_system ); + } + + self::wrap_report(); + return self::$complete_report; + } + + protected static function registered_plugins() { + /** + * Provides a mechanism for plugins to register information about their template/view + * setups. + * + * This should be done by adding an entry to $registere_template_systems where the key + * should be the plugin name and the element an array structured as follows: + * + * [ + * plugin_version, + * path_to_included_views, + * path_to_theme_overrides + * ] + * + * @var array $registered_template_systems + */ + return apply_filters( 'tribe_support_registered_template_systems', [] ); + } + + /** + * Creates a report for the specified plugin. + * + * @param string $plugin_name + * @param array $template_system + */ + protected static function generate_for( $plugin_name, array $template_system ) { + $report = '
    ' . esc_html( $plugin_name ) . '
    '; + + $scanner = new Tribe__Support__Template_Checker( + $template_system[ self::VERSION_INDEX ], + $template_system[ self::INCLUDED_VIEWS_INDEX ], + $template_system[ self::THEME_OVERRIDES_INDEX ] + ); + + $newly_introduced_or_updated = $scanner->get_views_tagged_this_release(); + $outdated_or_unknown = $scanner->get_outdated_overrides( true ); + + if ( empty( $newly_introduced_or_updated ) && empty( $outdated_or_unknown ) ) { + $report .= '
    ' . __( 'No notable changes detected', 'tribe-common' ) . '
    '; + } + + if ( ! empty( $newly_introduced_or_updated ) ) { + $report .= '

    ' . sprintf( __( 'Templates introduced or updated with this release (%s):', 'tribe-common' ), $template_system[ self::VERSION_INDEX ] ) . '

      '; + + foreach ( $newly_introduced_or_updated as $view_name => $version ) { + $report .= '
    • ' . esc_html( $view_name ) . '
    • '; + } + + $report .= '
    '; + } + + if ( ! empty( $outdated_or_unknown ) ) { + $report .= '

    ' . __( 'Existing theme overrides that may need revision:', 'tribe-common' ) . '

      '; + + foreach ( $outdated_or_unknown as $view_name => $version ) { + $version_note = empty( $version ) + ? __( 'version data missing from override', 'tribe-common' ) + : sprintf( __( 'based on %s version', 'tribe-common' ), $version ); + + $report .= '
    • ' . esc_html( $view_name ) . ' (' . $version_note . ')
    • '; + } + + $report .= '
    '; + } + + self::$plugin_reports[ $plugin_name ] = $report; + } + + /** + * Wraps the individual plugin template reports ready for display. + */ + protected static function wrap_report() { + if ( empty( self::$plugin_reports ) ) { + self::$complete_report = '

    ' . __( 'No notable template changes detected.', 'tribe-common' ) . '

    '; + } else { + self::$complete_report = '

    ' . __( 'Information about recent template changes and potentially impacted template overrides is provided below.', 'tribe-common' ) . '

    ' + . '
    ' . join( ' ', self::$plugin_reports ) . '
    '; + } + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Tabbed_View.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Tabbed_View.php new file mode 100644 index 000000000..38e7aa7fe --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Tabbed_View.php @@ -0,0 +1,381 @@ + => ] format. + */ + protected $items = []; + + /** + * The slug of the default tab + * + * @var string + */ + protected $default_tab; + + /** + * @var string The absolute path to this tabbed view template file. + */ + protected $template; + + /** + * An array or value object of data that should be used to render the tabbed view. + * + * @var array|object + */ + protected $data = []; + + /** + * @var string + */ + protected $label; + + /** + * @var string + */ + protected $url; + + /** + * @var string + */ + protected $active; + + /** + * Returns the tabbed view URL. + * + * @param array|string $args Query String or Array with the arguments + * @param boolean $relative Return a relative URL or absolute + * + * @return string + */ + public function get_url( $args, $relative ) { + $relative_path = add_query_arg( $args, $this->url ); + + return $relative ? $relative_path : admin_url( $relative_path ); + } + + /** + * The currently set template for this tabbed view. + * + * @return string + */ + public function get_template() { + return ! empty( $this->template ) ? $this->template : $this->get_default_template_path(); + } + + /** + * @param string $template + */ + public function set_template( $template ) { + $this->template = $template; + } + + /** + * @return string + */ + public function get_label() { + return $this->label; + } + + /** + * @param string $label + */ + public function set_label( $label ) { + $this->label = $label; + } + + /** + * Returns only the visible tabs for this tabbed view. + * + * @return Tribe__Tabbed_View__Tab[] An array of all the active and visible tabs. + */ + public function get_visibles() { + return array_filter( $this->get(), [ $this, 'is_tab_visible' ] ); + } + + /** + * @param string $url + */ + public function set_url( $url ) { + $this->url = $url; + } + + /** + * Sets the slug of the currently active tab. + * + * This value, if the tab exists, will override the value specified in the GET request. + * + * @param string $active + */ + public function set_active( $active ) { + $this->active = $active; + } + + /** + * A method to sort tabs by priority in ascending order. + * + * @param object $a First tab to compare + * @param object $b Second tab to compare + * + * @return int + */ + protected function sort_by_priority( $a, $b ) { + $a_priority = $a->get_priority(); + $b_priority = $b->get_priority(); + + if ( $a_priority == $b_priority ) { + return 0; + } + + return ( $a_priority < $b_priority ) ? - 1 : 1; + } + + /** + * Removes a tab from the tabbed view items. + * + * @param string $slug The slug of the tab to remove + * + * @return boolean `true` if the slug was registered and removed, `false` otherwise + */ + public function remove( $slug ) { + if ( ! $this->exists( $slug ) ) { + return false; + } + + unset( $this->items[ $slug ] ); + + return true; + } + + /** + * Checks if a given tab exist + * + * @param string $slug The slug of the tab + * + * @return boolean + */ + public function exists( $slug ) { + return is_object( $this->get( $slug ) ) ? true : false; + } + + /** + * Fetches the Instance of the Tab or all the tabs + * + * @param string $slug (optional) The Slug of the Tab + * + * @return null|array|object If we couldn't find the tab it will be null, if the slug is null will return all tabs + */ + public function get( $slug = null ) { + uasort( $this->items, [ $this, 'sort_by_priority' ] ); + + if ( is_null( $slug ) ) { + return $this->items; + } + + // Prevent weird stuff here + $slug = sanitize_title_with_dashes( $slug ); + + if ( ! empty( $this->items[ $slug ] ) ) { + return $this->items[ $slug ]; + } + + return null; + } + + /** + * Checks if a given Tab (slug) is active + * + * @param string $slug The Slug of the Tab + * + * @return boolean Is this tab active? + */ + public function is_active( $slug = null ) { + $slug = $this->get_requested_slug( $slug ); + $tab = $this->get_active(); + + return $slug === $tab->get_slug(); + } + + /** + * Returns the slug of tab requested in the `_GET` array or the default one. + * + * @param string|null $slug + * @param mixed $default A default value to return if the tab was not requested. + * + * @return string|bool Either the slug of the requested tab or `false` if no slug was requested + * and no default tab is set. + */ + protected function get_requested_slug( $slug = null, $default = null ) { + if ( is_null( $slug ) ) { + $default = null === $default ? $this->get_default_tab() : $default; + // Set the slug + $slug = ! empty( $_GET['tab'] ) && $this->exists( $_GET['tab'] ) ? $_GET['tab'] : $default; + } + + return $slug; + } + + /** + * Fetches the current active tab instance. + * + * @return Tribe__Tabbed_View__Tab|bool The active tab, the default tab if no tab is active, + * `false` if no tabs are registered in the Tabbed View. + */ + public function get_active() { + if ( ! empty( $this->active ) && $this->exists( $this->active ) ) { + return $this->get( $this->active ); + } + + $tab = ! empty( $_GET['tab'] ) && $this->exists( $_GET['tab'] ) ? $_GET['tab'] : $this->get_default_tab(); + + // Return the active tab or the default one + return ! empty( $tab ) ? $this->get( $tab ) : false; + } + + /** + * Returns the slug of the default tab for this tabbed view. + * + * @return string The slug of the default tab, the slug of the first tab if + * a default tab is not set, `false` otherwise. + */ + public function get_default_tab() { + if ( ! empty( $this->default_tab ) && $this->exists( $this->default_tab ) ) { + return $this->default_tab; + } + + $tabs = $this->get_tabs(); + + if ( empty( $tabs ) ) { + return false; + } + + return reset( $tabs )->get_slug(); + } + + /** + * @param Tribe__Tabbed_View__Tab|string $tab + * + * @return Tribe__Tabbed_View__Tab + */ + public function register( $tab ) { + $is_object = $tab instanceof Tribe__Tabbed_View__Tab; + if ( ! ( $is_object || ( is_string( $tab ) && class_exists( $tab ) ) ) ) { + return false; + } + + if ( ! $is_object ) { + $tab = $this->get_new_tab_instance( $tab ); + } + + // Set the Tab Item on the array of Tabs + $tab_slug = $tab->get_slug(); + + if ( empty( $tab_slug ) ) { + return false; + } + + $this->items[ $tab_slug ] = $tab; + + // Return the tab + return $tab; + } + + /** + * Returns all the registered tabs. + * + * @return Tribe__Tabbed_View__Tab[] + */ + public function get_tabs() { + uasort( $this->items, [ $this, 'sort_by_priority' ] ); + + return array_values( $this->items ); + } + + /** + * Builds an instance of the specified tab class. + * + * @param string $tab_class + * + * @return Tribe__Tabbed_View__Tab + */ + protected function get_new_tab_instance( $tab_class ) { + return new $tab_class( $this ); + } + + /** + * Renders the tabbed view and returns the resulting HTML. + * + * @return string + */ + public function render() { + $visibles = $this->get_visibles(); + if ( empty( $visibles ) ) { + return ''; + } + + if ( empty( $this->template ) ) { + $this->template = $this->get_default_template_path(); + } + + $template = $this->template; + + if ( empty( $template ) ) { + return ''; + } + + $default_data = [ + 'view' => $this, + ]; + + $data = array_merge( $default_data, (array) $this->data ); + + extract( $data ); + + ob_start(); + + include $template; + + $html = ob_get_clean(); + + return $html; + } + + /** + * Sets the default tab for the tabbed view. + * + * Please note that no check is made on the tabbed view items to ensure the value + * corresponds to a registered tab. + * + * @param string $default_tab The slug of the default tab. + */ + public function set_default_tab( $default_tab ) { + $this->default_tab = $default_tab; + } + + /** + * @param Tribe__Tabbed_View__Tab $tab + * + * @return bool + */ + protected function is_tab_visible( Tribe__Tabbed_View__Tab $tab ) { + return $tab->is_visible(); + } + + /** + * Returns the absolute path to the default template for the tabbed view. + * + * @return string + */ + public function get_default_template_path() { + return Tribe__Main::instance()->plugin_path . '/src/admin-views/tabbed-view/tabbed-view.php'; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Tabbed_View/Tab.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Tabbed_View/Tab.php new file mode 100644 index 000000000..8032ed62b --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Tabbed_View/Tab.php @@ -0,0 +1,238 @@ +tabbed_view = $tabbed_view; + $this->slug = ! empty( $slug ) ? $slug : $this->slug; + } + + /** + * @return int + */ + public function get_priority() { + return $this->priority; + } + + /** + * @param $priority + */ + public function set_priority( $priority ) { + $this->priority = $priority; + } + + /** + * @return array|object + */ + public function get_data() { + return $this->data; + } + + /** + * @param array $data + */ + public function set_data( $data ) { + $this->data = $data; + } + + /** + * @return string + */ + public function get_template() { + return ! empty( $this->template ) ? $this->template : $this->get_default_template_path(); + } + + /** + * @param string $template + */ + public function set_template( $template ) { + $this->template = $template; + } + + /** + * Returns the absolute path to the default template for the tab. + * + * @return string + */ + public function get_default_template_path() { + return Tribe__Main::instance()->plugin_path . '/src/admin-views/tabbed-view/tab.php'; + } + + /** + * Whether the tab should display or not. + * + * @return boolean + */ + public function is_visible() { + return $this->visible; + } + + /** + * @param boolean $visible + */ + public function set_visible( $visible ) { + $this->visible = $visible; + } + + /** + * @return string + */ + public function get_label() { + return $this->label; + } + + /** + * @param string $label + */ + public function set_label( $label ) { + $this->label = $label; + } + + /** + * Creates a way to include the this tab HTML easily + * + * @return string HTML content of the tab + */ + public function render() { + if ( empty( $this->template ) ) { + $this->template = Tribe__Main::instance()->plugin_path . '/src/admin-views/tabbed-view/tab.php'; + } + + $template = $this->template; + + if ( empty( $template ) ) { + return ''; + } + + $default_data = [ + 'tab' => $this, + ]; + + $data = array_merge( $default_data, (array) $this->data ); + + extract( $data ); + + ob_start(); + + include $template; + + $html = ob_get_clean(); + + return $html; + } + + /** + * Returns the link to this tab + * + * @param array|string $args Query String or Array with the arguments + * @param boolean $relative Return a relative URL or absolute + * + * @return string + */ + public function get_url( $args = [], $relative = false ) { + if ( ! empty( $this->url ) ) { + return $this->url; + } + + $defaults = [ + 'tab' => $this->get_slug(), + ]; + + // Allow the link to be "changed" on the fly + $args = wp_parse_args( $args, $defaults ); + + // Escape after the filter + return $this->tabbed_view->get_url( $args, $relative ); + } + + /** + * Sets this tab URL. + * + * This URL will override the tab natural URL. + * + * @param string $url + */ + public function set_url( $url ) { + $this->url = $url; + } + + /** + * Returns the tab slug + * + * @return string + */ + public function get_slug() { + return $this->slug; + } + + /** + * Determines if this Tab is currently displayed + * + * @return boolean + */ + public function is_active() { + $active = $this->tabbed_view->get_active(); + + return ! empty( $active ) ? $this->get_slug() === $active->get_slug() : false; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Template.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Template.php new file mode 100644 index 000000000..c752eacd1 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Template.php @@ -0,0 +1,1483 @@ + alias`. + * + * @since 4.12.10 + * + * @var array + */ + protected $aliases = []; + + /** + * Configures the class origin plugin path + * + * @since 4.6.2 + * + * @param object|string $origin The base origin for the templates + * + * @return self + */ + public function set_template_origin( $origin = null ) { + if ( empty( $origin ) ) { + $origin = $this->origin; + } + + if ( is_string( $origin ) ) { + // Origin needs to be a class with a `instance` method + if ( class_exists( $origin ) && method_exists( $origin, 'instance' ) ) { + $origin = call_user_func( [ $origin, 'instance' ] ); + } + } + + if ( + empty( $origin->plugin_path ) + && empty( $origin->pluginPath ) + && ! is_dir( $origin ) + ) { + throw new InvalidArgumentException( 'Invalid Origin Class for Template Instance' ); + } + + if ( is_string( $origin ) ) { + $this->template_base_path = array_filter( + (array) explode( + '/', + untrailingslashit( $origin ) + ) + ); + } else { + $this->origin = $origin; + + $this->template_base_path = untrailingslashit( + ! empty( $this->origin->plugin_path ) + ? $this->origin->plugin_path + : $this->origin->pluginPath + ); + } + + return $this; + } + + /** + * Configures the class with the base folder in relation to the Origin + * + * @since 4.6.2 + * + * @param array|string $folder Which folder we are going to look for templates + * + * @return self + */ + public function set_template_folder( $folder = null ) { + // Allows configuring a already set class + if ( ! isset( $folder ) ) { + $folder = $this->folder; + } + + // If Folder is String make it an Array + if ( is_string( $folder ) ) { + $folder = (array) explode( '/', $folder ); + } + + // Cast as Array and save + $this->folder = (array) $folder; + + return $this; + } + + /** + * Returns the array for which folder this template instance is looking into. + * + * @since 4.11.0 + * + * @return array Current folder we are looking for templates. + */ + public function get_template_folder() { + return $this->folder; + } + + /** + * Configures the class with the base folder in relation to the Origin + * + * @since 4.7.20 + * + * @param mixed $value Should we look for template files in the list of folders. + * + * @return self + */ + public function set_template_folder_lookup( $value = true ) { + $this->template_folder_lookup = tribe_is_truthy( $value ); + + return $this; + } + + /** + * Gets in this instance of the template engine whether we are looking public folders like themes. + * + * @since 4.12.1 + * + * @return bool Whether we are looking into theme folders. + */ + public function get_template_folder_lookup() { + return $this->template_folder_lookup; + } + + /** + * Configures the class global context + * + * @since 4.6.2 + * + * @param array $context Default global Context + * + * @return self + */ + public function add_template_globals( $context = [] ) { + // Cast as Array merge and save + $this->global = wp_parse_args( (array) $context, $this->global ); + + return $this; + } + + /** + * Configures if the class will extract context for template + * + * @since 4.6.2 + * + * @param bool $value Should we extract context for templates + * + * @return self + */ + public function set_template_context_extract( $value = false ) { + // Cast as bool and save + $this->template_context_extract = tribe_is_truthy( $value ); + + return $this; + } + + /** + * Set the current hook name for the template include. + * + * @since 4.12.1 + * + * @param string $value Which value will be saved as the current hook name. + * + * @return self Allow daisy-chaining. + */ + public function set_template_current_hook_name( $value ) { + $this->template_current_hook_name = (string) $value; + + return $this; + } + + /** + * Gets the hook name for the current template setup. + * + * @since 4.12.1 + * + * @return string Hook name currently set on the class. + */ + public function get_template_current_hook_name() { + return $this->template_current_hook_name; + } + + /** + * Sets an Index inside of the global or local context. + * Final to prevent extending the class when the `get` already exists on the child class. + * + * @see Tribe__Utils__Array::set() + * + * @since 4.6.2 + * + * @param array|string $index Specify each nested index in order. + * Example: [ 'lvl1', 'lvl2' ]; + * @param mixed $default Default value if the search finds nothing. + * @param boolean $is_local Use the Local or Global context. + * + * @return mixed The value of the specified index or the default if not found. + */ + final public function get( $index, $default = null, $is_local = true ) { + $context = $this->get_global_values(); + + if ( true === $is_local ) { + $context = $this->get_local_values(); + } + + /** + * Allows filtering the the getting of Context variables, also short circuiting + * Following the same structure as WP Core + * + * @since 4.6.2 + * + * @param mixed $value The value that will be filtered. + * @param array|string $index Specify each nested index in order. + * Example: [ 'lvl1', 'lvl2' ]; + * @param mixed $default Default value if the search finds nothing. + * @param boolean $is_local Use the Local or Global context. + * @param self $template Current instance of the Tribe__Template. + */ + $value = apply_filters( 'tribe_template_context_get', null, $index, $default, $is_local, $this ); + + if ( null !== $value ) { + return $value; + } + + return Tribe__Utils__Array::get( $context, $index, $default ); + } + + /** + * Sets a Index inside of the global or local context + * Final to prevent extending the class when the `set` already exists on the child class + * + * @since 4.6.2 + * + * @see Tribe__Utils__Array::set + * + * @param string|array $index To set a key nested multiple levels deep pass an array + * specifying each key in order as a value. + * Example: array( 'lvl1', 'lvl2', 'lvl3' ); + * @param mixed $value The value. + * @param boolean $is_local Use the Local or Global context + * + * @return array Full array with the key set to the specified value. + */ + final public function set( $index, $value = null, $is_local = true ) { + if ( true === $is_local ) { + $this->context = Tribe__Utils__Array::set( $this->context, $index, $value ); + + return $this->context; + } + + $this->global = Tribe__Utils__Array::set( $this->global, $index, $value ); + + return $this->global; + } + + /** + * Merges local and global context, and saves it locally. + * + * @since 4.6.2 + * + * @param array $context Local Context array of data. + * @param string $file Complete path to include the PHP File. + * @param array $name Template name. + * + * @return array + */ + public function merge_context( $context = [], $file = null, $name = null ) { + // Allow for simple null usage as well as array() for nothing + if ( is_null( $context ) ) { + $context = []; + } + + // Applies new local context on top of Global + Previous local. + $context = wp_parse_args( (array) $context, $this->get_values() ); + + /** + * Allows filtering the Local context. + * + * @since 4.6.2 + * + * @param array $context Local Context array of data. + * @param string $file Complete path to include the PHP File. + * @param array $name Template name. + * @param self $template Current instance of the Tribe__Template. + */ + $this->context = apply_filters( 'tribe_template_context', $context, $file, $name, $this ); + + $hook_name = $this->get_template_current_hook_name(); + + /** + * Allows filtering the Local context specifically to the template with the hook name passed to the method. + * + * @since 4.12.13 + * + * @param array $context Local Context array of data. + * @param string $file Complete path to include the PHP File. + * @param array $name Template name. + * @param self $template Current instance of the Tribe__Template. + */ + $this->context = apply_filters( "tribe_template_context:{$hook_name}", $this->context, $file, $name, $this ); + + return $this->context; + } + + /** + * Fetches the path for locating files in the Plugin Folder + * + * @since 4.7.20 + * + * @return string + */ + protected function get_template_plugin_path() { + // Craft the plugin Path + $path = array_merge( (array) $this->template_base_path, $this->folder ); + + // Implode to avoid Window Problems + $path = implode( DIRECTORY_SEPARATOR, $path ); + + /** + * Allows filtering of the base path for templates + * + * @since 4.7.20 + * + * @param string $path Complete path to include the base plugin folder + * @param self $template Current instance of the Tribe__Template + */ + return apply_filters( 'tribe_template_plugin_path', $path, $this ); + } + + /** + * Fetches the Namespace for the public paths, normally folders to look for + * in the theme's directory. + * + * @since 4.7.20 + * @since 4.11.0 Added param $plugin_namespace. + * + * @param string $plugin_namespace Overwrite the origin namespace with a given one. + * + * @return array Namespace where we to look for templates. + */ + protected function get_template_public_namespace( $plugin_namespace ) { + $namespace = [ + 'tribe', + ]; + + if ( ! empty( $plugin_namespace ) ) { + $namespace[] = $plugin_namespace; + } elseif ( ! empty( $this->origin->template_namespace ) ) { + $namespace[] = $this->origin->template_namespace; + } + + /** + * Allows filtering of the base path for templates + * + * @since 4.7.20 + * + * @param array $namespace Which is the namespace we will look for files in the theme + * @param self $template Current instance of the Tribe__Template + */ + return apply_filters( 'tribe_template_public_namespace', $namespace, $this ); + } + + /** + * Fetches which base folder we look for templates in the origin plugin. + * + * @since 4.10.2 + * + * @return array The base folders we look for templates in the origin plugin. + */ + public function get_template_origin_base_folder() { + /** + * Allows filtering of the base path for templates. + * + * @since 4.10.2 + * + * @param array $namespace Which is the base folder we will look for files in the plugin. + * @param self $template Current instance of the Tribe__Template. + */ + return apply_filters( 'tribe_template_origin_base_folder', $this->template_origin_base_folder, $this ); + } + + /** + * Fetches the path for locating files given a base folder normally theme related. + * + * @since 4.7.20 + * @since 4.11.0 Added the param $namespace. + * + * @param mixed $base Base path to look into. + * @param string $namespace Adds the plugin namespace to the path returned. + * + * @return string The public path for a given base.˙˙ + */ + protected function get_template_public_path( $base, $namespace ) { + + // Craft the plugin Path + $path = array_merge( (array) $base, (array) $this->get_template_public_namespace( $namespace ) ); + + // Pick up if the folder needs to be added to the public template path. + $folder = array_diff( $this->folder, $this->get_template_origin_base_folder() ); + + if ( ! empty( $folder ) ) { + $path = array_merge( $path, $folder ); + } + + // Implode to avoid Window Problems + $path = implode( DIRECTORY_SEPARATOR, $path ); + + /** + * Allows filtering of the base path for templates + * + * @since 4.7.20 + * + * @param string $path Complete path to include the base public folder + * @param self $template Current instance of the Tribe__Template + */ + return apply_filters( 'tribe_template_public_path', $path, $this ); + } + + /** + * Fetches the folders in which we will look for a given file + * + * @since 4.7.20 + * @since 4.12.10 Add support for common lookup. + * + * @return array A list of possible locations for the template file. + */ + protected function get_template_path_list() { + $folders = []; + + $folders['plugin'] = [ + 'id' => 'plugin', + 'priority' => 20, + 'path' => $this->get_template_plugin_path(), + ]; + + if ( $this->common_lookup ) { + // After the plugin (due to priority) look into Common too. + $folders['common'] = [ + 'id' => 'common', + 'priority' => 100, + 'path' => $this->get_template_common_path(), + ]; + } + + $folders = array_merge( $folders, $this->apply_aliases( $folders ) ); + + /** + * Allows filtering of the list of folders in which we will look for the + * template given. + * + * @since 4.7.20 + * + * @param array $folders Complete path to include the base public folder + * @param self $template Current instance of the Tribe__Template + */ + $folders = (array) apply_filters( 'tribe_template_path_list', $folders, $this ); + + uasort( $folders, 'tribe_sort_by_priority' ); + + return $folders; + } + + /** + * Get the list of theme related folders we will look up for the template. + * + * @since 4.11.0 + * + * @param string $namespace Which plugin namespace we are looking for. + * + * @return array + */ + protected function get_template_theme_path_list( $namespace ) { + $folders = []; + + $folders['child-theme'] = [ + 'id' => 'child-theme', + 'priority' => 10, + 'path' => $this->get_template_public_path( STYLESHEETPATH, $namespace ), + ]; + $folders['parent-theme'] = [ + 'id' => 'parent-theme', + 'priority' => 15, + 'path' => $this->get_template_public_path( TEMPLATEPATH, $namespace ), + ]; + + /** + * Allows filtering of the list of theme folders in which we will look for the template. + * + * @since 4.11.0 + * + * @param array $folders Complete path to include the base public folder. + * @param string $namespace Loads the files from a specified folder from the themes. + * @param self $template Current instance of the Tribe__Template. + */ + $folders = (array) apply_filters( 'tribe_template_theme_path_list', $folders, $namespace, $this ); + + uasort( $folders, 'tribe_sort_by_priority' ); + + return $folders; + } + + /** + * Tries to locate the correct file we want to load based on the Template class + * configuration and it's list of folders + * + * @since 4.7.20 + * + * @param mixed $name File name we are looking for. + * + * @return string + */ + public function get_template_file( $name ) { + // If name is String make it an Array + if ( is_string( $name ) ) { + $name = (array) explode( '/', $name ); + } + + $folders = $this->get_template_path_list(); + $found_file = false; + $namespace = false; + + foreach ( $folders as $folder ) { + if ( empty( $folder['path'] ) ) { + continue; + } + + // Build the File Path + $file = Paths::merge( $folder['path'], $name ); + + // Append the Extension to the file path + $file .= '.php'; + + // Skip non-existent files + if ( file_exists( $file ) ) { + $found_file = $file; + $namespace = ! empty( $folder['namespace'] ) ? $folder['namespace'] : false; + break; + } + } + + if ( $this->get_template_folder_lookup() ) { + $theme_folders = $this->get_template_theme_path_list( $namespace ); + + foreach ( $theme_folders as $folder ) { + if ( empty( $folder['path'] ) ) { + continue; + } + + // Build the File Path + $file = implode( DIRECTORY_SEPARATOR, array_merge( (array) $folder['path'], $name ) ); + + // Append the Extension to the file path + $file .= '.php'; + + // Skip non-existent files + if ( file_exists( $file ) ) { + $found_file = $file; + break; + } + } + } + + if ( $found_file ) { + /** + * A more Specific Filter that will include the template name + * + * @since 4.6.2 + * @since 4.7.20 The $name param no longer contains the extension + * + * @param string $file Complete path to include the PHP File + * @param array $name Template name + * @param self $template Current instance of the Tribe__Template + */ + return apply_filters( 'tribe_template_file', $found_file, $name, $this ); + } + + // Couldn't find a template on the Stack + return false; + } + + /** + * Runs the entry point hooks and filters. + * + * @param string $entry_point_name The name of the entry point. + * @param boolean $echo If we should also print the entry point content. + * + * @return null|string `null` if an entry point is disabled or the entry point HTML. + */ + public function do_entry_point( $entry_point_name, $echo = true ) { + $hook_name = $this->get_template_current_hook_name(); + + /** + * Filter if the entry points are enabled. + * + * @since 4.12.1 + * + * @param boolean $is_enabled Is entry_point enabled. + * @param string $hook_name For which template include this entry point belongs. + * @param string $entry_point_name Which entry point specifically we are triggering. + * @param self $template Current instance of the template class doing this entry point. + */ + $is_entry_point_enabled = apply_filters( 'tribe_template_entry_point_is_enabled', true, $hook_name, $entry_point_name, $this ); + + if ( ! $is_entry_point_enabled ) { + return null; + } + + ob_start(); + + if ( has_action( "tribe_template_entry_point:{$hook_name}" ) ) { + /** + * Generic entry point action for the current template. + * + * @since 4.12.1 + * + * @param string $hook_name For which template include this entry point belongs. + * @param string $entry_point_name Which entry point specifically we are triggering. + * @param self $template Current instance of the template class doing this entry point. + */ + do_action( "tribe_template_entry_point:{$hook_name}", $hook_name, $entry_point_name, $this ); + } + + if ( has_action( "tribe_template_entry_point:{$hook_name}:{$entry_point_name}" ) ) { + /** + * Specific named entry point action called. + * + * @since 4.12.1 + * + * @param string $hook_name For which template include this entry point belongs. + * @param string $entry_point_name Which entry point specifically we are triggering. + * @param self $template Current instance of the template class doing this entry point. + */ + do_action( "tribe_template_entry_point:{$hook_name}:{$entry_point_name}", $hook_name, $entry_point_name, $this ); + } + + $html = ob_get_clean(); + + if ( has_filter( "tribe_template_entry_point_html:{$hook_name}" ) ) { + /** + * Generic entry point action for the current template. + * + * @since 4.12.1 + * + * @param string $html HTML returned and/or echoed for this for this entry point. + * @param string $hook_name For which template include this entry point belongs. + * @param string $entry_point_name Which entry point specifically we are triggering. + * @param self $template Current instance of the template class doing this entry point. + */ + $html = apply_filters( "tribe_template_entry_point_html:{$hook_name}", $html, $hook_name, $entry_point_name, $this ); + } + + if ( has_filter( "tribe_template_entry_point_html:{$hook_name}:{$entry_point_name}" ) ) { + /** + * Specific named entry point action called. + * + * @since 4.12.1 + * + * @param string $html HTML returned and/or echoed for this for this entry point. + * @param string $hook_name For which template include this entry point belongs. + * @param string $entry_point_name Which entry point specifically we are triggering. + * @param self $template Current instance of the template class doing this entry point. + */ + $html = apply_filters( "tribe_template_entry_point_html:{$hook_name}:{$entry_point_name}", $html, $hook_name, $entry_point_name, $this ); + } + + if ( $echo ) { + echo $html; + } + + return $html; + } + + /** + * A very simple method to include a Template, allowing filtering and additions using hooks. + * + * @since 4.6.2 + * + * @param string|array $name Which file we are talking about including. + * If an array, each item will add a directory separator to get to the single template. + * @param array $context Any context data you need to expose to this file + * @param boolean $echo If we should also print the Template + * + * @return string|false Either the final content HTML or `false` if no template could be found. + */ + public function template( $name, $context = [], $echo = true ) { + static $file_exists = []; + static $files = []; + static $template_names = []; + + /** + * Allow users to disable templates before rendering it by returning empty string. + * + * @since 4.12.0 + * + * @param string null Whether to continue displaying the template or not. + * @param array $name Template name. + * @param array $context Any context data you need to expose to this file. + * @param boolean $echo If we should also print the Template. + */ + $done = apply_filters( 'tribe_template_done', null, $name, $context, $echo ); + + if ( null !== $done ) { + return false; + } + + // Key we'll use for in-memory caching of expensive operations. + $cache_name_key = is_array( $name ) ? implode( '/', $name ) : $name; + + // Cache template name massaging so we don't have to repeat these actions. + if ( ! isset( $template_names[ $cache_name_key ] ) ) { + // If name is String make it an Array + if ( is_string( $name ) ) { + $name = (array) explode( '/', $name ); + } + + // Clean this Variable + $name = array_map( 'sanitize_title_with_dashes', $name ); + + $template_names[ $cache_name_key ] = $name; + } + + // Cache file location and existence. + if ( + ! isset( $file_exists[ $cache_name_key ] ) + || ! isset( $files[ $cache_name_key ] ) + ) { + // Check if the file exists + $files[ $cache_name_key ] = $file = $this->get_template_file( $name ); + + // Check if it's a valid variable + if ( ! $file ) { + return $file_exists[ $cache_name_key ] = false; + } + + // Before we load the file we check if it exists + if ( ! file_exists( $file ) ) { + return $file_exists[ $cache_name_key ] = false; + } + + $file_exists[ $cache_name_key ] = true; + } + + // If the file doesn't exist, bail. + if ( ! $file_exists[ $cache_name_key ] ) { + return false; + } + + // Use filename stored in cache. + $file = $files[ $cache_name_key ]; + $name = $template_names[ $cache_name_key ]; + $origin_folder_appendix = array_diff( $this->folder, $this->template_origin_base_folder ); + + if ( $origin_namespace = $this->template_get_origin_namespace( $file ) ) { + $legacy_namespace = array_merge( (array) $origin_namespace, $name ); + $namespace = array_merge( (array) $origin_namespace, $origin_folder_appendix, $name ); + } else { + $legacy_namespace = $name; + $namespace = array_merge( $origin_folder_appendix, $legacy_namespace ); + } + + // Setup the Hook name. + $legacy_hook_name = implode( '/', $legacy_namespace ); + $hook_name = implode( '/', $namespace ); + $prev_hook_name = $this->get_template_current_hook_name(); + + // Store the current hook name for the purposes of entry-points. + $this->set_template_current_hook_name( $hook_name ); + + /** + * Allow users to filter the HTML before rendering + * + * @since 4.11.0 + * + * @param string $html The initial HTML + * @param string $file Complete path to include the PHP File + * @param array $name Template name + * @param self $template Current instance of the Tribe__Template + */ + $pre_html = apply_filters( 'tribe_template_pre_html', null, $file, $name, $this ); + + /** + * Allow users to filter the HTML by the name before rendering + * + * E.g.: + * `tribe_template_pre_html:events/blocks/parts/details` + * `tribe_template_pre_html:events/embed` + * `tribe_template_pre_html:tickets/login-to-purchase` + * + * @since 4.11.0 + * + * @param string $html The initial HTML + * @param string $file Complete path to include the PHP File + * @param array $name Template name + * @param self $template Current instance of the Tribe__Template + */ + $pre_html = apply_filters( "tribe_template_pre_html:{$hook_name}", $pre_html, $file, $name, $this ); + + if ( null !== $pre_html ) { + return $pre_html; + } + + // Merges the local data passed to template to the global scope + $this->merge_context( $context, $file, $name ); + + $before_include_html = $this->actions_before_template( $file, $name, $hook_name ); + $before_include_html = $this->filter_template_before_include_html( $before_include_html, $file, $name, $hook_name ); + + $include_html = $this->template_safe_include( $file ); + $include_html = $this->filter_template_include_html( $include_html, $file, $name, $hook_name ); + + $after_include_html = $this->actions_after_template( $file, $name, $hook_name ); + $after_include_html = $this->filter_template_after_include_html( $after_include_html, $file, $name, $hook_name ); + + // Only fetch the contents after the action + $html = $before_include_html . $include_html . $after_include_html; + + $html = $this->filter_template_html( $html, $file, $name, $hook_name ); + + // Tries to hook container entry points in the HTML. + $html = $this->template_hook_container_entry_points( $html ); + + if ( $echo ) { + echo $html; + } + + // Revert the current hook name. + $this->set_template_current_hook_name( $prev_hook_name ); + + return $html; + } + + /** + * Run the hooks for the container entry points. + * + * @since 4.12.1 + * + * @param string $html The html of the current template. + * + * @return string|false Either the final entry point content HTML or `false` if no entry point could be found or set to false. + */ + private function template_hook_container_entry_points( $html ) { + + $matches = $this->get_entry_point_matches( $html ); + $html_matches = $matches[0]; + + if ( 0 === count( $html_matches ) ) { + return $html; + } + + $html_tags = $matches['tag']; + $html_tags_ends = $matches['is_end']; + + // Get first and last tags. + $first_tag = reset( $html_tags ); + $last_tag = end( $html_tags ); + + // Determine if first last tags are tag ends. + $first_tag_is_end = '/' === reset( $html_tags_ends ); + $last_tag_is_end = '/' === end( $html_tags_ends ); + + // When first and last tag are not the same, bail. + if ( $first_tag !== $last_tag ) { + return $html; + } + + // If the first tag is a html tag end, bail. + if ( $first_tag_is_end ) { + return $html; + } + + // If the last tag is not and html tag end, bail. + if ( ! $last_tag_is_end ) { + return $html; + } + + $first_tag_html = reset( $html_matches ); + $last_tag_html = end( $html_matches ); + + $open_container_entry_point_html = $this->do_entry_point( 'after_container_open', false ); + $close_container_entry_point_html = $this->do_entry_point( 'before_container_close', false ); + + $html = Strings::replace_first( $first_tag_html, $first_tag_html . $open_container_entry_point_html, $html ); + $html = Strings::replace_last( $last_tag_html, $close_container_entry_point_html . $last_tag_html, $html ); + + return $html; + } + + /** + * Based on a path it determines what is the namespace that should be used. + * + * @since 4.11.0 + * + * @param string $path Which file we are going to load. + * + * @return string|false The found namespace for that path or false. + */ + public function template_get_origin_namespace( $path ) { + $matching_namespace = false; + /** + * Allows more namespaces to be added based on the path of the file we are loading. + * + * @since 4.11.0 + * + * @param array $namespace_map Indexed array containing the namespace as the key and path to `strpos`. + * @param string $path Path we will do the `strpos` to validate a given namespace. + * @param self $template Current instance of the template class. + */ + $namespace_map = (array) apply_filters( 'tribe_template_origin_namespace_map', [], $path, $this ); + + foreach ( $namespace_map as $namespace => $contains_string ) { + // Normalize the trailing slash to the current OS directory separator. + $contains_string = rtrim( $contains_string, '\\/' ) . DIRECTORY_SEPARATOR; + + // Skip when we don't have the namespace path. + if ( false === strpos( $path, $contains_string ) ) { + continue; + } + + $matching_namespace = $namespace; + + // Once the first namespace is found it breaks out. + break; + } + + if ( empty( $matching_namespace ) && ! empty( $this->origin->template_namespace ) ) { + $matching_namespace = $this->origin->template_namespace; + } + + return $matching_namespace; + } + + /** + * Includes a give PHP inside of a safe context. + * + * This method is required to prevent template files messing with local variables used inside of the + * `self::template` method. Also shelters the template loading from any possible variables that could + * be overwritten by the context. + * + * @since 4.11.0 + * + * @param string $file Which file will be included with safe context. + * + * @return string Contents of the included file. + */ + public function template_safe_include( $file ) { + ob_start(); + // We use this instance variable to prevent collisions. + $this->template_current_file_path = $file; + unset( $file ); + + // Only do this if really needed (by default it won't). + if ( true === $this->template_context_extract && ! empty( $this->context ) ) { + // Make any provided variables available in the template variable scope. + extract( $this->context ); // @phpcs:ignore + } + + include $this->template_current_file_path; + + // After the include we reset the variable. + unset( $this->template_current_file_path ); + return ob_get_clean(); + } + + /** + * Sets a number of values at the same time. + * + * @since 4.9.11 + * + * @param array $values An associative key/value array of the values to set. + * @param bool $is_local Whether to set the values as global or local; defaults to local as the `set` method does. + * + * @see Tribe__Template::set() + */ + public function set_values( array $values = [], $is_local = true ) { + foreach ( $values as $key => $value ) { + $this->set( $key, $value, $is_local ); + } + } + + /** + * Returns the Template global context. + * + * @since 4.9.11 + * + * @return array An associative key/value array of the Template global context. + */ + public function get_global_values() { + return $this->global; + } + + /** + * Returns the Template local context. + * + * @since 4.9.11 + * + * @return array An associative key/value array of the Template local context. + */ + public function get_local_values() { + return $this->context; + } + + /** + * Returns the Template global and local context values. + * + * Local values will override the template global context values. + * + * @since 4.9.11 + * + * @return array An associative key/value array of the Template global and local context. + */ + public function get_values() { + return array_merge( $this->get_global_values(), $this->get_local_values() ); + } + + /** + * Get the Entry Point Matches. + * + * @since 4.12.1 + * + * @param string $html The html of the current template. + * + * @return array An array of matches from the regular expression. + */ + private function get_entry_point_matches( $html ) { + $regexp = '/<(?\/)*(?[A-Z0-9]*)(?:\b)*[^>]*>/mi'; + + preg_match_all( $regexp, $html, $matches ); + + return $matches; + } + + /** + * Fetches the path for locating files in the Common folder part of the plugin that is currently providing it. + * + * Note: the Common path will be dependent on the version that is loaded from the plugin that is bundling it. + * E.g. if both TEC and ET are active (both will bundle Common) and the ET version of Common has been loaded as + * most recent and the ET version of Common does not have a template file, then the template file will not be found. + * This will allow versioning the existence and nature of the template files part of common. + * + * @since 4.12.10 + * + * @return string The absolute path, with no guarantee of its existence, to the Common version of the template file. + */ + protected function get_template_common_path() { + // As base path use the current location of Common, remove the trailing slash. + $common_abs_path = untrailingslashit( Tribe__Main::instance()->plugin_path ); + $path = array_merge( (array) $common_abs_path, $this->folder ); + + // Implode to avoid problems on Windows hosts. + $path = implode( DIRECTORY_SEPARATOR, $path ); + + /** + * Allows filtering the path to a template provided by Common. + * + * @since 4.12.10 + * + * @param string $path Complete path to include the base folder of common part of the plugin. + * @param self $template Current instance of the Tribe__Template. + */ + return apply_filters( 'tribe_template_common_path', $path, $this ); + } + + /** + * Sets the aliases the template should use. + * + * @since 4.12.10 + * + * @param array $aliases A map of aliases that should be used to add lookup locations, in the format + * `[ original => alias ]`; + * + * @return static This instance, for method chaining. + */ + public function set_aliases( array $aliases = [] ) { + $this->aliases = $aliases; + + return $this; + } + + /** + * Applies the template path aliases, if any, to a list of folders. + * + * @since 4.12.10 + * + * @param array $folders The list of folder to apply the aliases to, if any. + * + * @return array The list of new folder entries to add to the folders, in the same input format of the + * folders. + */ + protected function apply_aliases( array $folders ) { + $new_folders = []; + if ( ! empty( $this->aliases ) ) { + foreach ( $folders as $folder_name => $folder ) { + $original_path = $folder['path']; + foreach ( $this->aliases as $original => $alias ) { + // Since an alias could be a path, we take care to handle it with the current directory separator. + list( $normalized_original, $normalized_alias ) = str_replace(['\\','/'] , DIRECTORY_SEPARATOR, [ $original, $alias ] ); + if ( false === strpos( $original_path, $normalized_original ) ) { + continue; + } + + $alias_path = str_replace( $normalized_original, $normalized_alias, $original_path ); + + $new = $folder; + $new['path'] = $alias_path; + $new['priority'] = (int) $new['priority'] + 1; + $new_folders[ $folder_name . '_' . $alias ] = $new; + } + } + } + return $new_folders; + } + + + /** + * Filters the full HTML for the template. + * + * @since 4.13.0 + * + * @param string $html The final HTML. + * @param string $file Complete path to include the PHP File. + * @param array $name Template name. + * @param string $hook_name The hook used to create the filter by name. + * + * @return string HTML after filtering. + */ + protected function filter_template_html( $html, $file, $name, $hook_name ) { + /** + * Allow users to filter the final HTML. + * + * @since 4.6.2 + * @since 4.7.20 The $name param no longer contains the extension + * + * @param string $html The final HTML. + * @param string $file Complete path to include the PHP File. + * @param array $name Template name. + * @param self $template Current instance of the Tribe__Template. + */ + $html = apply_filters( 'tribe_template_html', $html, $file, $name, $this ); + + /** + * Allow users to filter the final HTML by the name. + * + * E.g.: + * `tribe_template_html:events/blocks/parts/details` + * `tribe_template_html:events/embed` + * `tribe_template_html:tickets/login-to-purchase` + * + * @since 4.7.20 + * + * @param string $html The final HTML. + * @param string $file Complete path to include the PHP File. + * @param array $name Template name. + * @param self $template Current instance of the Tribe__Template. + */ + $html = apply_filters( "tribe_template_html:{$hook_name}", $html, $file, $name, $this ); + + return $html; + } + + /** + * Filters the HTML for the Before include actions. + * + * @since 4.13.0 + * + * @param string $html The final HTML. + * @param string $file Complete path to include the PHP File. + * @param array $name Template name. + * @param string $hook_name The hook used to create the filter by name. + * + * @return string HTML after filtering. + */ + protected function filter_template_before_include_html( $html, $file, $name, $hook_name ) { + /** + * Allow users to filter the Before include actions. + * + * @since 4.13.0 + * + * @param string $html The final HTML. + * @param string $file Complete path to include the PHP File. + * @param array $name Template name. + * @param self $template Current instance of the Tribe__Template. + */ + $html = apply_filters( 'tribe_template_before_include_html', $html, $file, $name, $this ); + + /** + * Allow users to filter the Before include actions by name. + * + * E.g.: + * `tribe_template_before_include_html:events/blocks/parts/details` + * `tribe_template_before_include_html:events/embed` + * `tribe_template_before_include_html:tickets/login-to-purchase` + * + * @since 4.13.0 + * + * @param string $html The final HTML. + * @param string $file Complete path to include the PHP File. + * @param array $name Template name. + * @param self $template Current instance of the Tribe__Template. + */ + $html = apply_filters( "tribe_template_before_include_html:{$hook_name}", $html, $file, $name, $this ); + + return $html; + } + + /** + * Filters the HTML for the PHP safe include. + * + * @since 4.13.0 + * + * @param string $html The final HTML. + * @param string $file Complete path to include the PHP File. + * @param array $name Template name. + * @param string $hook_name The hook used to create the filter by name. + * + * @return string HTML after filtering. + */ + protected function filter_template_include_html( $html, $file, $name, $hook_name ) { + /** + * Allow users to filter the PHP template include actions. + * + * @since 4.13.0 + * + * @param string $html The final HTML. + * @param string $file Complete path to include the PHP File. + * @param array $name Template name. + * @param self $template Current instance of the Tribe__Template. + */ + $html = apply_filters( 'tribe_template_include_html', $html, $file, $name, $this ); + + /** + * Allow users to filter the PHP template include actions by name. + * + * E.g.: + * `tribe_template_include_html:events/blocks/parts/details` + * `tribe_template_include_html:events/embed` + * `tribe_template_include_html:tickets/login-to-purchase` + * + * @since 4.13.0 + * + * @param string $html The final HTML. + * @param string $file Complete path to include the PHP File. + * @param array $name Template name. + * @param self $template Current instance of the Tribe__Template. + */ + $html = apply_filters( "tribe_template_include_html:{$hook_name}", $html, $file, $name, $this ); + + return $html; + } + + /** + * Filters the HTML for the after include actions. + * + * @since 4.13.0 + * + * @param string $html The final HTML. + * @param string $file Complete path to include the PHP File. + * @param array $name Template name. + * @param string $hook_name The hook used to create the filter by name. + * + * @return string HTML after filtering. + */ + protected function filter_template_after_include_html( $html, $file, $name, $hook_name ) { + /** + * Allow users to filter the after include actions. + * + * @since 4.13.0 + * + * @param string $html The final HTML. + * @param string $file Complete path to include the PHP File. + * @param array $name Template name. + * @param self $template Current instance of the Tribe__Template. + */ + $html = apply_filters( 'tribe_template_after_include_html', $html, $file, $name, $this ); + + /** + * Allow users to filter the after include actions by name. + * + * E.g.: + * `tribe_template_after_include_html:events/blocks/parts/details` + * `tribe_template_after_include_html:events/embed` + * `tribe_template_after_include_html:tickets/login-to-purchase` + * + * @since 4.13.0 + * + * @param string $html The final HTML. + * @param string $file Complete path to include the PHP File. + * @param array $name Template name. + * @param self $template Current instance of the Tribe__Template. + */ + $html = apply_filters( "tribe_template_after_include_html:{$hook_name}", $html, $file, $name, $this ); + + return $html; + } + + /** + * Fires of actions before including the template. + * + * @since 4.13.0 + * + * @param string $file Complete path to include the PHP File. + * @param array $name Template name. + * @param string $hook_name The hook used to create the filter by name. + * + * @return string HTML printed by the before actions. + */ + protected function actions_before_template( $file, $name, $hook_name ) { + ob_start(); + + /** + * Fires an Action before including the template file + * + * @since 4.13.0 + * + * @param string $file Complete path to include the PHP File + * @param array $name Template name + * @param self $template Current instance of the Tribe__Template + */ + do_action( 'tribe_template_before_include', $file, $name, $this ); + + /** + * Fires an Action for a given template name before including the template file, + * + * E.g.: + * `tribe_template_before_include:events/blocks/parts/details` + * `tribe_template_before_include:events/embed` + * `tribe_template_before_include:tickets/login-to-purchase` + * + * @since 4.13.0 + * + * @param string $file Complete path to include the PHP File. + * @param array $name Template name. + * @param self $template Current instance of the Tribe__Template. + */ + do_action( "tribe_template_before_include:{$hook_name}", $file, $name, $this ); + + return ob_get_clean(); + } + + /** + * Fires of actions after including the template. + * + * @since 4.13.0 + * + * @param string $file Complete path to include the PHP File. + * @param array $name Template name. + * @param string $hook_name The hook used to create the filter by name. + * + * @return string HTML printed by the after actions. + */ + protected function actions_after_template( $file, $name, $hook_name ) { + ob_start(); + /** + * Fires an Action after including the template file. + * + * @since 4.13.0 + * + * @param string $file Complete path to include the PHP File. + * @param array $name Template name. + * @param self $template Current instance of the Tribe__Template. + */ + do_action( 'tribe_template_after_include', $file, $name, $this ); + + /** + * Fires an Action for a given template name after including the template file. + * + * E.g.: + * `tribe_template_after_include:events/blocks/parts/details` + * `tribe_template_after_include:events/embed` + * `tribe_template_after_include:tickets/login-to-purchase` + * + * @since 4.13.0 + * + * @param string $file Complete path to include the PHP File. + * @param array $name Template name. + * @param self $template Current instance of the Tribe__Template. + */ + do_action( "tribe_template_after_include:{$hook_name}", $file, $name, $this ); + + return ob_get_clean(); + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Template_Part_Cache.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Template_Part_Cache.php new file mode 100644 index 000000000..3a3e99ac6 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Template_Part_Cache.php @@ -0,0 +1,123 @@ +template = $template; + $this->key = $template . '_' . $id; + $this->expiration = $expiration; + $this->expiration_trigger = $expiration_trigger; + $this->cache = new Tribe__Cache(); + + $this->add_hooks(); + } + + /** + * Hook in to show cached content and bypass queries where needed + */ + public function add_hooks() { + + // set the cached html in transients after the template part is included + add_filter( 'tribe_get_template_part_content', [ $this, 'set' ], 10, 2 ); + + // get the cached html right before the setup_view runs so it's available for bypassing any view logic + add_action( 'tribe_events_before_view', [ $this, 'get' ], 9, 1 ); + + // when the specified template part is included, show the cached html instead + add_filter( 'tribe_get_template_part_path_' . $this->template, [ $this, 'display' ] ); + } + + /** + * Checks if there is a cached html fragment in the transients, if it's there, + * don't include the requested file path. If not, just return the file path like normal + * + * @param $path file path to the month view template part + * + * @return bool + * @uses tribe_get_template_part_path_[template] hook + */ + public function display( $path ) { + + if ( $this->html !== false ) { + echo $this->html; + + return false; + } + + return $path; + + } + + /** + * Set cached html in transients + * + * @param $html + * @param $template + * + * @return string + * @uses tribe_get_template_part_content hook + */ + public function set( $html, $template ) { + if ( $template == $this->template ) { + $this->cache->set_transient( $this->key, $html, $this->expiration, $this->expiration_trigger ); + } + + return $html; + } + + /** + * Retrieve the cached html from transients, set class property + * + * @uses tribe_events_before_view hook + */ + public function get() { + + if ( isset( $this->html ) ) { + + return $this->html; + } + + $this->html = $this->cache->get_transient( $this->key, $this->expiration_trigger ); + + return $this->html; + + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Templates.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Templates.php new file mode 100644 index 000000000..9e0857cbb --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Templates.php @@ -0,0 +1,79 @@ +is_main_query(); + } + + /** + * Look for the stylesheets. Fall back to $fallback path if the stylesheets can't be located or the array is empty. + * + * @param array|string $stylesheets Path to the stylesheet + * @param bool|string $fallback Path to fallback stylesheet + * + * @return bool|string Path to stylesheet + */ + public static function locate_stylesheet( $stylesheets, $fallback = false ) { + if ( ! is_array( $stylesheets ) ) { + $stylesheets = [ $stylesheets ]; + } + if ( empty( $stylesheets ) ) { + return $fallback; + } + foreach ( $stylesheets as $filename ) { + if ( file_exists( get_stylesheet_directory() . '/' . $filename ) ) { + $located = trailingslashit( get_stylesheet_directory_uri() ) . $filename; + break; + } else { + if ( file_exists( get_template_directory() . '/' . $filename ) ) { + $located = trailingslashit( get_template_directory_uri() ) . $filename; + break; + } + } + } + if ( empty( $located ) ) { + return $fallback; + } + + return $located; + } + + /** + * Add our own method is_embed to check by WordPress Version and function is_embed + * to prevent fatal errors in WordPress 4.3 and earlier + * + * @version 4.2.1 + */ + public static function is_embed() { + global $wp_version; + if ( version_compare( $wp_version, '4.4', '<' ) || ! function_exists( 'is_embed' ) ) { + return false; + } + + return is_embed(); + + } + +}//end class diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Terms.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Terms.php new file mode 100644 index 000000000..7810dd9fb --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Terms.php @@ -0,0 +1,56 @@ +to_array(); + } elseif ( is_numeric( $term ) ) { + $term = absint( $term ); + $term_info = get_term( $term, $taxonomy, ARRAY_A ); + } else { + $term_info = term_exists( $term, $taxonomy ); + } + + if ( ! $term_info ) { + // Skip if a non-existent term ID is passed. + if ( is_numeric( $term ) ) { + continue; + } + + if ( true == $create_missing ) { + $term_info = wp_insert_term( $term, $taxonomy ); + } else { + continue; + } + } + + if ( is_wp_error( $term_info ) ) { + continue; + } + + $term_ids[] = $term_info['term_id']; + } + + return array_unique( $term_ids ); + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Timezones.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Timezones.php new file mode 100644 index 000000000..2edd8f3d6 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Timezones.php @@ -0,0 +1,633 @@ +format( 'T' ); + + // If PHP date "T" format is a -03 or +03, it's a bugged abbreviation, we can find it manually. + if ( 0 === strpos( $abbr, '-' ) || 0 === strpos( $abbr, '+' ) ) { + $abbreviations = timezone_abbreviations_list(); + + foreach ( $abbreviations as $abbreviation => $timezones ) { + foreach ( $timezones as $timezone ) { + if ( $timezone['timezone_id'] === $timezone_string ) { + return strtoupper( $abbreviation ); + } + } + } + } + } catch ( Exception $e ) { + $abbr = ''; + } + + return $abbr; + } + + /** + * Helper function to retrieve the timezone string for a given UTC offset + * + * This is a close copy of WooCommerce's wc_timezone_string() method + * + * @param string $offset UTC offset + * + * @return string + */ + public static function generate_timezone_string_from_utc_offset( $offset ) { + if ( ! self::is_utc_offset( $offset ) ) { + return $offset; + } + + // ensure we have the minutes on the offset + if ( ! strpos( $offset, ':' ) ) { + $offset .= ':00'; + } + + $offset = str_replace( 'UTC', '', $offset ); + + list( $hours, $minutes ) = explode( ':', $offset ); + $seconds = $hours * 60 * 60 + $minutes * 60; + + // attempt to guess the timezone string from the UTC offset + $timezone = timezone_name_from_abbr( '', $seconds, 0 ); + + if ( false === $timezone ) { + $is_dst = (bool) date( 'I' ); + + foreach ( timezone_abbreviations_list() as $abbr ) { + foreach ( $abbr as $city ) { + if ( + (bool) $city['dst'] === $is_dst + && intval( $city['offset'] ) === intval( $seconds ) + && $city['timezone_id'] + ) { + return $city['timezone_id']; + } + } + } + + // fallback to UTC + return 'UTC'; + } + + return $timezone; + } + + /** + * Tests to see if the timezone string is a UTC offset, ie "UTC+2". + * + * @param string $timezone + * + * @return bool + */ + public static function is_utc_offset( $timezone ) { + $timezone = trim( $timezone ); + return ( 0 === strpos( $timezone, 'UTC' ) && strlen( $timezone ) > 3 ); + } + + /** + * Returns a DateTimeZone object matching the representation in $tzstring where + * possible, or else representing UTC (or, in the worst case, false). + * + * If optional parameter $with_fallback is true, which is the default, then in + * the event it cannot find/create the desired timezone it will try to return the + * UTC DateTimeZone before bailing. + * + * @param string $tzstring + * @param bool $with_fallback = true + * + * @return DateTimeZone|false + */ + public static function get_timezone( $tzstring, $with_fallback = true ) { + if ( isset( self::$timezones[ $tzstring ] ) ) { + return self::$timezones[ $tzstring ]; + } + + try { + self::$timezones[ $tzstring ] = new DateTimeZone( $tzstring ); + return self::$timezones[ $tzstring ]; + } + catch ( Exception $e ) { + if ( $with_fallback ) { + return self::get_timezone( 'UTC', true ); + } + } + + return false; + } + + /** + * Confirms if the current timezone mode matches the $possible_mode. + * + * @param string $possible_mode + * + * @return bool + */ + public static function is_mode( $possible_mode ) { + return $possible_mode === self::mode(); + } + + /** + * Returns a string representing the timezone/offset currently desired for + * the display of dates and times. + * + * @return string + */ + public static function mode() { + $mode = self::EVENT_TIMEZONE; + + if ( 'site' === tribe_get_option( 'tribe_events_timezone_mode' ) ) { + $mode = self::SITE_TIMEZONE; + } + + return apply_filters( 'tribe_events_current_display_timezone', $mode ); + } + + /** + * Tries to convert the provided $datetime to UTC from the timezone represented by $tzstring. + * + * Though the usual range of formats are allowed, $datetime ordinarily ought to be something + * like the "Y-m-d H:i:s" format (ie, no timezone information). If it itself contains timezone + * data, the results may be unexpected. + * + * In those cases where the conversion fails to take place, the $datetime string will be + * returned untouched. + * + * @param string $datetime + * @param string $tzstring + * @param string $format The optional format of the resulting date, defaults to + * `Tribe__Date_Utils::DBDATETIMEFORMAT`. + * + * @return string + */ + public static function to_utc( $datetime, $tzstring, $format = null ) { + if ( self::is_utc_offset( $tzstring ) ) { + return self::apply_offset( $datetime, $tzstring, true ); + } + + $local = self::get_timezone( $tzstring ); + $utc = self::get_timezone( 'UTC' ); + + $new_datetime = date_create( $datetime, $local ); + + if ( $new_datetime ) { + $new_datetime->setTimezone( $utc ); + $format = ! empty( $format ) ? $format : Tribe__Date_Utils::DBDATETIMEFORMAT; + + return $new_datetime->format( $format ); + } + + // Fallback to the unmodified datetime if there was a failure during conversion + return $datetime; + } + + /** + * Tries to convert the provided $datetime to the timezone represented by $tzstring. + * + * This is the sister function of self::to_utc() - please review the docs for that method + * for more information. + * + * @param string $datetime + * @param string $tzstring + * + * @return string + */ + public static function to_tz( $datetime, $tzstring ) { + + if ( self::is_utc_offset( $tzstring ) ) { + + return self::apply_offset( $datetime, $tzstring ); + } + + $local = self::get_timezone( $tzstring ); + $utc = self::get_timezone( 'UTC' ); + + $new_datetime = date_create( $datetime, $utc ); + + if ( $new_datetime && $new_datetime->setTimezone( $local ) ) { + return $new_datetime->format( Tribe__Date_Utils::DBDATETIMEFORMAT ); + } + + // Fallback to the unmodified datetime if there was a failure during conversion + return $datetime; + } + + /** + * Localizes a date or timestamp using WordPress timezone and returns it in the specified format. + * + * @param string $format The format the date shouuld be formatted to. + * @param string|int $date The date UNIX timestamp or `strtotime` parseable string. + * @param string $timezone An optional timezone string identifying the timezone the date shoudl be localized + * to; defaults to the WordPress installation timezone (if available) or to the system + * timezone. + * + * @return string|bool The parsed date in the specified format and localized to the system or specified + * timezone, or `false` if the specified date is not a valid date string or timestamp + * or the specified timezone is not a valid timezone string. + */ + public static function localize_date( $format = null, $date = null, $timezone = null ) { + if ( empty( $timezone ) ) { + $timezone = self::wp_timezone_string(); + } + + $timezone = self::generate_timezone_string_from_utc_offset( $timezone ); + + try { + $timezone_object = new DateTimeZone( $timezone ); + + if ( Tribe__Date_Utils::is_timestamp( $date ) ) { + $date = new DateTime( "@{$date}" ); + } else { + $date = new DateTime( $date ); + } + } catch ( Exception $e ) { + return false; + } + + $date->setTimezone( $timezone_object ); + + return $date->format( $format ); + } + + /** + * Converts a date string or timestamp to a destination timezone. + * + * @param string|int $date Either a string parseable by the `strtotime` function or a UNIX timestamp. + * @param string $from_timezone The timezone of the source date. + * @param string $to_timezone The timezone the destination date should use. + * @param string $format The format that should be used for the destination date. + * + * @return string The formatted and converted date. + */ + public static function convert_date_from_timezone( $date, $from_timezone, $to_timezone, $format ) { + if ( ! Tribe__Date_Utils::is_timestamp( $date ) ) { + $from_date = new DateTime( $date, new DateTimeZone( $from_timezone ) ); + $timestamp = $from_date->format( 'U' ); + } else { + $timestamp = $date; + } + + $to_date = new DateTime( "@{$timestamp}", new DateTimeZone( $to_timezone ) ); + + return $to_date->format( $format ); + } + + /** + * Whether the candidate timezone is a valid PHP timezone or a supported UTC offset. + * + * @param string $candidate + * + * @return bool + */ + public static function is_valid_timezone( $candidate ) { + if ( self::is_utc_offset( $candidate ) ) { + return true; + } + try { + new DateTimeZone( $candidate ); + } catch ( Exception $e ) { + return false; + } + + return true; + } + + /** + * Given a string in the form "UTC+2.5" returns the corresponding DateTimeZone object. + * + * If this is not possible or if $utc_offset_string does not match the expected pattern, + * boolean false is returned. + * + * @todo revise to eliminate all of these: maybe_get_tz_name, apply_offset, timezone_from_utc_offset, and adjust_timestamp + * + * @since 4.6.3 + * + * @param string $utc_offset_string + * + * @return DateTimeZone | bool + */ + public static function timezone_from_utc_offset( $utc_offset_string ) { + // Test for strings looking like "UTC-2" or "UTC+5.25" etc + if ( ! preg_match( '/^UTC[+-][0-9.]{1,4}$/', $utc_offset_string ) ) { + return false; + } + + // Breakdown into polarity, hours and minutes + $parts = explode( '.', substr( $utc_offset_string, 4 ) ); + $hours = (int) $parts[ 0 ]; + $fraction = isset( $parts[ 1 ] ) ? '0.' . (int) $parts[ 1 ] : 0; + $minutes = $fraction * 60; + $polarity = substr( $utc_offset_string, 3, 1 ); + + // Reassemble in the form +/-hhmm (ie "-0200" or "+0930") + $utc_offset = sprintf( $polarity . "%'.02d%'.02d", $hours, $minutes ); + + if ( '+0000' === $utc_offset || '-0000' === $utc_offset ) { + $utc_offset = 'UTC'; + } + + // Use this to build a new DateTimeZone + try { + return new DateTimeZone( $utc_offset ); + } catch ( Exception $e ) { + return false; + } + } + + /** + * Applies an time offset to the specified date time. + * + * @todo revise to eliminate all of these: maybe_get_tz_name, apply_offset, timezone_from_utc_offset, and adjust_timestamp + * + * @param string $datetime The date and time string in a valid date format. + * @param int|string $offset (string or numeric offset) + * @param bool $invert = false Whether the offset should be added (`true`) or + * subtracted (`false`); signum operations carry over so + * `-(-23) = +23`. + * + * @return string + */ + public static function apply_offset( $datetime, $offset, $invert = false ) { + // Normalize + $offset = strtolower( trim( $offset ) ); + + // Strip any leading "utc" text if set + if ( 0 === strpos( $offset, 'utc' ) ) { + $offset = substr( $offset, 3 ); + } + + // It's possible no adjustment will be needed + if ( 0 === (int) $offset ) { + return $datetime; + } + + // if the offset contains fractions like :15, :30 or :45 convert them + $supported_offsets = [ + '/:15$/' => '.25', + '/:30$/' => '.5', + '/:45$/' => '.75', + ]; + $offset = preg_replace( array_keys( $supported_offsets ), array_values( $supported_offsets ), $offset ); + + // Convert the offset to minutes for easier handling of fractional offsets + $offset = (int) ( $offset * 60 ); + + // Invert the offset? Useful for stripping an offset that has already been applied + if ( $invert ) { + $offset *= - 1; + } + + if ( $offset > 0 ) { + $offset = '+' . $offset; + } + + $offset = $offset . ' minutes'; + + $offset_datetime = date_create( $datetime ); + + if ( $offset_datetime && $offset_datetime->modify( $offset ) ) { + return $offset_datetime->format( Tribe__Date_Utils::DBDATETIMEFORMAT ); + } + + return $datetime; + } + + /** + * Try to figure out the Timezone name base on offset + * + * @since 4.0.7 + * + * @todo revise to eliminate all of these: maybe_get_tz_name, apply_offset, timezone_from_utc_offset, and adjust_timestamp + * + * @param string|int|float $timezone The timezone + * + * @return string The Guessed Timezone String + */ + public static function maybe_get_tz_name( $timezone ) { + if ( ! self::is_utc_offset( $timezone ) && ! is_numeric( $timezone ) ) { + return $timezone; + } + + if ( ! is_numeric( $timezone ) ) { + $offset = str_replace( 'utc', '', trim( strtolower( $timezone ) ) ); + } else { + $offset = $timezone; + } + + + // try to get timezone from gmt_offset, respecting daylight savings + $timezone = timezone_name_from_abbr( null, $offset * 3600, true ); + + // if that didn't work, maybe they don't have daylight savings + if ( false === $timezone ) { + $timezone = timezone_name_from_abbr( null, $offset * 3600, false ); + } + + // and if THAT didn't work, round the gmt_offset down and then try to get the timezone respecting daylight savings + if ( false === $timezone ) { + $timezone = timezone_name_from_abbr( null, (int) $offset * 3600, true ); + } + + // lastly if that didn't work, round the gmt_offset down and maybe that TZ doesn't do daylight savings + if ( false === $timezone ) { + $timezone = timezone_name_from_abbr( null, (int) $offset * 3600, false ); + } + + return $timezone; + } + + /** + * Accepts a unix timestamp and adjusts it so that when it is used to constitute + * a new datetime string, that string reflects the designated timezone. + * + * @todo revise to eliminate all of these: maybe_get_tz_name, apply_offset, timezone_from_utc_offset, and adjust_timestamp + * + * @deprecated 4.7.12 + * + * @param string $unix_timestamp + * @param string $tzstring + * + * @return string + */ + public static function adjust_timestamp( $unix_timestamp, $tzstring ) { + try { + $local = self::get_timezone( $tzstring ); + + $datetime = date_create_from_format( 'U', $unix_timestamp )->format( Tribe__Date_Utils::DBDATETIMEFORMAT ); + + // We prefer format('U') to getTimestamp() here due to our requirement for compatibility with PHP 5.2 + return date_create_from_format( 'Y-m-d H:i:s', $datetime, $local )->format( 'U' ); + } + catch( Exception $e ) { + return $unix_timestamp; + } + } + + /** + * Returns a valid timezone object built from the passed timezone or from the + * site one if a timezone in not passed. + * + * @since 4.9.5 + * + * @param string|null|DateTimeZone $timezone A DateTimeZone object, a timezone string + * or `null` to build an object using the site one. + * + * @return DateTimeZone The built DateTimeZone object. + */ + public static function build_timezone_object( $timezone = null ) { + if ( $timezone instanceof DateTimeZone ) { + return $timezone; + } + + /** @var Tribe__Cache $cache */ + $cache = tribe('cache'); + + if ( is_string( $timezone ) && $cached = $cache[ __METHOD__ . $timezone ] ) { + return clone $cached; + } + + $timezone = null === $timezone ? self::wp_timezone_string() : $timezone; + + try { + $object = new DateTimeZone( self::get_valid_timezone( $timezone ) ); + } catch ( Exception $e ) { + return new DateTimeZone( 'UTC' ); + } + + if ( is_string( $timezone ) ) { + $cache[ __METHOD__ . $timezone ] = $object; + } + + return $object; + } + + /** + * Parses the timezone string to validate or convert it into a valid one. + * + * @since 4.9.5 + * + * @param string|\DateTimeZone $timezone_candidate The timezone string candidate. + * + * @return string The validated timezone string or a valid timezone string alternative. + */ + public static function get_valid_timezone( $timezone_candidate ) { + if ( $timezone_candidate instanceof DateTimeZone ) { + return $timezone_candidate->getName(); + } + + $timezone_string = preg_replace( '/[+-]0$/', '', $timezone_candidate ); + $timezone_string = self::is_utc_offset( $timezone_string ) + ? self::generate_timezone_string_from_utc_offset( $timezone_string ) + : $timezone_string; + + return $timezone_string; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Tooltip/View.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Tooltip/View.php new file mode 100644 index 000000000..f1cab9b35 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Tooltip/View.php @@ -0,0 +1,138 @@ +set_template_origin( \Tribe__Main::instance() ); + $this->set_template_folder( 'src/views/tooltip' ); + + // Configures this templating class to extract variables + $this->set_template_context_extract( true ); + + // Uses the public folders + $this->set_template_folder_lookup( true ); + } + + /** + * Public wrapper for build method + * + * @since 4.9.8 + * + * @param array|string $message Array of messages or single message as string. + * @param array $args { + * List of arguments to override tooltip template. + * + * @var array $context Any additional context data you need to expose to this file (optional). + * @var string $classes Additional classes for the icon span (optional). + * @var string $direction Direction the tooltip should be from the trigger (down). + * @var string $icon dashicon classname to use, without the `dashicon-` (info). + * @var string $wrap_classes Classes for the tooltip wrapper (optional). + * } + * @return string A string of html for the tooltip. + */ + public function render_tooltip( $message, $args = [] ) { + if ( empty( $message ) ) { + return; + } + + /** @var \Tribe__Assets $assets */ + $assets = tribe( 'assets' ); + $assets->enqueue_group( 'tribe-tooltip' ); + + $html = $this->build_tooltip( $message, $args ); + + return $html; + } + + /** + * Factory method for tooltip HTML + * + * @since 4.9.8 + * + * @param array|string $message array of messages or single message as string. + * @param array $args { + * List of arguments to override tooltip template. + * + * @var array $context Any additional context data you need to expose to this file (optional). + * @var string $classes Additional classes for the icon span (optional). + * @var string $direction Direction the tooltip should be from the trigger (down). + * @var string $icon dashicon classname to use, without the `dashicon-` (info). + * @var string $wrap_classes Classes for the tooltip wrapper (optional). + * } + * @return string A string of html for the tooltip. + */ + private function build_tooltip( $message, $original_args ) { + $default_args = [ + 'classes' => '', + 'context' => '', + 'direction' => 'down', + 'icon' => 'info', + 'wrap_classes' => '', + ]; + + $args = wp_parse_args( $original_args, $default_args ); + + // Check for message to be passed. + if ( empty( $message ) ) { + return ''; + } + + // Setup message as an array of messages + $messages = (array) $message; + + $args['messages'] = $messages; + + ob_start(); + + /** + * Allow us to filter the tooltip template + * + * @since 4.9.8 + * + * @param string $template The tooltip template name. + * @param array $args Extra arguments, defaults include icon, classes, direction, and context. + */ + $template_name = apply_filters( 'tribe_tooltip_template', 'tooltip', $args ); + + $template = $this->template( $template_name, $args, false ); + + if ( ! empty( $template ) ) { + echo $template; + } + + $html = ob_get_clean(); + + /** + * Allow us to filter the tooltip output + * + * @since 4.9.8 + * + * @param string $html The tooltip HTML. + * @param array $messages An array of message strings. + * @param array $args Extra arguments, defaults include icon, classes, direction, and context. + */ + return apply_filters( 'tribe_tooltip_html', $html, $messages, $args ); + } + +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Tracker.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Tracker.php new file mode 100644 index 000000000..20e541391 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Tracker.php @@ -0,0 +1,471 @@ +$field ) ) { + return false; + } + + if ( isset( $new->$field ) && ! isset( $old->$field ) ) { + return true; + } + + if ( $new->$field !== $old->$field ) { + return true; + } + + return false; + } + + /** + * Get the date(timestamp) of last modification for a tracked field. + * + * @since 4.12.3 + * + * @param string $meta_key The key for the meta field we're interested in. + * @param int $post_id The ID of the post to check. + * + * @return boolean|string The change timestamp or false if the field is not found/empty. + */ + public function get_modified_date( $meta_key, $post_id ) { + $modified = get_post_meta( $post_id, self::$field_key, true ); + + // If the key is missing or empty/null return false - no recorded change. + return Tribe__Utils__Array::get( $modified, $meta_key, false ); + } + + /** + * Easy way to see currently which post types are being tracked by our code. + * + * @return array + */ + public function get_post_types() { + // By default we are not tracking anything + $tracked_post_types = []; + + /** + * Adds a way for Developers to add and remove which post types will be tracked + * + * Note: Removing any of the default methods will affect how we deal with fields + * affected by the authority settings defined on this installation + * + * @var array + */ + $tracked_post_types = (array) apply_filters( 'tribe_tracker_post_types', $this->tracked_post_types ); + + return $tracked_post_types; + } + + /** + * Easy way to see currenlty which meta values are been tracked by our code + * + * @return array + */ + public function get_excluded_meta_keys() { + // By default we are not tracking anything + $excluded_keys = [ + '_edit_lock', + self::$field_key, + ]; + + /** + * Adds a way for Developers remove Meta Keys that shouldn't be tracked + * + * Note: Removing any of the default methods will affect how we deal with fields + * affected by the authority settings defined on this installation + * + * @var array + */ + $excluded_keys = (array) apply_filters( 'tribe_tracker_excluded_meta_keys', $excluded_keys ); + + return $excluded_keys; + } + + /** + * Make sure we are tracking all meta fields related on the correct Post Types + * + * @since 4.5 + * + * @param int $meta_id Meta ID + * @param int $post_id Post ID. + * @param string $meta_key Meta key. + */ + public function register_added_deleted_meta( $meta_id, $post_id, $meta_key ) { + /** + * Allows toggling the Modified fields tracking + * @var bool + */ + $is_tracking_modified_fields = (bool) apply_filters( 'tribe_tracker_enabled', true ); + + // Bail if we shouldn't be tracking modifications + if ( false === $is_tracking_modified_fields ) { + return; + } + + // Try to fetch the post object + $post = get_post( $post_id ); + + // We only go forward if we have the Post Object + if ( ! $post instanceof WP_Post ) { + return; + } + + // Fetch from a unified method which meta keys are been excluded + $excluded_keys = $this->get_excluded_meta_keys(); + + // Bail when this meta is set to be excluded + if ( in_array( $meta_key, $excluded_keys ) ) { + return; + } + + // Fetch from a unified method which post types are been tracked + $tracked_post_types = $this->get_post_types(); + + // Only track if the meta is from a post that is been tracked + if ( ! in_array( $post->post_type, $tracked_post_types ) ) { + return; + } + + // Gets the Current Timestamp + $now = current_time( 'timestamp' ); + + // Fetch the current data from the modified fields + $modified = get_post_meta( $post->ID, self::$field_key, true ); + if ( ! is_array( $modified ) ) { + $modified = []; + } + + // If we got here we will update the Modified Meta + $modified[ $meta_key ] = $now; + + // Actually do the Update + update_post_meta( $post->ID, self::$field_key, $modified ); + } + + /** + * Make sure we are tracking all meta fields related to the correct Post Types + * + * @since 4.5 + * + * @param null|bool $check Whether to allow updating metadata for the given type. + * @param int $post_id Post ID. + * @param string $meta_key Meta key. + * @param mixed $meta_value Meta value. Must be serializable if non-scalar. + * @param mixed $prev_value Previous Value of the Meta, allowing to check the data + * + * @return null|bool This should be ignored, only used to not break the WordPress filters + */ + public function filter_watch_updated_meta( $check, $post_id, $meta_key, $meta_value, $prev_value = null ) { + /** + * Allows toggling the Modified fields tracking + * @var bool + */ + $is_tracking_modified_fields = (bool) apply_filters( 'tribe_tracker_enabled', true ); + + // Bail if we shouldn't be tracking modifications + if ( false === $is_tracking_modified_fields ) { + return $check; + } + + // Matching the WordPress filter, we actually don't care about Check at all! + if ( null !== $check ) { + return (bool) $check; + } + + // Try to fetch the post object + $post = get_post( $post_id ); + + // We only go forward if we have the Post Object + if ( ! $post instanceof WP_Post ) { + return $check; + } + + // Fetch from a unified method which meta keys are been excluded + $excluded_keys = $this->get_excluded_meta_keys(); + + // Bail when this meta is set to be excluded + if ( in_array( $meta_key, $excluded_keys ) ) { + return $check; + } + + // Fetch from a unified method which post types are been tracked + $tracked_post_types = $this->get_post_types(); + + // Only track if the meta is from a post that is been tracked + if ( ! in_array( $post->post_type, $tracked_post_types ) ) { + return $check; + } + + if ( empty( $prev_value ) ) { + $prev_value = get_post_meta( $post->ID, $meta_key, true ); + } + + // We don't care if the value didn't actually change + if ( $prev_value == $meta_value ) { + return $check; + } + + // Gets the Current Timestamp + $now = current_time( 'timestamp' ); + + // Fetch the current data from the modified fields + $modified = get_post_meta( $post->ID, self::$field_key, true ); + if ( ! is_array( $modified ) ) { + $modified = []; + } + + // If we got here we will update the Modified Meta + $modified[ $meta_key ] = $now; + + // Avoid loops! + remove_filter( 'update_post_metadata', [ $this, 'filter_watch_updated_meta' ], PHP_INT_MAX - 1 ); + // Actually do the Update + update_post_meta( $post->ID, self::$field_key, $modified ); + // Safe to filter again. + add_filter( 'update_post_metadata', [ $this, 'filter_watch_updated_meta' ], PHP_INT_MAX - 1, 5 ); + + // We need to return this, because we are still on a filter + return $check; + } + + /** + * Tracks fields that are changed when an event is updated + * + * @param int $post_id Post ID + * @param WP_Post $post_after New post object + * @param WP_Post $post_before Old post object + */ + public function filter_watch_post_fields( $post_id, $post_after, $post_before ) { + /** + * Allows toggling the Modified fields tracking + * @var bool + */ + $is_tracking_modified_fields = (bool) apply_filters( 'tribe_tracker_enabled', true ); + + // Bail if we shouldn't be tracking modifications + if ( false === $is_tracking_modified_fields ) { + return; + } + + // Fetch from a unified method which post types are been tracked + $tracked_post_types = $this->get_post_types(); + + // Only track if the meta is from a post that is been tracked + if ( ! in_array( $post_before->post_type, $tracked_post_types ) ) { + return; + } + + // Fetch the current Time + $now = current_time( 'timestamp' ); + + if ( ! $modified = get_post_meta( $post_id, self::$field_key, true ) ) { + $modified = []; + } + + $fields_to_check_for_changes = [ + 'post_title', + 'post_content', + 'post_excerpt', + 'post_status', + 'post_type', + 'post_parent', + ]; + + foreach ( $fields_to_check_for_changes as $field ) { + if ( ! $this->has_field_changed( $field, $post_after, $post_before ) ) { + continue; + } + + $modified[ $field ] = $now; + } + + if ( $modified ) { + update_post_meta( $post_id, self::$field_key, $modified ); + } + } + + /** + * Track term changes for the tracked post types and terms. + * + * Meant to run on the `set_object_terms` action. + * + * @see wp_set_object_terms() + * + * @param $object_id + * @param $terms + * @param $tt_ids + * @param $taxonomy + * @param $append + * @param $old_tt_ids + * + * @return bool `true` if the post type and taxonomy are tracked, `false` otherwise. + */ + public function track_taxonomy_term_changes( $object_id, $terms, $tt_ids, $taxonomy, $append, $old_tt_ids ) { + /** + * Allows toggling the post taxonomy terms tracking + * + * @var bool $track_terms Whether the class is currently tracking terms or not. + */ + $is_tracking_taxonomy_terms = (bool) apply_filters( 'tribe_tracker_enabled_for_terms', $this->track_terms ); + + if ( false === $is_tracking_taxonomy_terms ) { + return false; + } + + $tracked_post_types = $this->get_post_types(); + + $post_id = tribe_post_exists( $object_id ); + + if ( + empty( $post_id ) + || ! ( $post = get_post( $post_id ) ) + || ! in_array( $post->post_type, $tracked_post_types ) + ) { + return false; + } + + $tracked_taxonomies = $this->get_taxonomies(); + + if ( ! in_array( $taxonomy, $tracked_taxonomies ) ) { + return false; + } + + if ( ! $modified = get_post_meta( $post->ID, self::$field_key, true ) ) { + $modified = []; + } + + if ( $tt_ids == $old_tt_ids ) { + // nothing to update, still we did the job + return true; + } + + $modified[ $taxonomy ] = time(); + update_post_meta( $post->ID, self::$field_key, $modified ); + + return true; + } + + /** + * Easy way to see currently which taxonomies are been tracked by our code. + * + * @return array + */ + public function get_taxonomies() { + /** + * Adds a way for Developers to add and remove which taxonomies will be tracked + * + * Note: Removing any of the default methods will affect how we deal with fields + * affected by the authority settings defined on this installation + * + * @var array $tracked_taxonomies An array of the tracker taxonomies names. + */ + $tracked_taxonomies = (array) apply_filters( 'tribe_tracker_taxonomies', $this->tracked_taxonomies ); + + return $tracked_taxonomies; + } + + /** + * Whether taxonomy term changes should be tracked or not by the class. + * + * @param bool $track_terms + */ + public function should_track_terms( $track_terms ) { + $this->track_terms = $track_terms; + } + + /** + * Sets the taxonomies the tracker should track. + * + * @param array $tracked_taxonomies + */ + public function set_tracked_taxonomies( array $tracked_taxonomies ) { + $this->tracked_taxonomies = $tracked_taxonomies; + } + + /** + * Sets the post types the tracker should track. + * + * @param array $tracked_post_types + */ + public function set_tracked_post_types( array $tracked_post_types ) { + $this->tracked_post_types = $tracked_post_types; + } + + /** + * Make sure to remove the changed field if the event is deleted to ensure there are no left meta fields when + * the event is deleted. + * + * @since 4.7.6 + * + * @param int Post ID + * @return bool + */ + public function cleanup_meta_fields( $post_id ) { + return delete_post_meta( (int) $post_id, self::$field_key ); + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Traits/Cache_User.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Traits/Cache_User.php new file mode 100644 index 000000000..9d73bbc01 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Traits/Cache_User.php @@ -0,0 +1,145 @@ +warmup_cache( 'computation', WEEK_IN_SECONDS, 'save_post' ); + * if( isset( $this->computation_cache[$key] ) ){ + * return $this->computation_cache[$key]; + * } + * + * $computated = 23; + * + * $this->computation_cache[$key] = $computated; + * + * return $computated; + * } + * } + * ``` + * + * @package Tribe + */ + +namespace Tribe\Traits; + +/** + * Class Cache_User + * + * @since 4.9.11 + * + * @package Tribe + */ +trait Cache_User { + + + /** + * An array of caches and data for each key. + * + * @var array + */ + protected $caches = []; + + /** + * Dumps the temporary cache to the persistent one. + */ + public function dump_cache() { + foreach ( $this->caches as $key => $cache ) { + + list( $cache, $prefix, $expiration, $expiration_trigger ) = array_values( $this->caches[ $key ] ); + + if ( isset( $this->{$key . '_cache'} ) ) { + /** @var \Tribe__Cache $cache */ + $cache->set( $prefix . $key, + $this->{$key . '_cache'}, + $expiration, + $expiration_trigger + ); + } + } + } + + /** + * Warms up one of the caches used by the class, if not warmed up already. + * + * @since 4.9.11 + * + * @param string $key The key of the cache to warm up. + * @param int $expiration The expiration, in seconds, to set on the cache. + * @param string $expiration_trigger The expiration trigger to set on the cache; this should be one of those + * supported by the `Tribe__Cache_Listener` class. + * + * @see \Tribe__Cache_Listener::add_hooks() + */ + protected function warmup_cache( $key, $expiration = 0, $expiration_trigger = '' ) { + if ( ! isset( $this->caches[ $key ] ) ) { + $this_class = get_class( $this ); + + if ( ! property_exists( $this, $key . '_cache' ) ) { + throw new \BadMethodCallException( + sprintf( + 'The %s class should explicitly define a "%s" property to use the %s trait.', + $this_class, + $key . '_cache', + __TRAIT__ + ) + ); + } + + $this->caches[ $key ] = [ + 'cache_object' => tribe( 'cache' ), + 'prefix' => $this_class, + 'expiration' => $expiration, + 'expiration_trigger' => $expiration_trigger, + ]; + } + + list( $cache, $prefix, $expiration, $expiration_trigger ) = array_values( $this->caches[ $key ] ); + + if ( null === $this->{$key . '_cache'} ) { + /** @var \Tribe__Cache $cache */ + $this->{$key . '_cache'} = $cache->get( + $prefix . $key, + $expiration_trigger, + [], + $expiration + ); + if ( false === $this->{$key . '_cache'} ) { + $this->{$key . '_cache'} = []; + } + } + } + + /** + * Resets the instance caches for the this instance. + * + * @since 4.11.0 + * + * @return string[] A list of the emptied cache properties. + */ + public function reset_caches() { + $emptied = []; + foreach ( array_keys( $this->caches ) as $key ) { + $emptied[] = $key; + $this->{"{$key}_cache"} = []; + } + + return $emptied; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Traits/With_DB_Lock.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Traits/With_DB_Lock.php new file mode 100644 index 000000000..5eb8f28cc --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Traits/With_DB_Lock.php @@ -0,0 +1,50 @@ +acquire_db_lock( $lock_key ); + } + + /** + * Releases the database lock of the record. + * + * Release a not held db lock will return `null`, not `false`. + * + * @since 4.12.6 + * + * @param string $lock_key The name of the lock to release. + * + * @return bool Whether the lock was correctly released or not. + */ + private function release_db_lock( $lock_key ) { + return tribe( 'db-lock' )->release_db_lock( $lock_key ); + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Traits/With_Meta_Updates_Handling.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Traits/With_Meta_Updates_Handling.php new file mode 100644 index 000000000..f11e4a657 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Traits/With_Meta_Updates_Handling.php @@ -0,0 +1,66 @@ +taxonomies ) ) { + // If we're here, then the developer made an error: throw an exception to bring this up as early as possible. + throw new \RuntimeException( + 'The ' . __TRAIT__ . ' trait requires the user class to define a $taxonomies array parameter.' + ); + } + + return in_array( $key, $this->taxonomies, true ); + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Updater.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Updater.php new file mode 100644 index 000000000..d5b6119f2 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Updater.php @@ -0,0 +1,180 @@ +current_version = $current_version; + } + + /** + * We've had problems with the notoptions and + * alloptions caches getting out of sync with the DB, + * forcing an eternal update cycle + * + * @since 4.9.4 + * + */ + protected function clear_option_caches() { + wp_cache_delete( 'notoptions', 'options' ); + wp_cache_delete( 'alloptions', 'options' ); + } + + /** + * Run Updates for a Plugin + * + * @since 4.9.4 + * + */ + public function do_updates() { + $this->clear_option_caches(); + $updates = $this->get_update_callbacks(); + uksort( $updates, 'version_compare' ); + + try { + foreach ( $updates as $version => $callback ) { + + if ( ! $this->is_new_install() && version_compare( $version, $this->current_version, '<=' ) && $this->is_version_in_db_less_than( $version ) ) { + call_user_func( $callback ); + } + } + + foreach ( $this->get_constant_update_callbacks() as $callback ) { + call_user_func( $callback ); + } + + $this->update_version_option( $this->current_version ); + } catch ( Exception $e ) { + // fail silently, but it should try again next time + } + } + + /** + * Update Version Number for a Plugin + * + * @since 4.9.4 + * + * @param int $new_version the current version number of a plugin + */ + public function update_version_option( $new_version ) { + Tribe__Settings_Manager::set_option( $this->version_option, $new_version ); + } + + /** + * Returns an array of callbacks with version strings as keys. + * Any key higher than the version recorded in the DB + * and lower than $this->current_version will have its + * callback called. + * + * @since 4.9.4 + * + * @return array + */ + public function get_update_callbacks() { + return []; + } + + /** + * Returns an array of callbacks that should be called + * every time the version is updated + * + * @since 4.9.4 + * + * @return array + */ + public function get_constant_update_callbacks() { + return [ + [ $this, 'flush_rewrites' ], + ]; + } + + /** + * Get version from Tribe Settings for the Plugin + * + * @since 4.9.4 + * + * @return mixed the version number of the plugin saved in the options + */ + public function get_version_from_db() { + return Tribe__Settings_Manager::get_option( $this->version_option ); + } + + /** + * Returns true if the version in the DB is less than the provided version + * + * @since 4.9.4 + * + * @return boolean + */ + public function is_version_in_db_less_than( $version ) { + $version_in_db = $this->get_version_from_db(); + + return ( version_compare( $version, $version_in_db ) > 0 ); + } + + /** + * Returns true if this is a new install + * + * @since 4.9.4 + * + * @return boolean + */ + public function is_new_install() { + $version_in_db = $this->get_version_from_db(); + + return empty( $version_in_db ); + } + + /** + * Returns true if an update is required + * + * @since 4.9.4 + * + * @return boolean + */ + public function update_required() { + return $this->is_version_in_db_less_than( $this->current_version ); + } + + /** + * Flush Rewrite rules + * + * @since 4.9.4 + * + */ + public function flush_rewrites() { + // run after 'init' to ensure that all CPTs are registered + add_action( 'wp_loaded', 'flush_rewrite_rules' ); + } + + /** + * Reset update flags. All updates past $this->reset_version will + * run again on the next page load + * + * @since 4.9.4 + * + */ + public function reset() { + $this->update_version_option( $this->reset_version ); + } + +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Array.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Array.php new file mode 100644 index 000000000..0f7961886 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Array.php @@ -0,0 +1,732 @@ + $value ) { + if ( ! is_array( $value ) && ! is_object( $value ) ) { + $data[ $key ] = esc_attr( trim( $value ) ); + } + if ( is_array( $value ) ) { + $data[ $key ] = self::escape_multidimensional_array( $value ); + } + } + + return $data; + } + + /** + * Returns an array of values obtained by using the keys on the map; keys + * that do not have a match in map are discarded. + * + * To discriminate from not found results and legitimately `false` + * values from the map the `$found` parameter will be set by reference. + * + * @since 4.7.19 + * + * @param string|array $keys One or more keys that should be used to get + * the new values + * @param array $map An associative array relating the keys to the new + * values. + * @param bool $found When using a single key this argument will be + * set to indicate whether the mapping was successful + * or not. + * + * @return array|mixed|false An array of mapped values, a single mapped value when passing + * one key only or `false` if one key was passed but the key could + * not be mapped. + */ + public static function map_or_discard( $keys, array $map, &$found = true ) { + $hash = md5( time() ); + $mapped = []; + + foreach ( (array) $keys as $key ) { + $meta_key = Tribe__Utils__Array::get( $map, $key, $hash ); + if ( $hash === $meta_key ) { + continue; + } + $mapped[] = $meta_key; + } + + $found = (bool) count( $mapped ); + + if ( is_array( $keys ) ) { + return $mapped; + } + + return $found ? $mapped[0] : false; + } + + /** + * Duplicates any key prefixed with '_' creating an un-prefixed duplicate one. + * + * The un-prefixing and duplication is recursive. + * + * @since 4.9.5 + * + * @param mixed $array The array whose keys should be duplicated. + * @param bool $recursive Whether the un-prefixing and duplication should be + * recursive or shallow. + * + * @return array The array with the duplicate, unprefixed, keys or the + * original input if not an array. + */ + public static function add_unprefixed_keys_to( $array, $recursive = false ) { + if ( ! is_array( $array ) ) { + return $array; + } + + $unprefixed = []; + foreach ( $array as $key => $value ) { + if ( $recursive && is_array( $value ) ) { + $value = self::add_unprefixed_keys_to( $value, true ); + // And also add it to the original array. + $array[ $key ] = array_merge( $array[ $key ], $value ); + } + + if ( 0 !== strpos( $key, '_' ) ) { + continue; + } + $unprefixed[ substr( $key, 1 ) ] = $value; + } + + return array_merge( $array, $unprefixed ); + } + + /** + * Filters an associative array non-recursively, keeping only the values attached + * to keys starting with the specified prefix. + * + * @since 4.9.5 + * + * @param array $array The array to filter. + * @param string $prefix The prefix, or prefixes, of the keys to keep. + * + * @return array The filtered array. + */ + public static function filter_prefixed( array $array, $prefix ) { + $prefixes = implode( '|', array_map( 'preg_quote', (array) $prefix ) ); + $pattern = '/^(' . $prefixes . ')/'; + $filtered = []; + foreach ( $array as $key => $value ) { + if ( ! preg_match( $pattern, $key ) ) { + continue; + } + $filtered[ $key ] = $value; + } + + return $filtered; + } + + /** + * Flattens an array transforming each value that is an array and only contains one + * element into that one element. + * + * Typical use case is to flatten arrays like those returned by `get_post_meta( $id )`. + * Empty arrays are replaced with an empty string. + * + * @since 4.9.5 + * + * @param array $array The array to flatten. + * + * @return array The flattened array. + */ + public static function flatten( array $array ) { + foreach ( $array as $key => &$value ) { + if ( ! is_array( $value ) ) { + continue; + } + + $count = count( $value ); + + switch ( $count ) { + case 0: + $value = ''; + break; + case 1: + $value = reset( $value ); + break; + default: + break; + } + } + + return $array; + } + + /** + * Duplicates any key not prefixed with '_' creating a prefixed duplicate one. + * + * The prefixing and duplication is recursive. + * + * @since 4.9.5 + * + * @param mixed $array The array whose keys should be duplicated. + * @param bool $recursive Whether the prefixing and duplication should be + * recursive or shallow. + * + * @return array The array with the duplicate, prefixed, keys or the + * original input if not an array. + */ + public static function add_prefixed_keys_to( $array, $recursive = false ) { + if ( ! is_array( $array ) ) { + return $array; + } + + $prefixed = []; + foreach ( $array as $key => $value ) { + if ( $recursive && is_array( $value ) ) { + $value = self::add_prefixed_keys_to( $value, true ); + // And also add it to the original array. + $array[ $key ] = array_merge( $array[ $key ], $value ); + } + + if ( 0 === strpos( $key, '_' ) ) { + continue; + } + + $prefixed[ '_' . $key ] = $value; + } + + return array_merge( $array, $prefixed ); + } + + /** + * Recursively key-sort an array. + * + * @since 4.9.5 + * + * @param array $array The array to sort, modified by reference. + * + * @return bool The sorting result. + */ + public static function recursive_ksort( array &$array ) { + foreach ( $array as &$value ) { + if ( is_array( $value ) ) { + static::recursive_ksort( $value ); + } + } + + return ksort( $array ); + } + + /** + * Returns the value associated with the first index, among the indexes, that is set in the array.. + * + * @since 4.9.11 + * + * @param array $array The array to search. + * @param array $indexes The indexes to search; in order the function will look from the first to the last. + * @param null $default The value that will be returned if the array does not have any of the indexes set. + * + * @return mixed|null The set value or the default value. + */ + public static function get_first_set( array $array, array $indexes, $default = null ) { + foreach ( $indexes as $index ) { + if ( ! isset( $array[ $index ] ) ) { + continue; + } + + return $array[ $index ]; + } + + return $default; + } + + /** + * Discards everything other than array values having string keys and scalar values, ensuring a + * one-dimensional, associative array result. + * + * @link https://www.php.net/manual/language.types.array.php Keys cast to non-strings will be discarded. + * + * @since 4.12.2 + * + * @param array $array + * + * @return array Associative or empty array. + */ + public static function filter_to_flat_scalar_associative_array( array $array ) { + $result = []; + + if ( ! is_array( $array ) ) { + return $result; + } + + foreach ( $array as $k => $v ) { + if ( ! is_string( $k ) ) { + continue; + } + + if ( ! is_scalar( $v ) ) { + continue; + } + + $result[ $k ] = $v; + } + + return $result; + } + + /** + * Build an array from migrating aliased key values to their canonical key values, removing all alias keys. + * + * If the original array has values for both the alias and its canonical, keep the canonical's value and + * discard the alias' value. + * + * @since 4.12.2 + * + * @param array $original An associative array of values, such as passed shortcode arguments. + * @param array $alias_map An associative array of aliases: key as alias, value as mapped canonical. + * Example: [ 'alias' => 'canonical', 'from' => 'to', 'that' => 'becomes_this' ] + * + * @return array + */ + public static function parse_associative_array_alias( array $original, array $alias_map ) { + // Ensure array values. + $original = (array) $original; + $alias_map = static::filter_to_flat_scalar_associative_array( (array) $alias_map ); + + // Fail gracefully if alias array wasn't setup as [ 'from' => 'to' ]. + if ( empty( $alias_map ) ) { + return $original; + } + + $result = $original; + + // Parse aliases. + foreach ( $alias_map as $from => $to ) { + // If this alias isn't in use, go onto the next. + if ( ! isset( $result[ $from ] ) ) { + continue; + } + + // Only allow setting alias value if canonical value is not already present. + if ( ! isset( $result[ $to ] ) ) { + $result[ $to ] = $result[ $from ]; + } + + // Always remove the alias key. + unset( $result[ $from ] ); + } + + return $result; + } + + /** + * Stringifies the numeric keys of an array. + * + * @since 4.12.14 + * + * @param array $input The input array whose keys should be stringified. + * @param string|null $prefix The prefix that should be use to stringify the keys, if not provided + * then it will be generated. + * + * @return array The input array with each numeric key stringified. + */ + public static function stringify_keys( array $input, $prefix = null ) { + $prefix = null === $prefix ? uniqid( 'sk_', true ) : $prefix; + $visitor = static function ( $key, $value ) use ( $prefix ) { + $string_key = is_numeric( $key ) ? $prefix . $key : $key; + + return [ $string_key, $value ]; + }; + + return static::array_visit_recursive( $input, $visitor ); + } + + /** + * The inverse of the `stringify_keys` method, it will restore numeric keys for previously + * stringified keys. + * + * @since 4.12.14 + * + * @param array $input The input array whose stringified keys should be + * destringified. + * @param string $prefix The prefix that should be used to target only specific string keys. + * + * @return array The input array, its stringified keys destringified. + */ + public static function destringify_keys( array $input, $prefix = 'sk_' ) { + $visitor = static function ( $key, $value ) use ( $prefix ) { + $destringified_key = 0 === self::strpos( $key, $prefix ) ? null : $key; + + return [ $destringified_key, $value ]; + }; + + return static::array_visit_recursive( $input, $visitor ); + } + + /** + * Recursively visits all elements of an array applying the specified callback to each element + * key and value. + * + * @since 4.12.14 + * + * @param array $input The input array whose nodes should be visited. + * @param callable $visitor A callback function that will be called on each array item; the callback will + * receive the item key and value as input and should return an array that contains + * the update key and value in the shape `[ , ]`. Returning a `null` + * key will cause the element to be removed from the array. + */ + public static function array_visit_recursive( $input, callable $visitor ) { + if ( ! is_array( $input ) ) { + return $input; + } + + $return = []; + + foreach ( $input as $key => &$value ) { + if ( is_array( $value ) ) { + $value = static::array_visit_recursive( $value, $visitor ); + } + // Ensure visitors can quickly return `null` to remove an element. + list( $updated_key, $update_value ) = array_replace( [ $key, $value ], (array) $visitor( $key, $value ) ); + if ( false === $updated_key ) { + // Visitor will be able to remove an element by returning a `false` key for it. + continue; + } + if ( null === $updated_key ) { + // Automatically assign the first available numeric index to the element. + $return[] = $update_value; + } else { + $return[ $updated_key ] = $update_value; + } + } + + return $return; + } + + /** + * Recursively remove associative, non numeric, keys from an array. + * + * @since 4.12.14 + * + * @param array $input The input array. + * + * @return array An array that only contains integer keys at any of its levels. + */ + public static function remove_numeric_keys_recursive( array $input ) { + return self::array_visit_recursive( + $input, + static function ( $key ) { + return is_numeric( $key ) ? false : $key; + } + ); + } + + /** + * Recursively remove numeric keys from an array. + * + * @since 4.12.14 + * + * @param array $input The input array. + * + * @return array An array that only contains non numeric keys at any of its levels. + */ + public static function remove_string_keys_recursive( array $input ) { + return self::array_visit_recursive( + $input, + static function ( $key ) { + return !is_numeric( $key ) ? false : $key; + } + ); + } + + /** + * Merges two or more arrays in the nested format used by WP_Query arguments preserving and merging them correctly. + * + * The method will recursively replace named keys and merge numeric keys. The method takes its name from its intended + * primary use, but it's not limited to query arguments only. + * + * @since 4.12.14 + * + * @param array ...$arrays A set of arrays to merge. + * + * @return array The recursively merged array. + */ + public static function merge_recursive_query_vars( array ...$arrays ) { + if ( ! count( $arrays ) ) { + return []; + } + + // Temporarily transform numeric keys to string keys generated with time-related randomness. + $stringified = array_map( [ static::class, 'stringify_keys' ], $arrays ); + // Replace recursive will recursively replace any entry that has the same string key, stringified keys will never match due to randomness. + $merged = array_replace_recursive( ...$stringified ); + + // Finally destringify the keys to return something that will resemble, in shape, the original arrays. + return static::destringify_keys( $merged ); + } + + /** + * Shapes, filtering it, an array to the specified expected set of required keys. + * + * @since 5.0.0 + * + * @param array $array The input array to shape. + * @param array $shape The shape to update the array with. It should only define keys + * or arrays of keys. Keys that have no values will be set to `null`. + * To add the key only if set, prefix the key with `?`, e.g. `?foo`. + * + * @return array The input array shaped and ordered per the shape. + */ + public static function shape_filter( array $array, array $shape ): array { + $shaped = []; + foreach ( $shape as $shape_index => $shape_key ) { + $optional = is_array( $shape_key ) ? + strpos( $shape_index, '?' ) === 0 + : strpos( $shape_key, '?' ) === 0; + + if ( is_array( $shape_key ) ) { + $shape_index = $optional ? substr( $shape_index, 1 ) : $shape_index; + if ( $optional && ! isset( $array[ $shape_index ] ) ) { + continue; + } + $shaped[ $shape_index ] = self::shape_filter( $array[$shape_index] ?? [], $shape_key ); + } else { + $shape_key = $optional ? substr( $shape_key, 1 ) : $shape_key; + if ( ! isset( $array[ $shape_key ] ) && $optional ) { + continue; + } + $shaped[ $shape_key ] = $array[ $shape_key ] ?? null; + } + } + + return $shaped; + } + + /** + * Searches an array using a callback and returns the index of the first match. + * + * This method fills the gap left by the non-existence of an `array_usearch` function. + * + * @since 5.0.0 + * + * @param mixed $needle The element to search in the array. + * @param array $haystack The array to search. + * @param callable $callback A callback function with signature `fn($needle, $value, $key) :bool` + * that will be used to find the first match of needle in haystack. + * + * @return string|int|false Either the index of the first match or `false` if no match was found. + */ + public static function usearch( $needle, array $haystack, callable $callback ) { + foreach ( $haystack as $key => $value ) { + if ( $callback( $needle, $value, $key ) ) { + return $key; + } + } + + return false; + } + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Body_Classes.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Body_Classes.php new file mode 100644 index 000000000..7f8da5d37 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Body_Classes.php @@ -0,0 +1,350 @@ + true, 'class => false ] + * + * @var array + */ + protected $classes = []; + + /** + * Stores all the admin classes. + * In the format: ['class' => true, 'class => false ] + * + * @var array + */ + protected $admin_classes = []; + + /** + * Queue-aware method to get the classes array. + * Returns the array of classes to add. + * + * @since 4.12.6 + * + * @param string $queue The queue we want to get 'admin', 'display', 'all'. + * @return array A map of the classes for the queue. + */ + public function get_classes( $queue = 'display' ) { + switch( $queue ) { + case 'admin': + return $this->admin_classes; + break; + case 'all': + return array_merge( $this->classes, $this->admin_classes ); + break; + default: + return $this->classes; + break; + } + } + + /** + * Returns the array of classnames to add + * + * @since 4.12.6 + * + * @param string $queue The queue we want to get 'admin', 'display', 'all'. + * @return array The list of class names. + */ + public function get_class_names( $queue = 'display' ) { + $classes = $this->get_classes( $queue ); + + return array_keys( + array_filter( + $classes, + static function( $v ) { + return $v; + }, + ARRAY_FILTER_USE_KEY + ) + ); + } + + /** + * Checks if a class is in the queue, + * wether it's going to be added or not. + * + * @since 4.12.6 + * + * @param string $class The class we are checking for. + * @param string $queue The queue we want to check 'admin', 'display', 'all' + * @return boolean Whether a class exists or not in the queue. + */ + public function class_exists( $class, $queue = 'display' ) { + $classes = $this->get_classes( $queue ); + + return array_key_exists( $class, $classes ); + } + + /** + * Checks if a class is in the queue and going to be added. + * + * @since 4.12.6 + * + * @param string $class The class we are checking for. + * @param string $queue The queue we want to check 'admin', 'display', 'all' + * @return boolean Whether a class is currently queued or not. + */ + public function class_is_enqueued( $class, $queue = 'display' ) { + $classes = $this->get_classes( $queue ); + if ( ! $this->class_exists( $class, $queue ) ) { + return false; + } + + return $classes[ $class ]; + } + + /** + * Dequeues a class. + * + * @since 4.12.6 + * + * @param string $class + * @param string $queue The queue we want to alter 'admin', 'display', 'all' + * @return boolean + */ + public function dequeue_class( $class, $queue = 'display' ) { + if ( ! $this->class_exists( $class, $queue ) ) { + return false; + } + + if ( 'admin' !== $queue ) { + $this->classes[ $class ] = false; + } + + if ( 'display' !== $queue ) { + $this->admin_classes[ $class ] = false; + } + + return true; + + } + + /** + * Enqueues a class. + * + * @since 4.12.6 + * + * @param string $class + * @param string $queue The queue we want to alter 'admin', 'display', 'all' + * @return false + */ + public function enqueue_class( $class, $queue = 'display' ) { + if ( ! $this->class_exists( $class, $queue ) ) { + return false; + } + + if ( 'admin' !== $queue ) { + $this->classes[ $class ] = true; + } + + if ( 'display' !== $queue ) { + $this->admin_classes[ $class ] = true; + } + + return true; + } + + /** + * Add a single class to the queue. + * + * @since 4.12.6 + * + * @param string $class The class to add. + * @param string $queue The queue we want to alter 'admin', 'display', 'all' + * @return void + */ + public function add_class( $class, $queue = 'display' ) { + if ( empty( $class ) ) { + return; + } + + if ( is_array( $class ) ) { + $this->add_classes( $class, $queue ); + } elseif ( $this->should_add_body_class_to_queue( $class, $queue ) ) { + + $class = sanitize_html_class( $class ); + + if ( 'admin' !== $queue ) { + $this->classes[ $class ] = true ; + } + + if ( 'display' !== $queue ) { + $this->admin_classes[ $class ] = true ; + } + + } + } + + /** + * Add an array of classes to the queue. + * + * @since 4.12.6 + * + * @param array $class The classes to add. + * @return void + */ + public function add_classes( array $classes, $queue = 'display' ) { + foreach ( $classes as $key => $value ) { + // If the classes are passed as class => bool, only add ones set to true. + if ( is_bool( $value ) && false !== $value ) { + $this->add_class( $key, $queue ); + } else { + $this->add_class( $value, $queue ); + } + } + } + + /** + * Remove a single class from the queue. + * + * @since 4.12.6 + * + * @param string $class The class to remove. + * @return void + */ + public function remove_class( $class, $queue = 'display' ) { + if ( 'admin' !== $queue ) { + $this->classes = array_filter( + $this->classes, + static function( $k ) use ( $class ) { + return $k !== $class; + }, + ARRAY_FILTER_USE_KEY + ); + } + + if ( 'display' !== $queue ) { + $this->admin_classes = array_filter( + $this->admin_classes, + static function( $k ) use ( $class ) { + return $k !== $class; + }, + ARRAY_FILTER_USE_KEY + ); + } + } + + /** + * Remove an array of classes from the queue. + * + * @since 4.12.6 + * + * @param array $classes The classes to remove. + * @return void + */ + public function remove_classes( array $classes, $queue = 'display' ) { + if ( empty( $classes ) || ! is_array( $classes) ) { + return; + } + + foreach ( $classes as $class ) { + $this->remove_class( $class, $queue ); + } + } + + /** + * Adds the enqueued classes to the body class array. + * + * @since 4.12.6 + * + * @param array $classes An array of body class names. + * @return array Array of body classes. + */ + public function add_body_classes( $classes = [] ) { + // Make sure they should be added. + if( ! $this->should_add_body_classes( $this->get_class_names(), (array) $classes, 'display' ) ) { + return $classes; + } + + $element_classes = new Element_Classes( $this->get_class_names() ); + + return array_merge( $classes, $element_classes->get_classes() ); + } + + /** + * Adds the enqueued classes to the body class array. + * + * @since 4.12.6 + * + * @param string $classes The existing body class names. + * + * @return string String of admin body classes. + */ + public function add_admin_body_classes( $classes ) { + $existing_classes = explode( ' ', $classes ); + // Make sure they should be added. + if ( ! $this->should_add_body_classes( $this->get_class_names( 'admin' ), (array) $existing_classes, 'admin' ) ) { + // Ensure we return the current string on false! + return $classes; + } + + $element_classes = new Element_Classes( $this->get_class_names( 'admin' ) ); + + return implode( ' ', array_merge( $existing_classes, $element_classes->get_classes() ) ); + + } + + /** + * Should a individual class be added to the queue. + * + * @since 4.12.6 + * + * @param string $class The body class we wish to add. + * + * @return boolean Whether to add tribe body classes to the queue. + */ + private function should_add_body_class_to_queue( $class, $queue = 'display' ) { + /** + * Filter whether to add the body class to the queue or not. + * + * @since 4.12.6 + * + * @param boolean $add Whether to add the class to the queue or not. + * @param array $class The array of body class names to add. + * @param string $queue The queue we want to get 'admin', 'display', 'all'. + */ + return (bool) apply_filters( 'tribe_body_class_should_add_to_queue', false, $class, $queue ); + } + + /** + * Logic for whether the body classes, as a whole, should be added. + * + * @since 4.12.6 + * + * @param array $add_classes An array of body class names to add. + * @param array $existing_classes An array of existing body class names from WP. + * @param string $queue The queue we want to get 'admin', 'display', 'all'. + * + * @return boolean Whether to add tribe body classes. + */ + private function should_add_body_classes( array $add_classes, array $existing_classes, $queue ) { + /** + * Filter whether to add tribe body classes or not. + * + * @since 4.12.6 + * + * @param boolean $add Whether to add classes or not. + * @param string $queue The queue we want to get 'admin', 'display', 'all'. + * @param array $add_classes The array of body class names to add. + * @param array $existing_classes An array of existing body class names from WP. + * + */ + return (bool)apply_filters( 'tribe_body_classes_should_add', false, $queue, $add_classes, $existing_classes ); + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Callback.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Callback.php new file mode 100644 index 000000000..e1efa1303 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Callback.php @@ -0,0 +1,177 @@ + $slug, + 'method' => $method, + 'arguments' => $arguments, + 'is_empty' => $is_empty, + ]; + + $key = md5( json_encode( $item ) ); + + // Prevent this from been reset + if ( isset( $this->items[ $key ] ) ) { + return $this->items[ $key ]; + } + + $item->callback = $container->callback( $item->slug, $item->method ); + + $this->items[ $key ] = $item; + + return [ $this, $this->prefix . $key ]; + } + + /** + * Returns the Value passed as a simple Routing method for tribe_callback_return + * + * @since 4.6.2 + * + * @param mixed $value Value to be Routed + * + * @return mixed + */ + public function return_value( $value ) { + return $value; + } + + /** + * Calls the Lambda function provided by Di52 to allow passing of Params without having to create more + * methods into classes for simple callbacks that will only have a pre-determined value. + * + * @since 4.6.2 + * + * @param string $slug A class or interface fully qualified name or a string slug. + * @param string $method The method that should be called on the resolved implementation with the + * specified array arguments. + * + * @return mixed The Return value used + */ + public function __call( $method, $args ) { + $key = str_replace( $this->prefix, '', $method ); + + if ( ! isset( $this->items[ $key ] ) ) { + return false; + } + + $item = $this->items[ $key ]; + + // Allow for previous compatibility with tribe_callback + if ( ! $item->is_empty ) { + $args = $item->arguments; + } + + return call_user_func_array( $item->callback, $args ); + } + + /** + * Tribe__Utils__Callback constructor. + * + * This is used to wrap a Tribe callable couple, a bound slug and method, to be used as a serializable callback. + * + * @since 4.9.5 + * + * @param string $slug The slug or class to call. + * @param string $method The method to call on the slug or class. + */ + public function __construct( $slug = null, $method = null ) { + $this->slug = $slug; + $this->method = $method; + } + + /** + * Returns the list of properties that should be serialized for the object. + + * + * @since 4.9.5 + * + * @return array An array of properties that should be serialized. + */ + public function __sleep() { + return [ 'slug', 'method' ]; + } + + /** + * Returns this callback slug or class. + * + * This only makes sense if this class is being used to wrap a Tribe callback couple (slug and method). + * + * @since 4.9.5 + * + * @return string|null This Tribe callback wrapper slug or class. + */ + public function get_slug() { + return $this->slug; + } + + /** + * Returns this callback method. + * + * This only makes sense if this class is being used to wrap a Tribe callback couple (slug and method). + * + * @since 4.9.5 + * + * @return string|null This Tribe callback method. + */ + public function get_method() { + + return $this->method; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Collection.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Collection.php new file mode 100644 index 000000000..aec8cb446 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Collection.php @@ -0,0 +1,56 @@ +items = $items; + foreach ( $items as $item ) { + $this->push( $item ); + } + } + + /** + * Runs a callback function on all the collection items and returns the results. + * + * This is just a wrapper around the `array_map` method. + * + * @since 4.9.5 + * + * @param callable $callback The callback to run on each collection item. + * + * @return array An array of results returned by running the callback on all + * collection items. + */ + public function map( $callback ) { + return array_map( $callback, $this->items ); + } +} \ No newline at end of file diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Collection_Interface.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Collection_Interface.php new file mode 100644 index 000000000..3dbcd6aec --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Collection_Interface.php @@ -0,0 +1,57 @@ +all(); + + return reset( $items ); + } + + /** + * Returns the last item in the collection. + * + * @since 4.9.14 + * + * @return mixed The last item in the collection. + */ + public function last() { + $items = $this->all(); + + return end( $items ); + } + + /** + * Returns the nth item in the collection. + * + * @since 4.9.14 + * + * @param int $n The 1-based index of the item to return. It's not 0-based, `1` will return the first item. + * + * @return mixed|null The nth item in the collection or `null` if not set. + */ + public function nth( $n ) { + $items = array_values( $this->all() ); + + return isset( $items[ $n - 1 ] ) ? $items[ $n - 1 ] : null; + } + + /** + * {@inheritDoc} + */ + #[\ReturnTypeWillChange] + public function offsetExists( $offset ) { + $items = $this->all(); + + return isset( $items[ $offset ] ); + } + + /** + * {@inheritDoc} + */ + #[\ReturnTypeWillChange] + public function offsetGet( $offset ) { + $items = $this->all(); + + return isset( $items[ $offset ] ) + ? $items[ $offset ] + : null; + } + + /** + * {@inheritDoc} + */ + #[\ReturnTypeWillChange] + public function offsetSet( $offset, $value ) { + $this->items = $this->all(); + + $this->items[ $offset ] = $value; + } + + /** + * {@inheritDoc} + */ + #[\ReturnTypeWillChange] + public function offsetUnset( $offset ) { + $this->items = $this->all(); + + unset( $this->items[ $offset ] ); + } + + /** + * {@inheritDoc} + */ + #[\ReturnTypeWillChange] + public function next() { + $this->items_index ++; + } + + /** + * {@inheritDoc} + */ + #[\ReturnTypeWillChange] + public function valid() { + $items = $this->all(); + + return ( isset( $items[ $this->items_index ] ) ); + } + + /** + * {@inheritDoc} + */ + #[\ReturnTypeWillChange] + public function key() { + return $this->items_index; + } + + /** + * {@inheritDoc} + */ + #[\ReturnTypeWillChange] + public function current() { + $items = array_values( $this->all() ); + + return isset( $items[ $this->items_index ] ) ? $items[ $this->items_index ] : null; + } + + /** + * {@inheritDoc} + */ + #[\ReturnTypeWillChange] + public function rewind() { + $this->items_index = 0; + } + + /** + * {@inheritDoc} + */ + #[\ReturnTypeWillChange] + public function count() { + return count( $this->all() ); + } + + /** + * {@inheritDoc} + */ + public function serialize() { + $to_serialize = $this->all(); + + if ( method_exists( $this, 'before_serialize' ) ) { + $to_serialize = $this->before_serialize( $this->all() ); + } + + return serialize( $to_serialize ); + } + + /** + * {@inheritDoc} + */ + public function unserialize( $serialized ) { + $to_unserialize = $serialized; + + if ( method_exists( $this, 'custom_unserialize' ) ) { + $this->items = $this->custom_unserialize( $to_unserialize ); + + return; + } + + $this->items = unserialize( $to_unserialize ); + } + + /** + * {@inheritDoc} + */ + #[\ReturnTypeWillChange] + public function seek( $position ) { + $this->items_index = $position; + } + + /** + * Applies a filter callback to each element of this collection changing the collection elements to only those + * passing the filter. + * + * @since 4.10.2 + * + * @param callable $filter_callback The filter callback that will be applied to each element of the collection; the + * callback will receive the element as parameter. + * + * @return Collection_Trait A new collection instance, that contains only the elements that passed the filter. + */ + public function filter( $filter_callback ) { + if ( $this->count() === 0 ) { + // If there is nothing to filter to begin with, just return this. + return $this; + } + + $filtered = new static(); + $filtered->items = array_filter( $this->all(), $filter_callback ); + + return $filtered; + } + + /** + * PHP 8.0+ compatible implementation of the serialization logic. + * + * @since 5.0.6 + * + * @return array The data to serialize. + */ + public function __serialize(): array { + $to_serialize = $this->all(); + + if ( method_exists( $this, 'before_serialize' ) ) { + $to_serialize = $this->before_serialize( $this->all() ); + } + + return $to_serialize; + } + + /** + * PHP 8.0+ compatible implementation of the unserialization logic. + * + * @since 5.0.6 + * + * @param array $data The data to unserialize. + */ + public function __unserialize( array $data ): void { + if ( method_exists( $this, 'custom_unserialize' ) ) { + $this->items = $this->custom_unserialize( serialize( $data ) ); + + return; + } + + $this->items = $data; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Color.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Color.php new file mode 100644 index 000000000..2e8e34aaf --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Color.php @@ -0,0 +1,574 @@ + + * Info: http://mexitek.github.io/phpColors/ + */ + +/** + * PHP 5.2 Compatibility + * Author: Gustavo Bordoni + */ + +class Tribe__Utils__Color { + + private $_hex; + private $_hsl; + private $_rgb; + + /** + * Auto darkens/lightens by 10% for sexily-subtle gradients. + * Set this to FALSE to adjust automatic shade to be between given color + * and black (for darken) or white (for lighten) + */ + const DEFAULT_ADJUST = 10; + + /** + * Instantiates the class with a HEX value + * @param string $hex + * @throws Exception "Bad color format" + */ + public function __construct( $hex ) { + // Strip # sign is present + $color = str_replace( '#', '', $hex ); + + // Make sure it's 6 digits + if ( strlen( $color ) === 3 ) { + $color = preg_replace( '/(.)(.)(.)/', '$1$1$2$2$3$3', $color ); + } elseif ( strlen( $color ) != 6 ) { + throw new Exception( 'HEX color needs to be 6 or 3 digits long' ); + } + + $this->_hsl = self::hexToHsl( $color ); + $this->_hex = $color; + $this->_rgb = self::hexToRgb( $color ); + } + + // ==================== + // = Public Interface = + // ==================== + + /** + * Given a HEX string returns a HSL array equivalent. + * @param string $color + * @return array HSL associative array + */ + public static function hexToHsl( $color ) { + + // Sanity check + $color = self::_checkHex( $color ); + + // Convert HEX to DEC + $R = hexdec( $color[0] . $color[1] ); + $G = hexdec( $color[2] . $color[3] ); + $B = hexdec( $color[4] . $color[5] ); + + $HSL = []; + + $var_R = ( $R / 255 ); + $var_G = ( $G / 255 ); + $var_B = ( $B / 255 ); + + $var_Min = min( $var_R, $var_G, $var_B ); + $var_Max = max( $var_R, $var_G, $var_B ); + $del_Max = $var_Max - $var_Min; + + $L = ( $var_Max + $var_Min ) / 2; + + if ( 0 == $del_Max ) { + $H = 0; + $S = 0; + } else { + if ( $L < 0.5 ) { + $S = $del_Max / ( $var_Max + $var_Min ); + } else { + $S = $del_Max / ( 2 - $var_Max - $var_Min ); + } + + $del_R = ( ( ( $var_Max - $var_R ) / 6 ) + ( $del_Max / 2 ) ) / $del_Max; + $del_G = ( ( ( $var_Max - $var_G ) / 6 ) + ( $del_Max / 2 ) ) / $del_Max; + $del_B = ( ( ( $var_Max - $var_B ) / 6 ) + ( $del_Max / 2 ) ) / $del_Max; + + if ( $var_R == $var_Max ) { + $H = $del_B - $del_G; + } elseif ( $var_G == $var_Max ) { + $H = ( 1 / 3 ) + $del_R - $del_B; + } elseif ( $var_B == $var_Max ) { + $H = ( 2 / 3 ) + $del_G - $del_R; + } + + if ( $H < 0 ) { + $H++; + } + if ( $H > 1 ) { + $H--; + } + } + + $HSL['H'] = ( $H * 360 ); + $HSL['S'] = $S; + $HSL['L'] = $L; + + return $HSL; + } + + /** + * Given a HSL associative array returns the equivalent HEX string + * @param array $hsl + * @return string HEX string + * @throws Exception "Bad HSL Array" + */ + public static function hslToHex( $hsl = [] ) { + // Make sure it's HSL + if ( empty( $hsl ) || ! isset( $hsl['H'] ) || ! isset( $hsl['S'] ) || ! isset( $hsl['L'] ) ) { + throw new Exception( 'Param was not an HSL array' ); + } + + list( $H, $S, $L ) = [ $hsl['H'] / 360, $hsl['S'], $hsl['L'] ]; + + if ( 0 == $S ) { + $r = $L * 255; + $g = $L * 255; + $b = $L * 255; + } else { + if ( $L < 0.5 ) { + $var_2 = $L * ( 1 + $S ); + } else { + $var_2 = ( $L + $S ) - ( $S * $L ); + } + + $var_1 = 2 * $L - $var_2; + + $r = round( 255 * self::_huetorgb( $var_1, $var_2, $H + ( 1 / 3 ) ) ); + $g = round( 255 * self::_huetorgb( $var_1, $var_2, $H ) ); + $b = round( 255 * self::_huetorgb( $var_1, $var_2, $H - ( 1 / 3 ) ) ); + + } + + // Convert to hex + $r = dechex( $r ); + $g = dechex( $g ); + $b = dechex( $b ); + + // Make sure we get 2 digits for decimals + $r = ( strlen( '' . $r ) === 1 ) ? '0' . $r : $r; + $g = ( strlen( '' . $g ) === 1 ) ? '0' . $g : $g; + $b = ( strlen( '' . $b ) === 1 ) ? '0' . $b : $b; + + return $r.$g.$b; + } + + /** + * Given a HEX string returns a RGB array equivalent. + * @param string $color + * @return array RGB associative array + */ + public static function hexToRgb( $color ) { + + // Sanity check + $color = self::_checkHex( $color ); + + // Convert HEX to DEC + $R = hexdec( $color[0] . $color[1] ); + $G = hexdec( $color[2] . $color[3] ); + $B = hexdec( $color[4] . $color[5] ); + + $RGB['R'] = $R; + $RGB['G'] = $G; + $RGB['B'] = $B; + + return $RGB; + } + + /** + * Given an RGB associative array returns the equivalent HEX string + * @param array $rgb + * @return string RGB string + * @throws Exception "Bad RGB Array" + */ + public static function rgbToHex( $rgb = [] ) { + // Make sure it's RGB + if ( empty( $rgb ) || ! isset( $rgb['R'] ) || ! isset( $rgb['G'] ) || ! isset( $rgb['B'] ) ) { + throw new Exception( 'Param was not an RGB array' ); + } + + // Convert RGB to HEX + $hex[0] = dechex( $rgb['R'] ); + $hex[1] = dechex( $rgb['G'] ); + $hex[2] = dechex( $rgb['B'] ); + + return implode( '', $hex ); + } + + /** + * Given a HEX value, returns a darker color. If no desired amount provided, then the color halfway between + * given HEX and black will be returned. + * @param int $amount + * @return string Darker HEX value + */ + public function darken( $amount = self::DEFAULT_ADJUST ) { + // Darken + $darkerHSL = $this->_darken( $this->_hsl, $amount ); + // Return as HEX + return self::hslToHex( $darkerHSL ); + } + + /** + * Given a HEX value, returns a lighter color. If no desired amount provided, then the color halfway between + * given HEX and white will be returned. + * @param int $amount + * @return string Lighter HEX value + */ + public function lighten( $amount = self::DEFAULT_ADJUST ) { + // Lighten + $lighterHSL = $this->_lighten( $this->_hsl, $amount ); + // Return as HEX + return self::hslToHex( $lighterHSL ); + } + + /** + * Given a HEX value, returns a mixed color. If no desired amount provided, then the color mixed by this ratio + * @param string $hex2 Secondary HEX value to mix with + * @param int $amount = -100..0..+100 + * @return string mixed HEX value + */ + public function mix( $hex2, $amount = 0 ) { + $rgb2 = self::hexToRgb( $hex2 ); + $mixed = $this->_mix( $this->_rgb, $rgb2, $amount ); + + // Return as HEX + return self::rgbToHex( $mixed ); + } + + /** + * Creates an array with two shades that can be used to make a gradient + * @param int $amount Optional percentage amount you want your contrast color + * @return array An array with a 'light' and 'dark' index + */ + public function makeGradient( $amount = self::DEFAULT_ADJUST ) { + // Decide which color needs to be made + if ( $this->isLight() ) { + $lightColor = $this->_hex; + $darkColor = $this->darken( $amount ); + } else { + $lightColor = $this->lighten( $amount ); + $darkColor = $this->_hex; + } + + // Return our gradient array + return [ 'light' => $lightColor, 'dark' => $darkColor ]; + } + + /** + * Returns whether or not given color is considered "light" + * @param string|Boolean $color + * @return boolean + */ + public function isLight( $color = false ) { + // Get our color + $color = ( $color ) ? $color : $this->_hex; + + // Calculate straight from rbg + $r = hexdec( $color[0] . $color[1] ); + $g = hexdec( $color[2] . $color[3] ); + $b = hexdec( $color[4] . $color[5] ); + + return ( ( $r * 299 + $g * 587 + $b * 114 ) / 1000 > 130 ); + } + + /** + * Returns whether or not a given color is considered "dark" + * @param string|Boolean $color + * @return boolean + */ + public function isDark( $color = false ) { + // Get our color + $color = ( $color ) ? $color:$this->_hex; + + // Calculate straight from rbg + $r = hexdec( $color[0] . $color[1] ); + $g = hexdec( $color[2] . $color[3] ); + $b = hexdec( $color[4] . $color[5] ); + + return ( ( $r * 299 + $g * 587 + $b * 114 ) / 1000 <= 130 ); + } + + /** + * Returns the complimentary color + * @return string Complementary hex color + * + */ + public function complementary() { + // Get our HSL + $hsl = $this->_hsl; + + // Adjust Hue 180 degrees + $hsl['H'] += ( $hsl['H'] > 180 ) ? -180 : 180; + + // Return the new value in HEX + return self::hslToHex( $hsl ); + } + + /** + * Returns your color's HSL array + */ + public function getHsl() { + return $this->_hsl; + } + + /** + * Returns your original color minus any hash mark. + */ + public function getHex() { + return $this->_hex; + } + + /** + * Returns your original color with the hash mark. + * + * @since 4.14.2 + * + * @return string Hex color code with hash prefix. + */ + public function get_hex_with_hash() { + return '#' . $this->_hex; + } + + /** + * Returns your color's RGB array + */ + public function getRgb() { + return $this->_rgb; + } + + /** + * Returns the cross browser CSS3 gradient + * @param int $amount Optional: percentage amount to light/darken the gradient + * @param boolean $vintageBrowsers Optional: include vendor prefixes for browsers that almost died out already + * @param string $prefix Optional: prefix for every lines + * @param string $suffix Optional: suffix for every lines + * @link http://caniuse.com/css-gradients Resource for the browser support + * @return string CSS3 gradient for chrome, safari, firefox, opera and IE10 + */ + public function getCssGradient( $amount = self::DEFAULT_ADJUST, $vintageBrowsers = false, $suffix = '', $prefix = '' ) { + + // Get the recommended gradient + $g = $this->makeGradient( $amount ); + + $css = ''; + /* fallback/image non-cover color */ + $css .= "{$prefix}background-color: #".$this->_hex.";{$suffix}"; + + /* IE Browsers */ + $css .= "{$prefix}filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#".$g['light']."', endColorstr='#".$g['dark']."');{$suffix}"; + + /* Safari 4+, Chrome 1-9 */ + if ( $vintageBrowsers ) { + $css .= "{$prefix}background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, from(#{$g['light']}), to(#{$g['dark']}));{$suffix}"; + } + + /* Safari 5.1+, Mobile Safari, Chrome 10+ */ + $css .= "{$prefix}background-image: -webkit-linear-gradient(top, #{$g['light']}, #{$g['dark']});{$suffix}"; + + /* Firefox 3.6+ */ + if ( $vintageBrowsers ) { + $css .= "{$prefix}background-image: -moz-linear-gradient(top, #{$g['light']}, #{$g['dark']});{$suffix}"; + } + + /* Opera 11.10+ */ + if ( $vintageBrowsers ) { + $css .= "{$prefix}background-image: -o-linear-gradient(top, #{$g['light']}, #{$g['dark']});{$suffix}"; + } + + /* Unprefixed version (standards): FF 16+, IE10+, Chrome 26+, Safari 7+, Opera 12.1+ */ + $css .= "{$prefix}background-image: linear-gradient(to bottom, #{$g['light']}, #{$g['dark']});{$suffix}"; + + // Return our CSS + return $css; + } + + // =========================== + // = Private Functions Below = + // =========================== + + + /** + * Darkens a given HSL array + * @param array $hsl + * @param int $amount + * @return array $hsl + */ + private function _darken( $hsl, $amount = self::DEFAULT_ADJUST ) { + // Check if we were provided a number + if ( $amount ) { + $hsl['L'] = ( $hsl['L'] * 100 ) - $amount; + $hsl['L'] = ( $hsl['L'] < 0 ) ? 0 : $hsl['L'] / 100; + } else { + // We need to find out how much to darken + $hsl['L'] = $hsl['L'] / 2 ; + } + + return $hsl; + } + + /** + * Lightens a given HSL array + * @param array $hsl + * @param int $amount + * @return array $hsl + */ + private function _lighten( $hsl, $amount = self::DEFAULT_ADJUST ) { + // Check if we were provided a number + if ( $amount ) { + $hsl['L'] = ( $hsl['L'] * 100 ) + $amount; + $hsl['L'] = ( $hsl['L'] > 100 ) ? 1 : $hsl['L'] / 100; + } else { + // We need to find out how much to lighten + $hsl['L'] += ( 1 - $hsl['L'] ) / 2; + } + + return $hsl; + } + + /** + * Mix 2 rgb colors and return an rgb color + * @param array $rgb1 + * @param array $rgb2 + * @param int $amount ranged -100..0..+100 + * @return array $rgb + * + * ported from http://phpxref.pagelines.com/nav.html?includes/class.colors.php.source.html + */ + private function _mix( $rgb1, $rgb2, $amount = 0 ) { + + $r1 = ( $amount + 100 ) / 100; + $r2 = 2 - $r1; + + $rmix = ( ( $rgb1['R'] * $r1 ) + ( $rgb2['R'] * $r2 ) ) / 2; + $gmix = ( ( $rgb1['G'] * $r1 ) + ( $rgb2['G'] * $r2 ) ) / 2; + $bmix = ( ( $rgb1['B'] * $r1 ) + ( $rgb2['B'] * $r2 ) ) / 2; + + return [ 'R' => $rmix, 'G' => $gmix, 'B' => $bmix ]; + } + + /** + * Given a Hue, returns corresponding RGB value + * @param int $v1 + * @param int $v2 + * @param int $vH + * @return int + */ + private static function _huetorgb( $v1, $v2, $vH ) { + if ( $vH < 0 ) { + $vH += 1; + } + + if ( $vH > 1 ) { + $vH -= 1; + } + + if ( ( 6 * $vH ) < 1 ) { + return ( $v1 + ( $v2 - $v1 ) * 6 * $vH ); + } + + if ( ( 2 * $vH ) < 1 ) { + return $v2; + } + + if ( ( 3 * $vH ) < 2 ) { + return ( $v1 + ( $v2 - $v1 ) * ( ( 2 / 3 ) - $vH ) * 6 ); + } + + return $v1; + + } + + /** + * You need to check if you were given a good hex string + * @param string $hex + * @return string Color + * @throws Exception "Bad color format" + */ + private static function _checkHex( $hex ) { + // Strip # sign is present + $color = str_replace( '#', '', $hex ); + + // Make sure it's 6 digits + if ( strlen( $color ) == 3 ) { + $color = $color[0] . $color[0] . $color[1] . $color[1] . $color[2] . $color[2]; + } elseif ( strlen( $color ) != 6 ) { + throw new Exception( 'HEX color needs to be 6 or 3 digits long' ); + } + + return $color; + } + + /** + * Get color that contrasts the most. + * + * @since 5.0.10 + * + * @param string $hex_color Hex value of color to compare. + * @param string $light_color Hex value of light contrast color. + * @param string $dark_color Hex value of dark contrast color. + * + * @return string + */ + public static function get_contrast_color( $hex_color, $light_color = '#ffffff', $dark_color = '#000000' ) { + // Get contrast ratios for both light and dark. + $dark_ratio = self::get_contrast_ratio( $hex_color, $dark_color ); + $light_ratio = self::get_contrast_ratio( $hex_color, $light_color ); + + // Return the highest ratio. + if ( $dark_ratio > $light_ratio ) { + return $dark_color; + } + return $light_color +; + } + + /** + * Get contrast ratio of two colors. + * + * @since 5.0.10 + * + * @param string $hex_color Hex value of color. + * @param string $compare_color Hax value of comparing color. + * + * @return int + */ + public static function get_contrast_ratio( $hex_color, $compare_color ) { + // Separate into hexColor RGB. + $R1 = hexdec( substr( $hex_color, 1, 2 ) ); + $G1 = hexdec( substr( $hex_color, 3, 2 ) ); + $B1 = hexdec( substr( $hex_color, 5, 2 ) ); + + $R2comp = hexdec( substr( $compare_color, 1, 2 ) ); + $G2comp = hexdec( substr( $compare_color, 3, 2 ) ); + $B2comp = hexdec( substr( $compare_color, 5, 2 ) ); + + // Calculate contrast ratio. + $L1 = 0.2126 * pow( $R1 / 255, 2.2 ) + + 0.7152 * pow( $G1 / 255, 2.2 ) + + 0.0722 * pow( $B1 / 255, 2.2 ); + $L2 = 0.2126 * pow( $R2comp / 255, 2.2 ) + + 0.7152 * pow( $G2comp / 255, 2.2 ) + + 0.0722 * pow( $B2comp / 255, 2.2 ); + + if ( $L1 > $L2 ) { + return (int)( ( $L1 + 0.05 ) / ( $L2 + 0.05 ) ); + } + return (int)( ( $L2 + 0.05 ) / ( $L1 + 0.05 ) ); + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Compatibility_Classes.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Compatibility_Classes.php new file mode 100644 index 000000000..972c0112c --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Compatibility_Classes.php @@ -0,0 +1,352 @@ + true, 'class => false ] + * + * @var array + */ + protected $classes = []; + + /** + * Stores all the admin classes. + * In the format: ['class' => true, 'class => false ] + * + * @var array + */ + protected $admin_classes = []; + + /** + * Queue-aware method to get the classes array. + * Returns the array of classes to add. + * + * @since 4.14.0 + * + * @param string $queue The queue we want to get 'admin', 'display', 'all'. + * @return array A map of the classes for the queue. + */ + public function get_classes( $queue = 'display' ) { + switch( $queue ) { + case 'admin': + return $this->admin_classes; + break; + case 'all': + return array_merge( $this->classes, $this->admin_classes ); + break; + default: + return $this->classes; + break; + } + } + + /** + * Returns the array of classnames to add + * + * @since 4.14.0 + * + * @param string $queue The queue we want to get 'admin', 'display', 'all'. + * @return array The list of class names. + */ + public function get_class_names( $queue = 'display' ) { + $classes = $this->get_classes( $queue ); + + return array_keys( + array_filter( + $classes, + static function( $v ) { + return $v; + }, + ARRAY_FILTER_USE_KEY + ) + ); + } + + /** + * Checks if a class is in the queue, + * wether it's going to be added or not. + * + * @since 4.14.0 + * + * @param string $class The class we are checking for. + * @param string $queue The queue we want to check 'admin', 'display', 'all' + * @return boolean Whether a class exists or not in the queue. + */ + public function class_in_queue( $class, $queue = 'display' ) { + $classes = $this->get_classes( $queue ); + + return array_key_exists( $class, $classes ); + } + + /** + * Checks if a class is in the queue and going to be added. + * + * @since 4.14.0 + * + * @param string $class The class we are checking for. + * @param string $queue The queue we want to check 'admin', 'display', 'all' + * @return boolean Whether a class is currently queued or not. + */ + public function class_is_enqueued( $class, $queue = 'display' ) { + $classes = $this->get_classes( $queue ); + if ( ! $this->class_in_queue( $class, $queue ) ) { + return false; + } + + return $classes[ $class ]; + } + + /** + * Dequeues a class. + * + * @since 4.14.0 + * + * @param string $class + * @param string $queue The queue we want to alter 'admin', 'display', 'all' + * @return boolean + */ + public function dequeue_class( $class, $queue = 'display' ) { + if ( ! $this->class_in_queue( $class, $queue ) ) { + return false; + } + + if ( 'admin' !== $queue ) { + $this->classes[ $class ] = false; + } + + if ( 'display' !== $queue ) { + $this->admin_classes[ $class ] = false; + } + + return true; + + } + + /** + * Enqueues a class. + * + * @since 4.14.0 + * + * @param string $class + * @param string $queue The queue we want to alter 'admin', 'display', 'all' + * @return false + */ + public function enqueue_class( $class, $queue = 'display' ) { + if ( ! $this->class_in_queue( $class, $queue ) ) { + return false; + } + + if ( 'admin' !== $queue ) { + $this->classes[ $class ] = true; + return true; + } + + if ( 'display' !== $queue ) { + $this->admin_classes[ $class ] = true; + return true; + } + + // Something went wrong. + return false; + } + + /** + * Add a single class to the queue. + * + * @since 4.14.0 + * + * @param string $class The class to add. + * @param string $queue The queue we want to alter 'admin', 'display', 'all' + * @return void + */ + public function add_class( $class, $queue = 'display' ) { + if ( empty( $class ) ) { + return; + } + + if ( is_array( $class ) ) { + $this->add_classes( $class, $queue ); + } elseif ( $this->should_add_compatibility_class_to_queue( $class, $queue ) ) { + + $class = sanitize_html_class( $class ); + + if ( 'admin' !== $queue ) { + $this->classes[ $class ] = true ; + } + + if ( 'display' !== $queue ) { + $this->admin_classes[ $class ] = true ; + } + } + } + + /** + * Add an array of classes to the queue. + * + * @since 4.14.0 + * + * @param array $class The classes to add. + * @return void + */ + public function add_classes( array $classes, $queue = 'display' ) { + foreach ( $classes as $key => $value ) { + // If the classes are passed as class => bool, only add ones set to true. + if ( is_bool( $value ) && false !== $value ) { + $this->add_class( $key, $queue ); + } else { + $this->add_class( $value, $queue ); + } + } + } + + /** + * Remove a single class from the queue. + * + * @since 4.14.0 + * + * @param string $class The class to remove. + * @return void + */ + public function remove_class( $class, $queue = 'display' ) { + if ( 'admin' !== $queue ) { + $this->classes = array_filter( + $this->classes, + static function( $k ) use ( $class ) { + return $k !== $class; + }, + ARRAY_FILTER_USE_KEY + ); + } + + if ( 'display' !== $queue ) { + $this->admin_classes = array_filter( + $this->admin_classes, + static function( $k ) use ( $class ) { + return $k !== $class; + }, + ARRAY_FILTER_USE_KEY + ); + } + } + + /** + * Remove an array of classes from the queue. + * + * @since 4.14.0 + * + * @param array $classes The classes to remove. + * @return void + */ + public function remove_classes( array $classes, $queue = 'display' ) { + if ( empty( $classes ) || ! is_array( $classes) ) { + return; + } + + foreach ( $classes as $class ) { + $this->remove_class( $class, $queue ); + } + } + + /** + * Adds the enqueued classes to the compatibility class array. + * + * @since 4.14.0 + * + * @param array $classes An array of compatibility class names. + * @return array Array of compatibility classes. + */ + public function add_compatibility_classes( $classes = [] ) { + // Make sure they should be added. + if( ! $this->should_add_compatibility_classes( $this->get_class_names(), (array) $classes, 'display' ) ) { + return $classes; + } + + $element_classes = new Element_Classes( $this->get_class_names() ); + + return array_merge( $classes, $element_classes->get_classes() ); + } + + /** + * Adds the enqueued classes to the compatibility class array. + * + * @since 4.14.0 + * + * @param string $classes The existing compatibility class names. + * + * @return string String of admin compatibility classes. + */ + public function add_admin_compatibility_classes( $classes ) { + $existing_classes = explode( ' ', $classes ); + // Make sure they should be added. + if ( ! $this->should_add_compatibility_classes( $this->get_class_names( 'admin' ), (array) $existing_classes, 'admin' ) ) { + // Ensure we return the current string on false! + return $classes; + } + + $element_classes = new Element_Classes( array_merge( $existing_classes, $this->get_class_names( 'admin' ) ) ); + + return $element_classes->get_classes_as_string(); + + } + + /** + * Should a individual class be added to the queue. + * + * @since 4.14.0 + * + * @param string $class The compatibility class we wish to add. + * + * @return boolean Whether to add tribe compatibility classes to the queue. + */ + private function should_add_compatibility_class_to_queue( $class, $queue = 'display' ) { + /** + * Filter whether to add the compatibility class to the queue or not. + * + * @since 4.14.0 + * + * @param boolean $add Whether to add the class to the queue or not. + * @param array $class The array of compatibility class names to add. + * @param string $queue The queue we want to get 'admin', 'display', 'all'. + */ + return (bool) apply_filters( 'tribe_compatibility_class_should_add_to_queue', false, $class, $queue ); + } + + /** + * Logic for whether the compatibility classes, as a whole, should be added. + * + * @since 4.14.0 + * + * @param array $add_classes An array of compatibility class names to add. + * @param array $existing_classes An array of existing compatibility class names from WP. + * @param string $queue The queue we want to get 'admin', 'display', 'all'. + * + * @return boolean Whether to add tribe compatibility classes. + */ + private function should_add_compatibility_classes( array $add_classes, array $existing_classes, $queue ) { + /** + * Filter whether to add tribe compatibility classes or not. + * + * @since 4.14.0 + * + * @param boolean $add Whether to add classes or not. + * @param array $add_classes The array of compatibility class names to add. + * @param array $existing_classes An array of existing compatibility class names from WP. + * @param string $queue The queue we want to get 'admin', 'display', 'all'. + * + */ + return (bool)apply_filters( 'tribe_compatibility_classes_should_add', false, $queue, $add_classes, $existing_classes ); + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Coordinates_Provider.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Coordinates_Provider.php new file mode 100644 index 000000000..2928a2731 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Coordinates_Provider.php @@ -0,0 +1,134 @@ +http = ! empty( $https ) ? $https : _wp_http_get_object(); + } + + /** + * @param string|array $address + */ + public function provide_coordinates_for_address( $address ) { + + if ( is_array( $address ) ) { + $address = implode( ', ', array_filter( array_map( 'trim', $address ) ) ); + } + + $address = trim( $address ); + + if ( $location = $this->get_resolved( $address ) ) { + return $location; + } + + $base_request_url = trailingslashit( $this->get_google_api_base() ) . $this->get_google_api_json_format(); + $url = esc_url( add_query_arg( [ 'address' => $address ], $base_request_url ) ); + $response = $this->http->get( $url ); + + if ( is_wp_error( $response ) ) { + return false; + } + + $decoded = json_decode( $response['body'], true ); + + if ( empty( $decoded['status'] ) || 'OK' !== $decoded['status'] ) { + return false; + } + + if ( empty( $decoded['results'][0]['place_id'] ) || empty( $decoded['results'][0]['geometry']['location']['lat'] ) || empty( $decoded['results'][0]['geometry']['location']['lng'] ) ) { + return false; + } + + $location = $decoded['results'][0]['geometry']['location']; + + $updated_transient = array_merge( $this->get_transient(), [ $address => $location ] ); + set_transient( self::$transient_name, $updated_transient ); + $this->transient = $updated_transient; + + return $location; + } + + /** + * @return null|WP_Http + */ + public function get_http() { + return $this->http; + } + + protected function get_google_api_base() { + return self::$google_api_base; + } + + protected function get_google_api_json_format() { + return self::$google_api_json_format; + } + + protected function get_transient() { + if ( ! is_array( $this->transient ) ) { + $transient = get_transient( self::$transient_name ); + $this->transient = is_array( $transient ) ? $transient : []; + } + + return $this->transient; + } + + protected function get_resolved( $address ) { + $transient = $this->get_transient(); + + return isset( $transient[ $address ] ) ? $transient[ $address ] : false; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Date_I18n.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Date_I18n.php new file mode 100644 index 000000000..bd159c011 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Date_I18n.php @@ -0,0 +1,49 @@ +setTimestamp( $datetime->getTimestamp() ); + $date_object->setTimezone( $datetime->getTimezone() ); + return $date_object; + } + + /** + * Returns a translated string using the params from this DateTime instance. + * + * @since 4.11.0 + * + * @param string $date_format Format to be used in the translation. + * + * @return string Translated date. + */ + public function format_i18n( $date_format ) { + $unix_with_tz = $this->getTimestamp() + $this->getOffset(); + $translated = date_i18n( $date_format, $unix_with_tz ); + + return $translated; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Date_I18n_Immutable.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Date_I18n_Immutable.php new file mode 100644 index 000000000..5175ecc4b --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Date_I18n_Immutable.php @@ -0,0 +1,49 @@ +setTimestamp( $datetime->getTimestamp() ); + $date_object = $date_object->setTimezone( $datetime->getTimezone() ); + + return $date_object; + } + + /** + * Returns a translated string using the params from this Immutable DateTime instance. + * + * @since 4.11.0 + * + * @param string $date_format Format to be used in the translation. + * + * @return string Translated date. + */ + public function format_i18n( $date_format ) { + $unix_with_tz = $this->getTimestamp() + $this->getOffset(); + $translated = date_i18n( $date_format, $unix_with_tz ); + + return $translated; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Element_Attributes.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Element_Attributes.php new file mode 100644 index 000000000..4906f3e62 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Element_Attributes.php @@ -0,0 +1,241 @@ + + */ + protected $results = []; + + /** + * Stores the arguments passed. + * + * @since 4.12.3 + * + * @var array + */ + protected $arguments = []; + + /** + * Setups an instance of Element Attributes. + * + * @since 4.12.3 + * + * @return void + */ + public function __construct() { + $this->arguments = func_get_args(); + } + + /** + * When invoked this class will return the full HTML attributes. + * + * @since 4.12.3 + * + * @return string In the format ` attribute1="value1" attribute2="value2" ` + */ + public function __invoke() { + $this->arguments = func_get_args(); + return $this->get_attributes(); + } + + + /** + * When cast to string an instance will return the full HTML attributes. + * + * @since 4.12.3 + * + * @return string In the format ` attribute1="value1" attribute2="value2" ` + */ + public function __toString() { + return $this->get_attributes(); + } + + /** + * Gets the full HTML attributes for this instance of Element Attributes. + * It will contain a space on each end of the attribute. + * + * @since 4.12.3 + * + * @return string In the format ` attribute1="value1" attribute2="value2" ` + */ + public function get_attributes() { + $attributes = $this->get_attributes_as_string(); + + // Bail with empty string when no attributes are present + if ( ! $attributes ) { + return ''; + } + + return " {$attributes} "; + } + + /** + * Gets a space separated string of all attributes to be printed. + * + * @since 4.12.3 + * + * @return string + */ + public function get_attributes_as_string() { + return implode( ' ', $this->get_attributes_array() ); + } + + /** + * Get the array of attributes to be printed. + * + * @since 4.12.3 + * + * @return array + */ + public function get_attributes_array() { + $this->results = []; + $attributes = []; + + $this->parse_array( $this->arguments ); + + foreach ( $this->results as $key => $val ) { + if ( ! $val && '0' !== $val ) { + continue; + } + + if ( is_bool( $val ) ) { + $attributes[] = esc_attr( $key ); + } else { + // Remove double quotes that might be surrounding the value. + trim( $val, '"' ); + $attributes[] = esc_attr( $key ) . '="' . esc_attr( $val ) . '"'; + } + } + + return $attributes; + } + + /** + * Parse arguments or argument for this instance, and store values on results. + * + * @since 4.12.3 + * + * @param mixed $arguments Any possible set of arguments that this class supports. + * + * @return void + */ + protected function parse( $arguments ) { + if ( ! $arguments ) { + return; + } + + if ( is_numeric( $arguments ) ) { // phpcs:ignore + // Bail on any numeric values. + } elseif ( is_array( $arguments ) ) { + // ['foo', 'bar', ...] || ['foo' => TRUE, 'bar' => FALSE, 'baz' => 'foo', ...] + $this->parse_array( $arguments ); + } elseif ( is_string( $arguments ) ) { + // 'foo bar' + $this->parse_string( $arguments ); + } elseif ( $arguments instanceof \Closure || is_callable( $arguments ) ) { + // function() {} + $this->parse_callable( $arguments ); + } elseif ( is_object( $arguments ) ) { + // stdClass + $this->parse_object( $arguments ); + } + } + + /** + * Parse an array into an array of acceptable values for the instance. + * + * @since 4.12.3 + * + * @param array $values Array of values to be parsed. + * + * @return void + */ + protected function parse_array( array $values ) { + foreach ( $values as $key => $value ) { + if ( is_int( $key ) ) { + $this->parse( $value ); + } elseif ( is_string( $key ) ) { + if ( ! is_bool( $value ) && ! is_string( $value ) ) { + throw new \UnexpectedValueException( 'Value for key ' . $key . ' must be of type boolean or string' ); + } + + $this->results[ $key ] = $value; + } + } + } + + /** + * Parse a string into an array of acceptable values for the instance. + * + * @since 4.12.3 + * + * @param string $arguments Space separated string of attributes to be parsed. + * + * @return void + */ + protected function parse_string( $arguments ) { + $values = preg_split( '/\s+/', $arguments, -1, PREG_SPLIT_NO_EMPTY ); + + // When it doesn't match, bail early. + if ( ! $values ) { + return; + } + + $attrs = []; + + foreach ( $values as $key => $value ) { + if ( preg_match( '/^(?[^=]+)="*(?.*?)"*$/', $value, $m ) ) { + // Something like `f="boo"` or `foo=bar`. + $attrs[ $m['key'] ] = $m['value']; + + continue; + } + + $attrs[ $value ] = true; + } + + $this->parse_array( $attrs ); + } + + /** + * Parses an object into the array of considered attributes. + * + * @since 4.12.3 + * + * @param mixed $object Object to be converted into array and parsed. + * + * @return void + */ + protected function parse_object( $object ) { + $this->parse_array( (array) $object ); + } + + /** + * Parses a callable method or function into the array of considered attributes. + * + * The result of the callable will REPLACE the current attributes, callables will work like filters. + * + * @since 4.12.3 + * + * @param callable $method_or_function Method or Function to be called. + * + * @return void + */ + protected function parse_callable( callable $method_or_function ) { + $filtered = $method_or_function( $this->results ); + $this->results = []; + $this->parse( $filtered ); + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Element_Classes.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Element_Classes.php new file mode 100644 index 000000000..18496b0a9 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Element_Classes.php @@ -0,0 +1,246 @@ +arguments = func_get_args(); + } + + /** + * When invoked this class will return the full HTML class attribute. + * + * @since 4.9.13 + * + * @return string In the format ` class="class1 class2" ` + */ + public function __invoke() { + $this->arguments = func_get_args(); + return $this->get_attribute(); + } + + + /** + * When cast to string an instance will return the full HTML class attribute. + * + * @since 4.9.13 + * + * @return string In the format ` class="class1 class2" ` + */ + public function __toString() { + return $this->get_attribute(); + } + + /** + * Gets the full HTML class attribute for this instance of Element Classes. + * It will contain a space on each end of the attribute. + * + * @since 4.9.13 + * + * @return string In the format ` class="class1 class2" ` + */ + public function get_attribute() { + $classes = $this->get_classes_as_string(); + + // Bail with empty string when no classes are present + if ( ! $classes ) { + return ''; + } + + return " class=\"{$classes}\" "; + } + + /** + * Gets a space separated string of all classes to be printed. + * + * @since 4.9.13 + * + * @return string + */ + public function get_classes_as_string() { + return implode( ' ', $this->get_classes() ); + } + + /** + * Get the array of classes to be printed. + * + * @since 4.9.13 + * + * @return array + */ + public function get_classes() { + $this->results = []; + $classes = []; + + $this->parse_array( $this->arguments ); + + foreach ( $this->results as $key => $val ) { + if ( ! $val ) { + continue; + } + + $classes[] = $key; + } + + $classes = array_map( 'sanitize_html_class', $classes ); + $classes = array_filter( array_unique( $classes ) ); + + return $classes; + } + + + /** + * Get the array of the classes, using [ class_name => bool ] as the format. + * + * @since 4.9.13 + * + * @return array [ class_name => bool ] + */ + public function get_conditions() { + $this->results = []; + $this->parse_array( $this->arguments ); + + return $this->results; + } + + /** + * Parse arguments or argument for this instance, and store values on results. + * + * @since 4.9.13 + * + * @param mixed $arguments Any possible set of arguments that this class supports. + * @param boolean $default_value What is the default value for a given class. + * + * @return void + */ + protected function parse( $arguments, $default_value = true ) { + if ( ! $arguments ) { + return; + } + + if ( is_numeric( $arguments ) ) { // phpcs:ignore + // Bail on any numeric values + } elseif ( is_string( $arguments ) ) { + // 'foo bar' + $this->parse_string( $arguments ); + } elseif ( $arguments instanceof \Closure || is_callable( $arguments ) ) { + // function() {} + $this->parse_callable( $arguments ); + } elseif ( is_array( $arguments ) ) { + // ['foo', 'bar', ...] || ['foo' => TRUE, 'bar' => FALSE, ...] + $this->parse_array( $arguments ); + } elseif ( is_object( $arguments ) ) { + // stdClass + $this->parse_object( $arguments ); + } + } + + /** + * Parse a string into an array of acceptable values for the instance. + * + * @since 4.9.13 + * + * @param string $arguments Space separated string of classes to be parsed. + * @param boolean $default_value What is the default value for a given class. + * + * @return void + */ + protected function parse_string( $arguments, $default_value = true ) { + $values = preg_split( '/\s+/', $arguments, -1, PREG_SPLIT_NO_EMPTY ); + + // When it doesn't match, bail early. + if ( ! $values ) { + return; + } + + foreach ( $values as $class_name ) { + $this->results[ $class_name ] = $default_value; + } + } + + /** + * Parse an array into an array of acceptable values for the instance. + * + * @since 4.9.13 + * + * @param array $values Array of values to be parsed. + * + * @return void + */ + protected function parse_array( array $values ) { + foreach ( $values as $key => $value ) { + if ( is_int( $key ) ) { + if ( is_bool( $value ) ) { + $this->parse( $key, $value ); + } else { + $this->parse( $value ); + } + } elseif ( is_string( $key ) ) { + if ( $value instanceof \Closure || is_callable( $value ) ) { + $value = $value( $this->results ); + } + + $this->parse_string( $key, tribe_is_truthy( $value ) ); + } + } + } + + /** + * Parses an object, only if it contains __toString it will be considered. + * + * @since 4.9.13 + * + * @param mixed $object Object to be checked for the __toString method + * + * @return void + */ + protected function parse_object( $object ) { + if ( method_exists( $object, '__toString' ) ) { + $this->parse( (string) $object ); + } + } + + /** + * Parses a callable method or function into the array of considered classes.s + * + * @since 4.9.13 + * + * @param callable $method_or_function Method or Function to be called. + * + * @return void + */ + protected function parse_callable( callable $method_or_function ) { + $this->parse( $method_or_function( $this->results ) ); + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Global_ID.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Global_ID.php new file mode 100644 index 000000000..8ff4e91d4 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Global_ID.php @@ -0,0 +1,165 @@ + 'meetup.com', + 'facebook' => 'facebook.com', + 'eventbrite' => 'eventbrite.com', + ]; + + /** + * Tribe__Utils__Global_ID constructor. + */ + public function __construct() { + + /** + * Filters the registered origin types for Global IDs. + * + * @since 4.7.21 + * + * @param array $type_origins List of origin types. + */ + $this->valid_types = apply_filters( 'tribe_global_id_valid_types', $this->valid_types ); + + /** + * Filters the registered origin URLs for Global IDs. + * + * @since 4.7.21 + * + * @param array $type_origins List of origin URLs. + */ + $this->type_origins = apply_filters( 'tribe_global_id_type_origins', $this->type_origins ); + + } + + /** + * A setter and getter for the Type of ID + * + * @param string|null $name When null is passed it will return the current Type + * @return mixed Will return False on invalid type or the Type in String + */ + public function type( $name = null ) { + if ( is_null( $name ) ) { + return $this->type; + } + + $name = strtolower( $name ); + + if ( ! in_array( $name, $this->valid_types ) ) { + return false; + } + + $this->type = $name; + + return $this->type; + } + + /** + * A setter and getter for the origin on this ID + * + * @param string|null $name When null is passed it will return the current Origin + * @return mixed Will return False on invalid origin or the Origin in String + */ + public function origin( $url = null ) { + if ( ! empty( $this->type_origins[ $this->type ] ) ) { + $this->origin = $this->type_origins[ $this->type ]; + } + + if ( is_null( $url ) ) { + return $this->origin; + } + + $parts = wp_parse_url( $url ); + + if ( ! $parts ) { + return false; + } + + $this->origin = $parts['host']; + + if ( ! empty( $parts['path'] ) ) { + $this->origin .= $parts['path']; + } + + if ( ! empty( $parts['query'] ) ) { + $this->origin .= '?' . $parts['query']; + } + + return $this->origin; + } + + /** + * A very simple Generation of IDs + * + * @param array $args Which query arguments will be added to the Origin + * + * @return string + */ + public function generate( array $args = [] ) { + // We can't do this without type or origin + if ( ! $this->type() || ! $this->origin() ) { + return false; + } + + return add_query_arg( $args, $this->origin() ); + } + + /** + * Parse the Global ID string. + * + * @param string $global_id The previously generated global ID string. + * + * @return array The parsed $args information built by self::generate() + * + * @since 4.7.15 + */ + public function parse( $global_id ) { + $parsed_global_id = null; + + if ( $global_id ) { + $global_id = html_entity_decode( $global_id ); // & characters replaced as expected + + $parsed = wp_parse_url( 'http://' . $global_id ); + + if ( ! empty( $parsed['query'] ) ) { + $parsed_query = []; + + wp_parse_str( $parsed['query'], $parsed_query ); + + if ( ! empty( $parsed_query ) ) { + $parsed_global_id = $parsed_query; + } + } + } + + return $parsed_global_id; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/JSON.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/JSON.php new file mode 100644 index 000000000..6268aa77a --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/JSON.php @@ -0,0 +1,38 @@ + $subvalue ) { + $escaped[ $key ] = self::escape_string( $subvalue ); + } + + return $escaped; + } + + $escapers = [ "\\", "/", "\"", "\n", "\r", "\t", "\x08", "\x0c" ]; + $replacements = [ "\\\\", "\\/", "\\\"", "\\n", "\\r", "\\t", "\\f", "\\b" ]; + + return str_replace( $escapers, $replacements, $value ); + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Lazy_Collection.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Lazy_Collection.php new file mode 100644 index 000000000..a3c6651c0 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Lazy_Collection.php @@ -0,0 +1,118 @@ +all; + * } + * ```` + * + * @since 4.9.14 + * @package Tribe\Utils + */ + +namespace Tribe\Utils; + +/** + * Class Lazy_Collection + * + * @since 4.9.14 + * @package Tribe\Utils + */ +class Lazy_Collection implements Collection_Interface { + use Collection_Trait; + use Lazy_Events; + + /** + * The callback in charge of providing the elements. + * + * @var callable + */ + protected $callback; + + /** + * The elements of the array. + * + * @var array + */ + protected $items; + + /** + * Array_Promise constructor. + * + * @since 4.9.14 + * + * @param callable $callback The callback that will be used to populate the elements. + */ + public function __construct( callable $callback ) { + $this->callback = $callback; + } + + /** + * Fetches the array items and returns them. + * + * @since 4.9.14 + * + * @return array The array items. + */ + public function all() { + $this->resolve(); + + return $this->items; + } + + /** + * Fills the array elements from the callback if required. + * + * @since 4.9.14 + */ + protected function resolve() { + if ( null !== $this->items ) { + return; + } + + $items = call_user_func( $this->callback ); + $this->items = (array) $items; + $this->resolved(); + } + + /** + * Allows accessing the collection methods using properties. + * + * E.g. `$collection->first` is equivalent to `$collection->first()`. + * + * @since 4.9.14 + * + * @param string $property The name of the property to access. + * + * @return mixed|null The return value of the collection corresponding method or `null` if the collection does not + * have that method. + */ + public function __get( $property ) { + if ( method_exists( $this, $property ) ) { + return call_user_func( [ $this, $property ] ); + } + + return null; + } + + /** + * {@inheritDoc} + */ + #[\ReturnTypeWillChange] + public function jsonSerialize() { + return $this->all(); + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Lazy_Events.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Lazy_Events.php new file mode 100644 index 000000000..279288ecb --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Lazy_Events.php @@ -0,0 +1,172 @@ +list ){ + * $this->list = really_expensive_calculation(); + * } + * + * $this->resolved(); + * } + * + * return $this->list; + * } + * } + * + * class Lazy_Value { + * use Tribe\Utils\Lazy_Events; + * + * protected $value; + * + * public function calculate_value(){ + * $cached = wp_cache_get( 'expensive_value' ); + * + * if( false !== $cached ){ + * return $cached; + * + * if( null === $this->value ){ + * $this->value = really_expensive_calculation(); + * } + * + * $this->resolved(); + * } + * + * return $this->value; + * } + * } + * + * class List_And_Value { + * protected $list; + * protected $value; + * + * public function __construct( Lazy_List_Of_Stuff $list, Lazy_Value $value ){ + * $this->list = $list; + * $this->value = $value; + * $this->list->on_resolve( [ $this, 'cache' ] ); + * $this->value->on_resolve( [ $this, 'cache' ] ); + * } + * + * public function cache(){ + * wp_cache_set( 'list_and_value', [ + * 'list' => $this->list->fetch_list(), + * 'value' => $this->value->calculate_value(), + * ]); + * } + * + * public function get_list(){ + * $cached = wp_cache_get( 'list_and_value' ); + * + * return $cached ? $cached['list'] : $this->list->fetch_list(); + * } + * + * public function get_value(){ + * $cached = wp_cache_get( 'list_and_value' ); + * + * return $cached ? $cached['value'] : $this->value->fetch_value(); + * } + * } + * + * + * $list = new Lazy_List_Of_Stuff(); + * $value = new Lazy_Value(); + * $list_and_value = new List_And_Value( $list, $value ); + * + * // Accessing `value` will make it so that `list` too will be cached. + * $list_and_value->get_value(); + * ```` + * + * @package Tribe\Utils + */ + +namespace Tribe\Utils; + +/** + * Trait Lazy_Events + * + * @since 4.9.16 + * + * @package Tribe\Utils + * + * @property string $lazy_resolve_action The action to which the trait will hook to run the callback if the object + * resolved. Using classes should define the property if the default `shutdown` + * one is not correct. + * @property int $lazy_resolve_priority The priority at which the resolution callback will be hooked on the + * `$lazy_resolve_action`; defaults to `10`. + */ +trait Lazy_Events { + + /** + * The callback that will be called when, and if, the lazy object resolved at least once. + * + * @since 4.9.16 + * + * @var + */ + protected $lazy_resolve_callback; + + /** + * Sets the callback that will be hooked to the resolve action when, and if, the `resolved` method is called. + * + * @since 4.9.16 + * + * @param callable $callback The callback that will be hooked on the `$lazy_resolve_action` (defaults to `shutdown`) + * if the `resolved` method is called. + * + * @return static The object instance. + * + * @see Lazy_Events::resolved() + */ + public function on_resolve( callable $callback = null ) { + if ( null === $callback ) { + return $this; + } + + $this->lazy_resolve_callback = $callback; + + return $this; + } + + /** + * Hooks the `$lazy_resolve_callback` to the `$lazy_resolve_action` with the `$lazy_resolve_priority` if set. + * + * @since 4.9.16 + */ + protected function resolved() { + if ( empty( $this->lazy_resolve_callback ) ) { + return; + } + + $action = property_exists( $this, 'lazy_resolve_action' ) ? + $this->lazy_resolve_action + : 'shutdown'; + $priority = property_exists( $this, 'lazy_resolve_priority' ) ? + $this->lazy_resolve_priority + : 10; + + $hooked = has_action( $action, $this->lazy_resolve_callback ); + + // Let's play it safe and move the resolution as late as possible. + $new_priority = false !== $hooked ? max( $hooked, $priority ) : $priority; + + if ( is_numeric( $hooked ) && $hooked !== $new_priority ) { + remove_action( $action, $this->lazy_resolve_callback, $hooked ); + } + + add_action( $action, $this->lazy_resolve_callback, $new_priority ); + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Lazy_String.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Lazy_String.php new file mode 100644 index 000000000..aa762b1d2 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Lazy_String.php @@ -0,0 +1,172 @@ +value_callback = $callback; + $this->escape_callback = $escape_callback; + } + + /** + * Inits, and returns, the string value of the string. + * + * @since 4.9.16 + * + * @return string The unescaped string value. + */ + public function __toString() { + if ( null === $this->string ) { + $value = call_user_func( $this->value_callback ); + if ( ! $value instanceof \Generator ) { + $value = (string) $value; + } + + $this->string = $value; + $this->resolved(); + } + + return $this->string; + } + + /** + * Returns the HTML ready, escaped version of the string. + * + * @since 4.9.16 + * + * @return string The escaped version of the string. + */ + public function escaped() { + if ( null !== $this->escaped ) { + return $this->escaped; + } + + $this->escaped = empty( $this->escape_callback ) + ? $this->__toString() + : call_user_func( $this->escape_callback, $this->__toString() ); + + return $this->escaped; + } + + /** + * Returns the string value, just a proxy of the `__toString` method. + * + * @since 4.9.16 + * + * @return string The string value. + */ + public function value() { + return $this->__toString(); + } + + /** + * {@inheritDoc} + * + * @since 4.9.16 + */ + public function serialize() { + $serialized = serialize( [ $this->__toString(), $this->escaped() ] ); + + unset( $this->value_callback, $this->escape_callback ); + + return $serialized; + } + + /** + * {@inheritDoc} + * + * @since 4.9.16 + */ + public function unserialize( $serialized ) { + $data = unserialize( $serialized ); + $this->__unserialize( $data ); + } + + /** + * {@inheritDoc} + */ + #[\ReturnTypeWillChange] + public function jsonSerialize() { + return $this->value(); + } + + /** + * PHP 8.0+ compatible implementation of the serialization logic. + * + * @since 5.0.6 + * + * @return array The data to serialize. + */ + public function __serialize(): array { + return [ + 'string' => $this->__toString(), + 'escaped' => $this->escaped(), + ]; + } + + /** + * PHP 8.0+ compatible implementation of the unserialization logic. + * + * @since 5.0.6 + * + * @param array $data The data to unserialize. + */ + public function __unserialize( array $data ): void { + $this->string = $data['string'] ?? null; + $this->escaped = $data['escaped'] ?? null; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Paths.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Paths.php new file mode 100644 index 000000000..1f8bb8029 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Paths.php @@ -0,0 +1,75 @@ +> ...$paths A set of paths to merge, each one either a string or an array + * of path fragments. + * + * @return string The merged path, the path intersecting fragments removed. + */ + public static function merge( ...$paths ) { + $merged_paths = ''; + + if ( count( $paths ) > 2 ) { + $slice = array_splice( $paths, 0, 1 ); + $paths = array_merge( $slice, [ static::merge( ...$paths ) ] ); + } + + $path_1 = isset( $paths[0] ) ? $paths[0] : ''; + $lead_slash = is_string( $path_1 ) && $path_1 !== ltrim( $path_1, '\\/' ) ? DIRECTORY_SEPARATOR : ''; + $path_2 = isset( $paths[1] ) ? $paths[1] : ''; + $trail_slash = is_string( $path_2 ) && $path_2 !== rtrim( $path_2, '\\/' ) ? DIRECTORY_SEPARATOR : ''; + // Handle *nix spacing escape sequence (`\ `) correctly. The Windows one (`^ `) is already handled. + $break_pattern = '/[\\\\\\/](?!\\s)/'; + $drop_empty_strings = static function ( $frag ) { + return $frag !== ''; + }; + $path_1_frags = is_array( $path_1 ) + ? $path_1 + : array_filter( (array) preg_split( $break_pattern, $path_1 ), $drop_empty_strings ); + $path_2_frags = is_array( $path_2 ) + ? $path_2 + : array_filter( (array) preg_split( $break_pattern, $path_2 ), $drop_empty_strings ); + $non_consecutive_common = array_intersect( $path_1_frags, $path_2_frags ); + + $trimmed_path_2 = trim( + preg_replace( + '#^' . preg_quote( implode( DIRECTORY_SEPARATOR, $non_consecutive_common ), '#' ) . '#', '', + implode( DIRECTORY_SEPARATOR, $path_2_frags ) + ), + '\\/' + ); + + $merged_paths .= $lead_slash . implode( DIRECTORY_SEPARATOR, $path_1_frags ); + + if ( $trimmed_path_2 ) { + $merged_paths .= DIRECTORY_SEPARATOR . $trimmed_path_2 . $trail_slash; + } + + return $merged_paths; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Plugins.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Plugins.php new file mode 100644 index 000000000..ee96a0d8c --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Plugins.php @@ -0,0 +1,80 @@ + 'Plugin Name', + 'PluginURI' => 'Plugin URI', + 'Version' => 'Version', + 'ExtensionClass' => 'Extension Class', + 'ExtensionFile' => 'Extension File', + 'Description' => 'Description', + 'Author' => 'Author', + 'AuthorURI' => 'Author URI', + 'TextDomain' => 'Text Domain', + 'DomainPath' => 'Domain Path', + 'Network' => 'Network', + ]; + + /** + * Filter which header keys passed to get_file_data(). + * + * @see get_file_data() + * + * @param array $headers The headers. + * @param string $plugin_file The plugin file path. + */ + $headers = apply_filters( 'tribe_get_plugin_data_headers', $headers, $plugin_file ); + $file_data = get_file_data( $plugin_file, $headers, 'plugin' ); + + /** + * Filter the parsed plugin header data. + * + * @param array $file_data Output from get_file_data(). + * @param string $plugin_file The plugin file path. + * @param array $headers The headers. + */ + return apply_filters( 'tribe_get_plugin_data', $file_data, $plugin_file, $headers ); + } + + /** + * Get list of active plugins with a given prefix in the plugin folder path. + * + * @param string|array $prefix Prefixes you want to retrieve. + * + * @return array List of plugins with prefix in path. + */ + public static function get_plugins_with_prefix( $prefix ) { + $full_list = wp_get_active_and_valid_plugins(); + + if ( is_multisite() ) { + $full_list = array_merge( $full_list, wp_get_active_network_plugins() ); + } + + $filtered_list = []; + + foreach ( $full_list as $plugin ) { + $base = plugin_basename( $plugin ); + + if ( 0 === Tribe__Utils__Array::strpos( $base, $prefix ) ) { + $filtered_list[] = $plugin; + } + } + + return $filtered_list; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Post_Collection.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Post_Collection.php new file mode 100644 index 000000000..7240fcfd7 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Post_Collection.php @@ -0,0 +1,292 @@ + + */ + protected $taxonomies; + + /** + * Tribe__Utils__Post_Collection constructor. + * + * Overrides the base constructor to ensure all elements in the collection are, in fact, posts. + * Elements that do not resolve to a post are discarded. + * + * @param array $items + */ + public function __construct( array $items ) { + parent::__construct( array_filter( array_map( 'get_post', $items ) ) ); + } + + /** + * Plucks fields from the posts in the collection creating a map using a field value as key and one + * or more fields as values. + * + * Note: the method does not make any check on the uniqueness of the fields used as keys, e.g. this will + * probably not return what intended: `$collection->pluck_combine( 'post_status', 'post_title' );`. + * If there's a chance of the key fields not being unique, then use `#` as key field to simply return an + * array of plucked values. + * + * @since 4.12.6 + * + * @param string $key_field The field to key the return map by, or `#` to use + * progressive integers to key the return value. Use fields + * as keys only when their uniqueness is sure. + * @param string|array|array $value_fields Either a single field name to populate the values with; + * a list of fields, each plucked with default settings; + * a map of fields to fetch, each defining a `single` and + * `args` key to define the pluck `$single` and `$args` + * parameters where applicable. + * Additionally an `as` parameter can be specified to alias + * the field in the results. + * If the only requirement is to alias fields, just use a + * flat map like `[ => , ... ]`. + * + * @return array A list of plucked fields or a map of plucked fields keyed by the + * specified field. + */ + public function pluck_combine( $key_field = '#', $value_fields = 'post_title' ) { + $value_req_is_array = is_array( $value_fields ); + $value_fields = (array) $value_fields; + $rows = []; + $field_names = []; + $field_index = 0; + foreach ( $value_fields as $k => $field ) { + if ( is_string( $k ) && is_string( $field ) ) { + $single = true; + $args = []; + $field_name = $field; + $pluck = $k; + } else { + list( $as, $single, $args ) = $this->parse_field_args( $field ); + $field = is_array( $field ) ? $k : $field; + $field_name = null === $as ? $field : $as; + $pluck = $field; + } + $field_names[ $field_index ] = $field_name; + $rows[ $field_name ] = $this->pluck( $pluck, $single, $args ); + $field_index ++; + } + $values = []; + + // Build a list with only numeric keys and string values. + $fields_list = array_replace( + array_filter( + array_filter( $value_fields, 'is_string' ), + 'is_numeric', + ARRAY_FILTER_USE_KEY + ), + $field_names + ); + + for ( $i = 0, $count = count( $this->items ); $i < $count; $i ++ ) { + $values[ $i ] = array_combine( $fields_list, array_column( $rows, $i ) ); + } + + if ( ! $value_req_is_array ) { + $values = array_column( $values, reset( $fields_list ) ); + } + + // If the key field is `#` then use a progressive number as key, else use the specified field. + $keys = '#' === $key_field + ? range( 0, count( $this->items ) - 1 ) + : $this->pluck( $key_field, true ); + + return array_combine( $keys, $values ); + } + + /** + * Parses a single field request to extract the `$single` and `$args` parameters from it. + * + * @since 4.12.6 + * + * @param string|array $field The field name or the field arguments map. + * + * @return array The `$as`, `$single` and `$args` parameters extracted from the field. + */ + protected function parse_field_args( $field ) { + $field = (array) $field; + + $as = isset( $field['as'] ) + ? (string) $field['as'] + : null; + $single = isset( $field['single'] ) + ? (bool) $field['single'] + : true; + $args = isset( $field['args'] ) + ? (array) $field['args'] + : null; + + return [ $as, $single, $args ]; + } + + /** + * Plucks a post field, a taxonomy or a custom field from the collection. + * + * @since 4.12.6 + * + * @param string $key The name of the field to pluck; the method will try to detect the type of field + * from its name. If any issues might arise due to fields of different types with the + * same name, then use the `pluck_` methods directly. + * @param bool $single Whether to pluck a single taxonomy term or custom fields or an array of all the taxonomy + * terms or custom fields for each post. + * @param array $args A list of n optional arguments that will be passed down to the `pluck_` methods. + * Currently only the the `pluck_taxonomy` will support one more argument to define the + * query arguments for the term query. + * + * @return array|array Either an array of plucked fields when plucking post fields or single + * custom fields or taxonomy terms, or an array of arrays, each one a list + * of all the taxonomy terms or custom fields entries for each post. + */ + public function pluck( $key, $single = true, array $args = null ) { + $type = $this->detect_field_type( $key ); + + switch ( $type ) { + case 'post_field': + return $this->pluck_field( $key ); + break; + case 'taxonomy': + return $this->pluck_taxonomy( $key, $single, $args ); + break; + default: + return $this->pluck_meta( $key, $single ); + break; + } + } + + /** + * Detects the type of a post field from its name. + * + * @since 4.12.6 + * + * @param string $key The name of the field to check. + * + * @return string The type of field detected for the key, either `post_field`, `taxonomy` or `custom_field`. + */ + protected function detect_field_type( $key ) { + if ( $this->is_a_post_field( $key ) ) { + return 'post_field'; + } + + // Init taxonomies as late as possible and only once. + $this->init_taxonomies(); + + if ( $this->is_a_taxonomy( $key ) ) { + return 'taxonomy'; + } + + return 'custom_field'; + } + + /** + * Initialize the post collection taxonomies by filling up the `$taxonomies` property. + * + * Note the collection will use the first post in the collection to fill the taxonomies array, + * this assumes the collection is homogeneous in its post types. + * + * @since 4.12.6 + */ + protected function init_taxonomies() { + if ( ! empty( $this->taxonomies ) ) { + // Already set up, return. + return; + } + + if ( empty( $this->items ) ) { + // We cannot detect taxonomies from an empty list of items. + $this->taxonomies = []; + + return; + } + + // Use the first post to detect the taxonomies. + $this->taxonomies = get_object_taxonomies( reset( $this->items ), 'names' ); + } + + /** + * Plucks a post field from all posts in the collection. + * + * Note: there is no check on the name of the plucked post field: if a non-existing post field is requested, then + * the method will return an empty array. + * + * @since 4.12.6 + * + * @param string $field The name of the post field to pluck. + * + * @return array A list of the plucked post fields from each item in the collection. + */ + public function pluck_field( $field ) { + return wp_list_pluck( $this->items, $field ); + } + + /** + * Plucks taxonomy terms assigned to the posts in the collection. + * + * Note: there is no check on the taxonomy being an existing one or not; that responsibility + * is on the user code. + * + * @since 4.12.6 + * + * @param string $taxonomy The name of the post taxonomy to pluck terms for. + * @param bool $single Whether to return only the first results or all of them. + * @param array $args A set of arguments as supported by the `WP_Term_Query::__construct` + * method. + * + * @return array|array Either an array of the requested results if `$single` is `true` + * or an array of arrays if `$single` is `false`. + */ + public function pluck_taxonomy( $taxonomy, $single = true, array $args = null ) { + $plucked = []; + $args = null === $args ? [ 'fields' => 'names' ] : $args; + + foreach ( $this as $item ) { + $terms = wp_get_object_terms( $item->ID, $taxonomy, $args ); + $plucked[] = $single ? reset( $terms ) : $terms; + } + + return $plucked; + } + + /** + * Plucks a meta key for all elements in the collection. + * + * Elements that are not posts or do not have the meta set will have an + * empty string value. + * + * @since 4.9.5 + * + * @param string $meta_key The meta key to pluck. + * @param bool $single Whether to fetch the meta key as single or not. + * + * @return array An array of meta values for each item in the collection; items that + * do not have the meta set or that are not posts, will have an empty + * string value. + */ + public function pluck_meta( $meta_key, $single = true ) { + $plucked = []; + + foreach ( $this as $item ) { + $plucked[] = get_post_meta( $item->ID, $meta_key, $single ); + } + + return $plucked; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Post_Root_Pool.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Post_Root_Pool.php new file mode 100644 index 000000000..de1b920ab --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Post_Root_Pool.php @@ -0,0 +1,203 @@ +post_name; + + // A lot fo these get urlencoded, so let's try to fix that first + $post_name = tribe_maybe_urldecode( $post_name ); + + $this->current_post = $post; + $flipped_pool = array_flip( $this->fetch_pool() ); + + if ( isset( $flipped_pool[ $this->current_post->ID ] ) ) { + return $flipped_pool[ $this->current_post->ID ] . $this->root_separator; + } + + $root = $this->build_root_from( $post_name ); + + return $root . $this->root_separator; + } + + /** + * @param string $post_name + * + * @param string $postfix + * + * @return string + */ + protected function build_root_from( $post_name, $postfix = '' ) { + $candidate = $this->build_root_candidate( $post_name, $postfix ); + + $initial_candidate = $candidate; + + while ( $this->is_in_pool( $candidate ) ) { + $postfix = $this->postfix; + $candidate = $initial_candidate . '-' . $postfix; + $this->postfix ++; + } + + $this->postfix = 1; + + $this->insert_root_in_pool( $candidate ); + + return $candidate; + } + + /** + * @return string + */ + public function get_pool_transient_name() { + return $this->pool_transient_name; + } + + /** + * @param $string + * + * @return string + * @deprecated 4.7.18 + */ + protected function uc_first_letter( $string ) { + _deprecated_function( __METHOD__, '4.7.18', 'tribe_uc_first_letter' ); + + return is_numeric( $string ) ? $string : tribe_uc_first_letter( $string ); + } + + /** + * @param $string + * + * @return string + * @deprecated 4.7.18 + */ + protected function safe_strtoupper( $string ) { + _deprecated_function( __METHOD__, '4.7.18', 'tribe_strtoupper' ); + + return is_numeric( $string ) ? $string : tribe_strtoupper( $string ); + } + + /** + * @param string $candidate + */ + protected function is_in_pool( $candidate ) { + $pool = $this->fetch_pool(); + + return isset( $pool[ $candidate ] ); + } + + /** + * @return array + */ + protected function fetch_pool() { + if ( false === self::$prefix_pool ) { + $this->maybe_init_pool(); + } + + return self::$prefix_pool; + } + + protected function maybe_init_pool() { + self::$prefix_pool = get_transient( $this->pool_transient_name ); + if ( self::$prefix_pool === false ) { + self::$prefix_pool = []; + set_transient( $this->pool_transient_name, [] ); + } + } + + /** + * @param string $unique_root + */ + protected function insert_root_in_pool( $unique_root ) { + $prefix_pool = $this->fetch_pool(); + $prefix_pool[ $unique_root ] = $this->current_post->ID; + self::$prefix_pool = $prefix_pool; + set_transient( $this->pool_transient_name, $prefix_pool ); + } + + public static function reset_pool() { + self::$prefix_pool = false; + } + + /** + * @param $post_name + * @param $postfix + * + * @return string + */ + protected function build_root_candidate( $post_name, $postfix ) { + $frags = explode( '-', $post_name ); + + $candidate = implode( '', array_map( 'strtoupper', $frags ) ); + + if ( strlen( $candidate ) > 9 ) { + $frags = array_filter( $frags ); + $candidate = implode( '', array_map( 'tribe_uc_first_letter', $frags ) ); + } + + $candidate = $candidate . $postfix; + + return $candidate; + } + + /** + * Primes the post pool. + * + * @param array $pool + * @param bool $override_transient If `true` the transient too will be overwritten. + */ + public function set_pool( array $pool, $override_transient = false ) { + self::$prefix_pool = $pool; + if ( $override_transient ) { + set_transient( $this->pool_transient_name, $pool ); + } + } + + /** + * Whether the pool transient has been primed or not. + * + * @return bool + */ + public function is_primed() { + return get_transient( $this->pool_transient_name ) !== false; + } + + /** + * @return array + */ + public function get_pool() { + return $this->fetch_pool(); + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Post_Thumbnail.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Post_Thumbnail.php new file mode 100644 index 000000000..28228a861 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Post_Thumbnail.php @@ -0,0 +1,356 @@ +large->url; + * ``` + * + * @since 4.9.14 + * @package Tribe\Utils + */ + + +namespace Tribe\Utils; + +use Tribe__Utils__Array as Arr; + +/** + * Class Post_Thumbnail + * + * @since 4.9.14 + * @package Tribe\Utils + */ +class Post_Thumbnail implements \ArrayAccess, \Serializable { + use Lazy_Events; + + /** + * An array of the site image sizes, including the `full` one. + * + * @since 4.9.14 + * + * @var array + */ + protected $image_sizes; + + /** + * The post ID this images collection is for. + * + * @since 4.9.14 + * + * @var int + */ + protected $post_id; + + /** + * The post thumbnail data. + * + * @since 4.9.14 + * + * @var array + */ + protected $data; + + /** + * A flag property indicating whether the post thumbnail for the post exists or not. + * + * @since 4.9.16 + * + * @var bool + */ + protected $exists; + + /** + * The post ID, if any, of the post thumbnail. + * + * @since 4.9.16 + * + * @var int + */ + protected $thumbnail_id; + + /** + * Post_Images constructor. + * + * @param int $post_id The post ID. + */ + public function __construct( $post_id ) { + $this->post_id = $post_id; + } + + /** + * {@inheritDoc} + */ + public function __get( $property ) { + if ( 'exists' === $property ) { + return $this->exists(); + } + + return $this->offsetGet( $property ); + } + + /** + * {@inheritDoc} + */ + public function __set( $property, $value ) { + if ( 'exists' === $property ) { + throw new \InvalidArgumentException( 'The `Post_Thumbnail::exists` property cannot be set.' ); + } + + $this->offsetSet( $property, $value ); + } + + /** + * {@inheritDoc} + */ + public function __isset( $property ) { + return $this->offsetExists( $property ); + } + + /** + * Fetches and returns the image sizes registered on the site, if any. + * + * @since 4.9.14 + * + * @return array An array of the registered image sizes. + */ + public function get_image_sizes() { + if ( null !== $this->image_sizes ) { + return $this->image_sizes; + } + + $image_sizes = array_merge( [ 'full' ], get_intermediate_image_sizes() ); + + /** + * Filters the image sizes the `Tribe\Utils\Post_Thumbnail` class will manage and fetch data for. + * + * @since 4.9.14 + * + * @param array $image_sizes All the available image sizes; this includes the default and the intermediate ones. + */ + $this->image_sizes = apply_filters( 'tribe_post_thumbnail_image_sizes', $image_sizes ); + + return $this->image_sizes; + } + + /** + * Returns the data about the post thumbnail, if any. + * + * @since 4.9.14 + * + * @return array An array of objects containing the post thumbnail data. + */ + public function fetch_data() { + static $cache_thumbnail = []; + + if ( ! $this->exists() ) { + return []; + } + + if ( null !== $this->data ) { + return $this->data; + } + + $image_sizes = $this->get_image_sizes(); + $thumbnail_id = $this->thumbnail_id; + + $cache_key = empty( $thumbnail_id ) ? -1 : $thumbnail_id; + + if ( empty( $cache_thumbnail[ $cache_key ] ) ) { + $thumbnail_data = array_combine( + $image_sizes, + array_map( + static function( $size ) use ( $thumbnail_id ) { + static $cache_size_data = []; + + $size_data_cache_key = empty( $thumbnail_id ) ? -1 : $thumbnail_id; + $size_data_cache_key = "{$size_data_cache_key}:{$size}"; + + if ( ! isset( $cache_size_data[ $size_data_cache_key ] ) ) { + $cache_size_data[ $size_data_cache_key ] = wp_get_attachment_image_src( $thumbnail_id, $size ); + } + + $size_data = $cache_size_data[ $size_data_cache_key ]; + + if ( false === $size_data ) { + return (object) [ + 'url' => '', + 'width' => '', + 'height' => '', + 'is_intermediate' => false, + ]; + } + + return (object) [ + 'url' => Arr::get( $size_data, 0, '' ), + 'width' => Arr::get( $size_data, 1, '' ), + 'height' => Arr::get( $size_data, 2, '' ), + 'is_intermediate' => (bool) Arr::get( $size_data, 3, false ), + ]; + }, + $image_sizes + ) + ); + + $srcset = wp_get_attachment_image_srcset( $thumbnail_id ); + $thumbnail_data['srcset'] = ! empty( $srcset ) ? $srcset : false; + + $title = get_the_title( $thumbnail_id ); + $thumbnail_data['title'] = ! empty( $title ) ? $title : false; + + $alt = trim( strip_tags( get_post_meta( $thumbnail_id, '_wp_attachment_image_alt', true ) ) ); + $thumbnail_data['alt'] = ! empty( $alt ) ? $alt : false; + + $cache_thumbnail[ $cache_key ] = $thumbnail_data; + } + + /** + * Filters the post thumbnail data and information that will be returned for a specific post. + * + * Note that the thumbnail data will be cast to an object after this filtering. + * + * @since 4.9.14 + * + * @param array $thumbnail_data The thumbnail data for the post. + * @param int $post_id The ID of the post the data is for. + */ + $thumbnail_data = apply_filters( 'tribe_post_thumbnail_data', $cache_thumbnail[ $cache_key ], $this->post_id ); + + $this->resolved(); + + return $thumbnail_data; + } + + /** + * {@inheritDoc} + */ + #[\ReturnTypeWillChange] + public function offsetExists( $offset ) { + $this->data = $this->fetch_data(); + + return isset( $this->data[ $offset ] ); + } + + /** + * {@inheritDoc} + */ + #[\ReturnTypeWillChange] + public function offsetGet( $offset ) { + $this->data = $this->fetch_data(); + + return isset( $this->data[ $offset ] ) + ? $this->data[ $offset ] + : null; + } + + /** + * {@inheritDoc} + */ + #[\ReturnTypeWillChange] + public function offsetSet( $offset, $value ) { + $this->data = $this->fetch_data(); + + $this->data[ $offset ] = $value; + } + + /** + * {@inheritDoc} + */ + #[\ReturnTypeWillChange] + public function offsetUnset( $offset ) { + $this->data = $this->fetch_data(); + + unset( $this->data[ $offset ] ); + } + + /** + * Returns an array representation of the post thumbnail data. + * + * @since 4.9.14 + * + * + * @return array An array representation of the post thumbnail data. + */ + public function to_array() { + $this->data = $this->fetch_data(); + + return json_decode( json_encode( $this->data ), true ); + } + + /** + * {@inheritDoc} + */ + public function serialize() { + return wp_json_encode( $this->__serialize() ); + } + + /** + * {@inheritDoc} + */ + public function unserialize( $serialized ) { + $data = json_decode( $serialized, true ); + $this->__unserialize( $data ); + } + + /** + * Returns whether a post thumbnail is set for the post or not. + * + * @since 4.9.16 + * + * @return bool Whether a post thumbnail is set for the post or not. + */ + public function exists() { + if ( null !== $this->exists ) { + return $this->exists; + } + + $thumbnail_id = get_post_thumbnail_id( $this->post_id ); + + if ( empty( $thumbnail_id ) ) { + $this->exists = false; + } else { + $this->thumbnail_id = $thumbnail_id; + $this->exists = true; + } + + return $this->exists; + } + + /** + * PHP 8.0+ compatible implementation of the serialization logic. + * + * @since 5.0.6 + * + * @return array The data to serialize. + */ + public function __serialize(): array { + $data = $this->fetch_data(); + $data['post_id'] = $this->post_id; + + return $data; + } + + /** + * PHP 8.0+ compatible implementation of the unserialization logic. + * + * @since 5.0.6 + * + * @param array $data The data to unserialize. + */ + public function __unserialize( array $data ): void { + array_walk( $data, static function ( &$data_entry ) { + if ( is_array( $data_entry ) ) { + $data_entry = (object) $data_entry; + } + } ); + $this->post_id = $data['post_id']; + unset( $data['post_id'] ); + $this->data = ! empty( $data ) ? $data : null; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Query.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Query.php new file mode 100644 index 000000000..dc8b64bf5 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Query.php @@ -0,0 +1,72 @@ +posts = $posts; + $query->found_posts = count( $posts ); + $query->post = reset( $posts ); + $query->query = [ 'p' => 0 ]; + $query->tribe_mock_query = true; + global $wpdb; + // Use a query that will never yield results. + $query->request = "SELECT ID FROM {$wpdb->posts} WHERE 1=0"; + + // Return the same set of posts on each method requiring posts. + $filter_posts_pre_query = static function ( $the_posts, $the_query ) use ( $posts, $query ) { + if ( $the_query !== $query ) { + return $the_posts; + } + + $fields = $query->get( 'fields', false ); + // We assume some uniformity here. + $posts_are_objects = ! is_numeric( reset( $posts ) ); + + switch ( $fields ) { + case 'ids': + return $posts_are_objects ? wp_list_pluck( $posts, 'ID' ) : $posts; + case 'id=>parent': + default: + return $posts_are_objects ? $posts : array_map( 'get_post', $posts ); + } + }; + + add_filter( 'posts_pre_query', $filter_posts_pre_query, 10, 2 ); + + return $query; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Strings.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Strings.php new file mode 100644 index 000000000..33b3f11dd --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Strings.php @@ -0,0 +1,65 @@ +by( $operation, $taxonomy, $terms ); + + // This will only build the query not execute it. + $built_query = $repo->build_query(); + + if ( ! empty( $built_query->query_vars['tax_query'] ) ) { + $tax_query = $built_query->query_vars['tax_query']; + } + + return $tax_query; + } + + /** + * Transform all Term IDs and Slugs into IDs of existing terms in a given taxonomy. + * + * @since 4.13.0 + * + * @param string|int|array $terms Terms to be cleaned up. + * @param string $taxonomy Which taxonomy we are querying for. + * + * @return array List of IDs of terms. + */ + public static function normalize_to_term_ids( $terms, $taxonomy ) { + if ( empty( $terms ) ) { + return $terms; + } + + /** + * Allow filtering of the needle for splitting terms, by default it will be a comma. + * + * @since 4.13.0 + * + * @param string $needle Defaults to a comma. Which character that we will split terms by. + * @param string|array $terms Terms string that we will split by the needle filtered. + * @param string $taxonomy Which taxonomy this will be for. + */ + $needle = apply_filters( 'tribe_normalize_to_term_ids_needle', ',', $terms, $taxonomy ); + if ( is_string( $terms ) && false !== strpos( $terms, $needle ) ) { + $terms = array_map( 'trim', explode( $needle, $terms ) ); + } + + $terms = array_map( static function ( $param ) use ( $taxonomy ) { + $param = preg_replace( '/^#/', '', $param ); + $term_by = is_numeric( $param ) ? 'ID' : 'slug'; + $term = get_term_by( $term_by, $param, $taxonomy ); + + if ( ! $term instanceof \WP_Term ) { + return false; + } + + return $term->term_id; + }, (array) $terms ); + + $terms = array_filter( $terms ); + $terms = array_unique( $terms ); + + return $terms; + } + + + /** + * When dealing with templates that make use of `get_post_class` the taxonomy + terms queries are very inefficient + * so this method primes the caching by doing a single query that will build the cache for all Posts involved on + * the template we are about to render, reducing about 2 queries for each Post that we prime the cache for. + * + * Important to note that + * + * @since 5.0.0 + * + * @param array $posts + * @param array $taxonomies + * @param bool $prime_term_meta + * + * @return array + */ + public static function prime_term_cache( array $posts = [], array $taxonomies = [ 'post_tag', \Tribe__Events__Main::TAXONOMY ], bool $prime_term_meta = false ): array { + $first = reset( $posts ); + $is_numeric = ( ! $first instanceof \WP_Post ); + if ( $is_numeric ) { + $ids = $posts; + } else { + $ids = wp_list_pluck( $posts, 'ID' ); + } + $cache = []; + + // Build the base cache. + foreach ( $ids as $id ) { + foreach ( $taxonomies as $taxonomy ) { + $cache[ $id ][ $taxonomy ] = []; + } + } + + $args = [ + 'fields' => 'all_with_object_id', + 'object_ids' => $ids, + 'taxonomy' => $taxonomies, + ]; + $terms = get_terms( $args ); + + // Drop invalid results. + $valid_terms = array_filter( (array) $terms, static function ( $term ) { + return $term instanceof \WP_Term; + } ); + + $term_ids = wp_list_pluck( $valid_terms, 'term_id' ); + + foreach ( $valid_terms as $term ) { + $cache[ $term->object_id ][ $term->taxonomy ][] = $term->term_id; + } + + foreach ( $cache as $id => $object_taxonomies ) { + // Skip when invalid object id is passed. + if ( empty( $id ) ) { + continue; + } + + foreach ( $object_taxonomies as $taxonomy => $term_ids ) { + // Skip when invalid taxonomy is passed. + if ( empty( $taxonomy ) ) { + continue; + } + + // Do not skip when `term_ids` are empty. + wp_cache_add( $id, $term_ids, $taxonomy . '_relationships' ); + } + } + + _prime_term_caches( $term_ids, $prime_term_meta ); + + return $cache; + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Theme_Compatibility.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Theme_Compatibility.php new file mode 100644 index 000000000..6e61a3650 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Utils/Theme_Compatibility.php @@ -0,0 +1,276 @@ +get_template() ) ) { + return false; + } + + $theme = $current_theme->get_template(); + } + + $required = in_array( $theme, static::get_registered_themes() ); + + /** + * Allows hooking in to enforce compatibility by other plugins. + * + * @since 4.14.0 + * + * @param boolean $required If compatibility is required. + * @param null|string $theme The optional theme name string. + */ + $required = apply_filters( 'tribe_compatibility_required', $required, $theme ); + + return tribe_is_truthy( $required ); + } + + /** + * Contains the logic for if this object's classes should be added to the queue. + * + * @since 4.14.0 + * + * @param boolean $add Whether to add the class to the queue or not. + * @param array $class The array of compatibility class names to add. + * @param string $queue The queue we want to get 'admin', 'display', 'all'. + * + * @return boolean Whether compatibility classes should be added or not. + */ + public static function should_add_compatibility_class_to_queue( $add, $class, $queue ) { + if ( + 'admin' === $queue + || ! static::is_compatibility_required() + ) { + return $add; + } + + if ( in_array( $class, static::get_compatibility_classes() ) ) { + $add = true; + } + + /** + * Filters whether we should add a specific class to the queue. + * + * @since 4.14.0 + * + * @param boolean $add Whether to add the class to the queue or not. + * @param array $class The array of compatibility class names to add. + * @param string $queue The queue we want to get 'admin', 'display', 'all'. + */ + return apply_filters( 'tribe_compatibility_add_class', $add, $class, $queue ); + } + + /** + * Add compatibility classes. + * + * @since 4.14.0 + * + * @return void + */ + public static function add_compatibility_classes() { + tribe( Compatibility_Classes::class )->add_classes( static::get_compatibility_classes() ); + } + + /** + * Fetches the correct class strings for theme and child theme if available + the container class. + * + * @since 4.14.0 + * + * @return array $classes + */ + public static function get_container_classes() { + $classes = [ 'tribe-compatibility-container' ]; + + if ( static::is_compatibility_required() ) { + $classes = array_merge( $classes, static::get_compatibility_classes() ); + } + + /** + * Filters the HTML classes applied to a compatibility container. + * + * @since 4.14.0 + * + * @param array $html_classes Array of classes used for this container. + */ + return apply_filters( 'tribe_compatibility_container_classes', $classes ); + } + + /** + * Fetches the correct class strings for theme and child theme if available. + * + * @since 4.14.0 + * + * @return array $classes + */ + public static function get_compatibility_classes() { + $classes = []; + $current_theme = static::get_current_theme( true ); + + if ( empty( $current_theme ) || empty( $current_theme->get_template() ) ) { + return $classes; + } + + // Detect if we're using a child theme. + if ( $parent = $current_theme->parent ) { + $classes[] = sanitize_html_class( 'tribe-theme-' . $parent->get_template() ); + $classes[] = sanitize_html_class( 'tribe-theme-child-' . $current_theme->get_template() ); + } else { + $classes[] = sanitize_html_class( 'tribe-theme-' . $current_theme->get_template() ); + } + + /** + * Filters the list of classes we're adding. + * + * @since 4.14.0 + * + * @param array $classes An array of classes in the shape `[ => boolean ]`. + */ + return apply_filters( 'tribe_compatibility_classes', $classes ); + } + + /** + * Returns a list of themes registered for compatibility with our Views. + * + * @since 4.14.0 + * + * @return array An array of the themes registered. + */ + public static function get_registered_themes() { + /** + * Filters the list of themes that are registered for compatibility. + * + * @since 4.14.0 + * + * @param array $registered An array of views in the shape `[ ]`. + */ + return (array) apply_filters( 'tribe_theme_compatibility_registered', self::$themes ); + } + + /** + * Returns an array of active themes (parent and child). + * + * @since 4.14.0 + * + * @return array $themes An array in the format [ 'parent' => 'theme name', 'child' => 'theme name' ]. + * Empty array if none found. + */ + public static function get_active_themes() { + $themes = []; + $current_theme = static::get_current_theme( true ); + + if ( empty( $current_theme ) ) { + return $themes; + } + + $parent_theme = $current_theme->parent(); + + // No parent theme. + if ( empty( $parent_theme ) ) { + $themes['parent'] = strtolower( $current_theme->get_template() ); + return $themes; + } + + $themes['parent'] = strtolower( $parent_theme->get_template() ); + $child_theme = $current_theme->get( 'stylesheet' ); + + // if the 2 options are the same, then there is no child theme. + if ( $child_theme !== $parent_theme ) { + $themes['child'] = strtolower( $child_theme ); + } + + return $themes; + } + + /** + * Get the current theme. + * + * @since 4.14.0 + * + * @param boolean $object Pass true if you want the theme object returned instead of the name. + * + * @return string|object|boolean Will return the theme name by default. + * Will return the theme object if passed boolean true as the parameter. + * Will return boolean false if the theme is not found. + */ + public static function get_current_theme( $object = false ) { + $current_theme = wp_get_theme(); + + // If we can't get it for some reason... + if ( ! $current_theme instanceof WP_Theme || ! $current_theme->exists() ) { + return false; + } + + if ( $object ) { + return $current_theme; + } + + return $current_theme->get_template(); + } + + /** + * Checks if the provided theme is active. + * + * @since 4.14.0 + * + * @param string $theme The theme name like 'avada' or 'twentytwenty', + * + * @return boolean True if the requested theme is active, + * false if the current theme could not be found or is not the requested theme. + */ + public static function is_active_theme( $check ) { + $current_theme = wp_get_theme(); + + // Current theme is not + if ( ! $current_theme instanceof \WP_Theme ) { + $theme = false; + } elseif ( ! $current_theme->exists() ) { + $theme = false; + } else { + $theme = $current_theme->get_template(); + } + + return ! empty( $theme ) && strtolower( $check ) === strtolower( $theme ); + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Validate.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Validate.php new file mode 100644 index 000000000..8f187daaf --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Validate.php @@ -0,0 +1,557 @@ +result = new stdClass; + $this->field = $field; + $this->field['id'] = $field_id; + $this->value = $value; + $this->additional_args = $additional_args; + + // if the field is invalid or incomplete, fail validation + if ( ! is_array( $this->field ) || ! ( isset( $this->field['validation_type'] ) || isset( $this->field['validation_callback'] ) ) ) { + $this->result->valid = false; + $this->result->error = esc_html__( 'Invalid or incomplete field passed', 'tribe-common' ); + $this->result->error .= ( isset( $this->field['id'] ) ) ? ' (' . esc_html__( 'Field ID:', 'tribe-common' ) . ' ' . $this->field['id'] . ' )' : ''; + } + + // call validation callback if a validation callback function is set + if ( isset( $this->field['validation_callback'] ) ) { + if ( is_callable( $this->field['validation_callback'] ) || function_exists( $this->field['validation_callback'] ) ) { + if ( ( ! isset( $_POST[ $field_id ] ) || ! $_POST[ $field_id ] || $_POST[ $field_id ] == '' ) && isset( $this->field['can_be_empty'] ) && $this->field['can_be_empty'] ) { + $this->result->valid = true; + } else { + $this->result->valid = call_user_func( $this->field['validation_callback'], $value ); + if ( ! $this->result->valid ) { + $this->result->error = esc_html__( 'Invalid or incomplete field passed', 'tribe-common' ); + $this->result->error .= ( isset( $this->field['id'] ) ) ? ' (' . esc_html__( 'Field ID:', 'tribe-common' ) . ' ' . $this->field['id'] . ' )' : ''; + } + } + } + } + + if ( isset( $this->field['validation_type'] ) ) { + if ( method_exists( $this, $this->field['validation_type'] ) ) { + // make sure there's a field validation type set for this validation and that such method exists + $this->type = $this->field['validation_type']; + $this->label = isset( $this->field['label'] ) ? $this->field['label'] : $this->field['id']; + if ( ( ! isset( $_POST[ $field_id ] ) || ! $_POST[ $field_id ] || $_POST[ $field_id ] == '' ) && isset( $this->field['can_be_empty'] ) && $this->field['can_be_empty'] ) { + $this->result->valid = true; + } else { + call_user_func( [ $this, $this->type ] ); // run the validation + } + } else { + // invalid validation type set, validation fails + $this->result->valid = false; + $this->result->error = esc_html__( 'Non-existant field validation function passed', 'tribe-common' ); + $this->result->error .= ( isset( $this->field['id'] ) ) ? ' (' . esc_html__( 'Field ID:', 'tribe-common' ) . ' ' . $this->field['id'] . ' ' . _x( 'with function name:', 'non-existant function name passed for field validation', 'tribe-common' ) . ' ' . $this->field['validation_type'] . ' )' : ''; + } + } + } + + /** + * validates a field as a string containing only letters and numbers + */ + public function alpha_numeric() { + if ( preg_match( '/^[a-zA-Z0-9]+$/', $this->value ) ) { + $this->result->valid = true; + } else { + $this->result->valid = false; + $this->result->error = sprintf( esc_html__( '%s must contain numbers and letters only', 'tribe-common' ), $this->label ); + } + } + + /** + * validates a field as a string containing only letters, + * numbers and carriage returns + */ + public function alpha_numeric_multi_line() { + if ( preg_match( '/^[a-zA-Z0-9\s]+$/', $this->value ) ) { + $this->result->valid = true; + $this->value = tribe_multi_line_remove_empty_lines( $this->value ); + } else { + $this->result->valid = false; + $this->result->error = sprintf( esc_html__( '%s must contain numbers and letters only', 'tribe-common' ), $this->label ); + } + } + + /** + * Validates a field as a string containing only letters, + * numbers, dots and carriage returns + */ + public function alpha_numeric_multi_line_with_dots_and_dashes() { + if ( preg_match( '/^[a-zA-Z0-9\s.-]+$/', $this->value ) ) { + $this->result->valid = true; + $this->value = tribe_multi_line_remove_empty_lines( $this->value ); + } else { + $this->result->valid = false; + $this->result->error = sprintf( esc_html__( '%s must contain numbers, letters and dots only', 'tribe-common' ), $this->label ); + } + } + + /** + * Validates a field as a string containing only letters, + * numbers, dashes and underscores + */ + public function alpha_numeric_with_dashes_and_underscores() { + $this->value = trim( $this->value ); + if ( preg_match( '/^[a-zA-Z0-9_-]+$/', $this->value ) ) { + $this->result->valid = true; + } else { + $this->result->valid = false; + $this->result->error = sprintf( esc_html__( '%s must contain numbers, letters, dashes and undescores only', 'tribe-common' ), $this->label ); + } + } + + /** + * Validates a field as just "not empty". + * + * @since 4.7.6 + */ + public function not_empty() { + $this->value = trim( $this->value ); + + if ( empty( $this->value ) ) { + $this->result->valid = false; + $this->result->error = sprintf( esc_html__( '%s must not be empty', 'tribe-common' ), $this->label ); + } else { + $this->result->valid = true; + } + } + + /** + * validates a field as being positive decimal + */ + public function positive_decimal() { + if ( preg_match( '/^[0-9]+(\.[0-9]+)?$/', $this->value ) && $this->value > 0 ) { + $this->result->valid = true; + } else { + $this->result->valid = false; + $this->result->error = sprintf( esc_html__( '%s must be a positive number.', 'tribe-common' ), $this->label ); + } + } + + /** + * validates a field as being positive decimal or percent + */ + public function positive_decimal_or_percent() { + if ( preg_match( '/^[0-9]+(\.[0-9]+)?%?$/', $this->value ) && $this->value > 0 ) { + $this->result->valid = true; + } else { + $this->result->valid = false; + $this->result->error = sprintf( esc_html__( '%s must be a positive number or percent.', 'tribe-common' ), $this->label ); + } + } + + /** + * validates a field as being positive integers + */ + public function positive_int() { + if ( preg_match( '/^[0-9]+$/', $this->value ) && $this->value > 0 ) { + $this->result->valid = true; + } else { + $this->result->valid = false; + $this->result->error = sprintf( esc_html__( '%s must be a positive number.', 'tribe-common' ), $this->label ); + } + } + + /** + * validates a field as being an integer + * + * The expected value is a whole number (positive or negative). This method is named "int" to + * match the mathematical definition of the word AND to closely match the pre-exiting method + * with a similar name: positive_int(). This method WILL validate whole numbers that go beyond + * values that PHP's int type supports, however, if someone enters something like that, that's + * on them. Smart people do smart things. + */ + public function int() { + if ( preg_match( '/^-?[0-9]+$/', $this->value ) ) { + $this->result->valid = true; + } else { + $this->result->valid = false; + $this->result->error = sprintf( esc_html__( '%s must be a whole number.', 'tribe-common' ), $this->label ); + } + } + + /** + * validates & sanitizes fields as URL slugs + */ + public function slug() { + $maybe_valid_value = esc_url_raw( $this->value ); + + // esc_url_raw does the work of validating chars, but returns the checked string with a + // prepended URL protocol; so let's use strpos to match the values. + if ( + ! empty( $maybe_valid_value ) + && false !== strpos( $maybe_valid_value, $this->value ) + ) { + $this->result->valid = true; + $this->value = sanitize_title( $this->value ); + } else { + $this->result->valid = false; + $this->result->error = sprintf( esc_html__( '%s must be a valid slug (numbers, letters, dashes, and underscores).', 'tribe-common' ), $this->label ); + } + } + + /** + * validates & sanitizes fields as URLs + */ + public function url() { + + if ( esc_url_raw( $this->value ) == $this->value ) { + $this->result->valid = true; + } else { + $this->result->valid = false; + $this->result->error = sprintf( esc_html__( '%s must be a valid URL.', 'tribe-common' ), $this->label ); + } + } + + /** + * validates fields that have options (radios, dropdowns, etc.) + * by making sure the value is part of the options array + */ + public function options() { + if ( array_key_exists( $this->value, $this->field['options'] ) ) { + $this->value = ( $this->value === 0 ) ? false : $this->value; + $this->result->valid = true; + } else { + $this->result->valid = false; + $this->result->error = sprintf( esc_html__( "%s must have a value that's part of its options.", 'tribe-common' ), $this->label ); + } + } + + /** + * Validates fields that have multiple options (checkbox list, etc.) + * by making sure the value is part of the options array. + */ + public function options_multi() { + // if we are here it cannot be empty + if ( empty( $this->value ) ) { + $this->result->valid = false; + $this->result->error = sprintf( esc_html__( "%s must have a value that's part of its options.", 'tribe-common' ), $this->label ); + + return; + } + + $this->value = is_array( $this->value ) ? $this->value : [ $this->value ]; + + foreach ( $this->value as $val ) { + if ( array_key_exists( $val, $this->field['options'] ) ) { + $this->value = ( $this->value === 0 ) ? false : $this->value; + $this->result->valid = true; + } else { + $this->result->valid = false; + $this->result->error = sprintf( esc_html__( "%s must have a value that's part of its options.", 'tribe-common' ), $this->label ); + } + } + } + + /** + * validates fields that have options (radios, dropdowns, etc.) + * by making sure the value is part of the options array + * then combines the value into an array containing the value + * and name from the option + */ + public function options_with_label() { + if ( array_key_exists( $this->value, $this->field['options'] ) ) { + $this->value = ( $this->value === 0 ) ? false : [ + $this->value, + $this->field['options'][ $this->value ], + ]; + $this->result->valid = true; + } else { + $this->result->valid = false; + $this->result->error = sprintf( esc_html__( "%s must have a value that's part of its options.", 'tribe-common' ), $this->label ); + } + } + + /** + * validates a field as not being able to be the same + * as the specified value as specified in + * $this->additional_args['compare_name'] + */ + public function cannot_be_the_same_as() { + if ( ! isset( $this->additional_args['compare'] ) ) { + $this->result->valid = false; + $this->result->error = sprintf( esc_html__( 'Comparison validation failed because no comparison value was provided, for field %s', 'tribe-common' ), $this->field['id'] ); + } else { + if ( $this->value != $this->additional_args['compare'] ) { + $this->result = true; + } else { + $this->result->valid = false; + if ( isset( $this->additional_args['compare_name'] ) ) { + $this->result->error = sprintf( esc_html__( '%s cannot be the same as %s.', 'tribe-common' ), $this->label, $this->additional_args['compare_name'] ); + } else { + $this->result->error = sprintf( esc_html__( '%s cannot be a duplicate', 'tribe-common' ), $this->label ); + } + } + } + } + + /** + * validates a field as being a number or a percentage + */ + public function number_or_percent() { + if ( preg_match( '/^[0-9]+%{0,1}$/', $this->value ) ) { + $this->result->valid = true; + } else { + $this->result->valid = false; + $this->result->error = sprintf( esc_html__( '%s must be a number or percentage.', 'tribe-common' ), $this->label ); + } + } + + /** + * sanitizes an html field + */ + public function html() { + $this->value = balanceTags( $this->value ); + $this->result->valid = true; + } + + /** + * sanitizes a license key + */ + public function license_key() { + $this->value = trim( $this->value ); + $this->result->valid = true; + } + + /** + * sanitizes a textarea field + */ + public function textarea() { + $this->value = wp_kses( $this->value, [] ); + $this->result->valid = true; + } + + /** + * sanitizes a field as being a boolean + */ + public function boolean() { + $this->value = (bool) $this->value; + $this->result->valid = true; + } + + /** + * validates a Google Maps Zoom field + */ + public function google_maps_zoom() { + if ( preg_match( '/^([0-9]|[0-1][0-9]|2[0-1])$/', $this->value ) ) { + $this->result->valid = true; + } else { + $this->result->valid = false; + $this->result->error = sprintf( esc_html__( '%s must be a number between 0 and 21.', 'tribe-common' ), $this->label ); + } + } + + /** + * validates a field as being part of an address + * allows for letters, numbers, dashes and spaces only + */ + public function address() { + $this->value = stripslashes( $this->value ); + if ( preg_match( "/^[0-9\S '-]+$/", $this->value ) ) { + $this->result->valid = true; + } else { + $this->result->valid = false; + $this->result->error = sprintf( esc_html__( '%s must consist of letters, numbers, dashes, apostrophes, and spaces only.', 'tribe-common' ), $this->label ); + } + } + + /** + * validates a field as being a city or province + * allows for letters, dashes and spaces only + */ + public function city_or_province() { + $this->value = stripslashes( $this->value ); + if ( preg_match( "/^[\D '\-]+$/", $this->value ) ) { + $this->result->valid = true; + } else { + $this->result->valid = false; + $this->result->error = sprintf( esc_html__( '%s must consist of letters, spaces, apostrophes, and dashes.', 'tribe-common' ), $this->label ); + } + } + + /** + * validates a field as being a zip code + */ + public function zip() { + if ( preg_match( '/^[0-9]{5}$/', $this->value ) ) { + $this->result->valid = true; + } else { + $this->result->valid = false; + $this->result->error = sprintf( esc_html__( '%s must consist of 5 numbers.', 'tribe-common' ), $this->label ); + } + } + + /** + * validates a field as being a phone number + */ + public function phone() { + if ( preg_match( '/^[0-9\(\)\+ -]+$/', $this->value ) ) { + $this->result->valid = true; + } else { + $this->result->valid = false; + $this->result->error = sprintf( esc_html__( '%s must be a phone number.', 'tribe-common' ), $this->label ); + } + } + + /** + * validates & sanitizes a field as being a country list + */ + public function country_list() { + $country_rows = explode( "\n", $this->value ); + if ( is_array( $country_rows ) ) { + foreach ( $country_rows as $crow ) { + $country = explode( ',', $crow ); + if ( ! isset( $country[0] ) || ! isset( $country[1] ) ) { + $this->result->valid = false; + $this->result->error = sprintf( esc_html__( 'Country List must be formatted as one country per line in the following format:
    US, United States
    UK, United Kingdom.', 'tribe-common' ), $this->label ); + $this->value = wp_kses( $this->value, [] ); + + return; + } + } + } + $this->result->valid = true; + } + + /** + * automatically validate a field regardless of the value + * Don't use this unless you know what you are doing + */ + public function none() { + $this->result->valid = true; + } + + /** + * Validates and sanitizes an email address. + * + * @since 4.7.4 + */ + public function email( ) { + $candidate = trim( $this->value ); + + $this->result->valid = filter_var( $candidate, FILTER_VALIDATE_EMAIL ); + + if ( ! $this->result->valid ) { + $this->result->error = sprintf( esc_html__( '%s must be an email address.', 'tribe-common' ), $this->label ); + } else { + $this->value = filter_var( trim( $candidate ), FILTER_SANITIZE_EMAIL ); + } + } + + /** + * Validates and sanitizes a list of email addresses. + * + * @since 5.0.14 + */ + public function email_list() { + $value = trim( $this->value ); + $this->result->valid = true; + $sanitized_emails = []; + + // Break emails into an array. + $emails = preg_split( '/[,;]+/', $value ); + + foreach ( $emails as $email ) { + // In case there's a blank email or extra comma/semicolon, skip with no error. + if ( empty( $email ) ) { + continue; + } + + // Sanitized email returns blank if invalid. + $email = sanitize_email( trim( $email ) ); + if ( empty( $email ) ) { + $this->result->valid = false; + break; + } + $sanitized_emails[] = $email; + } + + // If any of the emails are invalid, throw an error. + if ( ! $this->result->valid ) { + $this->result->error = sprintf( + // Translators: %s - Label of the form input field. + esc_html__( '%s must be a list of valid email addresses separated by commas or semicolons.', 'tribe-common' ), + $this->label + ); + } else { + $this->value = implode( ', ', $sanitized_emails ); + } + } + + /** + * Validates and sanitizes a HTML color codes, including hex, rgb, rgba, hsl and hsla. + * + * @since 5.0.0 + */ + public function color() { + if ( preg_match( '/^(#(?:[0-9a-f]{2}){2,4}|#[0-9a-f]{3}|(?:rgba?|hsla?)\((?:\d+%?(?:deg|rad|grad|turn)?(?:,|\s)+){2,3}[\s\/]*[\d\.]+%?\))$/i', $this->value ) ) { + $this->result->valid = true; + } else { + $this->result->valid = false; + $this->result->error = sprintf( esc_html__( '%s must be a valid HTML color code.', 'tribe-common' ), $this->label ); + } + } + + } // end class +} // endif class_exists diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Validator/Base.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Validator/Base.php new file mode 100644 index 000000000..a6ed9f812 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Validator/Base.php @@ -0,0 +1,266 @@ +is_string( $value ); + } + + /** + * Whether the value is a timestamp or a string parseable by the strtotime function or not. + * + * @param mixed $value + * + * @return bool + */ + public function is_time( $value ) { + return is_numeric( $value ) || ( is_string( $value ) && strtotime( $value ) ); + } + + /** + * Whether the value corresponds to an existing user ID or not. + * + * @param mixed $value + * + * @return bool + */ + public function is_user_id( $value ) { + return is_numeric( $value ) && (bool) get_user_by( 'ID', $value ); + } + + /** + * Whether the value is a positive integer or not. + * + * @param mixed $value + * + * @return bool + */ + public function is_positive_int( $value ) { + return is_numeric( $value ) && (int) $value == $value && (int) $value > 0; + } + + /** + * Whether the value is a list of positive integers only or not. + * + * @since 4.7.19 + * + * @param array|string|int $list + * @param string $sep + * + * @return bool + */ + public function is_positive_int_list( $list, $sep = ',' ) { + $sep = is_string( $sep ) ? $sep : ','; + $list = Tribe__Utils__Array::list_to_array( $list, $sep ); + + $valid = array_filter( $list, [ $this, 'is_positive_int' ] ); + + return ! empty( $valid ) && count( $valid ) === count( $list ); + } + + /** + * Trims a string. + * + * Differently from the trim method it will not use the second argument. + * + * @param string $value + * + * @return string + */ + public function trim( $value ) { + return is_string( $value ) ? trim( $value ) : $value; + } + + /** + * Whether the value(s) all map to existing post tags. + * + * @param mixed $tag + * + * @return bool + */ + public function is_post_tag( $tag ) { + return $this->is_term_of_taxonomy( $tag, 'post_tag' ); + } + + /** + * Whether the term exists and is a term of the specified taxonomy. + * + * @param mixed $term Either a single term `term_id` or `slug` or an array of + * `term_id`s and `slug`s + * @param string $taxonomy + * + * @return bool + */ + public function is_term_of_taxonomy( $term, $taxonomy ) { + $terms = Tribe__Utils__Array::list_to_array( $term, ',' ); + + if ( empty( $terms ) ) { + return false; + } + + foreach ( $terms as $t ) { + if ( ! term_exists( $t, $taxonomy ) ) { + return false; + } + } + + return true; + } + + /** + * Whether the provided value points to an existing attachment ID or an existing image URL. + * + * @param int|string $image + * + * @return mixed + */ + public function is_image( $image ) { + if ( $this->is_numeric( $image ) ) { + return wp_attachment_is_image( $image ); + } + + if ( is_string( $image ) && current_user_can( 'upload_files' ) ) { + $response = wp_remote_head( $image ); + + if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) { + return false; + } + + $content_type = wp_remote_retrieve_header( $response, 'content-type' ); + + if ( empty( $content_type ) || 0 !== strpos( $content_type, 'image' ) ) { + return false; + } + + $allowed_mime_types = get_allowed_mime_types(); + + return ( in_array( $content_type, $allowed_mime_types ) ); + } + + return false; + } + + /** + * Whether the provided value points to an existing attachment ID, an existing image URL, or is empty. + * + * @param int|string $image + * + * @return mixed + */ + public function is_image_or_empty( $image ) { + if ( empty( $image ) ) { + return true; + } + + return $this->is_image( $image ); + } + + /** + * @param mixed $value + * + * @return bool + */ + public function is_numeric( $value ) { + return is_numeric( $value ); + } + + /** + * Whether a string represents a valid array or not. + * + * Valid means that the string looks like a URL, not that the URL is online and reachable. + * + * @param string $input + * + * @return bool + */ + public function is_url( $input ) { + return (bool) filter_var( $input, FILTER_VALIDATE_URL ); + } + + /** + * Whether a string represents a valid array or not. + * + * Valid means that the string looks like a URL, not that the URL is online and reachable. + * + * @param string $input + * + * @return bool + */ + public function is_url_or_empty( $input ) { + if ( empty( $input ) ) { + return true; + } + + return $this->is_url( $input ); + } + + /** + * Whether a string represents a valid and registered post status or not. + * + * @param string $post_status + * + * @return bool + */ + public function is_post_status( $post_status ) { + $post_stati = get_post_stati(); + if ( empty( $post_stati ) ) { + return false; + } + + return in_array( $post_status, $post_stati ); + } + + /** + * Converts a string, a CSV list to an array. + * + * @since 4.7.19 + * + * @param string|array $list + * + * @return array + */ + public function list_to_array( $list ) { + return Tribe__Utils__Array::list_to_array( $list ); + } +} diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Validator/Interface.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Validator/Interface.php new file mode 100644 index 000000000..820677db1 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Validator/Interface.php @@ -0,0 +1,102 @@ +set_up_currency_details(); + + parent::__construct( $amount ); + } + + /** + * @inheritDoc + */ + public function get_currency() { + + /** + * Filter the value returned for get_currency() when implemented in a specific class type + * + * @since 4.14.9 + * + * @param string $currency the string representation of the value + * @param Abstract_Currency the object instance + * + * @return string + */ + $currency = apply_filters( "tec_common_value_{$this->get_value_type()}_get_currency", $this->currency, $this ); + + /** + * Filter the value returned for get_currency() when implemented in any class + * + * @since 4.14.9 + * + * @param string $currency the string representation of the value + * @param Abstract_Currency the object instance + * + * @return string + */ + return apply_filters( 'tec_common_value_get_currency', $currency, $this ); + } + + /** + * @inheritDoc + */ + public function get_decimal() { + + /** + * Filter the value returned for get_decimal() when implemented in a specific class type + * + * @since 4.14.9 + * + * @param float $decimal the float representation of the value, rounded to precision + * @param Abstract_Currency the object instance + * + * @return float + */ + $decimal = apply_filters( "tec_common_value_{$this->get_value_type()}_get_decimal", $this->decimal, $this ); + + /** + * Filter the value returned for get_decimal() when implemented in any class + * + * @since 4.14.9 + * + * @param float $decimal the string representation of the value + * @param Abstract_Currency the object instance + * + * @return float + */ + return apply_filters( 'tec_common_value_get_decimal', $decimal, $this ); + } + + /** + * @inheritDoc + */ + public function get_string() { + + /** + * Filter the value returned for get_string() when implemented in a specific class type + * + * @since 4.14.9 + * + * @param string $string the string representation of the value + * @param Abstract_Currency the object instance + * + * @return string + */ + $string = apply_filters( "tec_common_value_{$this->get_value_type()}_get_string", $this->string, $this ); + + /** + * Filter the value returned for get_string() when implemented in any class + * + * @since 4.14.9 + * + * @param string $string the string representation of the value + * @param Abstract_Currency the object instance + * + * @return string + */ + return apply_filters( 'tec_common_value_get_string', $string, $this ); + } + + /** + * @inheritDoc + */ + public function get_currency_code() { + + /** + * Filter the value returned for get_currency_code() when implemented in a specific class type + * + * @since 4.14.9 + * + * @param string $currency_code the string representation of the value + * @param Abstract_Currency the object instance + * + * @return string + */ + $currency_code = apply_filters( "tec_common_value_{$this->get_value_type()}_get_currency_code", $this->currency_code, $this ); + + /** + * Filter the value returned for get_currency_code() when implemented in any class + * + * @since 4.14.9 + * + * @param string $currency_code the string representation of the value + * @param Abstract_Currency the object instance + * + * @return string + */ + return apply_filters( 'tec_common_value_get_currency_code', $currency_code, $this ); + } + + /** + * @inheritDoc + */ + public function get_currency_symbol() { + + /** + * Filter the value returned for get_currency_symbol() when implemented in a specific class type + * + * @since 4.14.9 + * + * @param string $currency_symbol the string representation of the value + * @param Abstract_Currency the object instance + * + * @return string + */ + $currency_symbol = apply_filters( "tec_common_value_{$this->get_value_type()}_get_currency_symbol", $this->currency_symbol, $this ); + + /** + * Filter the value returned for get_currency_symbol() when implemented in any class + * + * @since 4.14.9 + * + * @param string $currency_symbol the string representation of the value + * @param Abstract_Currency the object instance + * + * @return string + */ + return apply_filters( 'tec_common_value_get_currency_symbol', $currency_symbol, $this ); + } + + /** + * @inheritDoc + */ + public function get_currency_symbol_position() { + + /** + * Filter the value returned for get_currency_symbol_position() when implemented in a specific class type + * + * @since 4.14.9 + * + * @param string $currency_symbol_position the string representation of the value + * @param Abstract_Currency the object instance + * + * @return string + */ + $currency_symbol_position = apply_filters( "tec_common_value_{$this->get_value_type()}_get_currency_symbol_position", $this->currency_symbol_position, $this ); + + /** + * Filter the value returned for get_currency_symbol_position() when implemented in any class + * + * @since 4.14.9 + * + * @param string $currency_symbol_position the string representation of the value + * @param Abstract_Currency the object instance + * + * @return string + */ + return apply_filters( 'tec_common_value_get_currency_symbol_position', $currency_symbol_position, $this ); + } + + /** + * @inheritDoc + */ + public function get_currency_separator_decimal() { + + /** + * Filter the value returned for get_currency_separator_decimal() when implemented in a specific class type + * + * @since 4.14.9 + * + * @param string $currency_separator_decimal the string representation of the value + * @param Abstract_Currency the object instance + * + * @return string + */ + $currency_separator_decimal = apply_filters( "tec_common_value_{$this->get_value_type()}_get_currency_separator_decimal", $this->currency_separator_decimal, $this ); + + /** + * Filter the value returned for get_currency_separator_decimal() when implemented in any class + * + * @since 4.14.9 + * + * @param string $currency_separator_decimal the string representation of the value + * @param Abstract_Currency the object instance + * + * @return string + */ + return apply_filters( 'tec_common_value_get_currency_separator_decimal', $currency_separator_decimal, $this ); + } + + /** + * @inheritDoc + */ + public function get_currency_separator_thousands() { + + /** + * Filter the value returned for get_currency_separator_thousands() when implemented in a specific class type + * + * @since 4.14.9 + * + * @param string $currency_separator_thousands the string representation of the value + * @param Abstract_Currency the object instance + * + * @return string + */ + $currency_separator_thousands = apply_filters( "tec_common_value_{$this->get_value_type()}_get_currency_separator_thousands", $this->currency_separator_thousands, $this ); + + /** + * Filter the value returned for get_currency_separator_thousands() when implemented in any class + * + * @since 4.14.9 + * + * @param string $currency_separator_thousands the string representation of the value + * @param Abstract_Currency the object instance + * + * @return string + */ + return apply_filters( 'tec_common_value_get_currency_separator_thousands', $currency_separator_thousands, $this ); + } + + /** + * Protected setter for the string representation of the object amount. This is a formatted string, including the + * currency symbol. + * + * @since 4.14.9 + * + * To set a new value use the public setter `$obj->set_value( $amount )` + */ + protected function set_currency_value() { + $this->currency = $this->to_currency( $this->get_normalized_value() ); + } + + /** + * Protected setter for the decimal representation of the object amount. This is a float, rounded to the precision. + * + * @since 4.14.9 + * + * To set a new value use the public setter `$obj->set_value( $amount )` + */ + protected function set_decimal_value() { + $this->decimal = $this->to_decimal( $this->get_normalized_value() ); + } + + /** + * Protected setter for the string representation of the object amount. This is a formatted string, without the + * currency symbol. + * + * @since 4.14.9 + * + * To set a new value use the public setter `$obj->set_value( $amount )` + */ + protected function set_string_value() { + $this->string = $this->to_string( $this->get_normalized_value() ); + } +} \ No newline at end of file diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Values/Abstract_Value.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Values/Abstract_Value.php new file mode 100644 index 000000000..34ecb0ca1 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Values/Abstract_Value.php @@ -0,0 +1,455 @@ +set_initial_representation( $amount ); + $this->set_normalized_amount( $amount ); + $this->update(); + } + + /** + * @inheritDoc + */ + public static function create( $value = 0 ) { + $class = get_called_class(); + return new $class( $value ); + } + + /** + * @inheritDoc + */ + public function set_value( $amount ) { + $this->set_normalized_amount( $amount ); + $this->update(); + } + + /** + * @inheritDoc + */ + public function set_precision( $amount ) { + $this->precision = $amount; + } + + + /** + * @inheritDoc + */ + public function get_integer() { + /** + * Filter the value returned for get_integer() when implemented in a specific class name + * + * @since 4.14.9 + * + * @param int $integer the integer representation of the value + * @param Abstract_Value the object instance + * + * @return int + */ + $integer = apply_filters( "tec_common_value_{$this->get_value_type()}_get_integer", $this->integer, $this ); + + /** + * Filter the value returned for get_integer() when implemented in any class + * + * @since 4.14.9 + * + * @param int $integer the integer representation of the value + * @param Abstract_Value the object instance + * + * @return int + */ + return apply_filters( 'tec_common_value_get_integer', $integer, $this ); + + } + + /** + * @inheritDoc + */ + public function get_float() { + /** + * Filter the value returned for get_float() when implemented in a specific class name + * + * @since 4.14.9 + * + * @param float $float the float representation of the value + * @param Abstract_Value the object instance + * + * @return float + */ + $float = apply_filters( "tec_common_value_{$this->get_value_type()}_get_float", $this->float, $this ); + + /** + * Filter the value returned for get_float() when implemented in any class + * + * @since 4.14.9 + * + * @param float $float the float representation of the value + * @param Abstract_Value the object instance + * + * @return float + */ + return apply_filters( 'tec_common_value_get_float', $float, $this ); + } + + /** + * @inheritDoc + */ + public function get_precision() { + /** + * Filter the value returned for get_precision() when implemented in a specific class name + * + * @since 4.14.9 + * + * @param int $precision the precision to which values will be calculated + * @param Abstract_Value the object instance + * + * @return int + */ + $precision = apply_filters( "tec_common_value_{$this->get_value_type()}_get_precision", $this->precision, $this ); + + /** + * Filter the value returned for get_precision() when implemented in any class + * + * @since 4.14.9 + * + * @param int $precision the precision to which values will be calculated + * @param Abstract_Value the object instance + * + * @return int + */ + return (int) apply_filters( 'tec_common_value_get_precision', $precision, $this ); + } + + /** + * @inheritDoc + */ + public function get_normalized_value() { + return $this->normalized_amount; + } + + /** + * @inheritDoc + */ + public function get_initial_representation() { + return $this->initial_value; + } + + /** + * @inheritDoc + */ + public function get_value_type() { + return $this->value_type; + } + + /** + * @inheritDoc + */ + public function normalize( $value ) { + + if ( is_numeric( $value ) ) { + return (float) $value; + } + + if ( $this->is_character_block( $value ) ) { + return (float) 0; + } + + $value = $this->remove_character_blocks( $value ); + $value = $this->remove_html( $value ); + + // Get all non-digits from the amount + preg_match_all( '/[^\d]/', $value, $non_digits ); + + // if the string is all digits, it is numeric + if ( empty( $non_digits[0] ) ) { + return (float) $value; + } + + $pieces = $this->remove_non_digits( $value, $non_digits ); + + return (float) $this->assemble_normalized_value( $pieces ); + } + + /** + * Removes any blocks composed of all non-digit characters from the numeric string. These will usually represent + * the currency code and any other pieces of text that may have been sent with the value. + * + * This is specially important in case the currency unit contains the same characters as the decimal/thousands + * separators such as in Moroccan Dirham (1,234.56 .د.م.) or Danish Krone (kr. 1.234,56) + * + * @since 4.14.9 + * + * @param string $value the numeric string being normalized + * + * @return string + */ + private function remove_character_blocks( $value ) { + foreach ( explode( ' ', $value ) as $block ) { + if ( ! $this->is_character_block( $block ) ) { + continue; + } + + $value = str_replace( $block, '', $value ); + } + + return $value; + } + + /** + * Removes all html tags and html entities from the value string + * + * @since 4.14.9 + * + * @param string $value the value being normalized + * + * @return string + */ + private function remove_html( $value ) { + return wp_strip_all_tags( preg_replace( '/&[^;]+;/', '', trim( $value ) ) ); + } + + /** + * Takes the value string and a list of non-digit characters and removes any of those characters. If the character + * is found to be a decimal separator, normalize it to a dot, so the number translates to a float. + * + * @since 4.14.9 + * + * @param string $value the value being normalized + * @param string[] $non_digits a list of non-digit characters present in $value + * @param string $separator a default separator to use when splitting the string + * + * @return string[] + */ + private function remove_non_digits( $value, $non_digits, $separator = '>>>' ) { + + $tokens = array_unique( $non_digits[0] ); + + foreach ( $tokens as $token ) { + if ( $this->is_decimal_separator( $token, $value ) ) { + $separator = $token; + continue; + } + + $value = str_replace( $token, '', $value ); + } + + return explode( $separator, $value ); + } + + /** + * Re-assemble the normalized value to store. + * + * @since 4.14.9 + * + * @param int[] $pieces the normalized value split in an array. + * + * @return float + */ + private function assemble_normalized_value( $pieces ) { + + // If the initial amount did not have decimals specified, $pieces will be an array of a single + // numeric value, so we just return it as a float. + if ( 1 === count( $pieces ) && is_numeric( reset( $pieces ) ) ) { + return (float) reset( $pieces ); + } + + $decimal = array_pop( $pieces ); + + return (float) implode( '', array_merge( $pieces, [ '.', $decimal ] ) ); + } + + /** + * Private setter for the initial value the object was created with. This value cannot be changed during the object + * lifecycle. + * + * @since 4.14.9 + * + * To set a new value discard the original object and create a new one. + */ + private function set_initial_representation( $amount ) { + if ( empty( $this->initial_value ) ) { + $this->initial_value = $amount; + } + } + + /** + * Private setter for the normalized amount extracted from the initial value. + * + * @since 4.14.9 + * + * To set a new value use the public setter `$obj->set_value( $amount )` + */ + private function set_normalized_amount( $amount ) { + + $normalized_value = $this->normalize( $amount ); + + /** + * Filter the value to be set as $normalized_amount for a specific implementation. + * + * @since 4.14.9 + * + * @param float $normalized_value the normalized value + * @param Abstract_Value the object instance + * + * @return float + */ + $normalized_value = (float) apply_filters( "tec_common_{$this->get_value_type()}_value_normalized", $normalized_value, $this ); + + /** + * Filter the value to be set as $normalized_amount for all implementations. + * + * @since 4.14.9 + * + * @param float $normalized_value the normalized value + * @param Abstract_Value the object instance + * + * @return float + */ + $normalized_value = (float) apply_filters( "tec_common_value_normalized", $normalized_value, $this ); + + /** + * Fire action right before setting the normalized value + * + * @since 4.14.9 + * + * @param float $normalized_value the normalized value + * @param Abstract_Value the object instance + */ + do_action( 'tec_common_value_normalized', $normalized_value, $this ); + + $this->normalized_amount = $normalized_value; + } + + /** + * Private setter for the integer representation of the object amount. + * + * @since 4.14.9 + * + * To set a new value use the public setter `$obj->set_value( $amount )` + */ + protected function set_integer_value() { + $this->integer = $this->to_integer( $this->normalized_amount ); + } + + /** + * Private setter for the floating point representation of the object amount. + * + * @since 4.14.9 + * + * To set a new value use the public setter `$obj->set_value( $amount )` + */ + protected function set_float_value() { + $this->float = $this->normalized_amount; + } + + /** + * Tries to determine if a token is serving as a decimal separator or something else + * in a string; + * + * The rule to determine a decimal is straightforward. It needs to exist only once + * in the string and the piece of the string after the separator cannot be longer + * than 2 digits. Anything else is serving another purpose. + * + * @since 4.14.9 + * + * @param $separator string a separator token, like . or , + * @param $value string a number formatted as a string + * + * @return bool + */ + private function is_decimal_separator( $separator, $value ) { + $pieces = array_filter( explode( $separator, $value ) ); + + foreach ( $pieces as $i => $block ) { + if ( $this->is_character_block( $block ) ) { + unset( $pieces[ $i ] ); + } + } + + if ( 2 === count( $pieces ) ) { + return strlen( array_pop( $pieces ) ) < 3; + } + + return false; + } + + /** + * Tests if a string is composed entirely of non-digit characters + * + * @since 4.14.9 + * + * @param string $block the string to check + * + * @return bool + */ + private function is_character_block( $block ) { + return empty( preg_replace( '/\D/', '', $block ) ); + } +} \ No newline at end of file diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Values/Currency_Interface.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Values/Currency_Interface.php new file mode 100644 index 000000000..aadf96364 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Values/Currency_Interface.php @@ -0,0 +1,86 @@ +set_value( $this->multiply( $multiplier ) ); + + return $this; + } + + /** + * Sets the current object value to be the sum of its current value plus the values of all objects received in + * $values. + * + * @since 4.14.9 + * + * @param Abstract_Value[] $values a list of Value objects + * + * @return $this + */ + public function total( $values ) { + $num = array_map( function ( $obj ) { + return $obj->get_float(); + }, $values ); + + $this->set_value( $this->sum( $num ) ); + + return $this; + } + + /** + * @inheritDoc + */ + public function sum( $values ) { + $values[] = $this->get_float(); + + return array_sum( $values ); + } + + /** + * @inheritDoc + */ + public function multiply( $multiplier ) { + return $this->get_float() * $multiplier; + } + + /** + * Rounds the current value to its precision and multiplies it by 10^precision to get an integer representation + * including decimals. + * + * @since 4.14.9 + * + * @param int|float $value the value to transform + * + * @return int + */ + public function to_integer( $value ) { + return (int) ( round( $value, $this->get_precision() ) * pow( 10, $this->get_precision() ) ); + } +} \ No newline at end of file diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Values/Value_Formatting.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Values/Value_Formatting.php new file mode 100644 index 000000000..33cbdd119 --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Values/Value_Formatting.php @@ -0,0 +1,58 @@ +to_decimal( $value ), + $this->get_precision(), + $this->get_currency_separator_decimal(), + $this->get_currency_separator_thousands() + ); + } + + /** + * Transforms a normalized value into a decimal representation by rounding the significant digits to the precision. + * + * @since 4.14.9 + * + * @param float $value the normalized value to transform + * + * @param float|\WP_Error the value rounded to the specified precision + */ + private function to_decimal( $value ) { + return round( $value, $this->get_precision() ); + } + + /** + * Transforms a normalized value into a currency representation using the defined currency symbol, position, + * separators and precision. + * + * @since 4.14.9 + * + * @param float $value the normalized value to transform + * + * @return string|\WP_Error the currency-formatted string + */ + private function to_currency( $value ) { + $value = $this->to_string( $value ); + + if ( 'prefix' === $this->get_currency_symbol_position() ) { + return $this->get_currency_symbol() . $value; + } + + return $value . $this->get_currency_symbol(); + } +} \ No newline at end of file diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/Values/Value_Interface.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/Values/Value_Interface.php new file mode 100644 index 000000000..aaba5247b --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/Values/Value_Interface.php @@ -0,0 +1,145 @@ +get_value_type()}_value_get_setters", $setters, $this ); + + /** + * Filter the value returned for get_setters() for all class names. + * + * @since 4.14.9 + * + * @param string[] $setters the list of setter methods returned + * @param Abstract_Value the object instance + * + * @return string[] + */ + return apply_filters( 'tec_tickets_commerce_value_get_setters', $setters, $this ); + } + + /** + * @inheritDoc + */ + public function update() { + foreach ( $this->get_setters() as $setter ) { + call_user_func( [ $this, $setter ] ); + } + } +} \ No newline at end of file diff --git a/wp-content/plugins/the-events-calendar/common/src/Tribe/View_Helpers.php b/wp-content/plugins/the-events-calendar/common/src/Tribe/View_Helpers.php new file mode 100644 index 000000000..0ee74d06e --- /dev/null +++ b/wp-content/plugins/the-events-calendar/common/src/Tribe/View_Helpers.php @@ -0,0 +1,348 @@ +get_countries(); + } + + // Perform a natural sort: this maintains the key -> index associations but ensures the countries + // are in the expected order, even once translated + natsort( $countries ); + + // Placeholder option ('Select a Country') first by default + $select_country = [ '' => esc_html__( 'Select a Country:', 'tribe-common' ) ]; + $countries = $select_country + $countries; + + if ( ( $postId || $useDefault ) ) { + $countryValue = get_post_meta( $postId, '_EventCountry', true ); + if ( $countryValue ) { + $defaultCountry = [ array_search( $countryValue, $countries ), $countryValue ]; + } else { + $defaultCountry = tribe_get_default_value( 'country' ); + } + if ( $defaultCountry && $defaultCountry[0] != '' ) { + $selectCountry = array_shift( $countries ); + asort( $countries ); + $countries = [ $defaultCountry[0] => $defaultCountry[1] ] + $countries; + $countries = [ '' => $selectCountry ] + $countries; + array_unique( $countries ); + } + } + + tribe_set_var( $cache_var_name, $countries ); + + return $countries; + } + + /** + * Get the i18ned states available to the plugin. + * + * @return array The states array. + */ + public static function loadStates() { + $states = tribe( 'languages.locations' )->get_us_states(); + + /** + * Enables filtering the list of states in the USA available to venues. + * + * @since 4.5.12 + * + * @param array $states The list of states. + */ + return apply_filters( 'tribe_get_state_options', $states ); + } + + /** + * Builds a set of options for displaying an hour chooser + * + * @param string $date the current date (optional) + * @param bool $isStart + * + * @return string a set of HTML options with hours (current hour selected) + */ + public static function getHourOptions( $date = '', $isStart = false ) { + $hours = self::hours(); + + if ( count( $hours ) == 12 ) { + $h = 'h'; + } else { + $h = 'H'; + } + $options = ''; + + if ( empty( $date ) ) { + $hour = ( $isStart ) ? '08' : ( count( $hours ) == 12 ? '05' : '17' ); + } else { + $timestamp = strtotime( $date ); + $hour = date( $h, $timestamp ); + // fix hours if time_format has changed from what is saved + if ( preg_match( '(pm|PM)', $timestamp ) && $h == 'H' ) { + $hour = $hour + 12; + } + if ( $hour > 12 && $h == 'h' ) { + $hour = $hour - 12; + } + } + + $hour = apply_filters( 'tribe_get_hour_options', $hour, $date, $isStart ); + + foreach ( $hours as $hourText ) { + if ( $hour == $hourText ) { + $selected = 'selected="selected"'; + } else { + $selected = ''; + } + $options .= "\n"; + } + + return $options; + } + + /** + * Builds a set of options for displaying a minute chooser + * + * @param string $date the current date (optional) + * @param bool $isStart + * + * @return string a set of HTML options with minutes (current minute selected) + */ + public static function getMinuteOptions( $date = '', $isStart = false ) { + $options = ''; + + if ( empty( $date ) ) { + $minute = '00'; + } else { + $minute = date( 'i', strtotime( $date ) ); + } + + $minute = apply_filters( 'tribe_get_minute_options', $minute, $date, $isStart ); + $minutes = self::minutes( $minute ); + + foreach ( $minutes as $minuteText ) { + if ( $minute == $minuteText ) { + $selected = 'selected="selected"'; + } else { + $selected = ''; + } + $options .= "\n"; + } + + return $options; + } + + /** + * Helper method to return an array of 1-12 for hours + * + * @return array The hours array. + */ + private static function hours() { + $hours = []; + $rangeMax = self::is_24hr_format() ? 23 : 12; + $rangeStart = $rangeMax > 12 ? 0 : 1; + foreach ( range( $rangeStart, $rangeMax ) as $hour ) { + if ( $hour < 10 ) { + $hour = '0' . $hour; + } + $hours[ $hour ] = $hour; + } + + // In a 12hr context lets put 12 at the start (so the sequence will run 12, 1, 2, 3 ... 11) + if ( 12 === $rangeMax ) { + array_unshift( $hours, array_pop( $hours ) ); + } + + return $hours; + } + + /** + * Determines if the provided date/time format (or else the default WordPress time_format) + * is 24hr or not. + * + * In inconclusive cases, such as if there are now hour-format characters, 12hr format is + * assumed. + * + * @param null $format + * @return bool + */ + public static function is_24hr_format( $format = null ) { + // Use the provided format or else use the value of the current time_format setting + $format = ( null === $format ) ? get_option( 'time_format', Tribe__Date_Utils::TIMEFORMAT ) : $format; + + // Count instances of the H and G symbols + $h_symbols = substr_count( $format, 'H' ); + $g_symbols = substr_count( $format, 'G' ); + + // If none have been found then consider the format to be 12hr + if ( ! $h_symbols && ! $g_symbols ) return false; + + // It's possible H or G have been included as escaped characters + $h_escaped = substr_count( $format, '\H' ); + $g_escaped = substr_count( $format, '\G' ); + + // Final check, accounting for possibility of escaped values + return ( $h_symbols > $h_escaped || $g_symbols > $g_escaped ); + } + + /** + * Helper method to return an array of 00-59 for minutes + * + * @param int $exact_minute optionally specify an exact minute to be included (outwith the default intervals) + * + * @return array The minutes array. + */ + private static function minutes( $exact_minute = 0 ) { + $minutes = []; + + // The exact minute should be an absint between 0 and 59 + $exact_minute = absint( $exact_minute ); + + if ( $exact_minute < 0 || $exact_minute > 59 ) { + $exact_minute = 0; + } + + /** + * Filters the amount of minutes to increment the minutes drop-down by + * + * @param int Increment amount (defaults to 5) + */ + $default_increment = apply_filters( 'tribe_minutes_increment', 5 ); + + // Unless an exact minute has been specified we can minimize the amount of looping we do + $increment = ( 0 === $exact_minute ) ? $default_increment : 1; + + for ( $minute = 0; $minute < 60; $minute += $increment ) { + // Skip if this $minute doesn't meet the increment pattern and isn't an additional exact minute + if ( 0 !== $minute % $default_increment && $exact_minute !== $minute ) { + continue; + } + + if ( $minute < 10 ) { + $minute = '0' . $minute; + } + $minutes[ $minute ] = $minute; + } + + return $minutes; + } + + /** + * Builds a set of options for diplaying a meridian chooser + * + * @param string $date YYYY-MM-DD HH:MM:SS to select (optional) + * @param bool $isStart + * + * @return string a set of HTML options with all meridians + */ + public static function getMeridianOptions( $date = '', $isStart = false ) { + if ( strstr( get_option( 'time_format', Tribe__Date_Utils::TIMEFORMAT ), 'A' ) ) { + $a = 'A'; + $meridians = [ 'AM', 'PM' ]; + } else { + $a = 'a'; + $meridians = [ 'am', 'pm' ]; + } + if ( empty( $date ) ) { + $meridian = ( $isStart ) ? $meridians[0] : $meridians[1]; + } else { + $meridian = date( $a, strtotime( $date ) ); + } + + $meridian = apply_filters( 'tribe_get_meridian_options', $meridian, $date, $isStart ); + + $return = ''; + foreach ( $meridians as $m ) { + $return .= "