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