PluginProbe ʕ •ᴥ•ʔ
Event Tickets with Ticket Scanner / 3.0.5
Event Tickets with Ticket Scanner v3.0.5
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 2 months ago css 2 months ago img 2 months ago includes 2 months ago js 2 months ago languages 2 months ago ticket 2 months ago vendors 2 months ago SASO_EVENTTICKETS.php 2 months ago backend.js 2 months ago changelog-features.json 2 months ago changelog.txt 2 months ago db.php 2 months ago index.php 2 months ago init_file.php 2 months ago order_details.js 2 months ago pwa-sw.js 2 months ago readme.txt 2 months ago saso-eventtickets-validator.js 2 months ago sasoEventtickets_AdminSettings.php 2 months ago sasoEventtickets_Authtoken.php 2 months ago sasoEventtickets_Base.php 2 months ago sasoEventtickets_Core.php 2 months ago sasoEventtickets_Frontend.php 2 months ago sasoEventtickets_Messenger.php 2 months ago sasoEventtickets_Options.php 2 months ago sasoEventtickets_PDF.php 2 months ago sasoEventtickets_Seating.php 2 months ago sasoEventtickets_Ticket.php 2 months ago sasoEventtickets_TicketBadge.php 2 months ago sasoEventtickets_TicketDesigner.php 2 months ago sasoEventtickets_TicketQR.php 2 months ago ticket_events.js 2 months ago ticket_scanner.js 2 months ago validator.js 2 months ago version-notices.json 2 months ago vollstart-cross-promo.php 2 months ago wc_backend.js 2 months ago wc_frontend.js 2 months ago woocommerce-hooks.php 2 months ago
index.php
2399 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: 3.0.5
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', '3.0.5');
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 /**
253 * After a valid license key is saved, automatically upgrade the premium
254 * plugin from starter/stop to the real premium version.
255 *
256 * Customers install the starter, enter their license, but don't manually
257 * click "Update" — they see expiration warnings and contact support.
258 * This triggers the upgrade in the background.
259 *
260 * Runs synchronously (takes ~5-10s) — acceptable since user just clicked Save.
261 * Errors are logged but not surfaced to the user.
262 */
263 public function autoUpgradePremiumAfterLicenseSave(): void {
264 try {
265 $premFolder = trim($this->getPremiumPluginFolder(), '/');
266 if (empty($premFolder)) return;
267
268 // Find the premium plugin file in active_plugins
269 $pluginFile = null;
270 foreach (get_option('active_plugins', []) as $p) {
271 if (strpos($p, $premFolder . '/') === 0) {
272 $pluginFile = $p;
273 break;
274 }
275 }
276 if (empty($pluginFile)) return;
277
278 // Force WP to re-check for plugin updates (clears PUC cache)
279 delete_site_transient('update_plugins');
280 delete_site_transient('puc_request_info_saso-event-tickets-with-ticket-scanner-premium');
281 wp_update_plugins();
282
283 // Check if an update is available
284 $updates = get_site_transient('update_plugins');
285 if (empty($updates->response[$pluginFile])) {
286 return; // Already on latest — nothing to do
287 }
288
289 // Silently upgrade
290 if (!class_exists('Plugin_Upgrader')) {
291 require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
292 }
293 if (!class_exists('Automatic_Upgrader_Skin')) {
294 require_once ABSPATH . 'wp-admin/includes/class-automatic-upgrader-skin.php';
295 }
296 $upgrader = new Plugin_Upgrader(new \Automatic_Upgrader_Skin());
297 $result = $upgrader->upgrade($pluginFile);
298
299 // Post-upgrade cleanup: the update_plugins transient still contains the
300 // "update available" entry until the next WP-Cron run (12h). This makes
301 // the red "update" badge stick around even though the plugin is already
302 // on the latest version. Clear + re-check now so the badge disappears
303 // immediately on the next page load.
304 if ($result === true) {
305 delete_site_transient('update_plugins');
306 delete_site_transient('puc_request_info_saso-event-tickets-with-ticket-scanner-premium');
307 // Clear PUC's own state store so it can't re-inject the stale
308 // "update available" entry on next request (belt + suspenders).
309 delete_site_option('external_updates-saso-event-tickets-with-ticket-scanner-premium');
310 wp_update_plugins();
311 }
312 } catch (\Throwable $e) {
313 error_log('Event Tickets: auto-upgrade after license save failed: ' . $e->getMessage());
314 }
315 }
316
317 public function getPremiumFunctions() {
318 if ($this->_isPrem == null && $this->PREMFUNCTIONS == null) {
319 $this->_isPrem = false;
320 $this->PREMFUNCTIONS = new sasoEventtickets_fakeprem();
321
322 // Check for Premium class - function-based compatibility check
323 if (class_exists('sasoEventtickets_PremiumFunctions')) {
324 // Check if this is Starter or Stop plugin (both are "update-only" placeholders)
325 $is_starter_or_stop = defined('SASO_EVENTTICKETS_STARTER_VERSION') || defined('SASO_EVENTTICKETS_STOP_VERSION');
326
327 if ($is_starter_or_stop) {
328 // Starter/Stop plugin detected - treat as "premium not active"
329 // Show message: Enter license, then update via plugin area
330 $this->_starterOrStopDetected = true;
331 $this->PREMFUNCTIONS = new sasoEventtickets_fakeprem();
332 } elseif (!defined('SASO_EVENTTICKETS_PREMIUM_PLUGIN_VERSION')) {
333 // Premium plugin exists but no version defined - likely very old or corrupted
334 $this->_oldPremiumDetected = true;
335 } else {
336 // Compatibility check BEFORE instantiation — the premium constructor
337 // registers 25+ WordPress hooks on $this. If we instantiate first and
338 // discard the object afterwards, WP keeps the callbacks alive in memory
339 // and fires them later, which crashes the site on old premiums that
340 // reference methods removed in the WC manager refactor (2.7.x+).
341 //
342 // Use ReflectionClass to inspect methods WITHOUT running the constructor.
343 $min_premium_version = '1.5.0';
344 $version_too_old = version_compare(SASO_EVENTTICKETS_PREMIUM_PLUGIN_VERSION, $min_premium_version, '<');
345
346 // Fingerprint methods added in Premium 1.5.0 — Premium 1.5.x still works
347 // with the current basic because it only calls the deprecated proxies
348 // getTicketsFromOrder() / add_serialcode_to_order(). 1.3.x / 1.4.x do
349 // not have these methods and their hook callbacks crash later.
350 $has_required_methods = false;
351 try {
352 $reflection = new ReflectionClass('sasoEventtickets_PremiumFunctions');
353 $has_required_methods = $reflection->hasMethod('maxValues')
354 && $reflection->hasMethod('ticket_outputTicketInfo_template')
355 && $reflection->hasMethod('wc_order_add_meta_boxes');
356 } catch (\Throwable $e) {
357 // Reflection itself failed — treat as incompatible
358 $has_required_methods = false;
359 }
360
361 if ($version_too_old || !$has_required_methods) {
362 // Old premium detected — DO NOT instantiate. Instantiating would
363 // register hooks that crash later when WooCommerce fires events.
364 $this->_oldPremiumDetected = true;
365 error_log('Event Tickets: Incompatible old premium detected (v' . SASO_EVENTTICKETS_PREMIUM_PLUGIN_VERSION . ') — not instantiated to prevent hook crashes.');
366 } else {
367 // Version and methods are OK — safe to instantiate.
368 // Keep try/catch as safety net for unexpected constructor errors.
369 try {
370 $this->PREMFUNCTIONS = new sasoEventtickets_PremiumFunctions($this, plugin_dir_path(__FILE__), $this->_prefix, $this->getDB());
371 $this->_isPrem = $this->PREMFUNCTIONS->isPremium();
372 } catch (\Throwable $e) {
373 $this->_oldPremiumDetected = true;
374 $this->PREMFUNCTIONS = new sasoEventtickets_fakeprem();
375 error_log('Event Tickets: Premium instantiation failed: ' . $e->getMessage());
376 }
377 }
378 }
379 } else {
380 // Premium class not yet available — basic plugin loaded before premium.
381 // Defer premium setup until all plugins are loaded (plugins_loaded hook).
382 $premPluginFolder = $this->getPremiumPluginFolder();
383 if (!empty($premPluginFolder)) {
384 add_action('plugins_loaded', [$this, '_lateLoadPremium'], 1);
385 }
386 }
387 }
388 return $this->PREMFUNCTIONS;
389 }
390 /**
391 * Late-load premium plugin when basic loaded before premium (plugin loading order).
392 * Hooked to plugins_loaded with priority 1 so it runs before WooCommercePluginLoaded (priority 20).
393 */
394 public function _lateLoadPremium(): void {
395 if (!class_exists('sasoEventtickets_PremiumFunctions')) {
396 return; // Premium plugin not installed/active
397 }
398 if ($this->PREMFUNCTIONS instanceof sasoEventtickets_PremiumFunctions) {
399 return; // Already loaded
400 }
401
402 // Check for Starter/Stop plugin first (update-only placeholders, no premium features)
403 if (defined('SASO_EVENTTICKETS_STARTER_VERSION') || defined('SASO_EVENTTICKETS_STOP_VERSION')) {
404 $this->_starterOrStopDetected = true;
405 return;
406 }
407
408 $min_premium_version = '1.5.0';
409 if (!defined('SASO_EVENTTICKETS_PREMIUM_PLUGIN_VERSION')
410 || version_compare(SASO_EVENTTICKETS_PREMIUM_PLUGIN_VERSION, $min_premium_version, '<')) {
411 $this->_oldPremiumDetected = true;
412 return;
413 }
414
415 // Reflection-based method check BEFORE instantiation — same reason as in
416 // getPremiumFunctions(): old premium constructors register hooks that crash
417 // later if methods were removed in the WC manager refactor.
418 // Fingerprint methods added in Premium 1.5.0.
419 try {
420 $reflection = new ReflectionClass('sasoEventtickets_PremiumFunctions');
421 $has_required_methods = $reflection->hasMethod('maxValues')
422 && $reflection->hasMethod('ticket_outputTicketInfo_template')
423 && $reflection->hasMethod('wc_order_add_meta_boxes');
424 } catch (\Throwable $e) {
425 $has_required_methods = false;
426 }
427 if (!$has_required_methods) {
428 $this->_oldPremiumDetected = true;
429 error_log('Event Tickets: Incompatible old premium detected in late-load — not instantiated.');
430 return;
431 }
432
433 try {
434 $this->PREMFUNCTIONS = new sasoEventtickets_PremiumFunctions($this, plugin_dir_path(__FILE__), $this->_prefix, $this->getDB());
435 $this->_isPrem = $this->PREMFUNCTIONS->isPremium();
436
437 if (method_exists($this->PREMFUNCTIONS, 'initHandlers')) {
438 $this->PREMFUNCTIONS->initHandlers();
439 }
440 } catch (\Throwable $e) {
441 // Premium loading failed — degrade gracefully to free mode
442 $this->PREMFUNCTIONS = new sasoEventtickets_fakeprem();
443 $this->_isPrem = false;
444 error_log('Event Tickets: Premium late-load failed: ' . $e->getMessage());
445 }
446 }
447
448 public function getPremiumPluginFolder(): string {
449 $plugins = get_option('active_plugins', []);
450 $premiumFile = "";
451 foreach($plugins as $plugin) {
452 if (strpos(" ".$plugin, $this->_premium_plugin_name) > 0) {
453 $premiumFile = plugin_dir_path($plugin);
454 break;
455 }
456 }
457 return $premiumFile;
458 }
459 public function isPremium() {
460 if ($this->_isPrem === null) {
461 $this->getPremiumFunctions();
462 // If PREMFUNCTIONS already existed, getPremiumFunctions() skipped re-evaluation.
463 // Re-query the premium plugin directly.
464 if ($this->_isPrem === null && $this->PREMFUNCTIONS !== null) {
465 $this->_isPrem = ($this->PREMFUNCTIONS instanceof sasoEventtickets_PremiumFunctions)
466 ? $this->PREMFUNCTIONS->isPremium()
467 : false;
468 }
469 }
470
471 // Eigene Verifikation: auch wenn Premium-Plugin "ja" sagt,
472 // Basic Plugin prüft den gespeicherten Lizenzstatus selbst.
473 // Rekursionsschutz: isPremium() -> getTicketHandler() -> Konstruktor -> getOptions() -> isPremium()
474 if ($this->_isPrem === true && !$this->_isCheckingSubscription) {
475 $this->_isCheckingSubscription = true;
476 try {
477 if (!$this->getTicketHandler()->isSubscriptionActive()) {
478 $this->_isPrem = false;
479 }
480 } finally {
481 $this->_isCheckingSubscription = false;
482 }
483 }
484
485 return $this->_isPrem;
486 }
487 /**
488 * Invalidate the cached premium status so the next isPremium() call re-evaluates.
489 */
490 public function invalidatePremiumCache(): void {
491 $this->_isPrem = null;
492 $this->PREMFUNCTIONS = null;
493 }
494 public function getPrefix() {
495 return $this->_prefix;
496 }
497 public function getMV() {
498 $v = ['storeip'=>false,'allowuserreg'=>false,'codes_total'=>0x13,'codes'=>0x12,'lists'=>5,'authtokens_total'=>3];
499 $v["codes"] = (int) hexdec(0x80 / 0x002) / 2;
500 $v["codes_total"] = (int) hexdec(0x80 / 0x002) / 2;
501 $v["seatingplans"] = (int) (0x04 >> 0x02);
502 $v["seats_per_plan"] = (int) (0x50 >> 0x02);
503 return $v;
504 }
505 public function listener_upgrader_process_complete( $upgrader_object, $options ) {
506 $current_plugin_path_name = plugin_basename( __FILE__ );
507 if ($options['action'] == 'update' && $options['type'] == 'plugin' ) {
508 if (isset($options['plugins'])) {
509 foreach($options['plugins'] as $each_plugin) {
510 if ($each_plugin==$current_plugin_path_name) {
511 // .......................... YOUR CODES .............
512 }
513 }
514 }
515 }
516 do_action( $this->_do_action_prefix.'main_listener_upgrader_process_complete' );
517 }
518 /**
519 * check for ticket detail page request
520 */
521 public function wc_checkTicketDetailPage() {
522 if( is_404() ){
523 include_once("SASO_EVENTTICKETS.php");
524 // /wp-content/plugins/event-tickets-with-ticket-scanner/ticket/
525 $p = $this->getCore()->getTicketURLPath(true);
526 $t = explode("/", $_SERVER["REQUEST_URI"]);
527 if (count($t) > 1) {
528 if ($t[count($t)-2] != "scanner") {
529 if(substr($_SERVER["REQUEST_URI"], 0, strlen($p)) == $p) {
530 $this->getTicketHandler()->initFilterAndActions();
531 } else {
532 $wcTicketCompatibilityModeURLPath = trim($this->getOptions()->getOptionValue('wcTicketCompatibilityModeURLPath'));
533 $wcTicketCompatibilityModeURLPath = trim(trim($wcTicketCompatibilityModeURLPath, "/"));
534 if (!empty($wcTicketCompatibilityModeURLPath)) {
535 $uri = trim($_SERVER["REQUEST_URI"]);
536 if (!empty($uri)) {
537 $pos = strpos($uri, $wcTicketCompatibilityModeURLPath);
538 if ($pos > 0) {
539 $this->getTicketHandler()->initFilterAndActions();
540 }
541 }
542 }
543 }
544 }
545
546 if ($t[count($t)-2] == "scanner") {
547 if(substr($_SERVER["REQUEST_URI"], 0, strlen($p)) == $p) {
548 //$this->replacingShortcodeTicketScanner();
549 $this->getTicketHandler()->initFilterAndActionsTicketScanner();
550 } else {
551 $wcTicketCompatibilityModeURLPath = trim($this->getOptions()->getOptionValue('wcTicketCompatibilityModeURLPath'));
552 $wcTicketCompatibilityModeURLPath = trim(trim($wcTicketCompatibilityModeURLPath, "/"));
553 if (!empty($wcTicketCompatibilityModeURLPath)) {
554 $uri = trim($_SERVER["REQUEST_URI"]);
555 if (!empty($uri)) {
556 $pos = strpos($_SERVER["REQUEST_URI"], $wcTicketCompatibilityModeURLPath."/scanner/");
557 if ($pos > 0) {
558 $this->getTicketHandler()->initFilterAndActionsTicketScanner();
559 }
560 }
561 }
562 }
563 }
564 }
565 } // endif 404
566 }
567 private function init_frontend() {
568 add_shortcode($this->_shortcode, [$this, 'replacingShortcode']);
569 add_shortcode($this->_shortcode_mycode, [$this, 'replacingShortcodeMyCode']);
570 add_shortcode($this->_shortcode_ticket_scanner, [$this, 'replacingShortcodeTicketScanner']);
571 add_shortcode($this->_shortcode_eventviews, [$this, 'replacingShortcodeEventViews']);
572 add_shortcode($this->_shortcode_feature_list, [$this, 'replacingShortcodeFeatureList']);
573 add_shortcode($this->_shortcode_ticket_detail, [$this, 'replacingShortcodeTicketDetail']);
574 do_action( $this->_do_action_prefix.'main_init_frontend' );
575 }
576 private function init_backend() {
577 add_action('admin_menu', [$this, 'register_options_page']);
578 register_activation_hook(__FILE__, [$this, 'plugin_activated']);
579 register_deactivation_hook( __FILE__, [$this, 'plugin_deactivated'] );
580 //register_uninstall_hook( __FILE__, 'sasoEventticketsDB::plugin_uninstall' ); // MUSS NOCH GETESTE WERDEN
581 add_action( 'plugins_loaded', [$this, 'plugins_loaded'] );
582 add_action( 'show_user_profile', [$this, 'show_user_profile'] );
583 add_action( 'admin_init', [$this, 'handleFormatWarningDismiss'] );
584 add_action( 'admin_notices', [$this, 'showSubscriptionWarning'] );
585 add_action( 'admin_notices', [$this, 'showFomoBanner'] );
586 add_action( 'admin_notices', [$this, 'showOutdatedPremiumWarning'] );
587 add_action( 'admin_notices', [$this, 'showFormatWarning'] );
588 add_action( 'admin_notices', [$this, 'showPhpVersionWarning'] );
589 add_action( 'admin_notices', [$this, 'showOptionsMigrationNotice'] );
590 add_action( 'wp_ajax_saso_et_dismiss_fomo', [$this, 'ajaxDismissFomo'] );
591
592 if (basename($_SERVER['SCRIPT_NAME'] ?? '') == "admin-ajax.php") {
593 add_action('wp_ajax_'.$this->_prefix.'_executeAdminSettings', [$this,'executeAdminSettings_a'], 10, 0);
594 add_action('wp_ajax_'.$this->_prefix.'_executeSeatingAdmin', [$this,'executeSeatingAdmin_a'], 10, 0);
595 }
596
597 add_action('admin_init', [$this, 'periodicLicenseCheck']);
598
599 do_action( $this->_do_action_prefix.'main_init_backend' );
600 }
601
602 /**
603 * Periodic license check on admin page loads.
604 * Hard-throttled via site transient so that even if many admin requests come in
605 * (bots, health-checks, other plugins hammering admin-ajax) only one actual
606 * license call fires per interval.
607 */
608 public function periodicLicenseCheck(): void {
609 // Hard throttle: site transient = max 1 check per interval, independent of
610 // option-based last_run throttling below. Prevents runaway requests when
611 // admin pages are hit rapidly (monitoring tools, page builders, broken cron).
612 $throttleKey = 'saso_et_periodic_license_check_lock';
613 if (get_site_transient($throttleKey)) return;
614
615 // Also run when premium plugin is installed with a serial key but isPremium() is false
616 // This breaks the deadlock where isPremium()=false prevents the license check from ever running
617 $hasPremiumPlugin = class_exists('sasoEventtickets_PremiumFunctions');
618 $hasSerial = !empty(trim(get_option("saso-event-tickets-premium_serial", "")));
619 if (!$this->isPremium() && !($hasPremiumPlugin && $hasSerial)) return;
620
621 // Set throttle lock IMMEDIATELY — before any other work — so concurrent
622 // admin_init calls bail out. Full interval even on recovery mode.
623 $interval = $this->isPremium() ? 86400 : 3600;
624 set_site_transient($throttleKey, time(), $interval);
625
626 $info = $this->getTicketHandler()->get_expiration();
627 $last_run = intval($info['last_run']);
628 if ($last_run > 0 && (time() - $last_run) < $interval) return;
629
630 // Check ausführen
631 $this->getTicketHandler()->checkForPremiumSerialExpiration();
632
633 // isPremium Cache invalidieren damit neuer Wert gilt
634 $this->invalidatePremiumCache();
635 }
636 public function WooCommercePluginLoaded() {
637 // DON'T load WC here - let relay functions do lazy loading
638 //$this->getWC(); // um die wc handler zu laden
639 add_action('woocommerce_review_order_after_cart_contents', [$this, 'relay_woocommerce_review_order_after_cart_contents']);
640 add_action('woocommerce_checkout_process', [$this, 'relay_woocommerce_checkout_process']);
641 add_action('woocommerce_before_cart_table', [$this, 'relay_woocommerce_before_cart_table']);
642 add_action('woocommerce_cart_updated', [$this, 'relay_woocommerce_cart_updated']);
643 add_filter('woocommerce_email_attachments', [$this, 'relay_woocommerce_email_attachments'], 10, 3);
644 add_action('woocommerce_checkout_create_order_line_item', [$this, 'relay_woocommerce_checkout_create_order_line_item'], 20, 4 );
645 add_action('woocommerce_check_cart_items', [$this, 'relay_woocommerce_check_cart_items'] );
646 add_action('woocommerce_new_order', [$this, 'relay_woocommerce_new_order'], 10, 1);
647 add_action('woocommerce_checkout_update_order_meta', [$this, 'relay_woocommerce_checkout_update_order_meta'], 20, 2);
648 add_action('woocommerce_order_status_changed', [$this, 'relay_woocommerce_order_status_changed'], 10, 3);
649 add_filter('woocommerce_order_item_display_meta_key', [$this, 'relay_woocommerce_order_item_display_meta_key'], 20, 3 );
650 add_filter('woocommerce_order_item_display_meta_value', [$this, 'relay_woocommerce_order_item_display_meta_value'], 20, 3);
651 add_action('wpo_wcpdf_after_item_meta', [$this, 'relay_wpo_wcpdf_after_item_meta'], 20, 3 );
652 add_action('woocommerce_order_item_meta_start', [$this, 'relay_woocommerce_order_item_meta_start'], 201, 4);
653 add_action('woocommerce_product_after_variable_attributes', [$this, 'relay_woocommerce_product_after_variable_attributes'], 10, 3);
654 add_action('woocommerce_save_product_variation',[$this, 'relay_woocommerce_save_product_variation'], 10 ,2 );
655 add_action('woocommerce_email_order_meta', [$this, 'relay_woocommerce_email_order_meta'], 10, 4 );
656 add_action('woocommerce_thankyou', [$this, 'relay_woocommerce_thankyou'], 5);
657 if (wp_doing_ajax()) {
658 // erlaube ajax nonpriv und registriere handler
659 add_action('wp_ajax_nopriv_'.$this->getPrefix().'_executeWCFrontend', [$this,'relay_executeWCFrontend']); // nicht angemeldete user, sollen eine antwort erhalten
660 add_action('wp_ajax_'.$this->getPrefix().'_executeWCFrontend', [$this,'relay_executeWCFrontend']); // nicht angemeldete user, sollen eine antwort erhalten
661 // Seating Frontend AJAX (seat selection in shop)
662 add_action('wp_ajax_nopriv_'.$this->getPrefix().'_executeSeatingFrontend', [$this,'relay_executeSeatingFrontend']);
663 add_action('wp_ajax_'.$this->getPrefix().'_executeSeatingFrontend', [$this,'relay_executeSeatingFrontend']);
664 }
665 if (is_admin()) {
666 add_action('woocommerce_delete_order', [$this, 'relay_woocommerce_delete_order'], 10, 1 );
667 add_action('woocommerce_delete_order_item', [$this, 'relay_woocommerce_delete_order_item'], 20, 1);
668 add_action('woocommerce_pre_delete_order_refund', [$this, 'relay_woocommerce_pre_delete_order_refund'], 10, 3);
669 add_action('woocommerce_delete_order_refund', [$this, 'relay_woocommerce_delete_order_refund'], 10, 1 );
670 add_action('woocommerce_order_partially_refunded', [$this, 'relay_woocommerce_order_partially_refunded'], 10, 2);
671 add_filter('woocommerce_product_data_tabs', [$this, 'relay_woocommerce_product_data_tabs'], 98 );
672 add_action('woocommerce_product_data_panels', [$this, 'relay_woocommerce_product_data_panels'] );
673 add_action('woocommerce_process_product_meta', [$this, 'relay_woocommerce_process_product_meta'], 10, 2 );
674 add_action('add_meta_boxes', [$this, 'relay_add_meta_boxes'], 10, 2);
675 add_filter('manage_edit-product_columns', [$this, 'relay_manage_edit_product_columns']);
676 add_action('manage_product_posts_custom_column', [$this, 'relay_manage_product_posts_custom_column'], 2);
677 add_filter("manage_edit-product_sortable_columns", [$this, 'relay_manage_edit_product_sortable_columns']);
678 } else {
679 add_action('woocommerce_single_product_summary', [$this, 'relay_woocommerce_single_product_summary']);
680 }
681
682 // set routing -- NEEDS to be replaced by add_rewrite_rule later
683 add_action( 'template_redirect', [$this, 'wc_checkTicketDetailPage'], 1 );
684 //$this->wc_checkTicketDetailPage();
685 add_action('rest_api_init', function () {
686 SASO_EVENTTICKETS::setRestRoutesTicket();
687 // Vollstart Wallet REST API (only if enabled)
688 if ($this->getOptions()->isOptionCheckboxActive('walletVollstartEnable')) {
689 include_once plugin_dir_path(__FILE__) . 'includes/wallet/class-wallet-rest.php';
690 $walletRest = new sasoEventtickets_Wallet_REST($this);
691 $walletRest->register_routes();
692 $walletRest->register_cors();
693 }
694 });
695
696 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
697 add_filter('woocommerce_add_to_cart_validation', [$this, 'relay_woocommerce_add_to_cart_validation'], 10, 3);
698 add_filter('woocommerce_add_cart_item_data', [$this, 'relay_woocommerce_add_cart_item_data'], 10, 3);
699 add_action('woocommerce_add_to_cart', [$this, 'relay_woocommerce_add_to_cart'], 10, 6);
700 add_action('woocommerce_cart_item_removed', [$this, 'relay_woocommerce_cart_item_removed'], 10, 2);
701 add_action('woocommerce_after_cart_item_quantity_update', [$this, 'relay_woocommerce_after_cart_item_quantity_update'], 10, 4);
702 add_filter('woocommerce_update_cart_validation', [$this, 'relay_woocommerce_update_cart_validation'], 10, 4);
703 add_action('woocommerce_before_add_to_cart_button', [$this, 'relay_woocommerce_before_add_to_cart_button'], 15);
704
705 // Vollstart Wallet: "Add to Wallet" button on ticket detail page
706 add_action($this->_do_action_prefix . 'ticket_outputTicketInfo_after', [$this, 'wallet_render_ticket_button'], 10, 2);
707
708 // Vollstart Wallet: link in order emails
709 add_action($this->_do_action_prefix . 'woocommerce-hooks_woocommerce_email_order_meta', [$this, 'wallet_render_email_link'], 10, 4);
710
711 do_action( $this->_do_action_prefix.'main_WooCommercePluginLoaded' );
712 }
713
714 /**
715 * Render the "Add to Vollstart Wallet" button on the ticket detail page.
716 */
717 public function wallet_render_ticket_button(array $codeObj, bool $forPDFOutput): void {
718 if ($forPDFOutput) return;
719 if (!$this->getOptions()->isOptionCheckboxActive('walletVollstartEnable')) return;
720
721 $codeObj = $this->getCore()->setMetaObj($codeObj);
722 $metaObj = $codeObj['metaObj'];
723 $walletUrl = $metaObj['wc_ticket']['_wallet_url'] ?? '';
724 if (empty($walletUrl)) return;
725
726 echo '<p style="text-align:center;margin-top:10px;">';
727 echo '<a class="button" href="' . esc_url($walletUrl) . '" target="_blank" rel="noopener">';
728 echo esc_html__('Add to Vollstart Wallet', 'event-tickets-with-ticket-scanner');
729 echo '</a>';
730 echo '</p>';
731 }
732
733 /**
734 * Render Vollstart Wallet links in WooCommerce order emails.
735 */
736 public function wallet_render_email_link($order, bool $sent_to_admin, bool $plain_text, $email): void {
737 if ($sent_to_admin) return;
738 if (!$this->getOptions()->isOptionCheckboxActive('walletVollstartEnable')) return;
739
740 $codes = $this->getCore()->getCodesByOrderId($order->get_id());
741 if (empty($codes)) return;
742
743 $walletLinks = [];
744 foreach ($codes as $codeRow) {
745 try {
746 $codeObj = $this->getCore()->retrieveCodeByCode($codeRow['code']);
747 $codeObj = $this->getCore()->setMetaObj($codeObj);
748 $metaObj = $codeObj['metaObj'];
749 if (intval($metaObj['wc_ticket']['is_ticket'] ?? 0) !== 1) continue;
750 $walletUrl = $metaObj['wc_ticket']['_wallet_url'] ?? '';
751 if (empty($walletUrl)) continue;
752
753 $walletLinks[] = $walletUrl;
754 } catch (\Exception $e) {
755 continue;
756 }
757 }
758
759 if (empty($walletLinks)) return;
760
761 if ($plain_text) {
762 echo "\n" . __('Add to Vollstart Wallet', 'event-tickets-with-ticket-scanner') . ":\n";
763 foreach ($walletLinks as $url) {
764 echo $url . "\n";
765 }
766 } else {
767 echo '<p><b>' . esc_html__('Add to Vollstart Wallet', 'event-tickets-with-ticket-scanner') . '</b></p>';
768 foreach ($walletLinks as $url) {
769 echo '<p><a href="' . esc_url($url) . '" target="_blank" rel="noopener">' . esc_html__('Add to Wallet', 'event-tickets-with-ticket-scanner') . '</a></p>';
770 }
771 }
772 }
773
774 public function relay_woocommerce_after_shop_loop_item() {
775 $this->getWC()->getFrontendManager()->woocommerce_after_shop_loop_item_handler();
776 }
777 public function relay_woocommerce_add_to_cart_validation() {
778 $args = func_get_args();
779 return $this->getWC()->getFrontendManager()->woocommerce_add_to_cart_validation_handler(...$args);
780 }
781 public function relay_woocommerce_add_cart_item_data() {
782 $args = func_get_args();
783 return $this->getWC()->getFrontendManager()->woocommerce_add_cart_item_data_handler(...$args);
784 }
785 public function relay_woocommerce_add_to_cart() {
786 $args = func_get_args();
787 return $this->getWC()->getFrontendManager()->woocommerce_add_to_cart_handler(...$args);
788 }
789 public function relay_woocommerce_cart_item_removed() {
790 $args = func_get_args();
791 $this->getWC()->getFrontendManager()->woocommerce_cart_item_removed_handler(...$args);
792 }
793 public function relay_woocommerce_after_cart_item_quantity_update() {
794 $args = func_get_args();
795 $this->getWC()->getFrontendManager()->woocommerce_after_cart_item_quantity_update_handler(...$args);
796 }
797 public function relay_woocommerce_update_cart_validation() {
798 $args = func_get_args();
799 return $this->getWC()->getFrontendManager()->woocommerce_update_cart_validation_handler(...$args);
800 }
801 public function relay_woocommerce_before_add_to_cart_button() {
802 $this->getWC()->getFrontendManager()->woocommerce_before_add_to_cart_button_handler();
803 }
804 public function relay_woocommerce_review_order_after_cart_contents() {
805 $this->getWC()->getFrontendManager()->woocommerce_review_order_after_cart_contents();
806 }
807 public function relay_woocommerce_checkout_process() {
808 $this->getWC()->getFrontendManager()->woocommerce_checkout_process();
809 }
810 public function relay_woocommerce_before_cart_table() {
811 $this->getWC()->getFrontendManager()->woocommerce_before_cart_table();
812 }
813 public function relay_woocommerce_cart_updated() {
814 $this->getWC()->getFrontendManager()->woocommerce_cart_updated_handler();
815 }
816 public function relay_woocommerce_email_attachments() {
817 $args = func_get_args();
818 return $this->getWC()->getEmailHandler()->woocommerce_email_attachments(...$args);
819 }
820 public function relay_woocommerce_checkout_create_order_line_item() {
821 $args = func_get_args();
822 return $this->getWC()->getOrderManager()->woocommerce_checkout_create_order_line_item(...$args);
823 }
824 public function relay_woocommerce_check_cart_items() {
825 $this->getWC()->getFrontendManager()->woocommerce_check_cart_items();
826 }
827 public function relay_woocommerce_new_order() {
828 $args = func_get_args();
829 return $this->getWC()->getOrderManager()->woocommerce_new_order(...$args);
830 }
831 public function relay_woocommerce_checkout_update_order_meta() {
832 $args = func_get_args();
833 return $this->getWC()->getOrderManager()->woocommerce_checkout_update_order_meta(...$args);
834 }
835 public function relay_executeWCFrontend() {
836 return $this->getWC()->getFrontendManager()->executeWCFrontend();
837 }
838 public function relay_executeSeatingFrontend() {
839 return $this->getSeating()->getFrontendManager()->executeSeatingFrontend();
840 }
841 public function relay_woocommerce_delete_order() {
842 $args = func_get_args();
843 $this->getWC()->getOrderManager()->woocommerce_delete_order(...$args);
844 }
845 public function relay_woocommerce_delete_order_item() {
846 $args = func_get_args();
847 $this->getWC()->getOrderManager()->woocommerce_delete_order_item(...$args);
848 }
849 public function relay_woocommerce_pre_delete_order_refund() {
850 $args = func_get_args();
851 $this->getWC()->getOrderManager()->woocommerce_pre_delete_order_refund(...$args);
852 }
853 public function relay_woocommerce_delete_order_refund() {
854 $args = func_get_args();
855 $this->getWC()->getOrderManager()->woocommerce_delete_order_refund(...$args);
856 }
857 public function relay_woocommerce_product_data_tabs() {
858 $args = func_get_args();
859 return $this->getWC()->getProductManager()->woocommerce_product_data_tabs(...$args);
860 }
861 public function relay_woocommerce_product_data_panels() {
862 $this->getWC()->getProductManager()->woocommerce_product_data_panels();
863 }
864 public function relay_woocommerce_process_product_meta() {
865 $args = func_get_args();
866 $this->getWC()->getProductManager()->woocommerce_process_product_meta(...$args);
867 }
868 public function relay_add_meta_boxes(...$args) {
869 $this->getWC()->add_meta_boxes(...$args);
870 }
871 public function relay_manage_edit_product_columns() {
872 $args = func_get_args();
873 return $this->getWC()->getProductManager()->manage_edit_product_columns(...$args);
874 }
875 public function relay_manage_product_posts_custom_column() {
876 $args = func_get_args();
877 $this->getWC()->getProductManager()->manage_product_posts_custom_column(...$args);
878 }
879 public function relay_manage_edit_product_sortable_columns() {
880 $args = func_get_args();
881 return $this->getWC()->getProductManager()->manage_edit_product_sortable_columns(...$args);
882 }
883 public function relay_woocommerce_single_product_summary() {
884 $this->getWC()->getFrontendManager()->woocommerce_single_product_summary();
885 }
886 public function relay_woocommerce_order_status_changed() {
887 $args = func_get_args();
888 $this->getWC()->getOrderManager()->woocommerce_order_status_changed(...$args);
889 }
890 public function relay_woocommerce_order_partially_refunded() {
891 $args = func_get_args();
892 $this->getWC()->getOrderManager()->woocommerce_order_partially_refunded(...$args);
893 }
894 public function relay_woocommerce_order_item_display_meta_key() {
895 $args = func_get_args();
896 return $this->getWC()->getOrderManager()->woocommerce_order_item_display_meta_key(...$args);
897 }
898 public function relay_woocommerce_order_item_display_meta_value() {
899 $args = func_get_args();
900 return $this->getWC()->getOrderManager()->woocommerce_order_item_display_meta_value(...$args);
901 }
902 public function relay_wpo_wcpdf_after_item_meta() {
903 $args = func_get_args();
904 $this->getWC()->getOrderManager()->wpo_wcpdf_after_item_meta(...$args);
905 }
906 public function relay_woocommerce_order_item_meta_start() {
907 $args = func_get_args();
908 $this->getWC()->getOrderManager()->woocommerce_order_item_meta_start(...$args);
909 }
910 public function relay_woocommerce_product_after_variable_attributes() {
911 $args = func_get_args();
912 $this->getWC()->getProductManager()->woocommerce_product_after_variable_attributes(...$args);
913 }
914 public function relay_woocommerce_save_product_variation() {
915 $args = func_get_args();
916 $this->getWC()->getProductManager()->woocommerce_save_product_variation(...$args);
917 }
918 public function relay_woocommerce_email_order_meta() {
919 $args = func_get_args();
920 $this->getWC()->getEmailHandler()->woocommerce_email_order_meta(...$args);
921 }
922 public function relay_woocommerce_thankyou() {
923 $args = func_get_args();
924 $this->getWC()->getFrontendManager()->woocommerce_thankyou(...$args);
925 }
926 public function relay_sasoEventtickets_cronjob_daily() {
927 $this->getTicketHandler()->cronJobDaily();
928 $this->getAdmin()->cleanupOptionsHistory();
929 }
930
931 public function plugin_deactivated() {
932 $this->cronjob_daily_deactivate();
933 $this->getDB();
934 sasoEventticketsDB::plugin_deactivated();
935 do_action( $this->_do_action_prefix.'main_plugin_deactivated' );
936 }
937 public function plugin_uninstall() {
938 $this->getDB();
939 sasoEventticketsDB::plugin_uninstall();
940 $this->getAdmin();
941 sasoEventtickets_AdminSettings::plugin_uninstall();
942 do_action( $this->_do_action_prefix.'main_WooCommercePluginLoaded' );
943 }
944 public function plugin_activated($is_network_wide=false) { // und auch für updates, macht es einfacher
945 $this->getDB(); // um installiere Tabellen auszuführen
946 update_option('SASO_EVENTTICKETS_PLUGIN_VERSION', SASO_EVENTTICKETS_PLUGIN_VERSION);
947 // Only reset migration flag if options exist in wp_options (downgrade scenario).
948 // Do NOT blindly delete — the migration already removed options from wp_options,
949 // so deleting the flag would cause one request to read empty wp_options → all defaults.
950 $sentinelKey = $this->_prefix . 'qrAttachQRFilesToMailAsOnePDF';
951 if (get_option($sentinelKey, '__NOT_SET__') !== '__NOT_SET__') {
952 delete_option('saso_eventtickets_options_migrated');
953 }
954 $this->getAdmin()->generateFirstCodeList();
955 $this->cronjob_daily_activate();
956 do_action( $this->_do_action_prefix.'activated' );
957 do_action( $this->_do_action_prefix.'main_plugin_activated' );
958 }
959 public function plugins_loaded() {
960 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
961 }
962 public function initialize_plugin() {
963 $this->getDB(); // um installiere Tabellen auszuführen
964 do_action( $this->_do_action_prefix.'initialized' );
965 do_action( $this->_do_action_prefix.'main_initialize_plugin' );
966 }
967 function show_user_profile($profileuser) {
968 $this->getAdmin()->show_user_profile($profileuser);
969 do_action( $this->_do_action_prefix.'main_show_user_profile' );
970 }
971 function register_options_page() {
972 $allowed = $this->isUserAllowedToAccessAdminArea();
973 $allowed = apply_filters( $this->_add_filter_prefix.'main_options_page', $allowed );
974 if ($allowed) {
975 //add_options_page(__('Event Tickets', 'event-tickets-with-ticket-scanner'), 'Event Tickets', 'manage_options', 'event-tickets-with-ticket-scanner', [$this,'options_page']);
976 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 );
977 }
978 do_action( $this->_do_action_prefix.'main_register_options_page' );
979 }
980
981 function options_page() {
982 $allowed = $this->isUserAllowedToAccessAdminArea();
983 $allowed = apply_filters( $this->_add_filter_prefix.'main_options_page', $allowed );
984 if ( !$allowed ) {
985 wp_die( __( 'You do not have sufficient permissions to access this page.', 'event-tickets-with-ticket-scanner' ) );
986 }
987
988 wp_enqueue_style("wp-jquery-ui-dialog");
989 wp_enqueue_style(
990 'event-tickets-backend',
991 plugins_url('css/styles_backend.css', __FILE__),
992 array(),
993 $this->getPluginVersion()
994 );
995
996 $js_url = "jquery.qrcode.min.js?_v=".$this->_js_version;
997 wp_register_script('ajax_script2', plugins_url( "3rd/".$js_url,__FILE__ ), array('jquery', 'jquery-ui-dialog'));
998 wp_enqueue_script('ajax_script2');
999
1000 wp_enqueue_media(); // um die js wp.media lib zu laden
1001
1002 // einbinden das js starter skript
1003 $js_url = $this->_js_file."?_v=".$this->_js_version;
1004 if (defined( 'WP_DEBUG')) $js_url .= '&debug=1';
1005 wp_register_script('ajax_script_backend', plugins_url( $js_url,__FILE__ ), array('jquery', 'jquery-ui-dialog', 'wp-i18n'));
1006 wp_enqueue_script('ajax_script_backend');
1007 wp_set_script_translations('ajax_script_backend', 'event-tickets-with-ticket-scanner', __DIR__.'/languages');
1008
1009 // per script eine variable einbinden, die url hat den wp-admin prefix
1010 // damit im backend.js dann die richtige callback url genutzt werden kann
1011 $vars = array(
1012 '_plugin_home_url' =>plugins_url( "",__FILE__ ),
1013 '_plugin_version' => $this->getPluginVersion(),
1014 '_action' => $this->_prefix.'_executeAdminSettings',
1015 '_max'=>$this->getBase()->getMaxValues(),
1016 '_isPremium'=>$this->isPremium(),
1017 '_isUserLoggedin'=>is_user_logged_in(),
1018 '_premJS'=>$this->isPremium() && method_exists($this->getPremiumFunctions(), "getJSBackendFile") ? $this->getPremiumFunctions()->getJSBackendFile() : '',
1019 'url' => admin_url( 'admin-ajax.php' ),
1020 'ticket_url' => $this->getCore()->getTicketURLPath(),
1021 'nonce' => wp_create_nonce( $this->_js_nonce ),
1022 'ajaxActionPrefix' => $this->_prefix,
1023 'divPrefix' => $this->_prefix,
1024 'divId' => $this->_divId,
1025 'jsFiles' => plugins_url( 'backend.js?_v='.$this->_js_version.'&_f='.filemtime(__DIR__.'/backend.js'),__FILE__ )
1026 );
1027 // Version notices for "What's New" banner
1028 $versionNoticesFile = __DIR__ . '/version-notices.json';
1029 $vars['_versionNotices'] = file_exists($versionNoticesFile) ? json_decode(file_get_contents($versionNoticesFile), true) : [];
1030
1031 $vars = apply_filters( $this->_add_filter_prefix.'main_options_page', $vars );
1032 wp_localize_script(
1033 'ajax_script_backend',
1034 'Ajax_'.$this->_prefix, // name der injected variable
1035 $vars
1036 );
1037
1038 do_action( $this->_do_action_prefix.'main_options_page' );
1039
1040 $versions = $this->getPluginVersions();
1041 $versions_tail = $versions['basic'].($versions['premium'] != "" ? ', Premium: '.$versions['premium'] : '');
1042 $version_tail_add = "";
1043 if ($versions['debug'] != "") $version_tail_add .= 'DEBUG: '.$versions['debug'].', LANG: '.determine_locale();
1044 ?>
1045 <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>
1046 <div class="event-tickets-with-ticket-scanner-admin-page">
1047 <div class="event-tickets-with-ticket-scanner-header">
1048 <div class="event-tickets-with-ticket-scanner-header-left">
1049 <img src="<?php echo plugins_url( "",__FILE__ ); ?>/img/logo_event-tickets-with-ticket-scanner.gif"
1050 alt="Event Tickets"
1051 class="event-tickets-with-ticket-scanner-header-logo">
1052
1053 <div class="event-tickets-with-ticket-scanner-header-title">
1054 <div class="event-tickets-with-ticket-scanner-header-name">
1055 Event Tickets with Ticket Scanner
1056 </div>
1057 <div class="event-tickets-with-ticket-scanner-header-meta">
1058 <?php esc_html_e('Version', 'event-tickets-with-ticket-scanner'); ?>: <?php echo $versions_tail; ?> <?php echo $version_tail_add; ?>
1059 </div>
1060 </div>
1061 </div>
1062
1063 <div class="event-tickets-with-ticket-scanner-header-right" id="event-tickets-with-ticket-scanner-header-actions">
1064 <!-- Button kommt via JS -->
1065 </div>
1066 </div>
1067
1068 <div style="clear:both;" data-id="plugin_addons"></div>
1069 <div style="clear:both;" data-id="plugin_info_area"></div>
1070 <div style="clear:both;" id="<?php echo esc_attr($this->_divId); ?>" class="et-content-area">
1071 <div class="et-loading"><span class="lds-dual-ring"></span></div>
1072 </div>
1073 <div class="et-footer">
1074 <a name="shortcodedetails"></a>
1075 <div class="et-footer-grid">
1076 <div class="et-card et-footer-card">
1077 <div class="et-card-header">
1078 <span class="dashicons dashicons-book" style="color:#9333ea;margin-right:6px;"></span>
1079 Documentation
1080 </div>
1081 <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>
1082 <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>
1083 </div>
1084 <div class="et-card et-footer-card">
1085 <div class="et-card-header">
1086 <span class="dashicons dashicons-star-filled" style="color:#f59e0b;margin-right:6px;"></span>
1087 <?php esc_html_e('Plugin Rating', 'event-tickets-with-ticket-scanner'); ?>
1088 </div>
1089 <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>
1090 </div>
1091 <div class="et-card et-footer-card">
1092 <div class="et-card-header">
1093 <span class="dashicons dashicons-superhero" style="color:#9333ea;margin-right:6px;"></span>
1094 <?php esc_html_e('Premium Homepage', 'event-tickets-with-ticket-scanner'); ?>
1095 </div>
1096 <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>
1097 </div>
1098 </div>
1099
1100 <div class="et-card et-footer-shortcodes">
1101 <div class="et-card-header">
1102 <span class="dashicons dashicons-shortcode" style="color:#9333ea;margin-right:6px;"></span>
1103 <?php esc_html_e('Shortcodes', 'event-tickets-with-ticket-scanner'); ?>
1104 </div>
1105
1106 <h3><?php esc_html_e('Shortcode to display the event calendar form within a page', 'event-tickets-with-ticket-scanner'); ?></h3>
1107 <b>[<?php echo esc_html($this->_shortcode_eventviews); ?>]</b>
1108 <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>
1109 <ul>
1110 <li>months_to_show - Values can be a number higher than 0. Default: 3</li>
1111 </ul>
1112 <p>CSS file: <a href="<?php echo plugins_url( "",__FILE__ ); ?>/css/calendar.css" target="_blank">calendar.css</a></p>
1113
1114 <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>
1115 <b>[<?php echo esc_html($this->_shortcode_mycode); ?>]</b>
1116 <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>
1117 <ul>
1118 <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>
1119 <?php esc_html_e('Example:', 'event-tickets-with-ticket-scanner'); ?> [<?php echo esc_html($this->_shortcode_mycode); ?> order_id="123"]</li>
1120 <li><b>format</b> - <?php esc_html_e('Output format. Values: json', 'event-tickets-with-ticket-scanner'); ?></li>
1121 <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>
1122 <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>
1123 </ul>
1124 <p>
1125 <?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"]
1126 </p>
1127
1128 <h3><?php esc_html_e('Shortcode to display the ticket scanner within a page', 'event-tickets-with-ticket-scanner'); ?></h3>
1129 <?php esc_html_e('Useful if you cannot open the ticket scanner due to security issues.', 'event-tickets-with-ticket-scanner'); ?><br>
1130 <b>[<?php echo esc_html($this->_shortcode_ticket_scanner); ?>]</b>
1131
1132 <h3><?php esc_html_e('Shortcode to display ticket detail view within a page', 'event-tickets-with-ticket-scanner'); ?></h3>
1133 <?php esc_html_e('Useful if the /ticket/ URL path does not work on your server.', 'event-tickets-with-ticket-scanner'); ?><br>
1134 <b>[<?php echo esc_html($this->_shortcode_ticket_detail); ?>]</b>
1135 <p>
1136 <?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>
1137 <?php esc_html_e('Example:', 'event-tickets-with-ticket-scanner'); ?> yoursite.com/ticket-page/?ticket=ABC-123-XYZ<br>
1138 <?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"]
1139 </p>
1140
1141 <h3><?php esc_html_e('PHP Filters', 'event-tickets-with-ticket-scanner'); ?></h3>
1142 <p><?php esc_html_e('You can use PHP code to register your filter functions for the validation check.', 'event-tickets-with-ticket-scanner'); ?>
1143 <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>
1144 </p>
1145 <ul>
1146 <li>add_filter('<?php echo $this->_add_filter_prefix.'beforeCheckCodePre'; ?>', 'myfunc', 20, 1)</li>
1147 <li>add_filter('<?php echo $this->_add_filter_prefix.'beforeCheckCode'; ?>', 'myfunc', 20, 1)</li>
1148 <li>add_filter('<?php echo $this->_add_filter_prefix.'afterCheckCodePre'; ?>', 'myfunc', 20, 1)</li>
1149 <li>add_filter('<?php echo $this->_add_filter_prefix.'afterCheckCode'; ?>', 'myfunc', 20, 1)</li>
1150 </ul>
1151 <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>
1152 </div>
1153
1154 <div class="et-footer-credits">
1155 <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>
1156 </div>
1157 </div>
1158 </div>
1159 <?php
1160 do_action( $this->_do_action_prefix.'options_page' );
1161 }
1162
1163 public function isUserAllowedToAccessAdminArea() {
1164 if ($this->isAllowedAccess != null) return $this->isAllowedAccess;
1165 if ($this->getOptions()->isOptionCheckboxActive('allowOnlySepcificRoleAccessToAdmin')) {
1166 // check welche rollen
1167 $user = wp_get_current_user();
1168 $user_roles = (array) $user->roles;
1169 if (in_array("administrator", $user_roles)) {
1170 $this->isAllowedAccess = true;
1171 } else {
1172 $adminAreaAllowedRoles = $this->getOptions()->getOptionValue('adminAreaAllowedRoles');
1173 if (!is_array($adminAreaAllowedRoles)) {
1174 if (empty($adminAreaAllowedRoles)) {
1175 $adminAreaAllowedRoles = [];
1176 } else {
1177 $adminAreaAllowedRoles = [$adminAreaAllowedRoles];
1178 }
1179 }
1180 foreach($adminAreaAllowedRoles as $role_name) {
1181 if (in_array($role_name, $user_roles)) {
1182 $this->isAllowedAccess = true;
1183 break;
1184 };
1185 }
1186 }
1187 } else {
1188 // Standard: Only administrators have access
1189 $this->isAllowedAccess = current_user_can('manage_options');
1190 }
1191 $this->isAllowedAccess = apply_filters( $this->_add_filter_prefix.'main_isUserAllowedToAccessAdminArea', $this->isAllowedAccess );
1192 return $this->isAllowedAccess;
1193 }
1194
1195 public function executeAdminSettings_a() {
1196 if (!SASO_EVENTTICKETS::issetRPara('a_sngmbh')) return wp_send_json_success("a_sngmbh not provided");
1197 return $this->executeAdminSettings(SASO_EVENTTICKETS::getRequestPara('a_sngmbh')); // to prevent WP adds parameters
1198 }
1199
1200 public function executeAdminSettings($a=0, $data=null) {
1201 if (!$this->isUserAllowedToAccessAdminArea()) {
1202 return wp_send_json_error("Access denied", 403);
1203 }
1204 if ($a === 0 && !SASO_EVENTTICKETS::issetRPara('a_sngmbh')) return wp_send_json_success("a not provided");
1205
1206 if ($data == null) {
1207 $data = SASO_EVENTTICKETS::issetRPara('data') ? SASO_EVENTTICKETS::getRequestPara('data') : [];
1208 }
1209 if ($a === 0 || empty($a) || trim($a) == "") {
1210 $a = SASO_EVENTTICKETS::getRequestPara('a_sngmbh');
1211 }
1212 do_action( $this->_do_action_prefix.'executeAdminSettings', $a, $data );
1213 return $this->getAdmin()->executeJSON($a, $data, false, false); // with nonce check
1214 }
1215
1216 public function executeSeatingAdmin_a() {
1217 return $this->executeSeatingAdmin(SASO_EVENTTICKETS::getRequestPara('a'));
1218 }
1219
1220 public function executeSeatingAdmin($a = '', $data = null) {
1221 if (!$this->isUserAllowedToAccessAdminArea()) {
1222 return wp_send_json_error('Access denied', 403);
1223 }
1224 if (empty($a) && !SASO_EVENTTICKETS::issetRPara('a')) {
1225 return wp_send_json_error('a not provided');
1226 }
1227 if ($data === null) {
1228 $data = SASO_EVENTTICKETS::getRequest();
1229 }
1230 if (empty($a)) {
1231 $a = SASO_EVENTTICKETS::getRequestPara('a');
1232 }
1233 return $this->getSeating()->getAdminHandler()->executeSeatingJSON($a, $data);
1234 }
1235
1236 public function executeFrontend_a() {
1237 return $this->executeFrontend(); // to prevent WP adds parameters
1238 }
1239
1240 public function executeWCBackend() {
1241 if (!$this->isUserAllowedToAccessAdminArea()) {
1242 return wp_send_json_error("Access denied", 403);
1243 }
1244 if (!SASO_EVENTTICKETS::issetRPara('a_sngmbh')) return wp_send_json_success("a_sngmbh not provided");
1245 $data = SASO_EVENTTICKETS::issetRPara('data') ? SASO_EVENTTICKETS::getRequestPara('data') : [];
1246 return $this->getWC()->executeJSON(SASO_EVENTTICKETS::getRequestPara('a_sngmbh'), $data);
1247 }
1248
1249 public function executeFrontend($a=0, $data=null) {
1250 $sasoEventtickets_Frontend = $this->getFrontend();
1251 if ($a === 0 && !SASO_EVENTTICKETS::issetRPara('a_sngmbh')) return wp_send_json_success("a not provided");
1252
1253 if ($data == null) {
1254 $data = SASO_EVENTTICKETS::issetRPara('data') ? SASO_EVENTTICKETS::getRequestPara('data') : [];
1255 }
1256 if ($a === 0 || empty($a) || trim($a) == "") {
1257 $a = SASO_EVENTTICKETS::getRequestPara('a_sngmbh');
1258 }
1259 do_action( $this->_do_action_prefix.'executeFrontend', $a, $data );
1260 return $sasoEventtickets_Frontend->executeJSON($a, $data);
1261 }
1262
1263 public function replacingShortcode($attr=[], $content = null, $tag = '') {
1264 add_filter( $this->_add_filter_prefix.'replaceShortcode', [$this, 'replaceShortcode'], 10, 3 );
1265 $ret = apply_filters( $this->_add_filter_prefix.'replaceShortcode', $attr, $content, $tag );
1266 return $ret;
1267 }
1268
1269 public function setTicketScannerJS() {
1270 wp_enqueue_style("wp-jquery-ui-dialog");
1271
1272 $js_url = "jquery.qrcode.min.js?_v=".$this->getPluginVersion();
1273 wp_enqueue_script(
1274 'ajax_script2',
1275 plugins_url( "3rd/".$js_url,__FILE__ ),
1276 array('jquery', 'jquery-ui-dialog')
1277 );
1278
1279 $js_url = plugin_dir_url(__FILE__)."3rd/html5-qrcode.min.js?_v=".$this->getPluginVersion();
1280 wp_register_script('html5-qrcode', $js_url, array('jquery', 'jquery-ui-dialog'));
1281 wp_enqueue_script('html5-qrcode');
1282
1283 // https://github.com/nimiq/qr-scanner - NEW scanner lib
1284 $js_url = plugin_dir_url(__FILE__)."3rd/qr-scanner-1.4.2/qr-scanner.umd.min.js?_v=".$this->getPluginVersion();
1285 wp_register_script('qr-scanner', $js_url, array('jquery', 'jquery-ui-dialog'));
1286 wp_enqueue_script('qr-scanner');
1287
1288 $js_url = "ticket_scanner.js?_v=".$this->getPluginVersion();
1289 if (defined('WP_DEBUG')) $js_url .= '&t='.time();
1290 $js_url = plugins_url( $js_url,__FILE__ );
1291 wp_register_script('ajax_script_ticket_scanner', $js_url, array('jquery', 'jquery-ui-dialog', 'wp-i18n'));
1292 wp_enqueue_script('ajax_script_ticket_scanner');
1293 wp_set_script_translations('ajax_script_ticket_scanner', 'event-tickets-with-ticket-scanner', __DIR__.'/languages');
1294
1295 $ticketScannerDontRememberCamChoice = $this->getOptions()->isOptionCheckboxActive("ticketScannerDontRememberCamChoice") ? true : false;
1296
1297 $pwaEnabled = $this->getOptions()->isOptionCheckboxActive('ticketScannerPWA');
1298 $vars = [
1299 'root' => esc_url_raw( rest_url() ),
1300 '_plugin_home_url' =>plugins_url( "",__FILE__ ),
1301 '_action' => $this->_prefix.'_executeAdminSettings',
1302 '_isPremium'=>$this->isPremium(),
1303 '_isUserLoggedin'=>is_user_logged_in(),
1304 '_userId'=>get_current_user_id(),
1305 '_restPrefixUrl'=>SASO_EVENTTICKETS::getRESTPrefixURL(),
1306 '_siteUrl'=>get_site_url(),
1307 '_params'=>["auth"=>$this->getAuthtokenHandler()::$authtoken_param],
1308 '_pwaSWUrl'=>$pwaEnabled ? rest_url(SASO_EVENTTICKETS::getRESTPrefixURL().'/ticket/scanner/pwa-sw') : '',
1309 //'url' => admin_url( 'admin-ajax.php' ), // not used for now in ticketscanner.js
1310 'url' => rest_get_server(), // not used for now in ticketscanner.js
1311 'nonce' => wp_create_nonce( 'wp_rest' ),
1312 //'nonce' => wp_create_nonce( $this->_js_nonce ),
1313 'ajaxActionPrefix' => $this->_prefix,
1314 'wcTicketCompatibilityModeRestURL' => $this->getOptions()->getOptionValue('wcTicketCompatibilityModeRestURL', ''),
1315 'IS_PRETTY_PERMALINK_ACTIVATED' => get_option('permalink_structure') ? true :false,
1316 'ticketScannerDontRememberCamChoice' => $ticketScannerDontRememberCamChoice,
1317 'ticketScannerStartCamWithoutButtonClicked' => $this->getOptions()->isOptionCheckboxActive('ticketScannerStartCamWithoutButtonClicked'),
1318 'ticketScannerDontShowOptionControls' => $this->getOptions()->isOptionCheckboxActive('ticketScannerDontShowOptionControls'),
1319 'ticketScannerScanAndRedeemImmediately' => $this->getOptions()->isOptionCheckboxActive('ticketScannerScanAndRedeemImmediately'),
1320 'ticketScannerHideTicketInformation' => $this->getOptions()->isOptionCheckboxActive('ticketScannerHideTicketInformation'),
1321 'ticketScannerHideTicketInformationShowShortDesc' => $this->getOptions()->isOptionCheckboxActive('ticketScannerHideTicketInformationShowShortDesc'),
1322 'ticketScannerDontShowBtnPDF' => $this->getOptions()->isOptionCheckboxActive('ticketScannerDontShowBtnPDF'),
1323 'ticketScannerDontShowBtnBadge' => $this->getOptions()->isOptionCheckboxActive('ticketScannerDontShowBtnBadge'),
1324 'ticketScannerDisplayTimes' => $this->getOptions()->isOptionCheckboxActive('ticketScannerDisplayTimes'),
1325 'ticketScannerThemeColor' => $this->getOptions()->getOptionValue('ticketScannerThemeColor', '#2e74b5'),
1326 'ticketScannerVibrate' => $this->getOptions()->isOptionCheckboxActive('ticketScannerVibrate')
1327 ];
1328 $vars = apply_filters( $this->_add_filter_prefix.'main_setTicketScannerJS', $vars );
1329 wp_localize_script(
1330 'ajax_script_ticket_scanner',
1331 'Ajax_'.$this->_prefix, // name der injected variable
1332 $vars
1333 );
1334
1335 do_action( $this->_do_action_prefix.'main_setTicketScannerJS', $js_url );
1336 }
1337
1338 public function replacingShortcodeTicketScanner($attr=[], $content = null, $tag = '') {
1339 $this->setTicketScannerJS();
1340 return '
1341 <center>
1342 <div style="width:90%;max-width:1024px;">'.$this->getTicketHandler()->getTicketScannerHTMLBoilerplate().'
1343 </div>
1344 </center>
1345 ';
1346 }
1347
1348 /**
1349 * Shortcode to display ticket detail view on any page
1350 * Usage: [sasoEventTicketsValidator_ticket_detail] with ?ticket=CODE in URL
1351 * Or: [sasoEventTicketsValidator_ticket_detail code="TICKET-CODE"]
1352 */
1353 public function replacingShortcodeTicketDetail($attr = [], $content = null, $tag = ''): string {
1354 $code = '';
1355 if (!empty($attr['code'])) {
1356 $code = sanitize_text_field($attr['code']);
1357 } elseif (isset($_GET['ticket'])) {
1358 $code = sanitize_text_field($_GET['ticket']);
1359 }
1360
1361 if (empty($code)) {
1362 return '<p>' . esc_html__('No ticket code provided. Use ?ticket=YOUR-CODE in the URL.', 'event-tickets-with-ticket-scanner') . '</p>';
1363 }
1364
1365 // Build a fake request URI for the ticket
1366 $ticketPath = $this->getCore()->getTicketURLPath(true);
1367 $fakeUri = $ticketPath . $code;
1368
1369 include_once plugin_dir_path(__FILE__) . "sasoEventtickets_Ticket.php";
1370 $ticketInstance = sasoEventtickets_Ticket::Instance($fakeUri);
1371
1372 return $ticketInstance->renderTicketDetailForShortcode();
1373 }
1374
1375 public function getCodesTextAsShortList($codes) {
1376 $ret = "";
1377 if (count($codes) > 0) {
1378 $ret = '<table>';
1379 $wcTicketUserProfileDisplayTicketDetailURL = $this->getOptions()->isOptionCheckboxActive("wcTicketUserProfileDisplayTicketDetailURL");
1380 $wcTicketUserProfileDisplayRedeemAmount = $this->getOptions()->isOptionCheckboxActive("wcTicketUserProfileDisplayRedeemAmount");
1381
1382 $label_expired = $this->getOptions()->getOptionValue('wcTicketTransTicketExpired', 'EXPIRED');
1383 $label_stolen = $this->getOptions()->getOptionValue('wcTicketTransTicketIsStolen', 'REPORTED AS STOLEN');
1384 $label_notvalid = $this->getOptions()->getOptionValue('wcTicketTransTicketNotValid', 'DISABLED');
1385
1386 $myCodes = [];
1387 foreach($codes as $idx => $codeObj) {
1388 $metaObj = $this->getCore()->encodeMetaValuesAndFillObject($codeObj['meta'], $codeObj);
1389
1390 $_c = '<tr><td style="text-align:right;">'.($idx + 1).'.</td><td>'.$codeObj['code_display'].'</td><td>';
1391 if ($codeObj['aktiv'] == 1) {
1392 if ($this->getCore()->checkCodeExpired($codeObj)) {
1393 $_c .= $label_expired;
1394 }
1395 } else if ($codeObj['aktiv'] == 0) {
1396 $_c .= $label_notvalid;
1397 } else if ($codeObj['aktiv'] == 2) {
1398 $_c .= $label_stolen;
1399 }
1400 $_c .= '</td>';
1401
1402 if ($wcTicketUserProfileDisplayTicketDetailURL) {
1403 $_c .= "<td>";
1404 $url = $this->getCore()->getTicketURL($codeObj, $metaObj);
1405 if (!empty($url)) {
1406 $_c .= '<a href="'.$url.'" target="_blank">Ticket Details</a>';
1407 }
1408 $_c .= "</td>";
1409 }
1410 if ($wcTicketUserProfileDisplayRedeemAmount && function_exists("wc_get_product")) {
1411 $_c .= "<td>";
1412 $text_redeem_amount = $this->getTicketHandler()->getRedeemAmountText($codeObj, $metaObj, false);
1413 if (!empty($text_redeem_amount)) {
1414 $_c .= $text_redeem_amount;
1415 }
1416 $_c .= "<td>";
1417 }
1418 $_c .= "</tr>";
1419 $myCodes[] = $_c;
1420 }
1421 $ret .= implode("", $myCodes);
1422 $ret .= "</table>";
1423 }
1424 $ret = apply_filters( $this->_add_filter_prefix.'main_getCodesTextAsShortList', $ret, $codes );
1425 return $ret;
1426 }
1427
1428 public function getMyCodeText($user_id, $attr=[], $content = null, $tag = '', $codes = null) {
1429 $ret = '';
1430 // check ob eingeloggt
1431 $pre_text = $this->getOptions()->getOptionValue('userDisplayCodePrefix', '');
1432 if (!empty($pre_text)) $pre_text .= " ";
1433
1434 // If codes are provided (e.g., from order_id), use them; otherwise fetch by user_id
1435 if ($codes === null && $user_id > 0) {
1436 $codes = $this->getCore()->getCodesByRegUserId($user_id);
1437 }
1438
1439 if ($codes !== null && count($codes) > 0) {
1440 $ret .= "<b>".$pre_text."</b><br>";
1441 $ret .= $this->getCodesTextAsShortList($codes);
1442
1443 // Download All as PDF button
1444 $show_download_btn = isset($attr['download_all_pdf']) &&
1445 in_array(strtolower($attr['download_all_pdf']), ['true', '1', 'yes'], true);
1446
1447 if ($show_download_btn && count($codes) > 0) {
1448 $max_tickets = isset($attr['download_all_pdf_max']) ? intval($attr['download_all_pdf_max']) : 100;
1449 $btn_label = isset($attr['download_all_pdf_label']) ?
1450 sanitize_text_field($attr['download_all_pdf_label']) :
1451 __('Download All Tickets as PDF', 'event-tickets-with-ticket-scanner');
1452
1453 $ticket_count = count($codes);
1454 if ($ticket_count > $max_tickets) {
1455 $ret .= '<p><em>' . sprintf(
1456 /* translators: %d: maximum number of tickets */
1457 esc_html__('Too many tickets (%1$d). Maximum %2$d tickets can be downloaded at once.', 'event-tickets-with-ticket-scanner'),
1458 $ticket_count,
1459 $max_tickets
1460 ) . '</em></p>';
1461 } else {
1462 // Generate secure download URL
1463 $nonce = wp_create_nonce('download_my_codes_pdf_' . $user_id);
1464 $download_url = admin_url('admin-ajax.php') . '?' . http_build_query([
1465 'action' => $this->_prefix . '_downloadMyCodesAsPDF',
1466 'nonce' => $nonce
1467 ]);
1468 $ret .= '<p style="margin-top:10px;"><a href="' . esc_url($download_url) . '" class="button" target="_blank">' .
1469 esc_html($btn_label) . ' (' . $ticket_count . ')</a></p>';
1470 }
1471 }
1472 }
1473 if (empty($ret) && $this->getOptions()->isOptionCheckboxActive('userDisplayCodePrefixAlways')) {
1474 $ret .= $pre_text;
1475 }
1476 $ret = apply_filters( $this->_add_filter_prefix.'main_getMyCodeText', $ret, $user_id, $attr, $content, $tag);
1477 return $ret;
1478 }
1479
1480 /**
1481 * AJAX handler: Download all user's tickets as one PDF
1482 * Used by shortcode [sasoEventTicketsValidator_code download_all_pdf="true"]
1483 */
1484 public function downloadMyCodesAsPDF(): void {
1485 $user_id = get_current_user_id();
1486
1487 // Must be logged in
1488 if ($user_id <= 0) {
1489 wp_die(esc_html__('You must be logged in to download tickets.', 'event-tickets-with-ticket-scanner'), 403);
1490 }
1491
1492 // Verify nonce
1493 $nonce = isset($_GET['nonce']) ? sanitize_text_field($_GET['nonce']) : '';
1494 if (!wp_verify_nonce($nonce, 'download_my_codes_pdf_' . $user_id)) {
1495 wp_die(esc_html__('Security check failed. Please refresh the page and try again.', 'event-tickets-with-ticket-scanner'), 403);
1496 }
1497
1498 // Get user's codes
1499 $codes = $this->getCore()->getCodesByRegUserId($user_id);
1500
1501 if (empty($codes)) {
1502 wp_die(esc_html__('No tickets found.', 'event-tickets-with-ticket-scanner'), 404);
1503 }
1504
1505 // Limit to 100 tickets
1506 $max_tickets = 100;
1507 if (count($codes) > $max_tickets) {
1508 wp_die(
1509 sprintf(
1510 /* translators: %d: maximum number of tickets */
1511 esc_html__('Too many tickets. Maximum %d tickets can be downloaded at once.', 'event-tickets-with-ticket-scanner'),
1512 $max_tickets
1513 ),
1514 400
1515 );
1516 }
1517
1518 // Extract code strings
1519 $code_strings = array_map(function($codeObj) {
1520 return $codeObj['code'];
1521 }, $codes);
1522
1523 // Generate merged PDF
1524 $filename = 'my_tickets_' . wp_date('Ymd_Hi') . '.pdf';
1525
1526 try {
1527 $this->getTicketHandler()->generateOnePDFForCodes($code_strings, $filename, 'I');
1528 } catch (Exception $e) {
1529 $this->getAdmin()->logErrorToDB($e, null, 'downloadMyCodesAsPDF');
1530 wp_die(esc_html__('Error generating PDF. Please try again later.', 'event-tickets-with-ticket-scanner'), 500);
1531 }
1532
1533 exit;
1534 }
1535
1536 public function getMyCodeFormatted($user_id, $attr=[], $content = null, $tag = '', $codes = null) {
1537 $format = "json";
1538 if (isset($attr["format"])) {
1539 $format = strtolower(trim(sanitize_key($attr["format"])));
1540 }
1541 $display = ["codes"];
1542 if (isset($attr["display"])) {
1543
1544 $_d = trim(sanitize_text_field($attr["display"]));
1545 $_da = explode(",", $_d);
1546 if (count($_da) > 0) {
1547 $display = [];
1548 }
1549 foreach ($_da as $item) {
1550 $item = trim($item);
1551 $display[] = $item;
1552 }
1553 }
1554
1555 $output = [];
1556 //codes,validation,user,used,confirmedCount,woocommerce,wc_rp,wc_ticket
1557 // If codes are provided (e.g., from order_id), use them; otherwise fetch by user_id
1558 if ($codes === null) {
1559 $codes = $this->getCore()->getCodesByRegUserId($user_id);
1560 }
1561 $metas = [];
1562 foreach($codes as $codeObj) {
1563 $metas[$codeObj["code"]] = $this->getCore()->encodeMetaValuesAndFillObject($codeObj['meta'], $codeObj);
1564 }
1565 foreach ($display as $item) {
1566 $output[$item] = [];
1567 if ($item == "codes") {
1568 foreach($codes as $codeObj) {
1569 if (isset($codeObj["meta"])) {
1570 unset($codeObj["meta"]);
1571 }
1572 $output[$item][] = $codeObj;
1573 }
1574 } elseif($item == "confirmedCount") {
1575 foreach($metas as $key => $meta) {
1576 $output[$item][] = array_merge(["value"=>$meta[$item]], ["code"=>$key]);
1577 }
1578 } else {
1579 foreach($metas as $key => $m) {
1580 if (is_array($m) && isset($m[$item])) {
1581 $meta = $m[$item];
1582 if (isset($meta["stats_redeemed"])) {
1583 unset($meta["stats_redeemed"]);
1584 }
1585 if (isset($meta["set_by_admin"])) {
1586 unset($meta["set_by_admin"]);
1587 }
1588 if (isset($meta["redeemed_by_admin"])) {
1589 unset($meta["redeemed_by_admin"]);
1590 }
1591 $output[$item][] = array_merge($meta, ["code"=>$key]);
1592 }
1593 }
1594 }
1595 }
1596
1597 switch($format) {
1598 case "json":
1599 default:
1600 $ret = $this->getCore()->json_encode_with_error_handling($output);
1601 }
1602 $ret = apply_filters( $this->_add_filter_prefix.'main_getMyCodeFormatted', $ret, $user_id, $attr, $content, $tag, $output);
1603 return $ret;
1604 }
1605
1606 public function replacingShortcodeMyCode($attr=[], $content = null, $tag = '') {
1607 $user_id = get_current_user_id();
1608 $codes = null; // Will be set if order_id is used
1609
1610 // Check if order_id parameter is provided
1611 if (isset($attr['order_id']) && !empty($attr['order_id'])) {
1612 $order_id = intval($attr['order_id']);
1613 if ($order_id > 0) {
1614 // Security check: can current user access this order?
1615 if (!$this->canUserAccessOrder($order_id)) {
1616 return '<p>' . esc_html__('You do not have permission to view tickets for this order.', 'event-tickets-with-ticket-scanner') . '</p>';
1617 }
1618 // Get codes by order_id
1619 $codes = $this->getCore()->getCodesByOrderId($order_id);
1620 }
1621 }
1622
1623 if (count($attr) > 0 && isset($attr["format"])) {
1624 return $this->getMyCodeFormatted($user_id, $attr, $content, $tag, $codes);
1625 } else {
1626 return $this->getMyCodeText($user_id, $attr, $content, $tag, $codes);
1627 }
1628 }
1629
1630 /**
1631 * Check if current user can access a specific order's tickets
1632 *
1633 * @param int $order_id WooCommerce order ID
1634 * @return bool True if user can access, false otherwise
1635 */
1636 private function canUserAccessOrder(int $order_id): bool {
1637 $order = wc_get_order($order_id);
1638 if (!$order) {
1639 return false;
1640 }
1641
1642 // 1. Admin/Shop-Manager can access all orders
1643 if (current_user_can('manage_woocommerce')) {
1644 return true;
1645 }
1646
1647 $current_user_id = get_current_user_id();
1648
1649 // 2. Logged-in user owns this order
1650 if ($current_user_id > 0 && $order->get_user_id() == $current_user_id) {
1651 return true;
1652 }
1653
1654 // 3. Valid order key in URL (WooCommerce thank-you page pattern)
1655 $order_key_from_url = isset($_GET['key']) ? sanitize_text_field($_GET['key']) : '';
1656 if (!empty($order_key_from_url) && $order->get_order_key() === $order_key_from_url) {
1657 return true;
1658 }
1659
1660 return false;
1661 }
1662
1663 public function replacingShortcodeFeatureList($attr=[], $content = null, $tag = '') {
1664 //$features = $this->getAdmin()->getOptions();
1665 //$options = $features["options"];
1666 $options = $this->getOptions()->getOptions();
1667 $features = [];
1668 $act_heading = "";
1669 foreach ($options as $option) {
1670 if ($option["key"] == "serial") continue;
1671 if ($option["type"] == "heading") {
1672 $act_heading = $option["key"];
1673 $features[$act_heading] = ["heading"=>$option, "options"=>[]];
1674 } else {
1675 if ($act_heading != "") {
1676 $features[$act_heading]["options"][] = $option;
1677 }
1678 }
1679 }
1680
1681 $ret = "";
1682 uasort($features, function($a, $b) {
1683 return strnatcasecmp($a["heading"]["label"], $b["heading"]["label"]);
1684 });
1685 foreach ($features as $key => $feature) {
1686 $ret .= '<h3>'.$feature["heading"]["label"].'</h3>';
1687 $video = $feature["heading"]["_doc_video"] != "" ? ' <a href="'.$feature["heading"]["_doc_video"].'" target="_blank"><span class="dashicons dashicons-video-alt3"></span> Introduction video</a>' : "";
1688 $ret .= '<p>'.trim($feature["heading"]["desc"].$video).'</p>';
1689 if (count($feature["options"]) > 0) {
1690 $ret .= '<ul>';
1691 foreach ($feature["options"] as $option) {
1692 $label = $option["label"];
1693 $desc = $option["desc"];
1694 $desc .= $option["_doc_video"] != "" ? ' <a href="'.$option["_doc_video"].'" target="_blank"><span class="dashicons dashicons-video-alt3"></span> Introduction video</a>' : "";
1695 $desc = trim($desc);
1696 if ($desc != "") {
1697 $desc = '<p>'.$desc.'</p>';
1698 }
1699 $label = '<span class="dashicons dashicons-yes"></span> '.$label;
1700 $ret .= '<li>'.$label.$desc.'</li>';
1701 }
1702 $ret .= '</ul>';
1703 }
1704 $ret .= '<hr>';
1705 }
1706
1707 return $ret;
1708 }
1709
1710 public function replacingShortcodeEventViews($attr=[], $content = null, $tag = '') {
1711 // iterate over all woocommerce products and check if they are an event
1712 $view = "calendar|list";
1713 if (isset($attr["view"])) {
1714 $view = strtolower(trim(sanitize_key($attr["view"])));
1715 }
1716 $months_to_show = 3;
1717 if (isset($attr["months_to_show"])) {
1718 $m = intval($attr["months_to_show"]);
1719 if ($m > 0) $months_to_show = $m;
1720 }
1721
1722 $month_start = strtotime(wp_date("Y-m-1 00:00:00"));
1723 //$month_end = strtotime(wp_date("Y-m-t 23:59:59"));
1724 $month_end = strtotime(wp_date("Y-m-1 23:59:59", strtotime("+".$months_to_show." month", $month_start)));
1725
1726 $products_args = array(
1727 'post_type' => 'product',
1728 'post_status' => 'publish',
1729 'posts_per_page' => -1, // -1 zeigt alle Produkte an
1730 'meta_query' => array(
1731 [
1732 'key' => 'saso_eventtickets_is_ticket',
1733 'value' => 'yes',
1734 'compare' => '='
1735 ]
1736 )
1737 );
1738 $posts = get_posts($products_args); // get only ticket products
1739
1740 $list_infos = [
1741 'months_to_show'=>$months_to_show,
1742 'month_start'=>$month_start,
1743 'month_end'=>$month_end,
1744 'view'=>$view
1745 ];
1746 $products_to_show = [];
1747 // Ergebnisse überprüfen
1748 foreach ($posts as $post) {
1749 $product_ids = [];
1750 $product = wc_get_product( $post->ID );
1751
1752 // check if event is variant product
1753 $is_variable = $product->get_type() == "variable";
1754 if ($is_variable) {
1755 // check if event dates are the same for all variants
1756 $date_is_for_all_variants = get_post_meta( $product_id, 'saso_eventtickets_is_date_for_all_variants', true ) == "yes" ? true : false;
1757 if ($date_is_for_all_variants == false) {
1758 // if not add also the variants
1759 $childs = $product->get_children();
1760 foreach ($childs as $child_id) {
1761 if (get_post_meta($child_id, '_saso_eventtickets_is_not_ticket', true) == "yes") {
1762 continue;
1763 }
1764 $product_ids[] = $child_id;
1765 }
1766 }
1767 } else {
1768 $product_ids[] = $product->get_id();
1769 }
1770
1771 foreach ($product_ids as $product_id) {
1772 $product = wc_get_product( $product_id );
1773 $dates = $this->getTicketHandler()->calcDateStringAllowedRedeemFrom($product_id);
1774 //if ($dates['ticket_end_date_timestamp'] > $month_start && $dates['ticket_start_date_timestamp'] < $month_end) {
1775 if ($dates['ticket_end_date_timestamp'] >= $month_start || ($dates['ticket_start_date_timestamp'] >= $month_start && $dates['ticket_start_date_timestamp'] <= $month_end)) {
1776 // set product to hidden
1777 $product_data = array(
1778 'ID' => $product_id,
1779 'dates' => $dates,
1780 'event'=> [
1781 'location' => trim(get_post_meta( $product_id, 'saso_eventtickets_event_location', true )),
1782 'location_label' => wp_kses_post(trim($this->getAdmin()->getOptionValue("wcTicketTransLocation")))
1783 ],
1784 'product' => [
1785 'title' => $product->get_name(),
1786 'url' => get_permalink($product_id),
1787 'price' =>$product->get_price(),
1788 'price_html' => $product->get_price_html(),
1789 'type' => $product->get_type(),
1790 ]
1791 );
1792 $products_to_show[] = $product_data;
1793 }
1794 }
1795 }
1796
1797 $divId = "sasoEventTicketsValidator_eventsview";
1798
1799 // add js for the events
1800 wp_enqueue_style("wp-jquery-ui-dialog");
1801
1802 $js_url = "ticket_events.js?_v=".$this->getPluginVersion();
1803 if (defined('WP_DEBUG')) $js_url .= '&t='.time();
1804 $js_url = plugins_url( $js_url,__FILE__ );
1805 wp_register_script('ajax_script_ticket_events', $js_url, array('jquery', 'jquery-ui-dialog', 'wp-i18n'));
1806 wp_enqueue_script('ajax_script_ticket_events');
1807 wp_set_script_translations('ajax_script_ticket_events', 'event-tickets-with-ticket-scanner', __DIR__.'/languages');
1808 wp_enqueue_style("ticket_events_css", plugins_url( "",__FILE__ ).'/css/calendar.css', [], true);
1809
1810 // add all events as an array for max 3 months??? or config parameter
1811 $vars = [
1812 'root' => esc_url_raw( rest_url() ),
1813 '_plugin_home_url' =>plugins_url( "",__FILE__ ),
1814 '_action' => $this->_prefix.'_executeFrontendEvents',
1815 '_isPremium'=>$this->isPremium(),
1816 '_isUserLoggedin'=>is_user_logged_in(),
1817 '_userId'=>get_current_user_id(),
1818 '_premJS'=>$this->isPremium() && method_exists($this->getPremiumFunctions(), "getJSFrontEventFile") ? $this->getPremiumFunctions()->getJSFrontEventFile() : '',
1819 '_siteUrl'=>get_site_url(),
1820 'events' => $products_to_show,
1821 'list_infos' => $list_infos,
1822 'format_date' => $this->getOptions()->getOptionDateFormat(),
1823 'format_time' => $this->getOptions()->getOptionTimeFormat(),
1824 'format_datetime' => $this->getOptions()->getOptionDateTimeFormat(),
1825 'url' => admin_url( 'admin-ajax.php' ),
1826 'nonce' => wp_create_nonce( $this->_js_nonce ),
1827 'ajaxActionPrefix' => $this->_prefix,
1828 'divId' => $divId
1829 ];
1830 $vars = apply_filters( $this->_add_filter_prefix.'main_setTicketEventJS', $vars );
1831 wp_localize_script(
1832 'ajax_script_ticket_events',
1833 'Ajax_ticket_events_'.$this->_prefix, // name der injected variable
1834 $vars
1835 );
1836
1837 do_action( $this->_do_action_prefix.'main_setTicketEventJS', $js_url );
1838
1839 $ret = '';
1840 if (!isset($attr['divid']) || trim($attr['divid']) == "") {
1841 $ret .= '<div id="'.$divId.'">'.__('...loading...', 'event-tickets-with-ticket-scanner').'</div>';
1842 }
1843
1844 $ret = apply_filters( $this->_add_filter_prefix.'main_replacingShortcodeEventViews', $ret );
1845 do_action( $this->_do_action_prefix.'main_replacingShortcodeEventViews', $vars, $ret );
1846
1847 return $ret;
1848 }
1849
1850 public function replaceShortcode($attr=[], $content = null, $tag = '') {
1851 // einbinden das js starter skript
1852 $js_url = $this->_js_file."?_v=".$this->_js_version;
1853 if (defined( 'WP_DEBUG')) $js_url .= '&debug=1';
1854 $userDivId = !isset($attr['divid']) || trim($attr['divid']) == "" ? '' : trim($attr['divid']);
1855
1856 $attr = array_change_key_case( (array) $attr, CASE_LOWER );
1857
1858 wp_enqueue_script(
1859 'ajax_script_validator',
1860 plugins_url( $js_url,__FILE__ ),
1861 array('jquery', 'wp-i18n')
1862 );
1863 wp_set_script_translations('ajax_script_validator', 'event-tickets-with-ticket-scanner', __DIR__.'/languages');
1864
1865 $vars = array(
1866 'shortcode_attr'=>json_encode($attr),
1867 '_plugin_home_url' =>plugins_url( "",__FILE__ ),
1868 '_action' => $this->_prefix.'_executeFrontend',
1869 '_isPremium'=>$this->isPremium(),
1870 '_isUserLoggedin'=>is_user_logged_in(),
1871 '_premJS'=>$this->isPremium() && method_exists($this->getPremiumFunctions(), "getJSFrontFile") ? $this->getPremiumFunctions()->getJSFrontFile() : '',
1872 'url' => admin_url( 'admin-ajax.php' ),
1873 'nonce' => wp_create_nonce( $this->_js_nonce ),
1874 'ajaxActionPrefix' => $this->_prefix,
1875 'divPrefix' => $userDivId == "" ? $this->_prefix : $userDivId,
1876 'divId' => $this->_divId,
1877 'jsFiles' => plugins_url( 'validator.js?_v='.$this->_js_version, __FILE__ )
1878 );
1879 $vars['_messages'] = [
1880 'msgCheck0'=>$this->getOptions()->getOptionValue('textValidationMessage0'),
1881 'msgCheck1'=>$this->getOptions()->getOptionValue('textValidationMessage1'),
1882 'msgCheck2'=>$this->getOptions()->getOptionValue('textValidationMessage2'),
1883 'msgCheck3'=>$this->getOptions()->getOptionValue('textValidationMessage3'),
1884 'msgCheck4'=>$this->getOptions()->getOptionValue('textValidationMessage4'),
1885 'msgCheck5'=>$this->getOptions()->getOptionValue('textValidationMessage5'),
1886 'msgCheck6'=>$this->getOptions()->getOptionValue('textValidationMessage6')
1887 ];
1888 $enableQR = $this->getOptions()->isOptionCheckboxActive('enableQRScanner');
1889 $vars['_enableQRScanner'] = $enableQR;
1890 if ($enableQR) {
1891 wp_enqueue_script(
1892 'saso-html5-qrcode',
1893 plugins_url('3rd/html5-qrcode.min.js', __FILE__),
1894 array(),
1895 $this->_js_version,
1896 true
1897 );
1898 }
1899
1900 $vars = apply_filters( $this->_add_filter_prefix.'main_replaceShortcode', $vars );
1901
1902 if ($this->isPremium() && method_exists($this->getPremiumFunctions(), "addJSFrontFile")) $this->getPremiumFunctions()->addJSFrontFile();
1903
1904 wp_localize_script(
1905 'ajax_script_validator',
1906 'Ajax_'.$this->_prefix, // name of the injected variable
1907 $vars
1908 );
1909 $ret = '';
1910 if (!isset($attr['divid']) || trim($attr['divid']) == "") {
1911 $ret .= '<div id="'.$this->_divId.'">'.__('...loading...', 'event-tickets-with-ticket-scanner').'</div>';
1912 }
1913
1914 $ret = apply_filters( $this->_add_filter_prefix.'main_replaceShortcode_2', $ret );
1915 do_action( $this->_do_action_prefix.'main_replaceShortcode', $vars, $ret );
1916
1917 return $ret;
1918 }
1919
1920 /**
1921 * Show admin notice when premium subscription is about to expire or has expired
1922 *
1923 * - Warning: 14 days before expiration
1924 * - Error: After expiration (during grace period or after)
1925 *
1926 * @return void
1927 */
1928 public function showSubscriptionWarning(): void {
1929 // Only show when premium plugin is installed (not dependent on subscription status,
1930 // otherwise the expiration warning itself would never be shown)
1931 if (!class_exists('sasoEventtickets_PremiumFunctions')) {
1932 return;
1933 }
1934
1935 // Only show in admin
1936 if (!is_admin()) {
1937 return;
1938 }
1939
1940 // Only show to users who can manage options
1941 if (!current_user_can('manage_options')) {
1942 return;
1943 }
1944
1945 // Don't show "expired" warning when no serial is configured yet —
1946 // customer is still setting up, not really expired.
1947 $serial = trim((string) get_option('saso-event-tickets-premium_serial', ''));
1948 if ($serial === '') {
1949 return;
1950 }
1951
1952 // Don't show during license-save grace period (suppresses flash of stale banner
1953 // after the user just entered a new license key)
1954 if (get_transient('saso_license_just_saved')) {
1955 return;
1956 }
1957
1958 $info = $this->getTicketHandler()->get_expiration();
1959
1960 // No expiration date or lifetime license - no warning needed
1961 if (empty($info['timestamp']) || $info['timestamp'] <= 0 || $info['timestamp'] == -1) {
1962 return;
1963 }
1964
1965 // Lifetime subscription type - no warning needed
1966 if (isset($info['subscription_type']) && $info['subscription_type'] === 'lifetime') {
1967 return;
1968 }
1969
1970 $days_left = ($info['timestamp'] - time()) / 86400;
1971 $renewal_url = 'https://vollstart.com/event-tickets-with-ticket-scanner/';
1972
1973 // Warning 14 days before expiration
1974 if ($days_left <= 14 && $days_left > 0) {
1975 $date = date_i18n(get_option('date_format'), $info['timestamp']);
1976 echo '<div class="notice notice-warning is-dismissible saso-license-banner"><p>';
1977 printf(
1978 /* translators: %1$s: expiration date, %2$s: renewal URL */
1979 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'),
1980 '<strong>' . esc_html($date) . '</strong>',
1981 '<a href="' . esc_url($renewal_url) . '" target="_blank">',
1982 '</a>'
1983 );
1984 echo '</p></div>';
1985 }
1986
1987 // Error after expiration
1988 if ($days_left <= 0) {
1989 $grace_days = isset($info['grace_period_days']) ? intval($info['grace_period_days']) : 7;
1990 $grace_left = $grace_days + $days_left; // days_left is negative
1991
1992 echo '<div class="notice notice-error saso-license-banner"><p>';
1993 if ($grace_left > 0) {
1994 printf(
1995 /* translators: %1$d: days remaining in grace period, %2$s: renewal URL */
1996 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'),
1997 ceil($grace_left),
1998 '<a href="' . esc_url($renewal_url) . '" target="_blank">',
1999 '</a>'
2000 );
2001 } else {
2002 printf(
2003 /* translators: %1$s: renewal URL */
2004 esc_html__('Your Premium subscription has expired. Premium features are now limited. %1$sReactivate now%2$s', 'event-tickets-with-ticket-scanner'),
2005 '<a href="' . esc_url($renewal_url) . '" target="_blank">',
2006 '</a>'
2007 );
2008 }
2009 echo '</p></div>';
2010 }
2011 }
2012
2013 /**
2014 * FOMO banner: show missed premium features to users with expired subscriptions.
2015 * Dismissable for 30 days, then re-appears with an even longer feature list.
2016 */
2017 public function showFomoBanner(): void {
2018 if (!current_user_can('manage_options')) return;
2019 if ($this->isPremium()) return;
2020 if (!class_exists('sasoEventtickets_PremiumFunctions')) return;
2021 if ($this->isStarterOrStopDetected()) return;
2022 if ($this->isOldPremiumDetected()) return;
2023
2024 $info = $this->getTicketHandler()->get_expiration();
2025
2026 if (empty($info['expiration_date'])) return;
2027 if (isset($info['subscription_type']) && $info['subscription_type'] === 'lifetime') return;
2028
2029 $expired_ts = strtotime($info['expiration_date']);
2030 if (!$expired_ts || $expired_ts > time()) return;
2031
2032 if (get_transient('saso_et_fomo_dismissed')) return;
2033 if (get_transient('saso_license_just_saved')) return;
2034
2035 $json_path = plugin_dir_path(__FILE__) . 'changelog-features.json';
2036 if (!file_exists($json_path)) return;
2037
2038 $changelog = json_decode(file_get_contents($json_path), true);
2039 if (!is_array($changelog)) return;
2040
2041 $missed = [];
2042 foreach ($changelog as $release) {
2043 $release_ts = strtotime($release['date'] ?? '');
2044 if ($release_ts && $release_ts > $expired_ts && !empty($release['features'])) {
2045 foreach ($release['features'] as $feat) {
2046 $missed[] = $feat;
2047 }
2048 }
2049 }
2050
2051 if (empty($missed)) return;
2052
2053 $total = count($missed);
2054 $show = array_slice($missed, 0, 5);
2055 $renewal_url = 'https://vollstart.com/event-tickets-with-ticket-scanner/';
2056 $nonce = wp_create_nonce('saso_et_dismiss_fomo');
2057
2058 echo '<div class="notice notice-info is-dismissible saso-license-banner" id="saso-et-fomo-banner" style="border-left-color:#9333ea;">';
2059 echo '<p><strong>';
2060 printf(
2061 /* translators: %d: number of missed features */
2062 esc_html__("You're missing %d new premium features!", 'event-tickets-with-ticket-scanner'),
2063 $total
2064 );
2065 echo '</strong></p>';
2066 echo '<ul style="list-style:disc;margin-left:20px;">';
2067 foreach ($show as $feat) {
2068 echo '<li>' . esc_html($feat) . '</li>';
2069 }
2070 if ($total > 5) {
2071 printf('<li><em>' . esc_html__('...and %d more', 'event-tickets-with-ticket-scanner') . '</em></li>', $total - 5);
2072 }
2073 echo '</ul>';
2074 echo '<p><a href="' . esc_url($renewal_url) . '" target="_blank" class="button button-primary" style="background:#9333ea;border-color:#7c22d0;">';
2075 esc_html_e('Renew now', 'event-tickets-with-ticket-scanner');
2076 echo '</a></p>';
2077 echo '</div>';
2078 echo '<script>jQuery(function($){$("#saso-et-fomo-banner").on("click",".notice-dismiss",function(){$.post(ajaxurl,{action:"saso_et_dismiss_fomo",_wpnonce:"' . $nonce . '"});});});</script>';
2079 }
2080
2081 /**
2082 * AJAX handler: dismiss FOMO banner for 30 days
2083 */
2084 public function ajaxDismissFomo(): void {
2085 check_ajax_referer('saso_et_dismiss_fomo');
2086 set_transient('saso_et_fomo_dismissed', 1, 30 * DAY_IN_SECONDS);
2087 wp_send_json_success();
2088 }
2089
2090 /**
2091 * Show admin notice when premium plugin version is outdated or incompatible
2092 *
2093 * Shows a prominent, non-dismissible error when old premium is detected.
2094 * Old premium (< 1.6.0) is NOT loaded to prevent fatal errors.
2095 *
2096 * @return void
2097 */
2098 public function showOutdatedPremiumWarning(): void {
2099 if (!is_admin() || !current_user_can('manage_options')) {
2100 return;
2101 }
2102
2103 $starter_or_stop = $this->isStarterOrStopDetected();
2104 $old_premium = $this->isOldPremiumDetected();
2105
2106 if (!$starter_or_stop && !$old_premium) {
2107 return;
2108 }
2109
2110 // Don't show during license-save grace period (upgrade is happening in background,
2111 // no point scaring the user while we're actively replacing the plugin for them)
2112 if (get_transient('saso_license_just_saved')) {
2113 return;
2114 }
2115
2116 $old_version = defined('SASO_EVENTTICKETS_PREMIUM_PLUGIN_VERSION')
2117 ? SASO_EVENTTICKETS_PREMIUM_PLUGIN_VERSION
2118 : __('unknown', 'event-tickets-with-ticket-scanner');
2119
2120 $upgrade_url = 'https://vollstart.com/event-tickets-with-ticket-scanner/';
2121 $support_url = 'https://vollstart.com/support/';
2122 $premium_url = 'https://vollstart.com/downloads/event-tickets-with-ticket-scanner/';
2123
2124 echo '<div class="notice notice-error saso-license-banner" style="border-left-color:#dc3232;padding:15px 20px;">';
2125 echo '<p style="font-size:15px;font-weight:bold;margin:0 0 10px 0;color:#dc2626;">';
2126 echo '&#9888; ';
2127
2128 if ($starter_or_stop) {
2129 $is_stop = defined('SASO_EVENTTICKETS_STOP_VERSION');
2130 if ($is_stop) {
2131 printf(
2132 esc_html__('Event Tickets: Your Premium subscription has expired. Please renew your license to continue using Premium features.', 'event-tickets-with-ticket-scanner')
2133 );
2134 } else {
2135 printf(
2136 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')
2137 );
2138 }
2139 } else {
2140 printf(
2141 esc_html__('Event Tickets: Your Premium plugin (v%s) is outdated and not compatible with this version.', 'event-tickets-with-ticket-scanner'),
2142 esc_html($old_version)
2143 );
2144 }
2145 echo '</p>';
2146
2147 if ($starter_or_stop) {
2148 // Starter/Stop plugin - different messages
2149 $is_stop = defined('SASO_EVENTTICKETS_STOP_VERSION');
2150 $hasSerial = !empty(trim(get_option("saso-event-tickets-premium_serial", "")));
2151
2152 echo '<div style="background:#f8f9fa;border-left:4px solid #2563eb;padding:12px;margin:12px 0;">';
2153
2154 if ($is_stop) {
2155 // Stop plugin - subscription expired, PUC can update once license is renewed
2156 echo '<p style="margin:0 0 10px 0;"><strong>' . esc_html__('Solution: Renew Your Premium License', 'event-tickets-with-ticket-scanner') . '</strong></p>';
2157 echo '<ol style="margin:0 0 12px 20px;padding-left:20px;">';
2158 echo '<li style="margin:0 0 6px 0;">' . esc_html__('Renew your license', 'event-tickets-with-ticket-scanner') . '</li>';
2159 if ($hasSerial) {
2160 echo '<li style="margin:0 0 6px 0;">' . esc_html__('Update your license key in Event Tickets settings', 'event-tickets-with-ticket-scanner') . '</li>';
2161 } else {
2162 echo '<li style="margin:0 0 6px 0;">' . esc_html__('Enter your license key in Event Tickets settings', 'event-tickets-with-ticket-scanner') . '</li>';
2163 }
2164 echo '<li style="margin:0 0 6px 0;">' . esc_html__('Click "Check License" to verify your new subscription', 'event-tickets-with-ticket-scanner') . '</li>';
2165 echo '<li style="margin:0;">' . esc_html__('Update the Premium plugin via Plugins > Updates', 'event-tickets-with-ticket-scanner') . '</li>';
2166 echo '</ol>';
2167 printf(
2168 '<p style="margin:0 0 8px 0;"><a href="%s" class="button button-primary button-hero" target="_blank">%s</a></p>',
2169 esc_url($upgrade_url . '?utm_source=plugin&utm_medium=stop-notice&utm_campaign=renew-license'),
2170 esc_html__('Renew License', 'event-tickets-with-ticket-scanner')
2171 );
2172 echo '<p style="margin:0;font-size:13px;color:#666;">';
2173 esc_html_e('Contact support via support@vollstart.com if you think this is not correct.', 'event-tickets-with-ticket-scanner');
2174 echo '</p>';
2175 } else {
2176 // Starter plugin - just update within plugins area
2177 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>';
2178 echo '<p style="margin:0 0 8px 0;">';
2179 esc_html_e('The Starter plugin needs to be updated to Premium. Please update within the Plugins page:', 'event-tickets-with-ticket-scanner');
2180 echo '</p>';
2181 echo '<ol style="margin:0 0 12px 20px;padding-left:20px;">';
2182 if (!$hasSerial) {
2183 echo '<li style="margin:0 0 6px 0;">' . esc_html__('Enter your license key in Event Tickets settings', 'event-tickets-with-ticket-scanner') . '</li>';
2184 }
2185 echo '<li style="margin:0 0 6px 0;">' . esc_html__('Go to Plugins > Add New > Upload Plugin', 'event-tickets-with-ticket-scanner') . '</li>';
2186 echo '<li style="margin:0 0 6px 0;">' . esc_html__('Upload the Premium ZIP file', 'event-tickets-with-ticket-scanner') . '</li>';
2187 echo '<li style="margin:0 0 6px 0;">' . esc_html__('WordPress will replace the Starter plugin with Premium', 'event-tickets-with-ticket-scanner') . '</li>';
2188 echo '<li style="margin:0;">' . esc_html__('Click "Replace current with uploaded" and activate', 'event-tickets-with-ticket-scanner') . '</li>';
2189 echo '</ol>';
2190 }
2191
2192 echo '</div>';
2193 } else {
2194 // Old premium detected - update instructions
2195 echo '<div style="background:#f8f9fa;border-left:4px solid #2563eb;padding:12px;margin:12px 0;">';
2196 echo '<p style="margin:0 0 10px 0;"><strong>' . esc_html__('Solution: Update Premium Plugin', 'event-tickets-with-ticket-scanner') . '</strong></p>';
2197 echo '<p style="margin:0 0 8px 0;">';
2198 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');
2199 echo '</p>';
2200 echo '<p style="margin:0 0 8px 0;">';
2201 printf(
2202 esc_html__('Required version: 1.6.0 or higher. You have: %s', 'event-tickets-with-ticket-scanner'),
2203 '<strong>' . esc_html($old_version) . '</strong>'
2204 );
2205 echo '</p>';
2206 echo '<p style="margin:0 0 12px 0;">';
2207 esc_html_e('To restore premium features:', 'event-tickets-with-ticket-scanner');
2208 echo '</p>';
2209 echo '<ol style="margin:0 0 12px 20px;padding-left:20px;">';
2210 echo '<li style="margin:0 0 6px 0;">' . sprintf(
2211 /* translators: %s: URL to account */
2212 esc_html__('Download the latest Premium from your %1$saccount%2$s', 'event-tickets-with-ticket-scanner'),
2213 '<a href="' . esc_url($upgrade_url) . '" target="_blank"><strong>',
2214 '</strong></a>'
2215 ) . '</li>';
2216 echo '<li style="margin:0 0 6px 0;">' . esc_html__('Go to Plugins > Add New > Upload Plugin', 'event-tickets-with-ticket-scanner') . '</li>';
2217 echo '<li style="margin:0 0 6px 0;">' . esc_html__('Upload the new Premium ZIP file', 'event-tickets-with-ticket-scanner') . '</li>';
2218 echo '<li style="margin:0;">' . esc_html__('WordPress will ask to replace - confirm to update', 'event-tickets-with-ticket-scanner') . '</li>';
2219 echo '</ol>';
2220 echo '<p style="margin:0;">';
2221 printf(
2222 '<a href="' . esc_url($support_url) . '" class="button" style="margin-right:8px;" target="_blank">%s</a>',
2223 esc_html__('Contact Support', 'event-tickets-with-ticket-scanner')
2224 );
2225 echo '</p>';
2226 }
2227
2228 echo '</div>'; // End gray box
2229
2230 echo '<p style="margin:12px 0 0 0;font-style:italic;color:#666;font-size:13px;">';
2231 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');
2232 echo '</p>';
2233
2234 $downgrade_url = 'https://plugins.trac.wordpress.org/browser/event-tickets-with-ticket-scanner/tags';
2235 echo '<p style="margin:6px 0 0 0;font-style:italic;color:#666;font-size:13px;">';
2236 printf(
2237 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'),
2238 '<a href="' . esc_url($downgrade_url) . '" target="_blank">',
2239 '</a>'
2240 );
2241 echo '</p>';
2242
2243 echo '</div>'; // End notice
2244 }
2245
2246 /**
2247 * Show admin notice when ticket format is running out of combinations
2248 *
2249 * Checks all ticket lists for format warnings and displays notice
2250 *
2251 * @return void
2252 */
2253 public function showPhpVersionWarning(): void {
2254 if (version_compare(PHP_VERSION, '8.1.0', '>=')) {
2255 return;
2256 }
2257 printf(
2258 '<div class="notice notice-warning is-dismissible"><p><strong>Event Tickets with Ticket Scanner:</strong> %s</p></div>',
2259 sprintf(
2260 /* translators: 1: current PHP version 2: required PHP version */
2261 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'),
2262 PHP_VERSION,
2263 '8.1'
2264 )
2265 );
2266 }
2267
2268 /**
2269 * Handle format warning dismiss — runs on admin_init (before output) so wp_redirect() works.
2270 */
2271 public function handleFormatWarningDismiss(): void {
2272 if (!isset($_GET['saso_eventtickets_clear_format_warning']) || !isset($_GET['_wpnonce'])) {
2273 return;
2274 }
2275 if (!current_user_can('manage_options')) {
2276 return;
2277 }
2278 $list_id = intval($_GET['saso_eventtickets_clear_format_warning']);
2279 $nonce = sanitize_text_field($_GET['_wpnonce']);
2280 if (wp_verify_nonce($nonce, 'clear_format_warning_' . $list_id)) {
2281 $this->getAdmin()->clearFormatWarning($list_id);
2282 wp_redirect(remove_query_arg(['saso_eventtickets_clear_format_warning', '_wpnonce']));
2283 exit;
2284 }
2285 }
2286
2287 /**
2288 * Show admin notice when options migration from wp_options to custom table is incomplete.
2289 * Includes a button to manually trigger the migration.
2290 */
2291 public function showOptionsMigrationNotice(): void {
2292 if (!current_user_can('manage_options')) {
2293 return;
2294 }
2295 // Fast path: migration already done
2296 if (get_option('saso_eventtickets_options_migrated', '0') === '1') {
2297 return;
2298 }
2299 // Check if custom table exists (DB upgrade may not have run yet)
2300 global $wpdb;
2301 $table = $wpdb->prefix . 'saso_eventtickets_options';
2302 if ($wpdb->get_var($wpdb->prepare("SHOW TABLES LIKE %s", $table)) !== $table) {
2303 return;
2304 }
2305 // Check sentinel: is the last known option still in wp_options?
2306 $sentinelKey = $this->_prefix . 'qrAttachQRFilesToMailAsOnePDF';
2307 if (get_option($sentinelKey, '__NOT_SET__') === '__NOT_SET__') {
2308 // Sentinel gone — migration probably completed, set flag
2309 update_option('saso_eventtickets_options_migrated', '1');
2310 return;
2311 }
2312 // Migration incomplete — show admin notice with button
2313 $nonce = wp_create_nonce($this->_prefix);
2314 printf(
2315 '<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>',
2316 esc_html__('Options migration to custom database table is incomplete.', 'event-tickets-with-ticket-scanner'),
2317 esc_attr($nonce),
2318 esc_html__('Migrate Options', 'event-tickets-with-ticket-scanner')
2319 );
2320 }
2321
2322 public function showFormatWarning(): void {
2323 // Only show in admin
2324 if (!is_admin()) {
2325 return;
2326 }
2327
2328 // Only show to users who can manage options
2329 if (!current_user_can('manage_options')) {
2330 return;
2331 }
2332
2333 try {
2334 // Get all ticket lists
2335 $lists = $this->getAdmin()->getLists([], false);
2336
2337 foreach ($lists as $list) {
2338 $warning = $this->getAdmin()->getFormatWarning($list['id']);
2339
2340 if ($warning) {
2341 $list_name = esc_html($warning['list_name']);
2342 $attempts = intval($warning['attempts']);
2343
2344 if ($warning['type'] === 'critical') {
2345 // Critical - format exhausted
2346 $clear_url = wp_nonce_url(
2347 add_query_arg(['saso_eventtickets_clear_format_warning' => $list['id']]),
2348 'clear_format_warning_' . $list['id']
2349 );
2350
2351 echo '<div class="notice notice-error"><p>';
2352 printf(
2353 /* translators: 1: list name, 2: attempts, 3: clear URL */
2354 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'),
2355 $list_name,
2356 $attempts,
2357 '<a href="' . esc_url(admin_url('admin.php?page=event-tickets-with-ticket-scanner')) . '">',
2358 '</a>',
2359 '<a href="' . esc_url($clear_url) . '">'
2360 );
2361 echo '</p></div>';
2362 } else {
2363 // Warning - running out
2364 $clear_url = wp_nonce_url(
2365 add_query_arg(['saso_eventtickets_clear_format_warning' => $list['id']]),
2366 'clear_format_warning_' . $list['id']
2367 );
2368
2369 echo '<div class="notice notice-warning is-dismissible"><p>';
2370 printf(
2371 /* translators: 1: list name, 2: attempts, 3: clear URL */
2372 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'),
2373 $list_name,
2374 $attempts,
2375 '<a href="' . esc_url(admin_url('admin.php?page=event-tickets-with-ticket-scanner')) . '">',
2376 '</a>',
2377 '<a href="' . esc_url($clear_url) . '">'
2378 );
2379 echo '</p></div>';
2380 }
2381
2382 // Only show one warning at a time
2383 break;
2384 }
2385 }
2386 } catch (Exception $e) {
2387 // Silently fail - don't break the admin
2388 }
2389 }
2390 }
2391 $sasoEventtickets = sasoEventtickets::Instance();
2392
2393 // Cross-Promotion: andere Vollstart Plugins empfehlen
2394 if (is_admin() && file_exists(__DIR__ . '/vollstart-cross-promo.php')) {
2395 require_once __DIR__ . '/vollstart-cross-promo.php';
2396 vollstart_cross_promo_init('event-tickets-with-ticket-scanner');
2397 }
2398 ?>
2399