PluginProbe ʕ •ᴥ•ʔ
Event Tickets with Ticket Scanner / 2.9.9
Event Tickets with Ticket Scanner v2.9.9
3.1.2 3.1.1 3.1.0 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.4 trunk 2.6.0 2.7.0 2.7.1 2.7.10 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 2.7.8 2.7.9 2.8.0 2.8.1 2.8.10 2.8.2 2.8.3 2.8.4 2.8.5 2.8.6 2.8.7 2.8.8 2.8.9 2.9.0 2.9.2 2.9.3 2.9.4 2.9.5 2.9.6 2.9.7 2.9.8 2.9.9 3.0.0 3.0.1 3.0.2 3.0.3
event-tickets-with-ticket-scanner / index.php
event-tickets-with-ticket-scanner Last commit date
3rd 3 months ago css 3 months ago img 3 months ago includes 3 months ago js 3 months ago languages 3 months ago ticket 3 months ago vendors 3 months ago SASO_EVENTTICKETS.php 3 months ago backend.js 3 months ago changelog-features.json 3 months ago changelog.txt 3 months ago db.php 3 months ago index.php 3 months ago init_file.php 3 months ago order_details.js 3 months ago pwa-sw.js 3 months ago readme.txt 3 months ago saso-eventtickets-validator.js 3 months ago sasoEventtickets_AdminSettings.php 3 months ago sasoEventtickets_Authtoken.php 3 months ago sasoEventtickets_Base.php 3 months ago sasoEventtickets_Core.php 3 months ago sasoEventtickets_Frontend.php 3 months ago sasoEventtickets_Messenger.php 3 months ago sasoEventtickets_Options.php 3 months ago sasoEventtickets_PDF.php 3 months ago sasoEventtickets_Seating.php 3 months ago sasoEventtickets_Ticket.php 3 months ago sasoEventtickets_TicketBadge.php 3 months ago sasoEventtickets_TicketDesigner.php 3 months ago sasoEventtickets_TicketQR.php 3 months ago ticket_events.js 3 months ago ticket_scanner.js 3 months ago validator.js 3 months ago wc_backend.js 3 months ago wc_frontend.js 3 months ago woocommerce-hooks.php 3 months ago
index.php
2178 lines
1 <?php
2 /**
3 * Plugin Name: Event Tickets with Ticket Scanner
4 * Plugin URI: https://vollstart.com/event-tickets-with-ticket-scanner/docs/
5 * Description: You can create and generate tickets and codes. You can redeem the tickets at entrance using the built-in ticket scanner. You customer can download a PDF with the ticket information. The Premium allows you also to activate user registration and more. This allows your user to register them self to a ticket.
6 * Version: 2.9.9
7 * Author: Vollstart
8 * Author URI: https://vollstart.com
9 * Requires at least: 6.0
10 * Tested up to: 6.9
11 * Requires PHP: 8.1
12 * Text Domain: event-tickets-with-ticket-scanner
13 * License: GPLv2 or later
14 * License URI: https://www.gnu.org/licenses/gpl-2.0.html
15 *
16 * Event Tickets with Ticket Scanner is distributed in the hope that it will be useful,
17 * but WITHOUT ANY WARRANTY; without even the implied warranty of
18 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 * GNU General Public License for more details.
20 */
21 // https://semver.org/
22 // https://developer.wordpress.org/plugins/security/securing-output/
23 // https://developer.wordpress.org/plugins/security/securing-input/
24
25 include_once(plugin_dir_path(__FILE__)."init_file.php");
26
27 if (!defined('SASO_EVENTTICKETS_PLUGIN_VERSION'))
28 define('SASO_EVENTTICKETS_PLUGIN_VERSION', '2.9.9');
29 if (!defined('SASO_EVENTTICKETS_PLUGIN_DIR_PATH'))
30 define('SASO_EVENTTICKETS_PLUGIN_DIR_PATH', plugin_dir_path(__FILE__));
31
32 include_once plugin_dir_path(__FILE__)."SASO_EVENTTICKETS.php";
33
34 class sasoEventtickets_fakeprem{}
35 class sasoEventtickets {
36 private $_js_version;
37 private $_js_file = 'saso-eventtickets-validator.js';
38 public $_js_nonce = 'sasoEventtickets';
39 public $_do_action_prefix = 'saso_eventtickets_';
40 public $_add_filter_prefix = 'saso_eventtickets_';
41 protected $_prefix = 'sasoEventtickets';
42 public $_prefix_session = 'sasoEventtickets_';
43 protected $_shortcode = 'sasoEventTicketsValidator';
44 protected $_shortcode_mycode = 'sasoEventTicketsValidator_code';
45 protected $_shortcode_eventviews = 'sasoEventTicketsValidator_eventsview';
46 protected $_shortcode_ticket_scanner = 'sasoEventTicketsValidator_ticket_scanner';
47 protected $_shortcode_feature_list = 'sasoEventTicketsValidator_feature_list';
48 protected $_shortcode_ticket_detail = 'sasoEventTicketsValidator_ticket_detail';
49 protected $_divId = 'sasoEventtickets';
50
51 private $_isPrem = null;
52 private $_isCheckingSubscription = false;
53 private $_premium_plugin_name = 'event-tickets-with-ticket-scanner-premium';
54 private $_premium_function_file = 'sasoEventtickets_PremiumFunctions.php';
55 private $PREMFUNCTIONS = null;
56 private $_oldPremiumDetected = false;
57 private $_starterOrStopDetected = false;
58 private $BASE = null;
59 private $CORE = null;
60 private $ADMIN = null;
61 private $FRONTEND = null;
62 private $OPTIONS = null;
63
64 private $isAllowedAccess = null;
65
66 public static function Instance() {
67 static $inst = null;
68 if ($inst === null) {
69 $inst = new self();
70 }
71 return $inst;
72 }
73
74 public function __construct() {
75 global $sasoEventtickets;
76 $sasoEventtickets = $this; // Set early to prevent circular dependency with sasoEventtickets_Ticket
77 $this->_js_version = $this->getPluginVersion() . (defined('WP_DEBUG') && WP_DEBUG ? '.' . time() : '');
78 $this->initHandlers();
79 }
80
81 public function initHandlers() {
82 add_action( 'init', [$this, 'load_plugin_textdomain'] );
83 add_action( 'upgrader_process_complete', [$this, 'listener_upgrader_process_complete'], 10, 2 );
84 //add_action('admin_init', [$this, 'initialize_plugin']);
85 if (is_admin()) { // called in backend admin, admin-ajax!
86 $this->init_backend();
87 } else { // called in front end
88 $this->init_frontend();
89 }
90 add_action( 'sasoEventtickets_cronjob_daily', [$this, 'relay_sasoEventtickets_cronjob_daily'], 10, 0 ); // set in tickets.php
91 add_action( 'plugins_loaded', [$this, 'WooCommercePluginLoaded'], 20, 0 );
92 if (basename($_SERVER['SCRIPT_NAME']) == "admin-ajax.php") {
93 add_action('wp_ajax_nopriv_'.$this->_prefix.'_executeFrontend', [$this,'executeFrontend_a'], 10, 0); // nicht angemeldete user, sollen eine antwort erhalten
94 add_action('wp_ajax_'.$this->_prefix.'_executeFrontend', [$this,'executeFrontend_a'], 10, 0); // falls eingeloggt ist
95 add_action('wp_ajax_'.$this->_prefix.'_executeWCBackend', [$this,'executeWCBackend'], 10, 0); // falls eingeloggt ist
96 add_action('wp_ajax_'.$this->_prefix.'_downloadMyCodesAsPDF', [$this,'downloadMyCodesAsPDF'], 10, 0); // logged in users only
97 }
98 if (method_exists($this->getPremiumFunctions(), "initHandlers")) {
99 $this->getPremiumFunctions()->initHandlers();
100 }
101 $this->cronjob_daily_activate();
102 }
103 public function cronjob_daily_activate() {
104 $args = [];
105 if (! wp_next_scheduled ( 'sasoEventtickets_cronjob_daily', $args )) {
106 wp_schedule_event( strtotime("00:05"), 'daily', 'sasoEventtickets_cronjob_daily', $args );
107 }
108 }
109 public function cronjob_daily_deactivate() {
110 wp_clear_scheduled_hook( 'sasoEventtickets_cronjob_daily' );
111 }
112 public function load_plugin_textdomain() {
113 load_plugin_textdomain( 'event-tickets-with-ticket-scanner', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' );
114 }
115 public function getPluginPath() {
116 return SASO_EVENTTICKETS_PLUGIN_DIR_PATH;
117 }
118 public function getPluginVersion() {
119 return SASO_EVENTTICKETS_PLUGIN_VERSION;
120 }
121 public function getPluginVersions() {
122 $ret = ['basic'=>SASO_EVENTTICKETS_PLUGIN_VERSION, 'premium'=>'', 'debug'=>''];
123 if (defined('SASO_EVENTTICKETS_PREMIUM_PLUGIN_VERSION')) {
124 $ret['premium'] = SASO_EVENTTICKETS_PREMIUM_PLUGIN_VERSION;
125 }
126 if (defined('WP_DEBUG') && WP_DEBUG) {
127 $ret['debug'] = esc_html__('is active', 'event-tickets-with-ticket-scanner');
128 }
129 return $ret;
130 }
131 public function getDB() {
132 return SASO_EVENTTICKETS::getDB(plugin_dir_path(__FILE__), "sasoEventticketsDB", $this);
133 }
134 public function getBase() {
135 if ($this->BASE == null) {
136 if (!class_exists('sasoEventtickets_Base')) {
137 include_once plugin_dir_path(__FILE__)."sasoEventtickets_Base.php";
138 }
139 $this->BASE = new sasoEventtickets_Base($this);
140 }
141 return $this->BASE;
142 }
143 public function getCore() {
144 if ($this->CORE == null) {
145 if (!class_exists('sasoEventtickets_Core')) {
146 include_once plugin_dir_path(__FILE__)."sasoEventtickets_Core.php";
147 }
148 $this->CORE = new sasoEventtickets_Core($this);
149 }
150 return $this->CORE;
151 }
152 public function getAdmin() {
153 if ($this->ADMIN == null) {
154 if (!class_exists('sasoEventtickets_AdminSettings')) {
155 include_once plugin_dir_path(__FILE__)."sasoEventtickets_AdminSettings.php";
156 }
157 $this->ADMIN = new sasoEventtickets_AdminSettings($this);
158 }
159 return $this->ADMIN;
160 }
161 public function getFrontend() {
162 if ($this->FRONTEND == null) {
163 if (!class_exists('sasoEventtickets_Frontend')) {
164 include_once plugin_dir_path(__FILE__)."sasoEventtickets_Frontend.php";
165 }
166 $this->FRONTEND = new sasoEventtickets_Frontend($this);
167 }
168 return $this->FRONTEND;
169 }
170 public function getOptions() {
171 if ($this->OPTIONS == null) {
172 if (!class_exists('sasoEventtickets_Options')) {
173 include_once plugin_dir_path(__FILE__)."sasoEventtickets_Options.php";
174 }
175 $this->OPTIONS = new sasoEventtickets_Options($this, $this->_prefix);
176 $this->OPTIONS->initOptions();
177 }
178 return $this->OPTIONS;
179 }
180 public function getNewPDFObject() {
181 if (!class_exists('sasoEventtickets_PDF')) {
182 require_once("sasoEventtickets_PDF.php");
183 }
184 $pdf = new sasoEventtickets_PDF();
185 $pdf->setFontSize($this->getOptions()->getOptionValue('wcTicketPDFFontSize'));
186 $pdf->setFontFamily($this->getOptions()->getOptionValue('wcTicketPDFFontFamily'));
187 $pdf = apply_filters( $this->_add_filter_prefix.'main_getNewPDFObject', $pdf );
188 return $pdf;
189 }
190 public function loadOnce($className, $filename="") {
191 if (!class_exists($className)) {
192 if ($filename == "") $filename = $className;
193 include_once __DIR__.'/'.$filename.'.php';
194 }
195 }
196
197 /**
198 * Load class from /includes/ folder structure
199 *
200 * @param string $className The class name to load
201 * @param string $relativePath Path relative to plugin root (e.g., 'includes/woocommerce/class-base.php')
202 * @return void
203 */
204 private function loadClass(string $className, string $relativePath): void {
205 if (class_exists($className)) {
206 return; // Already loaded
207 }
208
209 $path = __DIR__ . '/' . $relativePath;
210
211 if (file_exists($path)) {
212 require_once $path;
213 }
214 }
215 public function getWC() {
216 $this->loadOnce('sasoEventtickets_WC', "woocommerce-hooks");
217 return sasoEventtickets_WC::Instance();
218 }
219 public function getTicketHandler() {
220 $this->loadOnce('sasoEventtickets_Ticket');
221 return sasoEventtickets_Ticket::Instance($_SERVER["REQUEST_URI"]);
222 }
223 public function getTicketDesignerHandler($template="") {
224 $this->loadOnce('sasoEventtickets_TicketDesigner');
225 return sasoEventtickets_TicketDesigner::Instance($this, $template);
226 }
227 public function getTicketBadgeHandler() {
228 $this->loadOnce('sasoEventtickets_TicketBadge');
229 return sasoEventtickets_TicketBadge::Instance();
230 }
231 public function getTicketQRHandler() {
232 $this->loadOnce('sasoEventtickets_TicketQR');
233 return sasoEventtickets_TicketQR::Instance();
234 }
235 public function getAuthtokenHandler() {
236 $this->loadOnce('sasoEventtickets_Authtoken');
237 return sasoEventtickets_Authtoken::Instance();
238 }
239 public function getSeating() {
240 $this->loadOnce('sasoEventtickets_Seating');
241 return sasoEventtickets_Seating::Instance($this);
242 }
243
244 public function isOldPremiumDetected(): bool {
245 return $this->_oldPremiumDetected;
246 }
247
248 public function isStarterOrStopDetected(): bool {
249 return $this->_starterOrStopDetected;
250 }
251
252 public function getPremiumFunctions() {
253 if ($this->_isPrem == null && $this->PREMFUNCTIONS == null) {
254 $this->_isPrem = false;
255 $this->PREMFUNCTIONS = new sasoEventtickets_fakeprem();
256
257 // Check for Premium class - function-based compatibility check
258 if (class_exists('sasoEventtickets_PremiumFunctions')) {
259 // Check if this is Starter or Stop plugin (both are "update-only" placeholders)
260 $is_starter_or_stop = defined('SASO_EVENTTICKETS_STARTER_VERSION') || defined('SASO_EVENTTICKETS_STOP_VERSION');
261
262 if ($is_starter_or_stop) {
263 // Starter/Stop plugin detected - treat as "premium not active"
264 // Show message: Enter license, then update via plugin area
265 $this->_starterOrStopDetected = true;
266 $this->PREMFUNCTIONS = new sasoEventtickets_fakeprem();
267 } elseif (!defined('SASO_EVENTTICKETS_PREMIUM_PLUGIN_VERSION')) {
268 // Premium plugin exists but no version defined - likely very old or corrupted
269 $this->_oldPremiumDetected = true;
270 } else {
271 // Check version AND function compatibility
272 $min_premium_version = '1.6.0';
273
274 if (version_compare(SASO_EVENTTICKETS_PREMIUM_PLUGIN_VERSION, $min_premium_version, '<')) {
275 // Version too old - check if required functions exist
276 $prem_class = new sasoEventtickets_PremiumFunctions($this, plugin_dir_path(__FILE__), $this->_prefix, $this->getDB());
277
278 // Check for critical methods that were added/changed in 1.6.0
279 $has_required_methods = method_exists($prem_class, 'maxValues')
280 && method_exists($prem_class, 'getSeatingPlanById')
281 && method_exists($prem_class, 'generateTicketPdf');
282
283 if (!$has_required_methods) {
284 // Missing required functions even though version might say otherwise
285 $this->_oldPremiumDetected = true;
286 } else {
287 // Functions exist - load it
288 try {
289 $this->PREMFUNCTIONS = $prem_class;
290 $this->_isPrem = $this->PREMFUNCTIONS->isPremium();
291 } catch (Exception $e) {
292 // Error loading premium - fall back to free
293 $this->_oldPremiumDetected = true;
294 }
295 }
296 } else {
297 // Version is OK - try to load
298 try {
299 $this->PREMFUNCTIONS = new sasoEventtickets_PremiumFunctions($this, plugin_dir_path(__FILE__), $this->_prefix, $this->getDB());
300 $this->_isPrem = $this->PREMFUNCTIONS->isPremium();
301 } catch (Exception $e) {
302 // Error loading premium - fall back to free
303 $this->_oldPremiumDetected = true;
304 }
305 }
306 }
307 } else {
308 // Premium class not yet available — basic plugin loaded before premium.
309 // Defer premium setup until all plugins are loaded (plugins_loaded hook).
310 $premPluginFolder = $this->getPremiumPluginFolder();
311 if (!empty($premPluginFolder)) {
312 add_action('plugins_loaded', [$this, '_lateLoadPremium'], 1);
313 }
314 }
315 }
316 return $this->PREMFUNCTIONS;
317 }
318 /**
319 * Late-load premium plugin when basic loaded before premium (plugin loading order).
320 * Hooked to plugins_loaded with priority 1 so it runs before WooCommercePluginLoaded (priority 20).
321 */
322 public function _lateLoadPremium(): void {
323 if (!class_exists('sasoEventtickets_PremiumFunctions')) {
324 return; // Premium plugin not installed/active
325 }
326 if ($this->PREMFUNCTIONS instanceof sasoEventtickets_PremiumFunctions) {
327 return; // Already loaded
328 }
329
330 // Check for Starter/Stop plugin first (update-only placeholders, no premium features)
331 if (defined('SASO_EVENTTICKETS_STARTER_VERSION') || defined('SASO_EVENTTICKETS_STOP_VERSION')) {
332 $this->_starterOrStopDetected = true;
333 return;
334 }
335
336 $min_premium_version = '1.6.0';
337 if (!defined('SASO_EVENTTICKETS_PREMIUM_PLUGIN_VERSION')
338 || version_compare(SASO_EVENTTICKETS_PREMIUM_PLUGIN_VERSION, $min_premium_version, '<')) {
339 $this->_oldPremiumDetected = true;
340 return;
341 }
342
343 try {
344 $this->PREMFUNCTIONS = new sasoEventtickets_PremiumFunctions($this, plugin_dir_path(__FILE__), $this->_prefix, $this->getDB());
345 $this->_isPrem = $this->PREMFUNCTIONS->isPremium();
346
347 if (method_exists($this->PREMFUNCTIONS, 'initHandlers')) {
348 $this->PREMFUNCTIONS->initHandlers();
349 }
350 } catch (\Throwable $e) {
351 // Premium loading failed — degrade gracefully to free mode
352 $this->PREMFUNCTIONS = new sasoEventtickets_fakeprem();
353 $this->_isPrem = false;
354 error_log('Event Tickets: Premium late-load failed: ' . $e->getMessage());
355 }
356 }
357
358 public function getPremiumPluginFolder(): string {
359 $plugins = get_option('active_plugins', []);
360 $premiumFile = "";
361 foreach($plugins as $plugin) {
362 if (strpos(" ".$plugin, $this->_premium_plugin_name) > 0) {
363 $premiumFile = plugin_dir_path($plugin);
364 break;
365 }
366 }
367 return $premiumFile;
368 }
369 public function isPremium() {
370 if ($this->_isPrem === null) {
371 $this->getPremiumFunctions();
372 // If PREMFUNCTIONS already existed, getPremiumFunctions() skipped re-evaluation.
373 // Re-query the premium plugin directly.
374 if ($this->_isPrem === null && $this->PREMFUNCTIONS !== null) {
375 $this->_isPrem = ($this->PREMFUNCTIONS instanceof sasoEventtickets_PremiumFunctions)
376 ? $this->PREMFUNCTIONS->isPremium()
377 : false;
378 }
379 }
380
381 // Eigene Verifikation: auch wenn Premium-Plugin "ja" sagt,
382 // Basic Plugin prüft den gespeicherten Lizenzstatus selbst.
383 // Rekursionsschutz: isPremium() -> getTicketHandler() -> Konstruktor -> getOptions() -> isPremium()
384 if ($this->_isPrem === true && !$this->_isCheckingSubscription) {
385 $this->_isCheckingSubscription = true;
386 try {
387 if (!$this->getTicketHandler()->isSubscriptionActive()) {
388 $this->_isPrem = false;
389 }
390 } finally {
391 $this->_isCheckingSubscription = false;
392 }
393 }
394
395 return $this->_isPrem;
396 }
397 /**
398 * Invalidate the cached premium status so the next isPremium() call re-evaluates.
399 */
400 public function invalidatePremiumCache(): void {
401 $this->_isPrem = null;
402 $this->PREMFUNCTIONS = null;
403 }
404 public function getPrefix() {
405 return $this->_prefix;
406 }
407 public function getMV() {
408 $v = ['storeip'=>false,'allowuserreg'=>false,'codes_total'=>0x13,'codes'=>0x12,'lists'=>5,'authtokens_total'=>3];
409 $v["codes"] = (int) hexdec(0x80 / 0x002) / 2;
410 $v["codes_total"] = (int) hexdec(0x80 / 0x002) / 2;
411 $v["seatingplans"] = (int) (0x04 >> 0x02);
412 $v["seats_per_plan"] = (int) (0x50 >> 0x02);
413 return $v;
414 }
415 public function listener_upgrader_process_complete( $upgrader_object, $options ) {
416 $current_plugin_path_name = plugin_basename( __FILE__ );
417 if ($options['action'] == 'update' && $options['type'] == 'plugin' ) {
418 if (isset($options['plugins'])) {
419 foreach($options['plugins'] as $each_plugin) {
420 if ($each_plugin==$current_plugin_path_name) {
421 // .......................... YOUR CODES .............
422 }
423 }
424 }
425 }
426 do_action( $this->_do_action_prefix.'main_listener_upgrader_process_complete' );
427 }
428 /**
429 * check for ticket detail page request
430 */
431 public function wc_checkTicketDetailPage() {
432 if( is_404() ){
433 include_once("SASO_EVENTTICKETS.php");
434 // /wp-content/plugins/event-tickets-with-ticket-scanner/ticket/
435 $p = $this->getCore()->getTicketURLPath(true);
436 $t = explode("/", $_SERVER["REQUEST_URI"]);
437 if (count($t) > 1) {
438 if ($t[count($t)-2] != "scanner") {
439 if(substr($_SERVER["REQUEST_URI"], 0, strlen($p)) == $p) {
440 $this->getTicketHandler()->initFilterAndActions();
441 } else {
442 $wcTicketCompatibilityModeURLPath = trim($this->getOptions()->getOptionValue('wcTicketCompatibilityModeURLPath'));
443 $wcTicketCompatibilityModeURLPath = trim(trim($wcTicketCompatibilityModeURLPath, "/"));
444 if (!empty($wcTicketCompatibilityModeURLPath)) {
445 $uri = trim($_SERVER["REQUEST_URI"]);
446 if (!empty($uri)) {
447 $pos = strpos($uri, $wcTicketCompatibilityModeURLPath);
448 if ($pos > 0) {
449 $this->getTicketHandler()->initFilterAndActions();
450 }
451 }
452 }
453 }
454 }
455
456 if ($t[count($t)-2] == "scanner") {
457 if(substr($_SERVER["REQUEST_URI"], 0, strlen($p)) == $p) {
458 //$this->replacingShortcodeTicketScanner();
459 $this->getTicketHandler()->initFilterAndActionsTicketScanner();
460 } else {
461 $wcTicketCompatibilityModeURLPath = trim($this->getOptions()->getOptionValue('wcTicketCompatibilityModeURLPath'));
462 $wcTicketCompatibilityModeURLPath = trim(trim($wcTicketCompatibilityModeURLPath, "/"));
463 if (!empty($wcTicketCompatibilityModeURLPath)) {
464 $uri = trim($_SERVER["REQUEST_URI"]);
465 if (!empty($uri)) {
466 $pos = strpos($_SERVER["REQUEST_URI"], $wcTicketCompatibilityModeURLPath."/scanner/");
467 if ($pos > 0) {
468 $this->getTicketHandler()->initFilterAndActionsTicketScanner();
469 }
470 }
471 }
472 }
473 }
474 }
475 } // endif 404
476 }
477 private function init_frontend() {
478 add_shortcode($this->_shortcode, [$this, 'replacingShortcode']);
479 add_shortcode($this->_shortcode_mycode, [$this, 'replacingShortcodeMyCode']);
480 add_shortcode($this->_shortcode_ticket_scanner, [$this, 'replacingShortcodeTicketScanner']);
481 add_shortcode($this->_shortcode_eventviews, [$this, 'replacingShortcodeEventViews']);
482 add_shortcode($this->_shortcode_feature_list, [$this, 'replacingShortcodeFeatureList']);
483 add_shortcode($this->_shortcode_ticket_detail, [$this, 'replacingShortcodeTicketDetail']);
484 do_action( $this->_do_action_prefix.'main_init_frontend' );
485 }
486 private function init_backend() {
487 add_action('admin_menu', [$this, 'register_options_page']);
488 register_activation_hook(__FILE__, [$this, 'plugin_activated']);
489 register_deactivation_hook( __FILE__, [$this, 'plugin_deactivated'] );
490 //register_uninstall_hook( __FILE__, 'sasoEventticketsDB::plugin_uninstall' ); // MUSS NOCH GETESTE WERDEN
491 add_action( 'plugins_loaded', [$this, 'plugins_loaded'] );
492 add_action( 'show_user_profile', [$this, 'show_user_profile'] );
493 add_action( 'admin_init', [$this, 'handleFormatWarningDismiss'] );
494 add_action( 'admin_notices', [$this, 'showSubscriptionWarning'] );
495 add_action( 'admin_notices', [$this, 'showFomoBanner'] );
496 add_action( 'admin_notices', [$this, 'showOutdatedPremiumWarning'] );
497 add_action( 'admin_notices', [$this, 'showFormatWarning'] );
498 add_action( 'admin_notices', [$this, 'showPhpVersionWarning'] );
499 add_action( 'admin_notices', [$this, 'showOptionsMigrationNotice'] );
500 add_action( 'wp_ajax_saso_et_dismiss_fomo', [$this, 'ajaxDismissFomo'] );
501
502 if (basename($_SERVER['SCRIPT_NAME']) == "admin-ajax.php") {
503 add_action('wp_ajax_'.$this->_prefix.'_executeAdminSettings', [$this,'executeAdminSettings_a'], 10, 0);
504 add_action('wp_ajax_'.$this->_prefix.'_executeSeatingAdmin', [$this,'executeSeatingAdmin_a'], 10, 0);
505 }
506
507 add_action('admin_init', [$this, 'periodicLicenseCheck']);
508
509 do_action( $this->_do_action_prefix.'main_init_backend' );
510 }
511
512 /**
513 * Periodic license check on admin page loads.
514 * Runs at most once per 24h to catch cases where WP Cron is disabled.
515 */
516 public function periodicLicenseCheck(): void {
517 // Also run when premium plugin is installed with a serial key but isPremium() is false
518 // This breaks the deadlock where isPremium()=false prevents the license check from ever running
519 $hasPremiumPlugin = class_exists('sasoEventtickets_PremiumFunctions');
520 $hasSerial = !empty(trim(get_option("saso-event-tickets-premium_serial", "")));
521 if (!$this->isPremium() && !($hasPremiumPlugin && $hasSerial)) return;
522
523 $info = $this->getTicketHandler()->get_expiration();
524 $last_run = intval($info['last_run']);
525
526 // Maximal 1x pro 24h, nicht bei jedem Page Load
527 // In recovery mode (not premium but has serial), check more frequently (every 1h)
528 $interval = $this->isPremium() ? 86400 : 3600;
529 if ($last_run > 0 && (time() - $last_run) < $interval) return;
530
531 // Check ausführen
532 $this->getTicketHandler()->checkForPremiumSerialExpiration();
533
534 // isPremium Cache invalidieren damit neuer Wert gilt
535 $this->invalidatePremiumCache();
536 }
537 public function WooCommercePluginLoaded() {
538 // DON'T load WC here - let relay functions do lazy loading
539 //$this->getWC(); // um die wc handler zu laden
540 add_action('woocommerce_review_order_after_cart_contents', [$this, 'relay_woocommerce_review_order_after_cart_contents']);
541 add_action('woocommerce_checkout_process', [$this, 'relay_woocommerce_checkout_process']);
542 add_action('woocommerce_before_cart_table', [$this, 'relay_woocommerce_before_cart_table']);
543 add_action('woocommerce_cart_updated', [$this, 'relay_woocommerce_cart_updated']);
544 add_filter('woocommerce_email_attachments', [$this, 'relay_woocommerce_email_attachments'], 10, 3);
545 add_action('woocommerce_checkout_create_order_line_item', [$this, 'relay_woocommerce_checkout_create_order_line_item'], 20, 4 );
546 add_action('woocommerce_check_cart_items', [$this, 'relay_woocommerce_check_cart_items'] );
547 add_action('woocommerce_new_order', [$this, 'relay_woocommerce_new_order'], 10, 1);
548 add_action('woocommerce_checkout_update_order_meta', [$this, 'relay_woocommerce_checkout_update_order_meta'], 20, 2);
549 add_action('woocommerce_order_status_changed', [$this, 'relay_woocommerce_order_status_changed'], 10, 3);
550 add_filter('woocommerce_order_item_display_meta_key', [$this, 'relay_woocommerce_order_item_display_meta_key'], 20, 3 );
551 add_filter('woocommerce_order_item_display_meta_value', [$this, 'relay_woocommerce_order_item_display_meta_value'], 20, 3);
552 add_action('wpo_wcpdf_after_item_meta', [$this, 'relay_wpo_wcpdf_after_item_meta'], 20, 3 );
553 add_action('woocommerce_order_item_meta_start', [$this, 'relay_woocommerce_order_item_meta_start'], 201, 4);
554 add_action('woocommerce_product_after_variable_attributes', [$this, 'relay_woocommerce_product_after_variable_attributes'], 10, 3);
555 add_action('woocommerce_save_product_variation',[$this, 'relay_woocommerce_save_product_variation'], 10 ,2 );
556 add_action('woocommerce_email_order_meta', [$this, 'relay_woocommerce_email_order_meta'], 10, 4 );
557 add_action('woocommerce_thankyou', [$this, 'relay_woocommerce_thankyou'], 5);
558 if (wp_doing_ajax()) {
559 // erlaube ajax nonpriv und registriere handler
560 add_action('wp_ajax_nopriv_'.$this->getPrefix().'_executeWCFrontend', [$this,'relay_executeWCFrontend']); // nicht angemeldete user, sollen eine antwort erhalten
561 add_action('wp_ajax_'.$this->getPrefix().'_executeWCFrontend', [$this,'relay_executeWCFrontend']); // nicht angemeldete user, sollen eine antwort erhalten
562 // Seating Frontend AJAX (seat selection in shop)
563 add_action('wp_ajax_nopriv_'.$this->getPrefix().'_executeSeatingFrontend', [$this,'relay_executeSeatingFrontend']);
564 add_action('wp_ajax_'.$this->getPrefix().'_executeSeatingFrontend', [$this,'relay_executeSeatingFrontend']);
565 }
566 if (is_admin()) {
567 add_action('woocommerce_delete_order', [$this, 'relay_woocommerce_delete_order'], 10, 1 );
568 add_action('woocommerce_delete_order_item', [$this, 'relay_woocommerce_delete_order_item'], 20, 1);
569 add_action('woocommerce_pre_delete_order_refund', [$this, 'relay_woocommerce_pre_delete_order_refund'], 10, 3);
570 add_action('woocommerce_delete_order_refund', [$this, 'relay_woocommerce_delete_order_refund'], 10, 1 );
571 add_action('woocommerce_order_partially_refunded', [$this, 'relay_woocommerce_order_partially_refunded'], 10, 2);
572 add_filter('woocommerce_product_data_tabs', [$this, 'relay_woocommerce_product_data_tabs'], 98 );
573 add_action('woocommerce_product_data_panels', [$this, 'relay_woocommerce_product_data_panels'] );
574 add_action('woocommerce_process_product_meta', [$this, 'relay_woocommerce_process_product_meta'], 10, 2 );
575 add_action('add_meta_boxes', [$this, 'relay_add_meta_boxes'], 10, 2);
576 add_filter('manage_edit-product_columns', [$this, 'relay_manage_edit_product_columns']);
577 add_action('manage_product_posts_custom_column', [$this, 'relay_manage_product_posts_custom_column'], 2);
578 add_filter("manage_edit-product_sortable_columns", [$this, 'relay_manage_edit_product_sortable_columns']);
579 } else {
580 add_action('woocommerce_single_product_summary', [$this, 'relay_woocommerce_single_product_summary']);
581 }
582
583 // set routing -- NEEDS to be replaced by add_rewrite_rule later
584 add_action( 'template_redirect', [$this, 'wc_checkTicketDetailPage'], 1 );
585 //$this->wc_checkTicketDetailPage();
586 add_action('rest_api_init', function () {
587 SASO_EVENTTICKETS::setRestRoutesTicket();
588 });
589
590 add_action('woocommerce_after_shop_loop_item', [$this, 'relay_woocommerce_after_shop_loop_item'], 9); // with 9 we are just before the add to cart button
591 add_filter('woocommerce_add_to_cart_validation', [$this, 'relay_woocommerce_add_to_cart_validation'], 10, 3);
592 add_filter('woocommerce_add_cart_item_data', [$this, 'relay_woocommerce_add_cart_item_data'], 10, 3);
593 add_action('woocommerce_add_to_cart', [$this, 'relay_woocommerce_add_to_cart'], 10, 6);
594 add_action('woocommerce_cart_item_removed', [$this, 'relay_woocommerce_cart_item_removed'], 10, 2);
595 add_action('woocommerce_after_cart_item_quantity_update', [$this, 'relay_woocommerce_after_cart_item_quantity_update'], 10, 4);
596 add_filter('woocommerce_update_cart_validation', [$this, 'relay_woocommerce_update_cart_validation'], 10, 4);
597 add_action('woocommerce_before_add_to_cart_button', [$this, 'relay_woocommerce_before_add_to_cart_button'], 15);
598
599 do_action( $this->_do_action_prefix.'main_WooCommercePluginLoaded' );
600 }
601 public function relay_woocommerce_after_shop_loop_item() {
602 $this->getWC()->getFrontendManager()->woocommerce_after_shop_loop_item_handler();
603 }
604 public function relay_woocommerce_add_to_cart_validation() {
605 $args = func_get_args();
606 return $this->getWC()->getFrontendManager()->woocommerce_add_to_cart_validation_handler(...$args);
607 }
608 public function relay_woocommerce_add_cart_item_data() {
609 $args = func_get_args();
610 return $this->getWC()->getFrontendManager()->woocommerce_add_cart_item_data_handler(...$args);
611 }
612 public function relay_woocommerce_add_to_cart() {
613 $args = func_get_args();
614 return $this->getWC()->getFrontendManager()->woocommerce_add_to_cart_handler(...$args);
615 }
616 public function relay_woocommerce_cart_item_removed() {
617 $args = func_get_args();
618 $this->getWC()->getFrontendManager()->woocommerce_cart_item_removed_handler(...$args);
619 }
620 public function relay_woocommerce_after_cart_item_quantity_update() {
621 $args = func_get_args();
622 $this->getWC()->getFrontendManager()->woocommerce_after_cart_item_quantity_update_handler(...$args);
623 }
624 public function relay_woocommerce_update_cart_validation() {
625 $args = func_get_args();
626 return $this->getWC()->getFrontendManager()->woocommerce_update_cart_validation_handler(...$args);
627 }
628 public function relay_woocommerce_before_add_to_cart_button() {
629 $this->getWC()->getFrontendManager()->woocommerce_before_add_to_cart_button_handler();
630 }
631 public function relay_woocommerce_review_order_after_cart_contents() {
632 $this->getWC()->getFrontendManager()->woocommerce_review_order_after_cart_contents();
633 }
634 public function relay_woocommerce_checkout_process() {
635 $this->getWC()->getFrontendManager()->woocommerce_checkout_process();
636 }
637 public function relay_woocommerce_before_cart_table() {
638 $this->getWC()->getFrontendManager()->woocommerce_before_cart_table();
639 }
640 public function relay_woocommerce_cart_updated() {
641 $this->getWC()->getFrontendManager()->woocommerce_cart_updated_handler();
642 }
643 public function relay_woocommerce_email_attachments() {
644 $args = func_get_args();
645 return $this->getWC()->getEmailHandler()->woocommerce_email_attachments(...$args);
646 }
647 public function relay_woocommerce_checkout_create_order_line_item() {
648 $args = func_get_args();
649 return $this->getWC()->getOrderManager()->woocommerce_checkout_create_order_line_item(...$args);
650 }
651 public function relay_woocommerce_check_cart_items() {
652 $this->getWC()->getFrontendManager()->woocommerce_check_cart_items();
653 }
654 public function relay_woocommerce_new_order() {
655 $args = func_get_args();
656 return $this->getWC()->getOrderManager()->woocommerce_new_order(...$args);
657 }
658 public function relay_woocommerce_checkout_update_order_meta() {
659 $args = func_get_args();
660 return $this->getWC()->getOrderManager()->woocommerce_checkout_update_order_meta(...$args);
661 }
662 public function relay_executeWCFrontend() {
663 return $this->getWC()->getFrontendManager()->executeWCFrontend();
664 }
665 public function relay_executeSeatingFrontend() {
666 return $this->getSeating()->getFrontendManager()->executeSeatingFrontend();
667 }
668 public function relay_woocommerce_delete_order() {
669 $args = func_get_args();
670 $this->getWC()->getOrderManager()->woocommerce_delete_order(...$args);
671 }
672 public function relay_woocommerce_delete_order_item() {
673 $args = func_get_args();
674 $this->getWC()->getOrderManager()->woocommerce_delete_order_item(...$args);
675 }
676 public function relay_woocommerce_pre_delete_order_refund() {
677 $args = func_get_args();
678 $this->getWC()->getOrderManager()->woocommerce_pre_delete_order_refund(...$args);
679 }
680 public function relay_woocommerce_delete_order_refund() {
681 $args = func_get_args();
682 $this->getWC()->getOrderManager()->woocommerce_delete_order_refund(...$args);
683 }
684 public function relay_woocommerce_product_data_tabs() {
685 $args = func_get_args();
686 return $this->getWC()->getProductManager()->woocommerce_product_data_tabs(...$args);
687 }
688 public function relay_woocommerce_product_data_panels() {
689 $this->getWC()->getProductManager()->woocommerce_product_data_panels();
690 }
691 public function relay_woocommerce_process_product_meta() {
692 $args = func_get_args();
693 $this->getWC()->getProductManager()->woocommerce_process_product_meta(...$args);
694 }
695 public function relay_add_meta_boxes(...$args) {
696 $this->getWC()->add_meta_boxes(...$args);
697 }
698 public function relay_manage_edit_product_columns() {
699 $args = func_get_args();
700 return $this->getWC()->getProductManager()->manage_edit_product_columns(...$args);
701 }
702 public function relay_manage_product_posts_custom_column() {
703 $args = func_get_args();
704 $this->getWC()->getProductManager()->manage_product_posts_custom_column(...$args);
705 }
706 public function relay_manage_edit_product_sortable_columns() {
707 $args = func_get_args();
708 return $this->getWC()->getProductManager()->manage_edit_product_sortable_columns(...$args);
709 }
710 public function relay_woocommerce_single_product_summary() {
711 $this->getWC()->getFrontendManager()->woocommerce_single_product_summary();
712 }
713 public function relay_woocommerce_order_status_changed() {
714 $args = func_get_args();
715 $this->getWC()->getOrderManager()->woocommerce_order_status_changed(...$args);
716 }
717 public function relay_woocommerce_order_partially_refunded() {
718 $args = func_get_args();
719 $this->getWC()->getOrderManager()->woocommerce_order_partially_refunded(...$args);
720 }
721 public function relay_woocommerce_order_item_display_meta_key() {
722 $args = func_get_args();
723 return $this->getWC()->getOrderManager()->woocommerce_order_item_display_meta_key(...$args);
724 }
725 public function relay_woocommerce_order_item_display_meta_value() {
726 $args = func_get_args();
727 return $this->getWC()->getOrderManager()->woocommerce_order_item_display_meta_value(...$args);
728 }
729 public function relay_wpo_wcpdf_after_item_meta() {
730 $args = func_get_args();
731 $this->getWC()->getOrderManager()->wpo_wcpdf_after_item_meta(...$args);
732 }
733 public function relay_woocommerce_order_item_meta_start() {
734 $args = func_get_args();
735 $this->getWC()->getOrderManager()->woocommerce_order_item_meta_start(...$args);
736 }
737 public function relay_woocommerce_product_after_variable_attributes() {
738 $args = func_get_args();
739 $this->getWC()->getProductManager()->woocommerce_product_after_variable_attributes(...$args);
740 }
741 public function relay_woocommerce_save_product_variation() {
742 $args = func_get_args();
743 $this->getWC()->getProductManager()->woocommerce_save_product_variation(...$args);
744 }
745 public function relay_woocommerce_email_order_meta() {
746 $args = func_get_args();
747 $this->getWC()->getEmailHandler()->woocommerce_email_order_meta(...$args);
748 }
749 public function relay_woocommerce_thankyou() {
750 $args = func_get_args();
751 $this->getWC()->getFrontendManager()->woocommerce_thankyou(...$args);
752 }
753 public function relay_sasoEventtickets_cronjob_daily() {
754 $this->getTicketHandler()->cronJobDaily();
755 $this->getAdmin()->cleanupOptionsHistory();
756 }
757
758 public function plugin_deactivated() {
759 $this->cronjob_daily_deactivate();
760 $this->getDB();
761 sasoEventticketsDB::plugin_deactivated();
762 do_action( $this->_do_action_prefix.'main_plugin_deactivated' );
763 }
764 public function plugin_uninstall() {
765 $this->getDB();
766 sasoEventticketsDB::plugin_uninstall();
767 $this->getAdmin();
768 sasoEventtickets_AdminSettings::plugin_uninstall();
769 do_action( $this->_do_action_prefix.'main_WooCommercePluginLoaded' );
770 }
771 public function plugin_activated($is_network_wide=false) { // und auch für updates, macht es einfacher
772 $this->getDB(); // um installiere Tabellen auszuführen
773 update_option('SASO_EVENTTICKETS_PLUGIN_VERSION', SASO_EVENTTICKETS_PLUGIN_VERSION);
774 // Only reset migration flag if options exist in wp_options (downgrade scenario).
775 // Do NOT blindly delete — the migration already removed options from wp_options,
776 // so deleting the flag would cause one request to read empty wp_options → all defaults.
777 $sentinelKey = $this->_prefix . 'qrAttachQRFilesToMailAsOnePDF';
778 if (get_option($sentinelKey, '__NOT_SET__') !== '__NOT_SET__') {
779 delete_option('saso_eventtickets_options_migrated');
780 }
781 $this->getAdmin()->generateFirstCodeList();
782 $this->cronjob_daily_activate();
783 do_action( $this->_do_action_prefix.'activated' );
784 do_action( $this->_do_action_prefix.'main_plugin_activated' );
785 }
786 public function plugins_loaded() {
787 if (SASO_EVENTTICKETS_PLUGIN_VERSION !== get_option('SASO_EVENTTICKETS_PLUGIN_VERSION', '')) $this->plugin_activated(); // vermutlich wurde die aktivierung übersprungen, bei änderungen direkt an den files
788 }
789 public function initialize_plugin() {
790 $this->getDB(); // um installiere Tabellen auszuführen
791 do_action( $this->_do_action_prefix.'initialized' );
792 do_action( $this->_do_action_prefix.'main_initialize_plugin' );
793 }
794 function show_user_profile($profileuser) {
795 $this->getAdmin()->show_user_profile($profileuser);
796 do_action( $this->_do_action_prefix.'main_show_user_profile' );
797 }
798 function register_options_page() {
799 $allowed = $this->isUserAllowedToAccessAdminArea();
800 $allowed = apply_filters( $this->_add_filter_prefix.'main_options_page', $allowed );
801 if ($allowed) {
802 add_options_page(__('Event Tickets', 'event-tickets-with-ticket-scanner'), 'Event Tickets', 'manage_options', 'event-tickets-with-ticket-scanner', [$this,'options_page']);
803 add_menu_page( __('Event Tickets', 'event-tickets-with-ticket-scanner'), 'Event Tickets', 'read', 'event-tickets-with-ticket-scanner', [$this,'options_page'], plugins_url( "",__FILE__ )."/img/icon_event-tickets-with-ticket-scanner_18px.gif", null );
804 }
805 do_action( $this->_do_action_prefix.'main_register_options_page' );
806 }
807
808 function options_page() {
809 $allowed = $this->isUserAllowedToAccessAdminArea();
810 $allowed = apply_filters( $this->_add_filter_prefix.'main_options_page', $allowed );
811 if ( !$allowed ) {
812 wp_die( __( 'You do not have sufficient permissions to access this page.', 'event-tickets-with-ticket-scanner' ) );
813 }
814
815 wp_enqueue_style("wp-jquery-ui-dialog");
816
817 $js_url = "jquery.qrcode.min.js?_v=".$this->_js_version;
818 wp_register_script('ajax_script2', plugins_url( "3rd/".$js_url,__FILE__ ), array('jquery', 'jquery-ui-dialog'));
819 wp_enqueue_script('ajax_script2');
820
821 wp_enqueue_media(); // um die js wp.media lib zu laden
822
823 // einbinden das js starter skript
824 $js_url = $this->_js_file."?_v=".$this->_js_version;
825 if (defined( 'WP_DEBUG')) $js_url .= '&debug=1';
826 wp_register_script('ajax_script_backend', plugins_url( $js_url,__FILE__ ), array('jquery', 'jquery-ui-dialog', 'wp-i18n'));
827 wp_enqueue_script('ajax_script_backend');
828 wp_set_script_translations('ajax_script_backend', 'event-tickets-with-ticket-scanner', __DIR__.'/languages');
829
830 // per script eine variable einbinden, die url hat den wp-admin prefix
831 // damit im backend.js dann die richtige callback url genutzt werden kann
832 $vars = array(
833 '_plugin_home_url' =>plugins_url( "",__FILE__ ),
834 '_plugin_version' => $this->getPluginVersion(),
835 '_action' => $this->_prefix.'_executeAdminSettings',
836 '_max'=>$this->getBase()->getMaxValues(),
837 '_isPremium'=>$this->isPremium(),
838 '_isUserLoggedin'=>is_user_logged_in(),
839 '_premJS'=>$this->isPremium() && method_exists($this->getPremiumFunctions(), "getJSBackendFile") ? $this->getPremiumFunctions()->getJSBackendFile() : '',
840 'url' => admin_url( 'admin-ajax.php' ),
841 'ticket_url' => $this->getCore()->getTicketURLPath(),
842 'nonce' => wp_create_nonce( $this->_js_nonce ),
843 'ajaxActionPrefix' => $this->_prefix,
844 'divPrefix' => $this->_prefix,
845 'divId' => $this->_divId,
846 'jsFiles' => plugins_url( 'backend.js?_v='.$this->_js_version.'&_f='.filemtime(__DIR__.'/backend.js'),__FILE__ )
847 );
848 $vars = apply_filters( $this->_add_filter_prefix.'main_options_page', $vars );
849 wp_localize_script(
850 'ajax_script_backend',
851 'Ajax_'.$this->_prefix, // name der injected variable
852 $vars
853 );
854
855 do_action( $this->_do_action_prefix.'main_options_page' );
856
857 $versions = $this->getPluginVersions();
858 $versions_tail = $versions['basic'].($versions['premium'] != "" ? ', Premium: '.$versions['premium'] : '');
859 $version_tail_add = "";
860 if ($versions['debug'] != "") $version_tail_add .= 'DEBUG: '.$versions['debug'].', LANG: '.determine_locale();
861 ?>
862 <style>.event-tickets-with-ticket-scanner-admin-page{opacity:0;transition:opacity .3s ease}.event-tickets-with-ticket-scanner-admin-page.et-ready{opacity:1}</style>
863 <div class="event-tickets-with-ticket-scanner-admin-page">
864 <div class="event-tickets-with-ticket-scanner-header">
865 <div class="event-tickets-with-ticket-scanner-header-left">
866 <img src="<?php echo plugins_url( "",__FILE__ ); ?>/img/logo_event-tickets-with-ticket-scanner.gif"
867 alt="Event Tickets"
868 class="event-tickets-with-ticket-scanner-header-logo">
869
870 <div class="event-tickets-with-ticket-scanner-header-title">
871 <div class="event-tickets-with-ticket-scanner-header-name">
872 Event Tickets with Ticket Scanner
873 </div>
874 <div class="event-tickets-with-ticket-scanner-header-meta">
875 <?php esc_html_e('Version', 'event-tickets-with-ticket-scanner'); ?>: <?php echo $versions_tail; ?> <?php echo $version_tail_add; ?>
876 </div>
877 </div>
878 </div>
879
880 <div class="event-tickets-with-ticket-scanner-header-right" id="event-tickets-with-ticket-scanner-header-actions">
881 <!-- Button kommt via JS -->
882 </div>
883 </div>
884
885 <div style="clear:both;" data-id="plugin_addons"></div>
886 <div style="clear:both;" data-id="plugin_info_area"></div>
887 <div style="clear:both;" id="<?php echo esc_attr($this->_divId); ?>" class="et-content-area">
888 <div class="et-loading"><span class="lds-dual-ring"></span></div>
889 </div>
890 <div class="et-footer">
891 <a name="shortcodedetails"></a>
892 <div class="et-footer-grid">
893 <div class="et-card et-footer-card">
894 <div class="et-card-header">
895 <span class="dashicons dashicons-book" style="color:#9333ea;margin-right:6px;"></span>
896 Documentation
897 </div>
898 <p><a href="https://vollstart.com/event-tickets-with-ticket-scanner/docs/" target="_blank"><?php esc_html_e('Click here, to visit the documentation of this plugin.', 'event-tickets-with-ticket-scanner'); ?></a></p>
899 <p style="margin-top:12px;"><?php esc_html_e('You can use this plugin to sell tickets and even redeem them. Check out the documentation for', 'event-tickets-with-ticket-scanner'); ?> <a target="_blank" href="https://vollstart.com/event-tickets-with-ticket-scanner/docs/#ticket"><?php esc_html_e('more details here', 'event-tickets-with-ticket-scanner'); ?></a>.</p>
900 </div>
901 <div class="et-card et-footer-card">
902 <div class="et-card-header">
903 <span class="dashicons dashicons-star-filled" style="color:#f59e0b;margin-right:6px;"></span>
904 <?php esc_html_e('Plugin Rating', 'event-tickets-with-ticket-scanner'); ?>
905 </div>
906 <p><?php esc_html_e('If you like our plugin, then please give us a', 'event-tickets-with-ticket-scanner'); ?> <a target="_blank" href="https://wordpress.org/support/plugin/event-tickets-with-ticket-scanner/reviews?rate=5#new-post">5-Star Rating</a>.</p>
907 </div>
908 <div class="et-card et-footer-card">
909 <div class="et-card-header">
910 <span class="dashicons dashicons-superhero" style="color:#9333ea;margin-right:6px;"></span>
911 <?php esc_html_e('Premium Homepage', 'event-tickets-with-ticket-scanner'); ?>
912 </div>
913 <p><?php esc_html_e('You can find more details about the', 'event-tickets-with-ticket-scanner'); ?> <a target="_blank" href="https://vollstart.com/event-tickets-with-ticket-scanner/"><?php esc_html_e('premium version here', 'event-tickets-with-ticket-scanner'); ?></a>.</p>
914 </div>
915 </div>
916
917 <div class="et-card et-footer-shortcodes">
918 <div class="et-card-header">
919 <span class="dashicons dashicons-shortcode" style="color:#9333ea;margin-right:6px;"></span>
920 <?php esc_html_e('Shortcodes', 'event-tickets-with-ticket-scanner'); ?>
921 </div>
922
923 <h3><?php esc_html_e('Shortcode to display the event calendar form within a page', 'event-tickets-with-ticket-scanner'); ?></h3>
924 <b>[<?php echo esc_html($this->_shortcode_eventviews); ?>]</b>
925 <p><?php esc_html_e('The event calendar form will be displayed. You can add the following parameters to change the output:', 'event-tickets-with-ticket-scanner'); ?></p>
926 <ul>
927 <li>months_to_show - Values can be a number higher than 0. Default: 3</li>
928 </ul>
929 <p>CSS file: <a href="<?php echo plugins_url( "",__FILE__ ); ?>/css/calendar.css" target="_blank">calendar.css</a></p>
930
931 <h3><?php esc_html_e('Shortcode to display the assigned tickets and codes of an user within a page', 'event-tickets-with-ticket-scanner'); ?></h3>
932 <b>[<?php echo esc_html($this->_shortcode_mycode); ?>]</b>
933 <p><?php esc_html_e('Displays tickets assigned to the current logged-in user (default) or tickets from a specific order.', 'event-tickets-with-ticket-scanner'); ?></p>
934 <ul>
935 <li><b>order_id</b> - <?php esc_html_e('Show tickets from a specific order instead of user tickets. Security: User must own the order or have valid order key in URL.', 'event-tickets-with-ticket-scanner'); ?><br>
936 <?php esc_html_e('Example:', 'event-tickets-with-ticket-scanner'); ?> [<?php echo esc_html($this->_shortcode_mycode); ?> order_id="123"]</li>
937 <li><b>format</b> - <?php esc_html_e('Output format. Values: json', 'event-tickets-with-ticket-scanner'); ?></li>
938 <li><b>display</b> - <?php esc_html_e('Fields to show (comma-separated). Values: codes, validation, user, used, confirmedCount, woocommerce, wc_rp, wc_ticket', 'event-tickets-with-ticket-scanner'); ?></li>
939 <li><b>download_all_pdf</b> - <?php esc_html_e('Show download button for all tickets as one PDF. Values: true/false', 'event-tickets-with-ticket-scanner'); ?></li>
940 </ul>
941 <p>
942 <?php esc_html_e('Example with JSON output:', 'event-tickets-with-ticket-scanner'); ?> [<?php echo esc_html($this->_shortcode_mycode); ?> format="json" display="code,wc_ticket"]
943 </p>
944
945 <h3><?php esc_html_e('Shortcode to display the ticket scanner within a page', 'event-tickets-with-ticket-scanner'); ?></h3>
946 <?php esc_html_e('Useful if you cannot open the ticket scanner due to security issues.', 'event-tickets-with-ticket-scanner'); ?><br>
947 <b>[<?php echo esc_html($this->_shortcode_ticket_scanner); ?>]</b>
948
949 <h3><?php esc_html_e('Shortcode to display ticket detail view within a page', 'event-tickets-with-ticket-scanner'); ?></h3>
950 <?php esc_html_e('Useful if the /ticket/ URL path does not work on your server.', 'event-tickets-with-ticket-scanner'); ?><br>
951 <b>[<?php echo esc_html($this->_shortcode_ticket_detail); ?>]</b>
952 <p>
953 <?php esc_html_e('Usage: Add the shortcode to a page and access it with ?ticket=YOUR-TICKET-CODE in the URL.', 'event-tickets-with-ticket-scanner'); ?><br>
954 <?php esc_html_e('Example:', 'event-tickets-with-ticket-scanner'); ?> yoursite.com/ticket-page/?ticket=ABC-123-XYZ<br>
955 <?php esc_html_e('Or use the code attribute:', 'event-tickets-with-ticket-scanner'); ?> [<?php echo esc_html($this->_shortcode_ticket_detail); ?> code="ABC-123-XYZ"]
956 </p>
957
958 <h3><?php esc_html_e('PHP Filters', 'event-tickets-with-ticket-scanner'); ?></h3>
959 <p><?php esc_html_e('You can use PHP code to register your filter functions for the validation check.', 'event-tickets-with-ticket-scanner'); ?>
960 <a href="https://vollstart.com/event-tickets-with-ticket-scanner/docs/#filters" target="_blank"><?php esc_html_e('Click here for more help about the functions', 'event-tickets-with-ticket-scanner'); ?></a>
961 </p>
962 <ul>
963 <li>add_filter('<?php echo $this->_add_filter_prefix.'beforeCheckCodePre'; ?>', 'myfunc', 20, 1)</li>
964 <li>add_filter('<?php echo $this->_add_filter_prefix.'beforeCheckCode'; ?>', 'myfunc', 20, 1)</li>
965 <li>add_filter('<?php echo $this->_add_filter_prefix.'afterCheckCodePre'; ?>', 'myfunc', 20, 1)</li>
966 <li>add_filter('<?php echo $this->_add_filter_prefix.'afterCheckCode'; ?>', 'myfunc', 20, 1)</li>
967 </ul>
968 <p>More BETA filters and actions hooks can be found <a href="https://vollstart.com/event-tickets-with-ticket-scanner/docs/ticket-plugin-api/" target="_blank">here (NOT STABLE, be aware that they might be changed in the future)</a>.</p>
969 </div>
970
971 <div class="et-footer-credits">
972 <a target="_blank" href="https://vollstart.com">VOLLSTART</a> &middot; More plugins: <a target="_blank" href="https://wordpress.org/plugins/serial-codes-generator-and-validator/">Serial Code Validator</a>
973 </div>
974 </div>
975 </div>
976 <?php
977 do_action( $this->_do_action_prefix.'options_page' );
978 }
979
980 public function isUserAllowedToAccessAdminArea() {
981 if ($this->isAllowedAccess != null) return $this->isAllowedAccess;
982 if ($this->getOptions()->isOptionCheckboxActive('allowOnlySepcificRoleAccessToAdmin')) {
983 // check welche rollen
984 $user = wp_get_current_user();
985 $user_roles = (array) $user->roles;
986 if (in_array("administrator", $user_roles)) {
987 $this->isAllowedAccess = true;
988 } else {
989 $adminAreaAllowedRoles = $this->getOptions()->getOptionValue('adminAreaAllowedRoles');
990 if (!is_array($adminAreaAllowedRoles)) {
991 if (empty($adminAreaAllowedRoles)) {
992 $adminAreaAllowedRoles = [];
993 } else {
994 $adminAreaAllowedRoles = [$adminAreaAllowedRoles];
995 }
996 }
997 foreach($adminAreaAllowedRoles as $role_name) {
998 if (in_array($role_name, $user_roles)) {
999 $this->isAllowedAccess = true;
1000 break;
1001 };
1002 }
1003 }
1004 } else {
1005 // Standard: Only administrators have access
1006 $this->isAllowedAccess = current_user_can('manage_options');
1007 }
1008 $this->isAllowedAccess = apply_filters( $this->_add_filter_prefix.'main_isUserAllowedToAccessAdminArea', $this->isAllowedAccess );
1009 return $this->isAllowedAccess;
1010 }
1011
1012 public function executeAdminSettings_a() {
1013 if (!SASO_EVENTTICKETS::issetRPara('a_sngmbh')) return wp_send_json_success("a_sngmbh not provided");
1014 return $this->executeAdminSettings(SASO_EVENTTICKETS::getRequestPara('a_sngmbh')); // to prevent WP adds parameters
1015 }
1016
1017 public function executeAdminSettings($a=0, $data=null) {
1018 if (!$this->isUserAllowedToAccessAdminArea()) {
1019 return wp_send_json_error("Access denied", 403);
1020 }
1021 if ($a === 0 && !SASO_EVENTTICKETS::issetRPara('a_sngmbh')) return wp_send_json_success("a not provided");
1022
1023 if ($data == null) {
1024 $data = SASO_EVENTTICKETS::issetRPara('data') ? SASO_EVENTTICKETS::getRequestPara('data') : [];
1025 }
1026 if ($a === 0 || empty($a) || trim($a) == "") {
1027 $a = SASO_EVENTTICKETS::getRequestPara('a_sngmbh');
1028 }
1029 do_action( $this->_do_action_prefix.'executeAdminSettings', $a, $data );
1030 return $this->getAdmin()->executeJSON($a, $data, false, false); // with nonce check
1031 }
1032
1033 public function executeSeatingAdmin_a() {
1034 return $this->executeSeatingAdmin(SASO_EVENTTICKETS::getRequestPara('a'));
1035 }
1036
1037 public function executeSeatingAdmin($a = '', $data = null) {
1038 if (!$this->isUserAllowedToAccessAdminArea()) {
1039 return wp_send_json_error('Access denied', 403);
1040 }
1041 if (empty($a) && !SASO_EVENTTICKETS::issetRPara('a')) {
1042 return wp_send_json_error('a not provided');
1043 }
1044 if ($data === null) {
1045 $data = SASO_EVENTTICKETS::getRequest();
1046 }
1047 if (empty($a)) {
1048 $a = SASO_EVENTTICKETS::getRequestPara('a');
1049 }
1050 return $this->getSeating()->getAdminHandler()->executeSeatingJSON($a, $data);
1051 }
1052
1053 public function executeFrontend_a() {
1054 return $this->executeFrontend(); // to prevent WP adds parameters
1055 }
1056
1057 public function executeWCBackend() {
1058 if (!$this->isUserAllowedToAccessAdminArea()) {
1059 return wp_send_json_error("Access denied", 403);
1060 }
1061 if (!SASO_EVENTTICKETS::issetRPara('a_sngmbh')) return wp_send_json_success("a_sngmbh not provided");
1062 $data = SASO_EVENTTICKETS::issetRPara('data') ? SASO_EVENTTICKETS::getRequestPara('data') : [];
1063 return $this->getWC()->executeJSON(SASO_EVENTTICKETS::getRequestPara('a_sngmbh'), $data);
1064 }
1065
1066 public function executeFrontend($a=0, $data=null) {
1067 $sasoEventtickets_Frontend = $this->getFrontend();
1068 if ($a === 0 && !SASO_EVENTTICKETS::issetRPara('a_sngmbh')) return wp_send_json_success("a not provided");
1069
1070 if ($data == null) {
1071 $data = SASO_EVENTTICKETS::issetRPara('data') ? SASO_EVENTTICKETS::getRequestPara('data') : [];
1072 }
1073 if ($a === 0 || empty($a) || trim($a) == "") {
1074 $a = SASO_EVENTTICKETS::getRequestPara('a_sngmbh');
1075 }
1076 do_action( $this->_do_action_prefix.'executeFrontend', $a, $data );
1077 return $sasoEventtickets_Frontend->executeJSON($a, $data);
1078 }
1079
1080 public function replacingShortcode($attr=[], $content = null, $tag = '') {
1081 add_filter( $this->_add_filter_prefix.'replaceShortcode', [$this, 'replaceShortcode'], 10, 3 );
1082 $ret = apply_filters( $this->_add_filter_prefix.'replaceShortcode', $attr, $content, $tag );
1083 return $ret;
1084 }
1085
1086 public function setTicketScannerJS() {
1087 wp_enqueue_style("wp-jquery-ui-dialog");
1088
1089 $js_url = "jquery.qrcode.min.js?_v=".$this->getPluginVersion();
1090 wp_enqueue_script(
1091 'ajax_script2',
1092 plugins_url( "3rd/".$js_url,__FILE__ ),
1093 array('jquery', 'jquery-ui-dialog')
1094 );
1095
1096 $js_url = plugin_dir_url(__FILE__)."3rd/html5-qrcode.min.js?_v=".$this->getPluginVersion();
1097 wp_register_script('html5-qrcode', $js_url, array('jquery', 'jquery-ui-dialog'));
1098 wp_enqueue_script('html5-qrcode');
1099
1100 // https://github.com/nimiq/qr-scanner - NEW scanner lib
1101 $js_url = plugin_dir_url(__FILE__)."3rd/qr-scanner-1.4.2/qr-scanner.umd.min.js?_v=".$this->getPluginVersion();
1102 wp_register_script('qr-scanner', $js_url, array('jquery', 'jquery-ui-dialog'));
1103 wp_enqueue_script('qr-scanner');
1104
1105 $js_url = "ticket_scanner.js?_v=".$this->getPluginVersion();
1106 if (defined('WP_DEBUG')) $js_url .= '&t='.time();
1107 $js_url = plugins_url( $js_url,__FILE__ );
1108 wp_register_script('ajax_script_ticket_scanner', $js_url, array('jquery', 'jquery-ui-dialog', 'wp-i18n'));
1109 wp_enqueue_script('ajax_script_ticket_scanner');
1110 wp_set_script_translations('ajax_script_ticket_scanner', 'event-tickets-with-ticket-scanner', __DIR__.'/languages');
1111
1112 $ticketScannerDontRememberCamChoice = $this->getOptions()->isOptionCheckboxActive("ticketScannerDontRememberCamChoice") ? true : false;
1113
1114 $pwaEnabled = $this->getOptions()->isOptionCheckboxActive('ticketScannerPWA');
1115 $vars = [
1116 'root' => esc_url_raw( rest_url() ),
1117 '_plugin_home_url' =>plugins_url( "",__FILE__ ),
1118 '_action' => $this->_prefix.'_executeAdminSettings',
1119 '_isPremium'=>$this->isPremium(),
1120 '_isUserLoggedin'=>is_user_logged_in(),
1121 '_userId'=>get_current_user_id(),
1122 '_restPrefixUrl'=>SASO_EVENTTICKETS::getRESTPrefixURL(),
1123 '_siteUrl'=>get_site_url(),
1124 '_params'=>["auth"=>$this->getAuthtokenHandler()::$authtoken_param],
1125 '_pwaSWUrl'=>$pwaEnabled ? rest_url(SASO_EVENTTICKETS::getRESTPrefixURL().'/ticket/scanner/pwa-sw') : '',
1126 //'url' => admin_url( 'admin-ajax.php' ), // not used for now in ticketscanner.js
1127 'url' => rest_get_server(), // not used for now in ticketscanner.js
1128 'nonce' => wp_create_nonce( 'wp_rest' ),
1129 //'nonce' => wp_create_nonce( $this->_js_nonce ),
1130 'ajaxActionPrefix' => $this->_prefix,
1131 'wcTicketCompatibilityModeRestURL' => $this->getOptions()->getOptionValue('wcTicketCompatibilityModeRestURL', ''),
1132 'IS_PRETTY_PERMALINK_ACTIVATED' => get_option('permalink_structure') ? true :false,
1133 'ticketScannerDontRememberCamChoice' => $ticketScannerDontRememberCamChoice,
1134 'ticketScannerStartCamWithoutButtonClicked' => $this->getOptions()->isOptionCheckboxActive('ticketScannerStartCamWithoutButtonClicked'),
1135 'ticketScannerDontShowOptionControls' => $this->getOptions()->isOptionCheckboxActive('ticketScannerDontShowOptionControls'),
1136 'ticketScannerScanAndRedeemImmediately' => $this->getOptions()->isOptionCheckboxActive('ticketScannerScanAndRedeemImmediately'),
1137 'ticketScannerHideTicketInformation' => $this->getOptions()->isOptionCheckboxActive('ticketScannerHideTicketInformation'),
1138 'ticketScannerHideTicketInformationShowShortDesc' => $this->getOptions()->isOptionCheckboxActive('ticketScannerHideTicketInformationShowShortDesc'),
1139 'ticketScannerDontShowBtnPDF' => $this->getOptions()->isOptionCheckboxActive('ticketScannerDontShowBtnPDF'),
1140 'ticketScannerDontShowBtnBadge' => $this->getOptions()->isOptionCheckboxActive('ticketScannerDontShowBtnBadge'),
1141 'ticketScannerDisplayTimes' => $this->getOptions()->isOptionCheckboxActive('ticketScannerDisplayTimes'),
1142 'ticketScannerThemeColor' => $this->getOptions()->getOptionValue('ticketScannerThemeColor', '#2e74b5'),
1143 'ticketScannerVibrate' => $this->getOptions()->isOptionCheckboxActive('ticketScannerVibrate')
1144 ];
1145 $vars = apply_filters( $this->_add_filter_prefix.'main_setTicketScannerJS', $vars );
1146 wp_localize_script(
1147 'ajax_script_ticket_scanner',
1148 'Ajax_'.$this->_prefix, // name der injected variable
1149 $vars
1150 );
1151
1152 do_action( $this->_do_action_prefix.'main_setTicketScannerJS', $js_url );
1153 }
1154
1155 public function replacingShortcodeTicketScanner($attr=[], $content = null, $tag = '') {
1156 $this->setTicketScannerJS();
1157 return '
1158 <center>
1159 <div style="width:90%;max-width:1024px;">'.$this->getTicketHandler()->getTicketScannerHTMLBoilerplate().'
1160 </div>
1161 </center>
1162 ';
1163 }
1164
1165 /**
1166 * Shortcode to display ticket detail view on any page
1167 * Usage: [sasoEventTicketsValidator_ticket_detail] with ?ticket=CODE in URL
1168 * Or: [sasoEventTicketsValidator_ticket_detail code="TICKET-CODE"]
1169 */
1170 public function replacingShortcodeTicketDetail($attr = [], $content = null, $tag = ''): string {
1171 $code = '';
1172 if (!empty($attr['code'])) {
1173 $code = sanitize_text_field($attr['code']);
1174 } elseif (isset($_GET['ticket'])) {
1175 $code = sanitize_text_field($_GET['ticket']);
1176 }
1177
1178 if (empty($code)) {
1179 return '<p>' . esc_html__('No ticket code provided. Use ?ticket=YOUR-CODE in the URL.', 'event-tickets-with-ticket-scanner') . '</p>';
1180 }
1181
1182 // Build a fake request URI for the ticket
1183 $ticketPath = $this->getCore()->getTicketURLPath(true);
1184 $fakeUri = $ticketPath . $code;
1185
1186 include_once plugin_dir_path(__FILE__) . "sasoEventtickets_Ticket.php";
1187 $ticketInstance = sasoEventtickets_Ticket::Instance($fakeUri);
1188
1189 return $ticketInstance->renderTicketDetailForShortcode();
1190 }
1191
1192 public function getCodesTextAsShortList($codes) {
1193 $ret = "";
1194 if (count($codes) > 0) {
1195 $ret = '<table>';
1196 $wcTicketUserProfileDisplayTicketDetailURL = $this->getOptions()->isOptionCheckboxActive("wcTicketUserProfileDisplayTicketDetailURL");
1197 $wcTicketUserProfileDisplayRedeemAmount = $this->getOptions()->isOptionCheckboxActive("wcTicketUserProfileDisplayRedeemAmount");
1198
1199 $label_expired = $this->getOptions()->getOptionValue('wcTicketTransTicketExpired', 'EXPIRED');
1200 $label_stolen = $this->getOptions()->getOptionValue('wcTicketTransTicketIsStolen', 'REPORTED AS STOLEN');
1201 $label_notvalid = $this->getOptions()->getOptionValue('wcTicketTransTicketNotValid', 'DISABLED');
1202
1203 $myCodes = [];
1204 foreach($codes as $idx => $codeObj) {
1205 $metaObj = $this->getCore()->encodeMetaValuesAndFillObject($codeObj['meta'], $codeObj);
1206
1207 $_c = '<tr><td style="text-align:right;">'.($idx + 1).'.</td><td>'.$codeObj['code_display'].'</td><td>';
1208 if ($codeObj['aktiv'] == 1) {
1209 if ($this->getCore()->checkCodeExpired($codeObj)) {
1210 $_c .= $label_expired;
1211 }
1212 } else if ($codeObj['aktiv'] == 0) {
1213 $_c .= $label_notvalid;
1214 } else if ($codeObj['aktiv'] == 2) {
1215 $_c .= $label_stolen;
1216 }
1217 $_c .= '</td>';
1218
1219 if ($wcTicketUserProfileDisplayTicketDetailURL) {
1220 $_c .= "<td>";
1221 $url = $this->getCore()->getTicketURL($codeObj, $metaObj);
1222 if (!empty($url)) {
1223 $_c .= '<a href="'.$url.'" target="_blank">Ticket Details</a>';
1224 }
1225 $_c .= "</td>";
1226 }
1227 if ($wcTicketUserProfileDisplayRedeemAmount && function_exists("wc_get_product")) {
1228 $_c .= "<td>";
1229 $text_redeem_amount = $this->getTicketHandler()->getRedeemAmountText($codeObj, $metaObj, false);
1230 if (!empty($text_redeem_amount)) {
1231 $_c .= $text_redeem_amount;
1232 }
1233 $_c .= "<td>";
1234 }
1235 $_c .= "</tr>";
1236 $myCodes[] = $_c;
1237 }
1238 $ret .= implode("", $myCodes);
1239 $ret .= "</table>";
1240 }
1241 $ret = apply_filters( $this->_add_filter_prefix.'main_getCodesTextAsShortList', $ret, $codes );
1242 return $ret;
1243 }
1244
1245 public function getMyCodeText($user_id, $attr=[], $content = null, $tag = '', $codes = null) {
1246 $ret = '';
1247 // check ob eingeloggt
1248 $pre_text = $this->getOptions()->getOptionValue('userDisplayCodePrefix', '');
1249 if (!empty($pre_text)) $pre_text .= " ";
1250
1251 // If codes are provided (e.g., from order_id), use them; otherwise fetch by user_id
1252 if ($codes === null && $user_id > 0) {
1253 $codes = $this->getCore()->getCodesByRegUserId($user_id);
1254 }
1255
1256 if ($codes !== null && count($codes) > 0) {
1257 $ret .= "<b>".$pre_text."</b><br>";
1258 $ret .= $this->getCodesTextAsShortList($codes);
1259
1260 // Download All as PDF button
1261 $show_download_btn = isset($attr['download_all_pdf']) &&
1262 in_array(strtolower($attr['download_all_pdf']), ['true', '1', 'yes'], true);
1263
1264 if ($show_download_btn && count($codes) > 0) {
1265 $max_tickets = isset($attr['download_all_pdf_max']) ? intval($attr['download_all_pdf_max']) : 100;
1266 $btn_label = isset($attr['download_all_pdf_label']) ?
1267 sanitize_text_field($attr['download_all_pdf_label']) :
1268 __('Download All Tickets as PDF', 'event-tickets-with-ticket-scanner');
1269
1270 $ticket_count = count($codes);
1271 if ($ticket_count > $max_tickets) {
1272 $ret .= '<p><em>' . sprintf(
1273 /* translators: %d: maximum number of tickets */
1274 esc_html__('Too many tickets (%1$d). Maximum %2$d tickets can be downloaded at once.', 'event-tickets-with-ticket-scanner'),
1275 $ticket_count,
1276 $max_tickets
1277 ) . '</em></p>';
1278 } else {
1279 // Generate secure download URL
1280 $nonce = wp_create_nonce('download_my_codes_pdf_' . $user_id);
1281 $download_url = admin_url('admin-ajax.php') . '?' . http_build_query([
1282 'action' => $this->_prefix . '_downloadMyCodesAsPDF',
1283 'nonce' => $nonce
1284 ]);
1285 $ret .= '<p style="margin-top:10px;"><a href="' . esc_url($download_url) . '" class="button" target="_blank">' .
1286 esc_html($btn_label) . ' (' . $ticket_count . ')</a></p>';
1287 }
1288 }
1289 }
1290 if (empty($ret) && $this->getOptions()->isOptionCheckboxActive('userDisplayCodePrefixAlways')) {
1291 $ret .= $pre_text;
1292 }
1293 $ret = apply_filters( $this->_add_filter_prefix.'main_getMyCodeText', $ret, $user_id, $attr, $content, $tag);
1294 return $ret;
1295 }
1296
1297 /**
1298 * AJAX handler: Download all user's tickets as one PDF
1299 * Used by shortcode [sasoEventTicketsValidator_code download_all_pdf="true"]
1300 */
1301 public function downloadMyCodesAsPDF(): void {
1302 $user_id = get_current_user_id();
1303
1304 // Must be logged in
1305 if ($user_id <= 0) {
1306 wp_die(esc_html__('You must be logged in to download tickets.', 'event-tickets-with-ticket-scanner'), 403);
1307 }
1308
1309 // Verify nonce
1310 $nonce = isset($_GET['nonce']) ? sanitize_text_field($_GET['nonce']) : '';
1311 if (!wp_verify_nonce($nonce, 'download_my_codes_pdf_' . $user_id)) {
1312 wp_die(esc_html__('Security check failed. Please refresh the page and try again.', 'event-tickets-with-ticket-scanner'), 403);
1313 }
1314
1315 // Get user's codes
1316 $codes = $this->getCore()->getCodesByRegUserId($user_id);
1317
1318 if (empty($codes)) {
1319 wp_die(esc_html__('No tickets found.', 'event-tickets-with-ticket-scanner'), 404);
1320 }
1321
1322 // Limit to 100 tickets
1323 $max_tickets = 100;
1324 if (count($codes) > $max_tickets) {
1325 wp_die(
1326 sprintf(
1327 /* translators: %d: maximum number of tickets */
1328 esc_html__('Too many tickets. Maximum %d tickets can be downloaded at once.', 'event-tickets-with-ticket-scanner'),
1329 $max_tickets
1330 ),
1331 400
1332 );
1333 }
1334
1335 // Extract code strings
1336 $code_strings = array_map(function($codeObj) {
1337 return $codeObj['code'];
1338 }, $codes);
1339
1340 // Generate merged PDF
1341 $filename = 'my_tickets_' . wp_date('Ymd_Hi') . '.pdf';
1342
1343 try {
1344 $this->getTicketHandler()->generateOnePDFForCodes($code_strings, $filename, 'I');
1345 } catch (Exception $e) {
1346 $this->getAdmin()->logErrorToDB($e, null, 'downloadMyCodesAsPDF');
1347 wp_die(esc_html__('Error generating PDF. Please try again later.', 'event-tickets-with-ticket-scanner'), 500);
1348 }
1349
1350 exit;
1351 }
1352
1353 public function getMyCodeFormatted($user_id, $attr=[], $content = null, $tag = '', $codes = null) {
1354 $format = "json";
1355 if (isset($attr["format"])) {
1356 $format = strtolower(trim(sanitize_key($attr["format"])));
1357 }
1358 $display = ["codes"];
1359 if (isset($attr["display"])) {
1360
1361 $_d = trim(sanitize_text_field($attr["display"]));
1362 $_da = explode(",", $_d);
1363 if (count($_da) > 0) {
1364 $display = [];
1365 }
1366 foreach ($_da as $item) {
1367 $item = trim($item);
1368 $display[] = $item;
1369 }
1370 }
1371
1372 $output = [];
1373 //codes,validation,user,used,confirmedCount,woocommerce,wc_rp,wc_ticket
1374 // If codes are provided (e.g., from order_id), use them; otherwise fetch by user_id
1375 if ($codes === null) {
1376 $codes = $this->getCore()->getCodesByRegUserId($user_id);
1377 }
1378 $metas = [];
1379 foreach($codes as $codeObj) {
1380 $metas[$codeObj["code"]] = $this->getCore()->encodeMetaValuesAndFillObject($codeObj['meta'], $codeObj);
1381 }
1382 foreach ($display as $item) {
1383 $output[$item] = [];
1384 if ($item == "codes") {
1385 foreach($codes as $codeObj) {
1386 if (isset($codeObj["meta"])) {
1387 unset($codeObj["meta"]);
1388 }
1389 $output[$item][] = $codeObj;
1390 }
1391 } elseif($item == "confirmedCount") {
1392 foreach($metas as $key => $meta) {
1393 $output[$item][] = array_merge(["value"=>$meta[$item]], ["code"=>$key]);
1394 }
1395 } else {
1396 foreach($metas as $key => $m) {
1397 if (is_array($m) && isset($m[$item])) {
1398 $meta = $m[$item];
1399 if (isset($meta["stats_redeemed"])) {
1400 unset($meta["stats_redeemed"]);
1401 }
1402 if (isset($meta["set_by_admin"])) {
1403 unset($meta["set_by_admin"]);
1404 }
1405 if (isset($meta["redeemed_by_admin"])) {
1406 unset($meta["redeemed_by_admin"]);
1407 }
1408 $output[$item][] = array_merge($meta, ["code"=>$key]);
1409 }
1410 }
1411 }
1412 }
1413
1414 switch($format) {
1415 case "json":
1416 default:
1417 $ret = $this->getCore()->json_encode_with_error_handling($output);
1418 }
1419 $ret = apply_filters( $this->_add_filter_prefix.'main_getMyCodeFormatted', $ret, $user_id, $attr, $content, $tag, $output);
1420 return $ret;
1421 }
1422
1423 public function replacingShortcodeMyCode($attr=[], $content = null, $tag = '') {
1424 $user_id = get_current_user_id();
1425 $codes = null; // Will be set if order_id is used
1426
1427 // Check if order_id parameter is provided
1428 if (isset($attr['order_id']) && !empty($attr['order_id'])) {
1429 $order_id = intval($attr['order_id']);
1430 if ($order_id > 0) {
1431 // Security check: can current user access this order?
1432 if (!$this->canUserAccessOrder($order_id)) {
1433 return '<p>' . esc_html__('You do not have permission to view tickets for this order.', 'event-tickets-with-ticket-scanner') . '</p>';
1434 }
1435 // Get codes by order_id
1436 $codes = $this->getCore()->getCodesByOrderId($order_id);
1437 }
1438 }
1439
1440 if (count($attr) > 0 && isset($attr["format"])) {
1441 return $this->getMyCodeFormatted($user_id, $attr, $content, $tag, $codes);
1442 } else {
1443 return $this->getMyCodeText($user_id, $attr, $content, $tag, $codes);
1444 }
1445 }
1446
1447 /**
1448 * Check if current user can access a specific order's tickets
1449 *
1450 * @param int $order_id WooCommerce order ID
1451 * @return bool True if user can access, false otherwise
1452 */
1453 private function canUserAccessOrder(int $order_id): bool {
1454 $order = wc_get_order($order_id);
1455 if (!$order) {
1456 return false;
1457 }
1458
1459 // 1. Admin/Shop-Manager can access all orders
1460 if (current_user_can('manage_woocommerce')) {
1461 return true;
1462 }
1463
1464 $current_user_id = get_current_user_id();
1465
1466 // 2. Logged-in user owns this order
1467 if ($current_user_id > 0 && $order->get_user_id() == $current_user_id) {
1468 return true;
1469 }
1470
1471 // 3. Valid order key in URL (WooCommerce thank-you page pattern)
1472 $order_key_from_url = isset($_GET['key']) ? sanitize_text_field($_GET['key']) : '';
1473 if (!empty($order_key_from_url) && $order->get_order_key() === $order_key_from_url) {
1474 return true;
1475 }
1476
1477 return false;
1478 }
1479
1480 public function replacingShortcodeFeatureList($attr=[], $content = null, $tag = '') {
1481 //$features = $this->getAdmin()->getOptions();
1482 //$options = $features["options"];
1483 $options = $this->getOptions()->getOptions();
1484 $features = [];
1485 $act_heading = "";
1486 foreach ($options as $option) {
1487 if ($option["key"] == "serial") continue;
1488 if ($option["type"] == "heading") {
1489 $act_heading = $option["key"];
1490 $features[$act_heading] = ["heading"=>$option, "options"=>[]];
1491 } else {
1492 if ($act_heading != "") {
1493 $features[$act_heading]["options"][] = $option;
1494 }
1495 }
1496 }
1497
1498 $ret = "";
1499 uasort($features, function($a, $b) {
1500 return strnatcasecmp($a["heading"]["label"], $b["heading"]["label"]);
1501 });
1502 foreach ($features as $key => $feature) {
1503 $ret .= '<h3>'.$feature["heading"]["label"].'</h3>';
1504 $video = $feature["heading"]["_doc_video"] != "" ? ' <a href="'.$feature["heading"]["_doc_video"].'" target="_blank"><span class="dashicons dashicons-video-alt3"></span> Introduction video</a>' : "";
1505 $ret .= '<p>'.trim($feature["heading"]["desc"].$video).'</p>';
1506 if (count($feature["options"]) > 0) {
1507 $ret .= '<ul>';
1508 foreach ($feature["options"] as $option) {
1509 $label = $option["label"];
1510 $desc = $option["desc"];
1511 $desc .= $option["_doc_video"] != "" ? ' <a href="'.$option["_doc_video"].'" target="_blank"><span class="dashicons dashicons-video-alt3"></span> Introduction video</a>' : "";
1512 $desc = trim($desc);
1513 if ($desc != "") {
1514 $desc = '<p>'.$desc.'</p>';
1515 }
1516 $label = '<span class="dashicons dashicons-yes"></span> '.$label;
1517 $ret .= '<li>'.$label.$desc.'</li>';
1518 }
1519 $ret .= '</ul>';
1520 }
1521 $ret .= '<hr>';
1522 }
1523
1524 return $ret;
1525 }
1526
1527 public function replacingShortcodeEventViews($attr=[], $content = null, $tag = '') {
1528 // iterate over all woocommerce products and check if they are an event
1529 $view = "calendar|list";
1530 if (isset($attr["view"])) {
1531 $view = strtolower(trim(sanitize_key($attr["view"])));
1532 }
1533 $months_to_show = 3;
1534 if (isset($attr["months_to_show"])) {
1535 $m = intval($attr["months_to_show"]);
1536 if ($m > 0) $months_to_show = $m;
1537 }
1538
1539 $month_start = strtotime(wp_date("Y-m-1 00:00:00"));
1540 //$month_end = strtotime(wp_date("Y-m-t 23:59:59"));
1541 $month_end = strtotime(wp_date("Y-m-1 23:59:59", strtotime("+".$months_to_show." month", $month_start)));
1542
1543 $products_args = array(
1544 'post_type' => 'product',
1545 'post_status' => 'publish',
1546 'posts_per_page' => -1, // -1 zeigt alle Produkte an
1547 'meta_query' => array(
1548 [
1549 'key' => 'saso_eventtickets_is_ticket',
1550 'value' => 'yes',
1551 'compare' => '='
1552 ]
1553 )
1554 );
1555 $posts = get_posts($products_args); // get only ticket products
1556
1557 $list_infos = [
1558 'months_to_show'=>$months_to_show,
1559 'month_start'=>$month_start,
1560 'month_end'=>$month_end,
1561 'view'=>$view
1562 ];
1563 $products_to_show = [];
1564 // Ergebnisse überprüfen
1565 foreach ($posts as $post) {
1566 $product_ids = [];
1567 $product = wc_get_product( $post->ID );
1568
1569 // check if event is variant product
1570 $is_variable = $product->get_type() == "variable";
1571 if ($is_variable) {
1572 // check if event dates are the same for all variants
1573 $date_is_for_all_variants = get_post_meta( $product_id, 'saso_eventtickets_is_date_for_all_variants', true ) == "yes" ? true : false;
1574 if ($date_is_for_all_variants == false) {
1575 // if not add also the variants
1576 $childs = $product->get_children();
1577 foreach ($childs as $child_id) {
1578 if (get_post_meta($child_id, '_saso_eventtickets_is_not_ticket', true) == "yes") {
1579 continue;
1580 }
1581 $product_ids[] = $child_id;
1582 }
1583 }
1584 } else {
1585 $product_ids[] = $product->get_id();
1586 }
1587
1588 foreach ($product_ids as $product_id) {
1589 $product = wc_get_product( $product_id );
1590 $dates = $this->getTicketHandler()->calcDateStringAllowedRedeemFrom($product_id);
1591 //if ($dates['ticket_end_date_timestamp'] > $month_start && $dates['ticket_start_date_timestamp'] < $month_end) {
1592 if ($dates['ticket_end_date_timestamp'] >= $month_start || ($dates['ticket_start_date_timestamp'] >= $month_start && $dates['ticket_start_date_timestamp'] <= $month_end)) {
1593 // set product to hidden
1594 $product_data = array(
1595 'ID' => $product_id,
1596 'dates' => $dates,
1597 'event'=> [
1598 'location' => trim(get_post_meta( $product_id, 'saso_eventtickets_event_location', true )),
1599 'location_label' => wp_kses_post(trim($this->getAdmin()->getOptionValue("wcTicketTransLocation")))
1600 ],
1601 'product' => [
1602 'title' => $product->get_name(),
1603 'url' => get_permalink($product_id),
1604 'price' =>$product->get_price(),
1605 'price_html' => $product->get_price_html(),
1606 'type' => $product->get_type(),
1607 ]
1608 );
1609 $products_to_show[] = $product_data;
1610 }
1611 }
1612 }
1613
1614 $divId = "sasoEventTicketsValidator_eventsview";
1615
1616 // add js for the events
1617 wp_enqueue_style("wp-jquery-ui-dialog");
1618
1619 $js_url = "ticket_events.js?_v=".$this->getPluginVersion();
1620 if (defined('WP_DEBUG')) $js_url .= '&t='.time();
1621 $js_url = plugins_url( $js_url,__FILE__ );
1622 wp_register_script('ajax_script_ticket_events', $js_url, array('jquery', 'jquery-ui-dialog', 'wp-i18n'));
1623 wp_enqueue_script('ajax_script_ticket_events');
1624 wp_set_script_translations('ajax_script_ticket_events', 'event-tickets-with-ticket-scanner', __DIR__.'/languages');
1625 wp_enqueue_style("ticket_events_css", plugins_url( "",__FILE__ ).'/css/calendar.css', [], true);
1626
1627 // add all events as an array for max 3 months??? or config parameter
1628 $vars = [
1629 'root' => esc_url_raw( rest_url() ),
1630 '_plugin_home_url' =>plugins_url( "",__FILE__ ),
1631 '_action' => $this->_prefix.'_executeFrontendEvents',
1632 '_isPremium'=>$this->isPremium(),
1633 '_isUserLoggedin'=>is_user_logged_in(),
1634 '_userId'=>get_current_user_id(),
1635 '_premJS'=>$this->isPremium() && method_exists($this->getPremiumFunctions(), "getJSFrontEventFile") ? $this->getPremiumFunctions()->getJSFrontEventFile() : '',
1636 '_siteUrl'=>get_site_url(),
1637 'events' => $products_to_show,
1638 'list_infos' => $list_infos,
1639 'format_date' => $this->getOptions()->getOptionDateFormat(),
1640 'format_time' => $this->getOptions()->getOptionTimeFormat(),
1641 'format_datetime' => $this->getOptions()->getOptionDateTimeFormat(),
1642 'url' => admin_url( 'admin-ajax.php' ),
1643 'nonce' => wp_create_nonce( $this->_js_nonce ),
1644 'ajaxActionPrefix' => $this->_prefix,
1645 'divId' => $divId
1646 ];
1647 $vars = apply_filters( $this->_add_filter_prefix.'main_setTicketEventJS', $vars );
1648 wp_localize_script(
1649 'ajax_script_ticket_events',
1650 'Ajax_ticket_events_'.$this->_prefix, // name der injected variable
1651 $vars
1652 );
1653
1654 do_action( $this->_do_action_prefix.'main_setTicketEventJS', $js_url );
1655
1656 $ret = '';
1657 if (!isset($attr['divid']) || trim($attr['divid']) == "") {
1658 $ret .= '<div id="'.$divId.'">'.__('...loading...', 'event-tickets-with-ticket-scanner').'</div>';
1659 }
1660
1661 $ret = apply_filters( $this->_add_filter_prefix.'main_replacingShortcodeEventViews', $ret );
1662 do_action( $this->_do_action_prefix.'main_replacingShortcodeEventViews', $vars, $ret );
1663
1664 return $ret;
1665 }
1666
1667 public function replaceShortcode($attr=[], $content = null, $tag = '') {
1668 // einbinden das js starter skript
1669 $js_url = $this->_js_file."?_v=".$this->_js_version;
1670 if (defined( 'WP_DEBUG')) $js_url .= '&debug=1';
1671 $userDivId = !isset($attr['divid']) || trim($attr['divid']) == "" ? '' : trim($attr['divid']);
1672
1673 $attr = array_change_key_case( (array) $attr, CASE_LOWER );
1674
1675 wp_enqueue_script(
1676 'ajax_script_validator',
1677 plugins_url( $js_url,__FILE__ ),
1678 array('jquery', 'wp-i18n')
1679 );
1680 wp_set_script_translations('ajax_script_validator', 'event-tickets-with-ticket-scanner', __DIR__.'/languages');
1681
1682 $vars = array(
1683 'shortcode_attr'=>json_encode($attr),
1684 '_plugin_home_url' =>plugins_url( "",__FILE__ ),
1685 '_action' => $this->_prefix.'_executeFrontend',
1686 '_isPremium'=>$this->isPremium(),
1687 '_isUserLoggedin'=>is_user_logged_in(),
1688 '_premJS'=>$this->isPremium() && method_exists($this->getPremiumFunctions(), "getJSFrontFile") ? $this->getPremiumFunctions()->getJSFrontFile() : '',
1689 'url' => admin_url( 'admin-ajax.php' ),
1690 'nonce' => wp_create_nonce( $this->_js_nonce ),
1691 'ajaxActionPrefix' => $this->_prefix,
1692 'divPrefix' => $userDivId == "" ? $this->_prefix : $userDivId,
1693 'divId' => $this->_divId,
1694 'jsFiles' => plugins_url( 'validator.js?_v='.$this->_js_version, __FILE__ )
1695 );
1696 $vars['_messages'] = [
1697 'msgCheck0'=>$this->getOptions()->getOptionValue('textValidationMessage0'),
1698 'msgCheck1'=>$this->getOptions()->getOptionValue('textValidationMessage1'),
1699 'msgCheck2'=>$this->getOptions()->getOptionValue('textValidationMessage2'),
1700 'msgCheck3'=>$this->getOptions()->getOptionValue('textValidationMessage3'),
1701 'msgCheck4'=>$this->getOptions()->getOptionValue('textValidationMessage4'),
1702 'msgCheck5'=>$this->getOptions()->getOptionValue('textValidationMessage5'),
1703 'msgCheck6'=>$this->getOptions()->getOptionValue('textValidationMessage6')
1704 ];
1705 $vars = apply_filters( $this->_add_filter_prefix.'main_replaceShortcode', $vars );
1706
1707 if ($this->isPremium() && method_exists($this->getPremiumFunctions(), "addJSFrontFile")) $this->getPremiumFunctions()->addJSFrontFile();
1708
1709 wp_localize_script(
1710 'ajax_script_validator',
1711 'Ajax_'.$this->_prefix, // name of the injected variable
1712 $vars
1713 );
1714 $ret = '';
1715 if (!isset($attr['divid']) || trim($attr['divid']) == "") {
1716 $ret .= '<div id="'.$this->_divId.'">'.__('...loading...', 'event-tickets-with-ticket-scanner').'</div>';
1717 }
1718
1719 $ret = apply_filters( $this->_add_filter_prefix.'main_replaceShortcode_2', $ret );
1720 do_action( $this->_do_action_prefix.'main_replaceShortcode', $vars, $ret );
1721
1722 return $ret;
1723 }
1724
1725 /**
1726 * Show admin notice when premium subscription is about to expire or has expired
1727 *
1728 * - Warning: 14 days before expiration
1729 * - Error: After expiration (during grace period or after)
1730 *
1731 * @return void
1732 */
1733 public function showSubscriptionWarning(): void {
1734 // Only show when premium plugin is installed (not dependent on subscription status,
1735 // otherwise the expiration warning itself would never be shown)
1736 if (!class_exists('sasoEventtickets_PremiumFunctions')) {
1737 return;
1738 }
1739
1740 // Only show in admin
1741 if (!is_admin()) {
1742 return;
1743 }
1744
1745 // Only show to users who can manage options
1746 if (!current_user_can('manage_options')) {
1747 return;
1748 }
1749
1750 $info = $this->getTicketHandler()->get_expiration();
1751
1752 // No expiration date or lifetime license - no warning needed
1753 if (empty($info['timestamp']) || $info['timestamp'] <= 0 || $info['timestamp'] == -1) {
1754 return;
1755 }
1756
1757 // Lifetime subscription type - no warning needed
1758 if (isset($info['subscription_type']) && $info['subscription_type'] === 'lifetime') {
1759 return;
1760 }
1761
1762 $days_left = ($info['timestamp'] - time()) / 86400;
1763 $renewal_url = 'https://vollstart.com/event-tickets-with-ticket-scanner/';
1764
1765 // Warning 14 days before expiration
1766 if ($days_left <= 14 && $days_left > 0) {
1767 $date = date_i18n(get_option('date_format'), $info['timestamp']);
1768 echo '<div class="notice notice-warning is-dismissible"><p>';
1769 printf(
1770 /* translators: %1$s: expiration date, %2$s: renewal URL */
1771 esc_html__('Your Event-Tickets Premium subscription expires on %1$s. %2$sRenew now%3$s to keep all features.', 'event-tickets-with-ticket-scanner'),
1772 '<strong>' . esc_html($date) . '</strong>',
1773 '<a href="' . esc_url($renewal_url) . '" target="_blank">',
1774 '</a>'
1775 );
1776 echo '</p></div>';
1777 }
1778
1779 // Error after expiration
1780 if ($days_left <= 0) {
1781 $grace_days = isset($info['grace_period_days']) ? intval($info['grace_period_days']) : 7;
1782 $grace_left = $grace_days + $days_left; // days_left is negative
1783
1784 echo '<div class="notice notice-error"><p>';
1785 if ($grace_left > 0) {
1786 printf(
1787 /* translators: %1$d: days remaining in grace period, %2$s: renewal URL */
1788 esc_html__('Your Premium subscription has expired. You have %1$d days remaining before features are disabled. %2$sReactivate now%3$s', 'event-tickets-with-ticket-scanner'),
1789 ceil($grace_left),
1790 '<a href="' . esc_url($renewal_url) . '" target="_blank">',
1791 '</a>'
1792 );
1793 } else {
1794 printf(
1795 /* translators: %1$s: renewal URL */
1796 esc_html__('Your Premium subscription has expired. Premium features are now limited. %1$sReactivate now%2$s', 'event-tickets-with-ticket-scanner'),
1797 '<a href="' . esc_url($renewal_url) . '" target="_blank">',
1798 '</a>'
1799 );
1800 }
1801 echo '</p></div>';
1802 }
1803 }
1804
1805 /**
1806 * FOMO banner: show missed premium features to users with expired subscriptions.
1807 * Dismissable for 30 days, then re-appears with an even longer feature list.
1808 */
1809 public function showFomoBanner(): void {
1810 if (!current_user_can('manage_options')) return;
1811 if ($this->isPremium()) return;
1812 if (!class_exists('sasoEventtickets_PremiumFunctions')) return;
1813 if ($this->isStarterOrStopDetected()) return;
1814 if ($this->isOldPremiumDetected()) return;
1815
1816 $info = $this->getTicketHandler()->get_expiration();
1817
1818 if (empty($info['expiration_date'])) return;
1819 if (isset($info['subscription_type']) && $info['subscription_type'] === 'lifetime') return;
1820
1821 $expired_ts = strtotime($info['expiration_date']);
1822 if (!$expired_ts || $expired_ts > time()) return;
1823
1824 if (get_transient('saso_et_fomo_dismissed')) return;
1825
1826 $json_path = plugin_dir_path(__FILE__) . 'changelog-features.json';
1827 if (!file_exists($json_path)) return;
1828
1829 $changelog = json_decode(file_get_contents($json_path), true);
1830 if (!is_array($changelog)) return;
1831
1832 $missed = [];
1833 foreach ($changelog as $release) {
1834 $release_ts = strtotime($release['date'] ?? '');
1835 if ($release_ts && $release_ts > $expired_ts && !empty($release['features'])) {
1836 foreach ($release['features'] as $feat) {
1837 $missed[] = $feat;
1838 }
1839 }
1840 }
1841
1842 if (empty($missed)) return;
1843
1844 $total = count($missed);
1845 $show = array_slice($missed, 0, 5);
1846 $renewal_url = 'https://vollstart.com/event-tickets-with-ticket-scanner/';
1847 $nonce = wp_create_nonce('saso_et_dismiss_fomo');
1848
1849 echo '<div class="notice notice-info is-dismissible" id="saso-et-fomo-banner" style="border-left-color:#9333ea;">';
1850 echo '<p><strong>';
1851 printf(
1852 /* translators: %d: number of missed features */
1853 esc_html__("You're missing %d new premium features!", 'event-tickets-with-ticket-scanner'),
1854 $total
1855 );
1856 echo '</strong></p>';
1857 echo '<ul style="list-style:disc;margin-left:20px;">';
1858 foreach ($show as $feat) {
1859 echo '<li>' . esc_html($feat) . '</li>';
1860 }
1861 if ($total > 5) {
1862 printf('<li><em>' . esc_html__('...and %d more', 'event-tickets-with-ticket-scanner') . '</em></li>', $total - 5);
1863 }
1864 echo '</ul>';
1865 echo '<p><a href="' . esc_url($renewal_url) . '" target="_blank" class="button button-primary" style="background:#9333ea;border-color:#7c22d0;">';
1866 esc_html_e('Renew now', 'event-tickets-with-ticket-scanner');
1867 echo '</a></p>';
1868 echo '</div>';
1869 echo '<script>jQuery(function($){$("#saso-et-fomo-banner").on("click",".notice-dismiss",function(){$.post(ajaxurl,{action:"saso_et_dismiss_fomo",_wpnonce:"' . $nonce . '"});});});</script>';
1870 }
1871
1872 /**
1873 * AJAX handler: dismiss FOMO banner for 30 days
1874 */
1875 public function ajaxDismissFomo(): void {
1876 check_ajax_referer('saso_et_dismiss_fomo');
1877 set_transient('saso_et_fomo_dismissed', 1, 30 * DAY_IN_SECONDS);
1878 wp_send_json_success();
1879 }
1880
1881 /**
1882 * Show admin notice when premium plugin version is outdated or incompatible
1883 *
1884 * Shows a prominent, non-dismissible error when old premium is detected.
1885 * Old premium (< 1.6.0) is NOT loaded to prevent fatal errors.
1886 *
1887 * @return void
1888 */
1889 public function showOutdatedPremiumWarning(): void {
1890 if (!is_admin() || !current_user_can('manage_options')) {
1891 return;
1892 }
1893
1894 $starter_or_stop = $this->isStarterOrStopDetected();
1895 $old_premium = $this->isOldPremiumDetected();
1896
1897 if (!$starter_or_stop && !$old_premium) {
1898 return;
1899 }
1900
1901 $old_version = defined('SASO_EVENTTICKETS_PREMIUM_PLUGIN_VERSION')
1902 ? SASO_EVENTTICKETS_PREMIUM_PLUGIN_VERSION
1903 : __('unknown', 'event-tickets-with-ticket-scanner');
1904
1905 $upgrade_url = 'https://vollstart.com/event-tickets-with-ticket-scanner/';
1906 $support_url = 'https://vollstart.com/support/';
1907 $premium_url = 'https://vollstart.com/downloads/event-tickets-with-ticket-scanner/';
1908
1909 echo '<div class="notice notice-error" style="border-left-color:#dc3232;padding:15px 20px;">';
1910 echo '<p style="font-size:15px;font-weight:bold;margin:0 0 10px 0;color:#dc2626;">';
1911 echo '&#9888; ';
1912
1913 if ($starter_or_stop) {
1914 $is_stop = defined('SASO_EVENTTICKETS_STOP_VERSION');
1915 if ($is_stop) {
1916 printf(
1917 esc_html__('Event Tickets: Your Premium subscription has expired. Please renew your license to continue using Premium features.', 'event-tickets-with-ticket-scanner')
1918 );
1919 } else {
1920 printf(
1921 esc_html__('Event Tickets: The Starter plugin is installed. Please update to the latest Premium version within the Plugins page.', 'event-tickets-with-ticket-scanner')
1922 );
1923 }
1924 } else {
1925 printf(
1926 esc_html__('Event Tickets: Your Premium plugin (v%s) is outdated and not compatible with this version.', 'event-tickets-with-ticket-scanner'),
1927 esc_html($old_version)
1928 );
1929 }
1930 echo '</p>';
1931
1932 if ($starter_or_stop) {
1933 // Starter/Stop plugin - different messages
1934 $is_stop = defined('SASO_EVENTTICKETS_STOP_VERSION');
1935 $hasSerial = !empty(trim(get_option("saso-event-tickets-premium_serial", "")));
1936
1937 echo '<div style="background:#f8f9fa;border-left:4px solid #2563eb;padding:12px;margin:12px 0;">';
1938
1939 if ($is_stop) {
1940 // Stop plugin - subscription expired, PUC can update once license is renewed
1941 echo '<p style="margin:0 0 10px 0;"><strong>' . esc_html__('Solution: Renew Your Premium License', 'event-tickets-with-ticket-scanner') . '</strong></p>';
1942 echo '<ol style="margin:0 0 12px 20px;padding-left:20px;">';
1943 echo '<li style="margin:0 0 6px 0;">' . esc_html__('Renew your license', 'event-tickets-with-ticket-scanner') . '</li>';
1944 if ($hasSerial) {
1945 echo '<li style="margin:0 0 6px 0;">' . esc_html__('Update your license key in Event Tickets settings', 'event-tickets-with-ticket-scanner') . '</li>';
1946 } else {
1947 echo '<li style="margin:0 0 6px 0;">' . esc_html__('Enter your license key in Event Tickets settings', 'event-tickets-with-ticket-scanner') . '</li>';
1948 }
1949 echo '<li style="margin:0 0 6px 0;">' . esc_html__('Click "Check License" to verify your new subscription', 'event-tickets-with-ticket-scanner') . '</li>';
1950 echo '<li style="margin:0;">' . esc_html__('Update the Premium plugin via Plugins > Updates', 'event-tickets-with-ticket-scanner') . '</li>';
1951 echo '</ol>';
1952 printf(
1953 '<p style="margin:0 0 8px 0;"><a href="%s" class="button button-primary button-hero" target="_blank">%s</a></p>',
1954 esc_url($upgrade_url . '?utm_source=plugin&utm_medium=stop-notice&utm_campaign=renew-license'),
1955 esc_html__('Renew License', 'event-tickets-with-ticket-scanner')
1956 );
1957 echo '<p style="margin:0;font-size:13px;color:#666;">';
1958 esc_html_e('Contact support via support@vollstart.com if you think this is not correct.', 'event-tickets-with-ticket-scanner');
1959 echo '</p>';
1960 } else {
1961 // Starter plugin - just update within plugins area
1962 echo '<p style="margin:0 0 10px 0;"><strong>' . esc_html__('Solution: Update to Premium via Plugins Page', 'event-tickets-with-ticket-scanner') . '</strong></p>';
1963 echo '<p style="margin:0 0 8px 0;">';
1964 esc_html_e('The Starter plugin needs to be updated to Premium. Please update within the Plugins page:', 'event-tickets-with-ticket-scanner');
1965 echo '</p>';
1966 echo '<ol style="margin:0 0 12px 20px;padding-left:20px;">';
1967 if (!$hasSerial) {
1968 echo '<li style="margin:0 0 6px 0;">' . esc_html__('Enter your license key in Event Tickets settings', 'event-tickets-with-ticket-scanner') . '</li>';
1969 }
1970 echo '<li style="margin:0 0 6px 0;">' . esc_html__('Go to Plugins > Add New > Upload Plugin', 'event-tickets-with-ticket-scanner') . '</li>';
1971 echo '<li style="margin:0 0 6px 0;">' . esc_html__('Upload the Premium ZIP file', 'event-tickets-with-ticket-scanner') . '</li>';
1972 echo '<li style="margin:0 0 6px 0;">' . esc_html__('WordPress will replace the Starter plugin with Premium', 'event-tickets-with-ticket-scanner') . '</li>';
1973 echo '<li style="margin:0;">' . esc_html__('Click "Replace current with uploaded" and activate', 'event-tickets-with-ticket-scanner') . '</li>';
1974 echo '</ol>';
1975 }
1976
1977 echo '</div>';
1978 } else {
1979 // Old premium detected - update instructions
1980 echo '<div style="background:#f8f9fa;border-left:4px solid #2563eb;padding:12px;margin:12px 0;">';
1981 echo '<p style="margin:0 0 10px 0;"><strong>' . esc_html__('Solution: Update Premium Plugin', 'event-tickets-with-ticket-scanner') . '</strong></p>';
1982 echo '<p style="margin:0 0 8px 0;">';
1983 esc_html_e('Your current Premium plugin is outdated and incompatible. Premium features have been temporarily disabled to prevent errors. Your tickets and data are safe.', 'event-tickets-with-ticket-scanner');
1984 echo '</p>';
1985 echo '<p style="margin:0 0 8px 0;">';
1986 printf(
1987 esc_html__('Required version: 1.6.0 or higher. You have: %s', 'event-tickets-with-ticket-scanner'),
1988 '<strong>' . esc_html($old_version) . '</strong>'
1989 );
1990 echo '</p>';
1991 echo '<p style="margin:0 0 12px 0;">';
1992 esc_html_e('To restore premium features:', 'event-tickets-with-ticket-scanner');
1993 echo '</p>';
1994 echo '<ol style="margin:0 0 12px 20px;padding-left:20px;">';
1995 echo '<li style="margin:0 0 6px 0;">' . sprintf(
1996 /* translators: %s: URL to account */
1997 esc_html__('Download the latest Premium from your %1$saccount%2$s', 'event-tickets-with-ticket-scanner'),
1998 '<a href="' . esc_url($upgrade_url) . '" target="_blank"><strong>',
1999 '</strong></a>'
2000 ) . '</li>';
2001 echo '<li style="margin:0 0 6px 0;">' . esc_html__('Go to Plugins > Add New > Upload Plugin', 'event-tickets-with-ticket-scanner') . '</li>';
2002 echo '<li style="margin:0 0 6px 0;">' . esc_html__('Upload the new Premium ZIP file', 'event-tickets-with-ticket-scanner') . '</li>';
2003 echo '<li style="margin:0;">' . esc_html__('WordPress will ask to replace - confirm to update', 'event-tickets-with-ticket-scanner') . '</li>';
2004 echo '</ol>';
2005 echo '<p style="margin:0;">';
2006 printf(
2007 '<a href="' . esc_url($support_url) . '" class="button" style="margin-right:8px;" target="_blank">%s</a>',
2008 esc_html__('Contact Support', 'event-tickets-with-ticket-scanner')
2009 );
2010 echo '</p>';
2011 }
2012
2013 echo '</div>'; // End gray box
2014
2015 echo '<p style="margin:12px 0 0 0;font-style:italic;color:#666;font-size:13px;">';
2016 esc_html_e('Your tickets, orders, and data are completely safe. You can continue using the basic features while updating.', 'event-tickets-with-ticket-scanner');
2017 echo '</p>';
2018
2019 $downgrade_url = 'https://plugins.trac.wordpress.org/browser/event-tickets-with-ticket-scanner/tags';
2020 echo '<p style="margin:6px 0 0 0;font-style:italic;color:#666;font-size:13px;">';
2021 printf(
2022 esc_html__('Alternatively, your old Premium plugin still works with basic plugin versions below 2.8.0. You can %1$sdowngrade the basic plugin here%2$s.', 'event-tickets-with-ticket-scanner'),
2023 '<a href="' . esc_url($downgrade_url) . '" target="_blank">',
2024 '</a>'
2025 );
2026 echo '</p>';
2027
2028 echo '</div>'; // End notice
2029 }
2030
2031 /**
2032 * Show admin notice when ticket format is running out of combinations
2033 *
2034 * Checks all ticket lists for format warnings and displays notice
2035 *
2036 * @return void
2037 */
2038 public function showPhpVersionWarning(): void {
2039 if (version_compare(PHP_VERSION, '8.1.0', '>=')) {
2040 return;
2041 }
2042 printf(
2043 '<div class="notice notice-warning is-dismissible"><p><strong>Event Tickets with Ticket Scanner:</strong> %s</p></div>',
2044 sprintf(
2045 /* translators: 1: current PHP version 2: required PHP version */
2046 esc_html__('Your server is running PHP %1$s. This plugin requires PHP %2$s or higher. Please upgrade PHP to ensure full compatibility.', 'event-tickets-with-ticket-scanner'),
2047 PHP_VERSION,
2048 '8.1'
2049 )
2050 );
2051 }
2052
2053 /**
2054 * Handle format warning dismiss — runs on admin_init (before output) so wp_redirect() works.
2055 */
2056 public function handleFormatWarningDismiss(): void {
2057 if (!isset($_GET['saso_eventtickets_clear_format_warning']) || !isset($_GET['_wpnonce'])) {
2058 return;
2059 }
2060 if (!current_user_can('manage_options')) {
2061 return;
2062 }
2063 $list_id = intval($_GET['saso_eventtickets_clear_format_warning']);
2064 $nonce = sanitize_text_field($_GET['_wpnonce']);
2065 if (wp_verify_nonce($nonce, 'clear_format_warning_' . $list_id)) {
2066 $this->getAdmin()->clearFormatWarning($list_id);
2067 wp_redirect(remove_query_arg(['saso_eventtickets_clear_format_warning', '_wpnonce']));
2068 exit;
2069 }
2070 }
2071
2072 /**
2073 * Show admin notice when options migration from wp_options to custom table is incomplete.
2074 * Includes a button to manually trigger the migration.
2075 */
2076 public function showOptionsMigrationNotice(): void {
2077 if (!current_user_can('manage_options')) {
2078 return;
2079 }
2080 // Fast path: migration already done
2081 if (get_option('saso_eventtickets_options_migrated', '0') === '1') {
2082 return;
2083 }
2084 // Check if custom table exists (DB upgrade may not have run yet)
2085 global $wpdb;
2086 $table = $wpdb->prefix . 'saso_eventtickets_options';
2087 if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table)) !== $table) {
2088 return;
2089 }
2090 // Check sentinel: is the last known option still in wp_options?
2091 $sentinelKey = $this->_prefix . 'qrAttachQRFilesToMailAsOnePDF';
2092 if (get_option($sentinelKey, '__NOT_SET__') === '__NOT_SET__') {
2093 // Sentinel gone — migration probably completed, set flag
2094 update_option('saso_eventtickets_options_migrated', '1');
2095 return;
2096 }
2097 // Migration incomplete — show admin notice with button
2098 $nonce = wp_create_nonce($this->_prefix);
2099 printf(
2100 '<div class="notice notice-warning"><p><strong>Event Tickets:</strong> %s <button class="button button-primary" onclick="sasoEventticketsMigrateOptions(this)" data-nonce="%s">%s</button></p></div>',
2101 esc_html__('Options migration to custom database table is incomplete.', 'event-tickets-with-ticket-scanner'),
2102 esc_attr($nonce),
2103 esc_html__('Migrate Options', 'event-tickets-with-ticket-scanner')
2104 );
2105 }
2106
2107 public function showFormatWarning(): void {
2108 // Only show in admin
2109 if (!is_admin()) {
2110 return;
2111 }
2112
2113 // Only show to users who can manage options
2114 if (!current_user_can('manage_options')) {
2115 return;
2116 }
2117
2118 try {
2119 // Get all ticket lists
2120 $lists = $this->getAdmin()->getLists([], false);
2121
2122 foreach ($lists as $list) {
2123 $warning = $this->getAdmin()->getFormatWarning($list['id']);
2124
2125 if ($warning) {
2126 $list_name = esc_html($warning['list_name']);
2127 $attempts = intval($warning['attempts']);
2128
2129 if ($warning['type'] === 'critical') {
2130 // Critical - format exhausted
2131 $clear_url = wp_nonce_url(
2132 add_query_arg(['saso_eventtickets_clear_format_warning' => $list['id']]),
2133 'clear_format_warning_' . $list['id']
2134 );
2135
2136 echo '<div class="notice notice-error"><p>';
2137 printf(
2138 /* translators: 1: list name, 2: attempts, 3: clear URL */
2139 esc_html__('⚠️ CRITICAL: Ticket format for "%1$s" is exhausted! It took %2$d attempts to generate a code. Future ticket sales may fail. %3$sEdit list%4$s | %5$sDismiss%4$s', 'event-tickets-with-ticket-scanner'),
2140 $list_name,
2141 $attempts,
2142 '<a href="' . esc_url(admin_url('admin.php?page=event-tickets-with-ticket-scanner')) . '">',
2143 '</a>',
2144 '<a href="' . esc_url($clear_url) . '">'
2145 );
2146 echo '</p></div>';
2147 } else {
2148 // Warning - running out
2149 $clear_url = wp_nonce_url(
2150 add_query_arg(['saso_eventtickets_clear_format_warning' => $list['id']]),
2151 'clear_format_warning_' . $list['id']
2152 );
2153
2154 echo '<div class="notice notice-warning is-dismissible"><p>';
2155 printf(
2156 /* translators: 1: list name, 2: attempts, 3: clear URL */
2157 esc_html__('⚠️ WARNING: Ticket format for "%1$s" is running out of combinations. It took %2$d attempts to generate a code. Consider increasing code length. %3$sEdit list%4$s | %5$sDismiss%4$s', 'event-tickets-with-ticket-scanner'),
2158 $list_name,
2159 $attempts,
2160 '<a href="' . esc_url(admin_url('admin.php?page=event-tickets-with-ticket-scanner')) . '">',
2161 '</a>',
2162 '<a href="' . esc_url($clear_url) . '">'
2163 );
2164 echo '</p></div>';
2165 }
2166
2167 // Only show one warning at a time
2168 break;
2169 }
2170 }
2171 } catch (Exception $e) {
2172 // Silently fail - don't break the admin
2173 }
2174 }
2175 }
2176 $sasoEventtickets = sasoEventtickets::Instance();
2177 ?>
2178