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