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 / sasoEventtickets_Ticket.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
sasoEventtickets_Ticket.php
3466 lines
1 <?php
2 include_once(plugin_dir_path(__FILE__)."init_file.php");
3 final class sasoEventtickets_Ticket {
4 private $MAIN;
5
6 private $request_uri;
7 private $parts = null;
8
9 private $codeObj;
10 private $order;
11 private $orders_cache = [];
12
13 private $isScanner = null;
14 private $authtoken = null; // only set if the ticket scanner is sending the request with authtoken
15 private $authtoken_id = 0; // resolved DB id of $authtoken — used as audit trail on redeem
16
17 private $redeem_successfully = false;
18 private $onlyLoggedInScannerAllowed = null;
19
20 public static function Instance($request_uri) {
21 static $inst = null;
22 if ($inst === null) {
23 $inst = new sasoEventtickets_Ticket($request_uri);
24 } else {
25 $inst->setRequestURI($request_uri);
26 }
27 return $inst;
28 }
29
30 public function __construct($request_uri) {
31 global $sasoEventtickets;
32 if ($sasoEventtickets == null) {
33 $sasoEventtickets = sasoEventtickets::Instance();
34 }
35 $this->MAIN = $sasoEventtickets;
36
37 $this->setRequestURI($request_uri);
38 // Options are loaded lazily to avoid triggering translations before 'init'
39 add_action( 'init', [$this, 'initOptionsFromConstructor'], 1 );
40 }
41
42 public function initOptionsFromConstructor(): void {
43 $this->onlyLoggedInScannerAllowed = $this->MAIN->getOptions()->isOptionCheckboxActive('wcTicketOnlyLoggedInScannerAllowed');
44 if ($this->MAIN->getOptions()->isOptionCheckboxActive('wcTicketActivateOBFlush')) {
45 remove_action( 'shutdown', 'wp_ob_end_flush_all', 1 );
46 add_action( 'shutdown', function() {
47 while ( ob_get_level() > 0 ) {
48 @ob_end_flush();
49 }
50 } );
51 }
52 }
53
54 private function isOnlyLoggedInScannerAllowed(): bool {
55 if ($this->onlyLoggedInScannerAllowed === null) {
56 $this->onlyLoggedInScannerAllowed = $this->MAIN->getOptions()->isOptionCheckboxActive('wcTicketOnlyLoggedInScannerAllowed');
57 }
58 return $this->onlyLoggedInScannerAllowed;
59 }
60
61 public function getWPMLProductId($product_id) {
62 $pid = $product_id;
63 if ($product_id) {
64 $pid = apply_filters('wpml_object_id', $product_id, 'product', true );
65 // polygone error handling or any other idiot is overwriting the wpml filter
66 if ($pid == null || empty($pid) || !is_numeric($pid) || intval($pid) < 1) {
67 $pid = $product_id;
68 }
69 }
70 return $pid;
71 }
72
73 public function setRequestURI($request_uri) {
74 $this->request_uri = trim($request_uri);
75 }
76
77 public function cronJobDaily() {
78 $this->hideAllTicketProductsWithExpiredEndDate();
79 $this->checkForPremiumSerialExpiration();
80 do_action( $this->MAIN->_do_action_prefix.'ticket_cronJobDaily' );
81 }
82
83 /**
84 * Get premium subscription expiration info
85 *
86 * @return array Expiration info with keys: last_run, timestamp, expiration_date, timezone, subscription_type, grace_period_days
87 */
88 public function get_expiration(): array {
89 $option_name = $this->MAIN->getPrefix()."_premium_serial_expiration";
90 $info = get_option( $option_name );
91 $info_obj = [
92 "last_run" => 0,
93 "timestamp" => 0,
94 "expiration_date" => "",
95 "timezone" => "",
96 "subscription_type" => "abo", // 'abo' or 'lifetime'
97 "grace_period_days" => 7, // Days after expiration where features still work
98 "last_success" => 0, // Last successful license check
99 "consecutive_failures" => 0 // Failed license check attempts
100 ];
101 if (!empty($info)) {
102 $decoded = json_decode($info, true);
103 if (is_array($decoded)) {
104 $info_obj = array_merge($info_obj, $decoded);
105 }
106 }
107 $info_obj = apply_filters( $this->MAIN->_add_filter_prefix.'ticket_get_expiration', $info_obj );
108 return $info_obj;
109 }
110
111 /**
112 * Check if the premium subscription is currently active
113 *
114 * Considers:
115 * - Lifetime licenses (timestamp = -1 or subscription_type = 'lifetime')
116 * - Regular subscriptions with expiration date
117 * - Grace period after expiration
118 *
119 * @return bool True if subscription is active, false if expired
120 */
121 public function isSubscriptionActive(): bool {
122 $info = $this->get_expiration();
123
124 // Lifetime = immer aktiv
125 if (isset($info['timestamp']) && $info['timestamp'] == -1) return true;
126 if (isset($info['subscription_type']) && $info['subscription_type'] === 'lifetime') return true;
127
128 // Zu viele Fehler hintereinander = inaktiv
129 // Schwelle 10 statt 5: ein kurzer Serverausfall (z.B. Wartung) soll nicht
130 // sofort Premium deaktivieren. Bei 7-Tage-Intervall = 70 Tage Toleranz.
131 if (intval($info['consecutive_failures'] ?? 0) >= 10) return false;
132
133 // Checksum-Prüfung: wurden Werte manipuliert?
134 if (!$this->verifyExpirationChecksum($info)) return false;
135
136 // Noch nie erfolgreich geprüft?
137 $last_success = intval($info['last_success'] ?? 0);
138 if ($last_success === 0) {
139 // First-Install Grace: 3 Tage Zeit für ersten Server-Check
140 $first_seen = intval($info['first_seen'] ?? 0);
141 if ($first_seen === 0) {
142 // Allererster Aufruf - first_seen setzen
143 $this->setFirstSeen();
144 return true;
145 }
146 // Mehr als 3 Tage ohne erfolgreichen Check = inaktiv
147 return (time() - $first_seen) < (3 * 86400);
148 }
149
150 // Letzter erfolgreicher Check zu alt (>21 Tage) = inaktiv
151 // 21 statt 10 Tage: gibt dem System mehr Zeit, sich von einem
152 // Serverausfall zu erholen, bevor Premium deaktiviert wird.
153 if ((time() - $last_success) > (21 * 86400)) return false;
154
155 // Kein Timestamp vom Server = inaktiv (außer in Grace Period)
156 if (empty($info['timestamp']) || $info['timestamp'] <= 0) return false;
157
158 // Normale Expiration + Grace Period
159 $grace_days = intval($info['grace_period_days'] ?? 7);
160 return time() < ($info['timestamp'] + ($grace_days * 86400));
161 }
162
163 /**
164 * Calculate HMAC checksum over critical license fields to detect DB manipulation.
165 */
166 private function calculateExpirationChecksum(array $info): string {
167 $critical = [
168 'timestamp' => $info['timestamp'] ?? 0,
169 'last_success' => $info['last_success'] ?? 0,
170 'notvalid' => $info['notvalid'] ?? 0,
171 'consecutive_failures' => $info['consecutive_failures'] ?? 0,
172 ];
173 $salt = defined('AUTH_KEY') ? AUTH_KEY : 'saso_et_fallback_salt';
174 return hash_hmac('sha256', json_encode($critical), $salt);
175 }
176
177 /**
178 * Verify that stored license data has not been tampered with.
179 * Legacy data without checksum is accepted.
180 */
181 private function verifyExpirationChecksum(array $info): bool {
182 if (!isset($info['_checksum'])) return true; // Legacy-Daten ohne Checksum = OK
183 return hash_equals($info['_checksum'], $this->calculateExpirationChecksum($info));
184 }
185
186 /**
187 * Record the first time the plugin was seen (for grace period on fresh installs).
188 */
189 private function setFirstSeen(): void {
190 $option_name = $this->MAIN->getPrefix() . "_premium_serial_expiration";
191 $info = get_option($option_name);
192 $info_obj = !empty($info) ? json_decode($info, true) : [];
193 $info_obj['first_seen'] = time();
194 $info_obj['_checksum'] = $this->calculateExpirationChecksum($info_obj);
195 update_option($option_name, json_encode($info_obj));
196 }
197
198 public function checkForPremiumSerialExpiration(bool $force = false) {
199 $option_name = $this->MAIN->getPrefix()."_premium_serial_expiration";
200 // Also run when premium plugin is installed with a serial key but isPremium() is false (recovery from deadlock)
201 $hasPremiumPlugin = class_exists('sasoEventtickets_PremiumFunctions');
202 $serial = trim(get_option("saso-event-tickets-premium_serial", ""));
203 $hasSerial = !empty($serial);
204 if ($this->MAIN->isPremium() || ($hasPremiumPlugin && $hasSerial)) {
205 $info_obj = $this->get_expiration();
206
207 // Detect serial change: reset failure data so corrected serials get a fresh chance
208 $serial_hash = $hasSerial ? md5($serial) : '';
209 if (isset($info_obj["_serial_hash"]) && $info_obj["_serial_hash"] !== $serial_hash) {
210 $info_obj["consecutive_failures"] = 0;
211 $info_obj["last_run"] = 0;
212 $info_obj["last_success"] = 0;
213 unset($info_obj["notvalid"]);
214 }
215 // Force-check with failures = deadlock recovery: reset failures so the
216 // server response can actually fix the state (otherwise isSubscriptionActive
217 // stays false even after a successful server check).
218 if ($force && intval($info_obj["consecutive_failures"] ?? 0) > 0) {
219 $info_obj["consecutive_failures"] = 0;
220 }
221 $info_obj["_serial_hash"] = $serial_hash;
222
223 $doCheck = false;
224 if ($force) {
225 $doCheck = true;
226 } elseif ($info_obj["last_run"] == 0) {
227 $doCheck = true;
228 } else {
229 if (isset($info_obj["timestamp"])) {
230 if ($info_obj["timestamp"] >= 0) {
231 $doCheck = true;
232 if (strtotime("+21 days") > intval($info_obj["timestamp"])) {
233 // check if enough time past after the last check
234 if (strtotime("-7 days") < intval($info_obj["last_run"])) {
235 $doCheck = false; // wait till the cache expires
236 }
237 }
238 }
239 } else {
240 $doCheck = true;
241 }
242 }
243 if ($doCheck && $hasSerial && defined('SASO_EVENTTICKETS_PREMIUM_PLUGIN_VERSION')) {
244 $domain = parse_url( get_site_url(), PHP_URL_HOST );
245
246 $url = "https://vollstart.com/plugins/event-tickets-with-ticket-scanner-premium/"
247 .'?checking_for_updates=2&ver='.SASO_EVENTTICKETS_PREMIUM_PLUGIN_VERSION
248 ."&m=".get_option('admin_email')
249 ."&d=".$domain
250 ."&serial=".urlencode($serial);
251
252 $response = wp_remote_get($url, ['timeout' => 45]);
253 if (is_wp_error($response)) {
254 // Network error — count as transient failure (half weight)
255 $info_obj["consecutive_failures"] = intval($info_obj["consecutive_failures"] ?? 0) + 1;
256 $info_obj["last_run"] = time();
257 } else {
258 $http_code = intval(wp_remote_retrieve_response_code($response));
259 $body = wp_remote_retrieve_body( $response );
260 $data = json_decode( $body, true );
261 if (isset($data["isCheckCall"]) && $data["isCheckCall"] == 1) {
262 // Success: valid license response
263 $info_obj["last_run"] = time();
264 $info_obj["last_success"] = time();
265 $info_obj["consecutive_failures"] = 0;
266 // Clear notvalid if server doesn't send it (successful check = valid)
267 if (!isset($data["notvalid"])) {
268 unset($info_obj["notvalid"]);
269 }
270 // Server-Daten gezielt übernehmen (Server gewinnt)
271 foreach (['timestamp', 'expiration_date', 'timezone', 'notvalid', 'isCheckCall', 'subscription_type', 'grace_period_days'] as $key) {
272 if (isset($data[$key])) $info_obj[$key] = $data[$key];
273 }
274 } elseif ($http_code >= 500 || empty($body) || $data === null) {
275 // Server error (5xx), empty body, or non-JSON response (maintenance page, etc.)
276 // Treat as transient — count failure but don't touch last_success
277 $info_obj["consecutive_failures"] = intval($info_obj["consecutive_failures"] ?? 0) + 1;
278 $info_obj["last_run"] = time();
279 } else {
280 // HTTP OK with parseable JSON but no isCheckCall — genuine rejection
281 $info_obj["consecutive_failures"] = intval($info_obj["consecutive_failures"] ?? 0) + 2;
282 $info_obj["last_run"] = time();
283 }
284 }
285 $info_obj['_checksum'] = $this->calculateExpirationChecksum($info_obj);
286 $value = $this->MAIN->getCore()->json_encode_with_error_handling($info_obj);
287 update_option($option_name, $value);
288 }
289 }
290 do_action( $this->MAIN->_do_action_prefix.'ticket_checkForPremiumSerialExpiration' );
291 }
292
293 private function hideAllTicketProductsWithExpiredEndDate() {
294 if ($this->MAIN->getOptions()->isOptionCheckboxActive('wcTicketHideTicketAfterEventEnd')) {
295 // Produkte abrufen, die nicht als "Privat" markiert sind
296 $products_args = array(
297 'post_type' => 'product',
298 'post_status' => 'publish',
299 'posts_per_page' => -1, // -1 zeigt alle Produkte an
300 'meta_query' => array(
301 /*array(
302 'key' => '_visibility',
303 'value' => array('catalog', 'visible'), // Produkte, die nicht als "Privat" gelten
304 'compare' => 'IN',
305 ),*/
306 [
307 'key' => 'saso_eventtickets_is_ticket',
308 'value' => 'yes',
309 'compare' => '='
310 ]
311 )
312 );
313
314 $products = get_posts($products_args);
315 // Ergebnisse überprüfen
316 if ($products && function_exists("wp_update_post")) {
317 foreach ($products as $product) {
318 // check if ticket
319 $product_id = $product->ID; //$product->get_id();
320 $product_id_orig = $this->getWPMLProductId($product_id);
321 //if ($this->MAIN->getWC()->isTicketByProductId($product_id) ) {
322 // check if event date end is set
323 $dates = $this->calcDateStringAllowedRedeemFrom($product_id_orig);
324 if (!empty($dates['ticket_end_date_orig'])) { // only if end date is also set
325 // check if expired - non premium
326 if ($dates['ticket_end_date_timestamp'] < $dates['server_time_timestamp']) {
327 // set product to hidden
328 $product_data = array(
329 'ID' => $product_id,
330 'post_status' => 'private', // Setzen Sie den Status auf 'private'
331 );
332 wp_update_post($product_data);
333 if ($product_id != $product_id_orig) {
334 // update the original product too
335 $product_data = array(
336 'ID' => $product_id_orig,
337 'post_status' => 'private', // Setzen Sie den Status auf 'private'
338 );
339 wp_update_post($product_data);
340 }
341 }
342 }
343 //}
344 }
345 }
346
347 do_action( $this->MAIN->_do_action_prefix.'ticket_hideAllTicketProductsWithExpiredEndDate', $products );
348 }
349 }
350
351 function rest_permission_callback(WP_REST_Request $web_request) {
352 $ret = false;
353
354 // Path 1: Authtoken authentication (for scanner team without WP accounts)
355 if ($web_request->has_param($this->MAIN->getAuthtokenHandler()::$authtoken_param)) {
356 $authHandler = $this->MAIN->getAuthtokenHandler();
357 $this->authtoken = $web_request->get_param($authHandler::$authtoken_param);
358 $ret = $authHandler->checkAccessForAuthtoken($this->authtoken);
359 if ($ret) {
360 // Resolve token id once so the redeem record can reference it.
361 $tokenObj = $authHandler->getAuthtokenByCode($this->authtoken);
362 if ($tokenObj && !empty($tokenObj['id'])) {
363 $this->authtoken_id = (int) $tokenObj['id'];
364 }
365 }
366 } else {
367 // Path 2: Check if scanner is open to everyone (no login required)
368 $allowed_role = $this->MAIN->getOptions()->getOptionValue('wcTicketScannerAllowedRoles');
369 if ($allowed_role == "-" && !$this->isOnlyLoggedInScannerAllowed()) {
370 $ret = true;
371 } else {
372 // Path 3: WordPress user authentication (must be logged in)
373 if (!is_user_logged_in()) return false;
374
375 $user = wp_get_current_user();
376 $user_roles = (array) $user->roles;
377
378 // Administrators always have access
379 if (in_array("administrator", $user_roles)) return true;
380
381 if ($this->isOnlyLoggedInScannerAllowed()) {
382 // Strict mode: only administrators (already checked above)
383 $ret = false;
384 } else if ($allowed_role != "-") {
385 // Specific role required
386 $ret = in_array($allowed_role, $user_roles);
387 }
388 }
389 }
390
391 $ret = apply_filters( $this->MAIN->_add_filter_prefix.'ticket_rest_permission_callback', $ret, $web_request );
392 return $ret;
393 }
394 function rest_ping(?WP_REST_Request $web_request = null) {
395 return ['time'=>time(), 'img_pfad'=>plugins_url( "img/",__FILE__ ), '_ret'=>['_server'=>$this->getTimes()] ];
396 }
397 function rest_helper_tickets_redeemed($codeObj) {
398 $metaObj = $metaObj = $codeObj['metaObj'];
399 $ret = [];
400 $ret['tickets_redeemed'] = 0;
401 $ret['tickets_redeemed_by_codes'] = 0;
402 $ret['tickets_redeemed_not_by_codes'] = 0;
403 $ret['tickets_redeemed_show'] = false;
404 $ret['tickets_redeemed_show_c'] = false;
405 $ret['tickets_redeemed_show_cn'] = false;
406 if (isset($metaObj['woocommerce']) && isset($metaObj['woocommerce']['product_id'])) {
407 if ($this->MAIN->getOptions()->isOptionCheckboxActive('wcTicketDisplayRedeemedAtScanner') == false) {
408 $ret['tickets_redeemed_show'] = true;
409 if ($this->MAIN->isPremium() && method_exists($this->MAIN->getPremiumFunctions(), 'getTicketStats')) {
410 if (method_exists($this->MAIN->getPremiumFunctions()->getTicketStats(), 'getEntryAmountForProductId')) {
411 $ret['tickets_redeemed'] = $this->MAIN->getPremiumFunctions()->getTicketStats()->getEntryAmountForProductId($metaObj['woocommerce']['product_id']);
412 }
413 }
414 }
415 if ($this->MAIN->getOptions()->isOptionCheckboxActive('wcTicketDisplayRedeemedByCodesAtScanner') == true) {
416 $ret['tickets_redeemed_show_c'] = true;
417 if ($this->MAIN->isPremium() && method_exists($this->MAIN->getPremiumFunctions(), 'getTicketStats')) {
418 if (method_exists($this->MAIN->getPremiumFunctions()->getTicketStats(), 'getEntryAmountForProductIdRedeemed')) {
419 $ret['tickets_redeemed_by_codes'] = $this->MAIN->getPremiumFunctions()->getTicketStats()->getEntryAmountForProductIdRedeemed($metaObj['woocommerce']['product_id']);
420 }
421 }
422 }
423 if ($this->MAIN->getOptions()->isOptionCheckboxActive('wcTicketDisplayRedeemedNotByCodesAtScanner') == true) {
424 $ret['tickets_redeemed_show_cn'] = true;
425 if ($this->MAIN->isPremium() && method_exists($this->MAIN->getPremiumFunctions(), 'getTicketStats')) {
426 if (method_exists($this->MAIN->getPremiumFunctions()->getTicketStats(), 'getEntryAmountForProductIdNotRedeemed')) {
427 $ret['tickets_redeemed_not_by_codes'] = $this->MAIN->getPremiumFunctions()->getTicketStats()->getEntryAmountForProductIdNotRedeemed($metaObj['woocommerce']['product_id']);
428 }
429 }
430 }
431 }
432 $ret = apply_filters( $this->MAIN->_add_filter_prefix.'ticket_rest_permission_callback', $ret, $codeObj );
433 return $ret;
434 }
435
436 private function isProductAllowedByAuthToken($product_ids=[]) {
437 if (!is_array($product_ids)) {
438 $product_ids = [$product_ids];
439 }
440 $ret = false;
441 if ($this->authtoken == null){
442 $ret = true;
443 } else {
444 $authHandler = $this->MAIN->getAuthtokenHandler();
445 if ($authHandler->isProductAllowedByAuthToken($this->authtoken, $product_ids)) {
446 $ret = true;
447 }
448 }
449 $ret = apply_filters( $this->MAIN->_add_filter_prefix.'ticket_isProductAllowedByAuthToken', $ret, $product_ids );
450 if ($ret == false) {
451 throw new Exception("#301 - product id ".join(", ", $product_ids)." is not allowed to be rededemed with this ticket scanner authentication");
452 }
453 }
454 private function is_ticket_code_orderticket($code) {
455 // is it an order ticket id
456 $ret = false;
457 $code = trim($code);
458 if (strlen($code) > 13 && substr($code, 0, 13) == "ordertickets-") {
459 $ret = true;
460 }
461 $ret = apply_filters( $this->MAIN->_add_filter_prefix.'ticket_is_ticket_code_orderticket', $ret, $code );
462 return $ret;
463 }
464 function rest_retrieve_ticket($web_request) {
465 // Accept code from $_GET / $_POST (legacy AJAX path) OR from the
466 // WP_REST_Request object (REST API path / PHPUnit tests).
467 $code = '';
468 if (SASO_EVENTTICKETS::issetRPara('code')) {
469 $code = trim(SASO_EVENTTICKETS::getRequestPara('code'));
470 } elseif (is_object($web_request) && method_exists($web_request, 'get_param')) {
471 $paramCode = $web_request->get_param('code');
472 if ($paramCode !== null && $paramCode !== '') {
473 $code = trim((string) $paramCode);
474 // Propagate to $_GET so downstream helpers (getTicketURLComponents,
475 // issetRPara) can see the code without needing a browser request.
476 $_GET['code'] = $code;
477 }
478 }
479 if ($code === '') {
480 return wp_send_json_error(esc_html__("code missing", 'event-tickets-with-ticket-scanner'));
481 }
482 // Read CVV from REST request object (preferred) or superglobal fallback.
483 $cvv = '';
484 if (is_object($web_request) && method_exists($web_request, 'get_param')) {
485 $paramCvv = $web_request->get_param('cvv');
486 if ($paramCvv !== null) {
487 $cvv = trim((string) $paramCvv);
488 }
489 }
490 if ($cvv === '' && SASO_EVENTTICKETS::issetRPara('cvv')) {
491 $rawCvv = SASO_EVENTTICKETS::getRequestPara('cvv');
492 if (!is_array($rawCvv)) {
493 $cvv = trim((string) $rawCvv);
494 }
495 }
496 if ($this->is_ticket_code_orderticket($code)) {
497 // Order-ticket path: cvv may be a per-row map keyed by ticket code
498 // (cvv[ticketCode1]=val1&cvv[ticketCode2]=val2). Task 15.
499 $cvvMap = $this->readCVVMapFromRequest($web_request);
500 return $this->retrieve_order_ticket($code, $cvvMap);
501 }
502 return $this->retrieve_ticket($code, $cvv);
503 }
504
505 /**
506 * Read the per-row CVV map from a request. Order-ticket scans can supply
507 * CVVs per ticket as cvv[ticketCode1]=val1&cvv[ticketCode2]=val2 or as a
508 * JSON-encoded string. Returns an associative array keyed by ticket code.
509 *
510 * @param mixed $web_request WP_REST_Request or null
511 * @return array<string,string> map of ticket-code => cvv (trimmed strings)
512 */
513 private function readCVVMapFromRequest($web_request): array {
514 $cvvMap = [];
515 // Prefer REST request object — has its own param store independent of $_GET/$_POST.
516 if (is_object($web_request) && method_exists($web_request, 'get_param')) {
517 $paramCvv = $web_request->get_param('cvv');
518 if (is_array($paramCvv)) {
519 $cvvMap = $paramCvv;
520 } elseif ($paramCvv !== null && $paramCvv !== '') {
521 $decoded = json_decode((string) $paramCvv, true);
522 if (is_array($decoded)) {
523 $cvvMap = $decoded;
524 }
525 }
526 }
527 if (empty($cvvMap) && SASO_EVENTTICKETS::issetRPara('cvv')) {
528 $raw = SASO_EVENTTICKETS::getRequestPara('cvv');
529 if (is_array($raw)) {
530 $cvvMap = $raw;
531 } elseif (is_string($raw) && $raw !== '') {
532 $decoded = json_decode($raw, true);
533 if (is_array($decoded)) {
534 $cvvMap = $decoded;
535 }
536 }
537 }
538 $normalized = [];
539 foreach ($cvvMap as $k => $v) {
540 $normalized[(string) $k] = is_scalar($v) ? trim((string) $v) : '';
541 }
542 return $normalized;
543 }
544
545 /**
546 * Strip ticket details from an order-ticket info row when its CVV gate fails.
547 * Exposes only the fields the scanner JS needs before CVV is verified:
548 * code, public_ticket_id, requires_cvv, attempts_remaining, locked,
549 * product_id, product_parent_id. No name/seat/order details.
550 *
551 * product_id and product_parent_id are deliberately retained: ticket_scanner.js
552 * (displayOrderTicketInfo, line ~1397) matches every ticket_infos row to its
553 * product group using `item.product_id == product.product_id &&
554 * item.product_parent_id == product.product_parent_id`. Without these two fields
555 * CVV-required rows would not be rendered inside the correct product section.
556 * Both values are product-public (visible in the shop URL / HTML) and carry no
557 * personal or booking-sensitive information.
558 *
559 * @param array $ticketObj Original ticket info row (will be replaced).
560 * Expected keys: code, code_display, code_public,
561 * product_id, product_parent_id.
562 * @param array|null $cvvResult Result of verifyCVV, or null if CVV missing.
563 * @param array $metaObj metaObj of the code (for cvv_attempts fallback).
564 * @return array Stripped row exposing only the fields listed above.
565 */
566 private function stripDetailsForCVVLocked(array $ticketObj, ?array $cvvResult, array $metaObj): array {
567 $attemptsRemaining = 5;
568 $locked = false;
569 if (is_array($cvvResult)) {
570 $attemptsRemaining = (int) ($cvvResult['attempts_remaining'] ?? 0);
571 $locked = (bool) ($cvvResult['locked'] ?? false);
572 } else {
573 $attempts = $metaObj['cvv_attempts'] ?? ['count' => 0, 'locked' => false];
574 $attemptsRemaining = max(0, 5 - (int) ($attempts['count'] ?? 0));
575 $locked = (bool) ($attempts['locked'] ?? false);
576 }
577 return [
578 'requires_cvv' => true,
579 'attempts_remaining' => $attemptsRemaining,
580 'locked' => $locked,
581 'public_ticket_id' => (string) ($ticketObj['code_public'] ?? ''),
582 'code_public' => (string) ($ticketObj['code_public'] ?? ''),
583 'code' => (string) ($ticketObj['code'] ?? ''),
584 'code_display' => (string) ($ticketObj['code_display'] ?? ''),
585 // product_id / product_parent_id are product-public values needed by
586 // ticket_scanner.js to place this row in the correct product section.
587 'product_id' => (int) ($ticketObj['product_id'] ?? 0),
588 'product_parent_id' => (int) ($ticketObj['product_parent_id'] ?? 0),
589 ];
590 }
591
592 private function retrieve_order_ticket($code, array $cvvMap = []) {
593 $parts = $this->getParts($code);
594 if (!isset($parts["order_id"]) || !isset($parts["code"])) throw new Exception("#299 - wrong order ticket id");
595 if (empty($parts["order_id"]) || empty($parts["code"])) throw new Exception("#297 - wrong order ticket id");
596
597 $infos = $this->getOrderTicketsInfos($parts['order_id'], $parts['code']);
598 if (!is_array($infos)) throw new Exception("#298 - wrong order ticket id");
599
600 // Task 15 — per-row CVV gate: each ticket row may have its own CVV requirement.
601 // Rows whose product requires CVV but for which no valid CVV was supplied get
602 // their details stripped (anti-info-leak). Other rows pass through untouched.
603 if (isset($infos['ticket_infos']) && is_array($infos['ticket_infos'])) {
604 $infos['ticket_infos'] = $this->applyCVVGateToTicketInfos($infos['ticket_infos'], $cvvMap);
605 $infos['cvv_required_for_any_row'] = false;
606 foreach ($infos['ticket_infos'] as $row) {
607 if (!empty($row['requires_cvv'])) {
608 $infos['cvv_required_for_any_row'] = true;
609 break;
610 }
611 }
612 }
613
614 // TODO:check auch ob sofort redeem gemacht werden soll
615 // redeem liefert für jedes ticket eine Meldung - muss dann aufgelistet werden im ticket scanner
616
617 $infos = apply_filters( $this->MAIN->_add_filter_prefix.'ticket_retrieve_order_ticket', $infos, $code );
618
619 return $infos;
620 }
621
622 /**
623 * Apply per-row CVV gating to an order-ticket's ticket_infos array.
624 * Each row is checked against isCVVRequiredForScanner; rows that require CVV
625 * but have no/wrong CVV supplied are replaced with a stripped row that exposes
626 * only minimal anti-info-leak fields. Locked rows return the locked stub.
627 *
628 * @param array $ticketInfos Original ticket_infos array from getOrderTicketsInfos
629 * @param array<string,string> $cvvMap Map of raw ticket code => CVV input
630 * @return array Updated ticket_infos with stripped rows for CVV-blocked tickets
631 */
632 private function applyCVVGateToTicketInfos(array $ticketInfos, array $cvvMap): array {
633 foreach ($ticketInfos as $idx => $ticketObj) {
634 $rawCode = (string) ($ticketObj['code'] ?? '');
635 $displayCode = (string) ($ticketObj['code_display'] ?? '');
636 if ($rawCode === '') continue;
637
638 // Check locked state first via direct meta read — avoids needing to
639 // load codeObj for an already-locked ticket.
640 $lockedRow = $this->loadLockedMetaForCode($rawCode);
641 if ($lockedRow !== null) {
642 $ticketInfos[$idx] = [
643 'requires_cvv' => true,
644 'attempts_remaining' => 0,
645 'locked' => true,
646 'public_ticket_id' => (string) ($ticketObj['code_public'] ?? ($lockedRow['public_ticket_id'] ?? '')),
647 'code_public' => (string) ($ticketObj['code_public'] ?? ''),
648 'code' => $rawCode,
649 'code_display' => $displayCode,
650 'product_id' => (int) ($ticketObj['product_id'] ?? 0),
651 'product_parent_id' => (int) ($ticketObj['product_parent_id'] ?? 0),
652 ];
653 continue;
654 }
655
656 try {
657 $codeObj = $this->MAIN->getCore()->retrieveCodeByCode($rawCode);
658 } catch (Exception $e) {
659 continue;
660 }
661 $codeObj = $this->MAIN->getCore()->setMetaObj($codeObj);
662 $metaObj = $codeObj['metaObj'] ?? [];
663
664 if (!$this->MAIN->getCore()->isCVVRequiredForScanner($codeObj)) {
665 continue; // no CVV required — row passes through normally
666 }
667
668 // CVV map may be keyed by either raw or display code — accept both.
669 $cvvInput = '';
670 if (isset($cvvMap[$rawCode])) {
671 $cvvInput = (string) $cvvMap[$rawCode];
672 } elseif ($displayCode !== '' && isset($cvvMap[$displayCode])) {
673 $cvvInput = (string) $cvvMap[$displayCode];
674 }
675 if ($cvvInput === '') {
676 $ticketInfos[$idx] = $this->stripDetailsForCVVLocked($ticketObj, null, $metaObj);
677 continue;
678 }
679 $result = $this->MAIN->getCore()->verifyCVV($codeObj, $cvvInput);
680 if (!$result['ok']) {
681 $ticketInfos[$idx] = $this->stripDetailsForCVVLocked($ticketObj, $result, $metaObj);
682 continue;
683 }
684 // CVV verified — row passes through untouched
685 }
686 return $ticketInfos;
687 }
688
689 /**
690 * Quick locked-state probe for a raw code. Mirrors the locked pre-check in
691 * retrieve_ticket/redeem_ticket but operates on the raw code (no scanner-format
692 * decoding needed because the order-ticket already has the raw code).
693 *
694 * @param string $rawCode Raw code value (column codes.code)
695 * @return array{public_ticket_id:string}|null Returns minimal info if locked, null otherwise
696 */
697 private function loadLockedMetaForCode(string $rawCode): ?array {
698 if ($rawCode === '') return null;
699 global $wpdb;
700 $codesTable = $this->MAIN->getDB()->getTabelle('codes');
701 $row = $wpdb->get_row(
702 $wpdb->prepare("SELECT meta FROM $codesTable WHERE code = %s LIMIT 1", $rawCode),
703 ARRAY_A
704 );
705 if ($row === null) return null;
706 $rowMeta = json_decode($row['meta'] ?? '', true);
707 if (!is_array($rowMeta) || empty($rowMeta['cvv_attempts']['locked'])) {
708 return null;
709 }
710 return [
711 'public_ticket_id' => (string) ($rowMeta['wc_ticket']['_public_ticket_id'] ?? ''),
712 ];
713 }
714 private function retrieve_ticket($code, string $cvv = '') {
715 $ret = [];
716
717 // CVV-locked pre-check: if this ticket was locked by 5 wrong CVV attempts,
718 // return the structured locked response directly. We skip the normal
719 // getCodeObj() call because it throws on aktiv=0, which a CVV-lock sets.
720 try {
721 $_preCheckRawCode = $this->getParts($code)['code'] ?? '';
722 } catch (Exception $_e) {
723 $_preCheckRawCode = '';
724 }
725 if (!empty($_preCheckRawCode)) {
726 global $wpdb;
727 $codesTable = $this->MAIN->getDB()->getTabelle('codes');
728 $row = $wpdb->get_row(
729 $wpdb->prepare("SELECT meta FROM $codesTable WHERE code = %s LIMIT 1", $_preCheckRawCode),
730 ARRAY_A
731 );
732 if ($row !== null) {
733 $rowMeta = json_decode($row['meta'] ?? '', true);
734 if (is_array($rowMeta) && !empty($rowMeta['cvv_attempts']['locked'])) {
735 return [
736 'requires_cvv' => true,
737 'attempts_remaining' => 0,
738 'locked' => true,
739 'public_ticket_id' => (string) ($rowMeta['wc_ticket']['_public_ticket_id'] ?? ''),
740 ];
741 }
742 }
743 }
744
745 // Resolve codeObj early so the CVV gate can read product meta BEFORE the
746 // redeem-immediately path — otherwise ?redeem=1 would bypass the CVV check.
747 $codeObj = $this->getCodeObj(true, $code);
748 $codeObj = apply_filters( $this->MAIN->_add_filter_prefix.'filter_updateExpirationInfo', $codeObj );
749 $metaObj = $codeObj['metaObj'];
750
751 // CVV gate: short-circuit with minimal info if the per-product option requires
752 // a CVV and the request did not provide a correct one. Anti-information-leak —
753 // only the public ticket id is exposed; no name/seat/order details until verified.
754 // IMPORTANT: this gate runs BEFORE the redeem-immediately block so that
755 // ?redeem=1 cannot bypass CVV verification on a CVV-protected ticket.
756 if ($this->MAIN->getCore()->isCVVRequiredForScanner($codeObj)) {
757 $publicId = (string) ($metaObj['wc_ticket']['_public_ticket_id'] ?? '');
758 if ($cvv === '') {
759 $attempts = $metaObj['cvv_attempts'] ?? ['count' => 0, 'locked' => false];
760 return [
761 'requires_cvv' => true,
762 'attempts_remaining' => max(0, 5 - (int) ($attempts['count'] ?? 0)),
763 'locked' => (bool) ($attempts['locked'] ?? false),
764 'public_ticket_id' => $publicId,
765 ];
766 }
767 $result = $this->MAIN->getCore()->verifyCVV($codeObj, $cvv);
768 if (!$result['ok']) {
769 return [
770 'requires_cvv' => true,
771 'attempts_remaining' => $result['attempts_remaining'],
772 'locked' => $result['locked'],
773 'public_ticket_id' => $publicId,
774 ];
775 }
776 // CVV verified — fall through to normal retrieve flow below
777 }
778
779 // check if redeem immediately is requested (runs AFTER CVV gate above)
780 if (isset($_GET['redeem']) && $_GET['redeem'] == "1") {
781 // redeem immediately
782 $_redeem_ret = [];
783 try {
784 $_redeem_ret = $this->redeem_ticket($code, null, $cvv);
785 $this->setCodeObj(null); // reset object so the refresh below re-reads from DB
786 } catch(Exception $e) {
787 $_redeem_ret = ["error"=>$e->getMessage()];
788 }
789 $ret["redeem_operation"] = $_redeem_ret;
790 // Re-fetch codeObj after redeem so the retrieve response reflects the updated
791 // redeemed/aktiv state from the database (setCodeObj(null) cleared the cache above).
792 $codeObj = $this->getCodeObj(true, $code);
793 $codeObj = apply_filters( $this->MAIN->_add_filter_prefix.'filter_updateExpirationInfo', $codeObj );
794 $metaObj = $codeObj['metaObj'];
795 }
796
797 $order = $this->getOrderById($codeObj["order_id"]);
798 $order_item = $this->getOrderItem($order, $metaObj);
799 if ($order_item == null) return wp_send_json_error(__("Order item not found", 'event-tickets-with-ticket-scanner'));
800 $product = $order_item->get_product();
801 if ($product == null) return wp_send_json_error(esc_html__("product of the order and ticket not found!", 'event-tickets-with-ticket-scanner'));
802
803 $is_variation = $product->get_type() == "variation" ? true : false;
804 $product_parent = $product;
805 $product_parent_id = $product->get_parent_id();
806
807 if ($is_variation && $product_parent_id > 0) {
808 $product_parent = $this->get_product( $product_parent_id );
809 }
810
811 $product_original = $product; // original product, due to wpml
812 $product_parent_original = $product_parent; // original product parent, due to wpml
813 $product_original_id = $product->get_id();
814
815 // load a possible language based product
816 $product_original_id = $this->getWPMLProductId($product_original_id);
817 if ($product_original == null) {
818 return wp_send_json_error(esc_html__("original product of the order and ticket not found!", 'event-tickets-with-ticket-scanner'));
819 }
820 if ($product_original_id < 1) {
821 $product_original_id = $product->get_id(); // repair the product id
822 } else {
823 $product_original = $this->get_product($product_original_id);
824 if ($product_original_id > 0 && $product_original_id != $product->get_id()) {
825 $product_original = $this->get_product($product_original_id);
826 }
827 }
828
829 $saso_eventtickets_is_date_for_all_variants = true;
830 if ($is_variation && $product_parent_id > 0) {
831 $saso_eventtickets_is_date_for_all_variants = get_post_meta( $product_parent_original->get_id(), 'saso_eventtickets_is_date_for_all_variants', true ) == "yes" ? true : false;
832 }
833
834 $this->isProductAllowedByAuthToken([$product->get_id()]);
835
836 if ($this->MAIN->getOptions()->isOptionCheckboxActive('wcTicketScanneCountRetrieveAsConfirmed')) {
837 $codeObj = $this->MAIN->getFrontend()->countConfirmedStatus($codeObj, true);
838 $metaObj = $codeObj['metaObj'];
839 }
840
841 if (!isset($metaObj["wc_ticket"]["_public_ticket_id"])) $metaObj["wc_ticket"]["_public_ticket_id"] = "";
842 do_action( $this->MAIN->_do_action_prefix.'trackIPForTicketScannerCheck', array_merge($codeObj, ["_data_code"=>$metaObj["wc_ticket"]["_public_ticket_id"]]) );
843
844 $date_time_format = $this->MAIN->getOptions()->getOptionDateTimeFormat();
845
846 $is_expired = $this->MAIN->getCore()->checkCodeExpired($codeObj);
847
848 $ret['is_expired'] = $is_expired;
849 $ret['timezone_id'] = wp_timezone_string();
850 $ret['option_displayDateFormat'] = $this->MAIN->getOptions()->getOptionDateFormat();
851 $ret['option_displayTimeFormat'] = $this->MAIN->getOptions()->getOptionTimeFormat();
852 $ret['option_displayDateTimeFormat'] = $date_time_format;
853
854 $ret['is_paid'] = $this->isPaid($order);
855 $ret['allow_redeem_only_paid'] = $this->MAIN->getOptions()->isOptionCheckboxActive('wcTicketAllowRedeemOnlyPaid');
856 $ret['order_status'] = $order->get_status();
857 $ret = array_merge($ret, $this->rest_helper_tickets_redeemed($codeObj));
858 $ret['ticket_heading'] = esc_html($this->MAIN->getAdmin()->getOptionValue("wcTicketHeading"));
859 $ret['ticket_title'] = esc_html($product_parent->get_Title());
860 $ret['ticket_sub_title'] = "";
861 //if ($is_variation && $this->MAIN->getOptions()->isOptionCheckboxActive('wcTicketPDFDisplayVariantName') && count($product->get_attributes()) > 0) {
862 if ($is_variation && count($product->get_attributes()) > 0) {
863 foreach($product->get_attributes() as $k => $v){
864 $ret['ticket_sub_title'] .= $v." ";
865 }
866 $ret['ticket_sub_title'] = trim($ret['ticket_sub_title']);
867 }
868 $ret['ticket_location'] = trim(get_post_meta( $product_parent_original->get_id(), 'saso_eventtickets_event_location', true ));
869 $ret['ticket_info'] = wp_kses_post(nl2br(trim(get_post_meta( $product_parent_original->get_id(), 'saso_eventtickets_ticket_is_ticket_info', true ))));
870 $ret['ticket_location_label'] = wp_kses_post($this->MAIN->getAdmin()->getOptionValue("wcTicketTransLocation"));
871
872 $tmp_product = $product_parent_original;
873 if (!$saso_eventtickets_is_date_for_all_variants) $tmp_product = $product_original; // unter Umständen die Variante
874
875 $ret = array_merge($ret, $this->calcDateStringAllowedRedeemFrom($tmp_product->get_id(), $codeObj));
876
877 $ret['ticket_date_as_string'] = $this->displayTicketDateAsString($tmp_product->get_id(), $this->MAIN->getOptions()->getOptionDateFormat(), $this->MAIN->getOptions()->getOptionTimeFormat(), $codeObj);
878 $ret['short_desc'] = "";
879 if ($this->MAIN->getOptions()->isOptionCheckboxActive('wcTicketDisplayShortDesc')) {
880 $ret['short_desc'] = wp_kses_post(trim($product_parent->get_short_description()));
881 }
882 $ret['cst_label'] = "";
883 $ret['cst_billing_address'] = "";
884 if (!$this->MAIN->getOptions()->isOptionCheckboxActive('wcTicketDontDisplayCustomer')) {
885 $ret['cst_label'] = wp_kses_post($this->MAIN->getAdmin()->getOptionValue("wcTicketTransCustomer"));
886 $ret['cst_billing_address'] = wp_kses_post(trim($order->get_formatted_billing_address()));
887 }
888 $ret['payment_label'] = "";
889 $ret['payment_paid_at_label'] = "";
890 $ret['payment_paid_at'] = "";
891 $ret['payment_completed_at_label'] = "";
892 $ret['payment_completed_at'] = "";
893 $ret['payment_method'] = "";
894 $ret['payment_trx_id'] = "";
895 $ret['payment_method_label'] = "";
896 $ret['coupon_label'] = "";
897 $ret['coupon'] = "";
898 if (!$this->MAIN->getOptions()->isOptionCheckboxActive('wcTicketDontDisplayPayment')) {
899 $ret['payment_label'] = wp_kses_post(trim($this->MAIN->getAdmin()->getOptionValue("wcTicketTransPaymentDetail")));
900 $ret['payment_paid_at_label'] = wp_kses_post($this->MAIN->getAdmin()->getOptionValue("wcTicketTransPaymentDetailPaidAt"));
901 $ret['payment_completed_at_label'] = wp_kses_post($this->MAIN->getAdmin()->getOptionValue("wcTicketTransPaymentDetailCompletedAt"));
902 $ret['payment_paid_at'] = $order->get_date_paid() != null ? wp_date($date_time_format, strtotime($order->get_date_paid())) : "-";
903 $ret['payment_completed_at'] = $order->get_date_completed() != null ? wp_date($date_time_format, strtotime($order->get_date_completed())) : "-";
904 $payment_method = $order->get_payment_method_title();
905 if (!empty($payment_method)) {
906 $ret['payment_method_label'] = wp_kses_post($this->MAIN->getAdmin()->getOptionValue("wcTicketTransPaymentDetailPaidVia"));
907 $ret['payment_method'] = esc_html($payment_method);
908 $ret['payment_trx_id'] = esc_html($order->get_transaction_id());
909 } else {
910 $ret['payment_method_label'] = wp_kses_post($this->MAIN->getAdmin()->getOptionValue("wcTicketTransPaymentDetailFreeTicket"));
911 }
912 $coupons = $order->get_coupon_codes();
913 if (count($coupons) > 0) {
914 $ret['coupon_label'] = wp_kses_post($this->MAIN->getAdmin()->getOptionValue("wcTicketTransPaymentDetailCouponUsed"));
915 $ret['coupon'] = esc_html(implode(", ", $coupons));
916 }
917 }
918 $ret['product'] = [];
919 $ret['product']['id'] = $product->get_id();
920 $ret['product']['parent_id'] = $product_parent->get_id();
921 $ret['product']['id_original'] = $product_original->get_id();
922 $ret['product']['parent_id_original'] = $product_parent_original->get_id();
923 $ret['product']['name'] = esc_html($product_parent->get_Title());
924 $ret['product']['name_variant'] = "";
925 //if ($is_variation && $this->MAIN->getOptions()->isOptionCheckboxActive('wcTicketPDFDisplayVariantName') && count($product->get_attributes()) > 0) {
926 if ($is_variation && count($product->get_attributes()) > 0) {
927 foreach($product->get_attributes() as $k => $v){
928 $ret['product']['name_variant'] .= $v." ";
929 }
930 }
931 $ret['product']['sku'] = esc_html($product->get_sku());
932 $ret['product']['type'] = esc_html($product->get_type());
933
934 $order_quantity = $order_item->get_quantity();
935 $ticket_pos = "";
936 if ($order_quantity > 1) {
937 // ermittel ticket pos
938 $codes = explode(",", $order_item->get_meta('_saso_eventtickets_product_code', true));
939 $ticket_pos = $this->ermittelCodePosition($codeObj['code_display'], $codes);
940 }
941 $_vid = $is_variation ? $product_original->get_id() : 0;
942 $label = esc_attr($this->getLabelNamePerTicket($product_parent_original->get_id(), $_vid));
943 $ret['name_per_ticket_label'] = str_replace("{count}", $ticket_pos, $label);
944
945 $ticket_pos = "";
946 if ($order_quantity > 1) {
947 // ermittel ticket pos
948 $codes = explode(",", $order_item->get_meta('_saso_eventtickets_product_code', true));
949 $ticket_pos = $this->ermittelCodePosition($codeObj['code_display'], $codes);
950 }
951 $label = esc_attr($this->getLabelValuePerTicket($product_parent_original->get_id(), $_vid));
952 $ret['value_per_ticket_label'] = str_replace("{count}", $ticket_pos, $label);
953
954 $ticket_pos = "";
955 if ($order_quantity > 1) {
956 // ermittel ticket pos
957 $codes = explode(",", $order_item->get_meta('_saso_eventtickets_product_code', true));
958 $ticket_pos = $this->ermittelCodePosition($codeObj['code_display'], $codes);
959 }
960 $label = esc_attr($this->getLabelDaychooserPerTicket($product_parent_original->get_id()));
961 $ret['day_per_ticket_label'] = str_replace("{count}", $ticket_pos, $label);
962
963 // Seat information
964 $ret['seat_label'] = !empty($metaObj['wc_ticket']['seat_label']) ? esc_html($metaObj['wc_ticket']['seat_label']) : '';
965 $ret['seat_category'] = !empty($metaObj['wc_ticket']['seat_category']) ? esc_html($metaObj['wc_ticket']['seat_category']) : '';
966 $ret['seat_id'] = !empty($metaObj['wc_ticket']['seat_id']) ? intval($metaObj['wc_ticket']['seat_id']) : 0;
967 $ret['seat_desc'] = '';
968 $ret['seating_plan_id'] = 0;
969 $ret['seating_plan_name'] = '';
970 $ret['seat_label_text'] = '';
971 if ($ret['seat_id'] > 0) {
972 $ret['seat_label_text'] = esc_html($this->MAIN->getOptions()->getOptionValue('wcTicketTransSeat'));
973 if (empty($ret['seat_label_text'])) {
974 $ret['seat_label_text'] = __('Seat', 'event-tickets-with-ticket-scanner');
975 }
976 // Load seat description if option active
977 if ($this->MAIN->getOptions()->isOptionCheckboxActive('seatingShowDescInScanner')) {
978 $seat = $this->MAIN->getSeating()->getSeatManager()->getById($ret['seat_id']);
979 if ($seat && !empty($seat['meta'])) {
980 $seatMeta = is_array($seat['meta']) ? $seat['meta'] : json_decode($seat['meta'], true);
981 $ret['seat_desc'] = esc_html($seatMeta['seat_desc'] ?? '');
982 }
983 }
984 // Only load plan info if not hidden
985 if (!$this->MAIN->getOptions()->isOptionCheckboxActive('seatingHidePlanNameInScanner')) {
986 $planId = $this->MAIN->getSeating()->getSeatManager()->getSeatingPlanIdForSeatId($ret['seat_id']);
987 if ($planId) {
988 $ret['seating_plan_id'] = intval($planId);
989 $plan = $this->MAIN->getSeating()->getPlanManager()->getById($planId);
990 if ($plan) {
991 $ret['seating_plan_name'] = esc_html($plan['name']);
992 }
993 }
994 }
995 // Load seating plan data for buttons
996 $showVenueOption = $this->MAIN->getOptions()->isOptionCheckboxActive('ticketScannerShowVenueImage');
997 $showSeatingPlanOption = $this->MAIN->getOptions()->isOptionCheckboxActive('ticketScannerShowSeatingPlan');
998 if ($showVenueOption || $showSeatingPlanOption) {
999 $planId = $ret['seating_plan_id'] > 0 ? $ret['seating_plan_id'] : $this->MAIN->getSeating()->getSeatManager()->getSeatingPlanIdForSeatId($ret['seat_id']);
1000 if ($planId) {
1001 $plan = $this->MAIN->getSeating()->getPlanManager()->getById($planId);
1002 if ($plan) {
1003 $planMeta = is_array($plan['meta']) ? $plan['meta'] : json_decode($plan['meta'], true);
1004 $ret['seating_plan_layout_type'] = esc_html($plan['layout_type'] ?? 'simple');
1005 $ret['seating_plan_description'] = esc_html($planMeta['description'] ?? '');
1006
1007 // Venue image - available for ALL plan types
1008 $imageId = intval($planMeta['image_id'] ?? 0);
1009 $ret['seating_plan_image_url'] = '';
1010 if ($imageId > 0) {
1011 $ret['seating_plan_image_url'] = wp_get_attachment_url($imageId);
1012 }
1013 // Show venue image button if image exists AND option is active
1014 $ret['seating_plan_show_venue_button'] = $showVenueOption && !empty($ret['seating_plan_image_url']);
1015
1016 // For visual plans: show button, data loaded on demand via REST endpoint
1017 $ret['seating_plan_show_visual_button'] = ($plan['layout_type'] === 'visual' && $showSeatingPlanOption);
1018 }
1019 }
1020 }
1021 }
1022
1023 $ret['ticket_amount_label'] = "";
1024 if ($this->MAIN->getOptions()->isOptionCheckboxActive('wcTicketDisplayPurchasedTicketQuantity')) {
1025 $text_ticket_amount = wp_kses_post($this->MAIN->getOptions()->getOptionValue('wcTicketPrefixTextTicketQuantity'));
1026 //$order_quantity = $order_item->get_quantity();
1027 $ticket_pos = 1;
1028 if ($order_quantity > 1) {
1029 // ermittel ticket pos
1030 $codes = explode(",", $order_item->get_meta('_saso_eventtickets_product_code', true));
1031 $ticket_pos = $this->ermittelCodePosition($codeObj['code_display'], $codes);
1032 }
1033 $text_ticket_amount = str_replace("{TICKET_POSITION}", $ticket_pos, $text_ticket_amount);
1034 $text_ticket_amount = str_replace("{TICKET_TOTAL_AMOUNT}", $order_quantity, $text_ticket_amount);
1035 $ret['ticket_amount_label'] = $text_ticket_amount;
1036 }
1037 $ret['ticket_label'] = wp_kses_post($this->MAIN->getAdmin()->getOptionValue("wcTicketTransTicket"));
1038 $paid_price = $order_item->get_subtotal() / $order_item->get_quantity();
1039 $ret['paid_price_label'] = wp_kses_post($this->MAIN->getAdmin()->getOptionValue("wcTicketTransPrice"));
1040 $ret['paid_price'] = floatval($paid_price);
1041 $ret['paid_price_as_string'] = function_exists("wc_price") ? wc_price($paid_price, ['decimals'=>2]) : $paid_price;
1042 $product_price = $product_original->get_price();
1043 $ret['product_price_label'] = wp_kses_post($this->MAIN->getAdmin()->getOptionValue("wcTicketTransProductPrice"));
1044 $ret['product_price'] = floatval($product_price);
1045 $ret['product_price_as_string'] = function_exists("wc_price") ? wc_price($product_price, ['decimals'=>2]) : $product_price;
1046
1047 $ret['msg_redeemed'] = wp_kses_post($this->MAIN->getAdmin()->getOptionValue("wcTicketTransTicketRedeemed"));
1048 $ret['redeemed_date_label'] = wp_kses_post($this->MAIN->getAdmin()->getOptionValue("wcTicketTransRedeemDate"));
1049 $ret['msg_ticket_valid'] = wp_kses_post($this->MAIN->getAdmin()->getOptionValue("wcTicketTransTicketValid"));
1050 $ret['msg_ticket_expired'] = wp_kses_post($this->MAIN->getAdmin()->getOptionValue("wcTicketTransTicketExpired"));
1051
1052 $ret['msg_ticket_not_valid_yet'] = wp_kses_post($this->MAIN->getAdmin()->getOptionValue("wcTicketTransTicketNotValidToEarly"));
1053 $ret['msg_ticket_not_valid_anymore'] = wp_kses_post($this->MAIN->getAdmin()->getOptionValue("wcTicketTransTicketNotValidToLate"));
1054 $ret['msg_ticket_event_ended'] = wp_kses_post($this->MAIN->getAdmin()->getOptionValue("wcTicketTransTicketNotValidToLateEndEvent"));
1055
1056 $ret['max_redeem_amount'] = intval(get_post_meta( $product_parent_original->get_id(), 'saso_eventtickets_ticket_max_redeem_amount', true ));
1057 if ($ret['max_redeem_amount'] < 0) $ret['max_redeem_amount'] = 1;
1058
1059 $ret['max_redeem_per_day'] = intval(get_post_meta($product_parent_original->get_id(), 'saso_eventtickets_ticket_max_redeem_per_day', true));
1060 $ret['redeems_today'] = $this->countRedeemsToday($metaObj['wc_ticket']['stats_redeemed']);
1061
1062 $ret['_options'] = [
1063 "displayConfirmedCounter"=>$this->MAIN->getOptions()->isOptionCheckboxActive('wcTicketScannerDisplayConfirmedCount'),
1064 "wcTicketDontAllowRedeemTicketBeforeStart"=>$this->MAIN->getOptions()->isOptionCheckboxActive('wcTicketDontAllowRedeemTicketBeforeStart'),
1065 "wcTicketAllowRedeemTicketAfterEnd"=>$this->MAIN->getOptions()->isOptionCheckboxActive('wcTicketAllowRedeemTicketAfterEnd'),
1066 "wsticketDenyRedeemAfterstart"=>$this->MAIN->getOptions()->isOptionCheckboxActive('wsticketDenyRedeemAfterstart'),
1067 "isRedeemOperationTooEarly"=>$this->isRedeemOperationTooEarly($codeObj, $metaObj, $order),
1068 "isRedeemOperationTooLateEventEnded"=>$this->isRedeemOperationTooLateEventEnded($codeObj, $metaObj, $order),
1069 "isRedeemOperationTooLate"=>$this->isRedeemOperationTooLate($codeObj, $metaObj, $order)
1070 ];
1071
1072 $ret['_server'] = $this->getTimes();
1073
1074 $codeObj["_ret"] = $ret;
1075 $codeObj["metaObj"] = $metaObj;
1076
1077 $codeObj = apply_filters( $this->MAIN->_add_filter_prefix.'ticket_retrieve_ticket', $codeObj, $code );
1078
1079 return $codeObj;
1080 }
1081 function getTimes() {
1082 $timezone_utc = new DateTimeZone("UTC");
1083 $dt = new DateTime('now', $timezone_utc);
1084 return [
1085 "time"=>wp_date("Y-m-d H:i:s"),
1086 "timestamp"=>time(),
1087 "UTC_time"=>$dt->format("Y-m-d H:i:s"),
1088 "timezone"=>wp_timezone()
1089 ];
1090 }
1091 function rest_redeem_ticket(WP_REST_Request $web_request) {
1092 // Accept code from WP_REST_Request object (REST API / PHPUnit) OR from
1093 // $_GET/$_POST superglobals (legacy AJAX path). Mirror rest_retrieve_ticket.
1094 $code = '';
1095 if (is_object($web_request) && method_exists($web_request, 'get_param')) {
1096 $paramCode = $web_request->get_param('code');
1097 if ($paramCode !== null && $paramCode !== '') {
1098 $code = trim((string) $paramCode);
1099 $_GET['code'] = $code;
1100 }
1101 }
1102 if ($code === '' && SASO_EVENTTICKETS::issetRPara('code')) {
1103 $code = trim((string) SASO_EVENTTICKETS::getRequestPara('code'));
1104 }
1105 if ($code === '') {
1106 wp_send_json_error(esc_html__("code missing", 'event-tickets-with-ticket-scanner'));
1107 }
1108
1109 // Read CVV from REST request object (preferred) or superglobal fallback.
1110 // For order-ticket scans the cvv param may be a per-row map (array) — keep it
1111 // out of the scalar $cvv used by the single-ticket path.
1112 $cvv = '';
1113 if (is_object($web_request) && method_exists($web_request, 'get_param')) {
1114 $paramCvv = $web_request->get_param('cvv');
1115 if ($paramCvv !== null && !is_array($paramCvv)) {
1116 $cvv = trim((string) $paramCvv);
1117 }
1118 }
1119 if ($cvv === '' && SASO_EVENTTICKETS::issetRPara('cvv')) {
1120 $rawCvv = SASO_EVENTTICKETS::getRequestPara('cvv');
1121 if (!is_array($rawCvv)) {
1122 $cvv = trim((string) $rawCvv);
1123 }
1124 }
1125
1126 $ret = null;
1127 if ($this->is_ticket_code_orderticket($code)) {
1128 // Per-row CVV map (Task 15)
1129 $cvvMap = $this->readCVVMapFromRequest($web_request);
1130 $ret = $this->redeem_order_ticket($code, $cvvMap);
1131 }
1132 if ($ret == null) {
1133 $ret = $this->redeem_ticket($code, null, $cvv);
1134 }
1135 $ret = apply_filters( $this->MAIN->_add_filter_prefix.'ticket_rest_redeem_ticket', $ret, $web_request );
1136 return $ret;
1137 }
1138
1139 /**
1140 * REST endpoint to load seating plan data (lazy loading for ticket scanner)
1141 *
1142 * @param WP_REST_Request $web_request Request with plan_id and optional seat_id
1143 * @return array Seating plan data for rendering
1144 */
1145 function rest_seating_plan(WP_REST_Request $web_request): array {
1146 $planId = intval(SASO_EVENTTICKETS::getRequestPara('plan_id'));
1147 $seatIdToHighlight = intval(SASO_EVENTTICKETS::getRequestPara('seat_id'));
1148
1149 if ($planId <= 0) {
1150 throw new Exception(esc_html__("Plan ID missing", 'event-tickets-with-ticket-scanner'));
1151 }
1152
1153 $plan = $this->MAIN->getSeating()->getPlanManager()->getById($planId);
1154 if (!$plan) {
1155 throw new Exception(esc_html__("Seating plan not found", 'event-tickets-with-ticket-scanner'));
1156 }
1157
1158 $planMeta = is_array($plan['meta']) ? $plan['meta'] : json_decode($plan['meta'], true);
1159
1160 // Get published meta (same as frontend does)
1161 $designerMeta = !empty($plan['meta_published'])
1162 ? (is_array($plan['meta_published']) ? $plan['meta_published'] : json_decode($plan['meta_published'], true))
1163 : [];
1164
1165 // Merge plan meta with designer meta
1166 $fullMeta = array_replace_recursive(
1167 $this->MAIN->getSeating()->getPlanManager()->getMetaObject(),
1168 is_array($planMeta) ? $planMeta : [],
1169 is_array($designerMeta) ? $designerMeta : []
1170 );
1171
1172 // Venue image
1173 $imageId = intval($planMeta['image_id'] ?? 0);
1174 $planImageUrl = $imageId > 0 ? wp_get_attachment_url($imageId) : '';
1175
1176 $ret = [
1177 'planId' => intval($plan['id']),
1178 'planName' => $plan['name'] ?? '',
1179 'layoutType' => $plan['layout_type'],
1180 'planImage' => $planImageUrl,
1181 'currentSeatId' => $seatIdToHighlight,
1182 'meta' => [
1183 'canvas_width' => intval($fullMeta['canvas_width'] ?? 800),
1184 'canvas_height' => intval($fullMeta['canvas_height'] ?? 600),
1185 'background_color' => $fullMeta['background_color'] ?? '#ffffff',
1186 'background_image' => $fullMeta['background_image'] ?? '',
1187 'decorations' => $fullMeta['decorations'] ?? [],
1188 'lines' => $fullMeta['lines'] ?? [],
1189 'labels' => $fullMeta['labels'] ?? [],
1190 ],
1191 'seats' => []
1192 ];
1193
1194 // Get all seats with their full meta
1195 $allSeats = $this->MAIN->getSeating()->getSeatManager()->getByPlanId($planId, true);
1196 foreach ($allSeats as $s) {
1197 $sMeta = is_array($s['meta']) ? $s['meta'] : json_decode($s['meta'], true);
1198 $ret['seats'][] = [
1199 'id' => intval($s['id']),
1200 'seat_identifier' => $s['seat_identifier'],
1201 'meta' => $sMeta,
1202 'is_current' => intval($s['id']) === $seatIdToHighlight
1203 ];
1204 }
1205
1206 return $ret;
1207 }
1208
1209 private function redeem_order_ticket($code, array $cvvMap = []) {
1210 $parts = $this->getParts($code);
1211 if (!isset($parts["order_id"]) || !isset($parts["code"])) throw new Exception("#296 - wrong order ticket id");
1212 if (empty($parts["order_id"]) || empty($parts["code"])) throw new Exception("#295 - wrong order ticket id");
1213
1214 $order_id = intval($parts["order_id"]);
1215 $order = wc_get_order($order_id);
1216 if ($order == null) return "Wrong ticket code id for redeem order ticket";
1217 $idcode = $order->get_meta('_saso_eventtickets_order_idcode');
1218 if (empty($idcode) || $idcode != $parts["code"]) return "Wrong ticket code for redeem order ticket";
1219
1220 $products = $this->MAIN->getWC()->getTicketsFromOrder($order);
1221 $ret = ["is_order_ticket"=>true, "errors"=>[], "not_redeemed"=>[], "redeemed"=>[], "cvv_required"=>[], "products"=>[]];
1222 foreach($products as $obj) { // one ticket can have multiple
1223 $codes = [];
1224 if (!empty($obj['codes'])) {
1225 $codes = explode(",", $obj['codes']);
1226 $ret["products"][] = $obj;
1227 }
1228 foreach($codes as $code) {
1229 $public_ticket_id = "";
1230 try {
1231 $this->parts = null; // clear cache
1232 $codeObj = $this->MAIN->getCore()->retrieveCodeByCode($code);
1233 $metaObj = $this->MAIN->getCore()->encodeMetaValuesAndFillObject($codeObj['meta'], $codeObj);
1234 $codeObj["metaObj"] = $metaObj;
1235 $public_ticket_id = $metaObj["wc_ticket"]["_public_ticket_id"];
1236 // $code is the display form (with dashes); $codeObj['code'] is the raw form
1237 $rawCodeForGate = (string) ($codeObj['code'] ?? '');
1238 $displayCodeForGate = (string) $code;
1239
1240 // Task 15 — per-row CVV gate. If this row's product requires CVV, verify
1241 // the CVV from the per-row map BEFORE invoking redeem_ticket. Rows that
1242 // fail the gate are NOT redeemed; they get a requires_cvv stub.
1243 if ($this->MAIN->getCore()->isCVVRequiredForScanner($codeObj)) {
1244 // Build a ticketObj-compatible shape so stripDetailsForCVVLocked
1245 // can be used for all three stub cases below (locked, empty-CVV,
1246 // wrong-CVV). product_id / product_parent_id come from woocommerce
1247 // meta; these are the same product-public values carried by
1248 // getOrderTicketsInfos rows, so retrieve and redeem shapes stay
1249 // in lockstep when the helper changes.
1250 $stubTicketObj = [
1251 'code' => $rawCodeForGate,
1252 'code_display' => $displayCodeForGate,
1253 'code_public' => (string) $public_ticket_id,
1254 'product_id' => (int) ($metaObj['woocommerce']['product_id'] ?? 0),
1255 'product_parent_id'=> (int) ($metaObj['woocommerce']['product_parent_id'] ?? 0),
1256 ];
1257
1258 $lockedMeta = $rawCodeForGate !== '' ? $this->loadLockedMetaForCode($rawCodeForGate) : null;
1259 if ($lockedMeta !== null) {
1260 $ret["cvv_required"][] = $this->stripDetailsForCVVLocked(
1261 $stubTicketObj,
1262 ['attempts_remaining' => 0, 'locked' => true],
1263 $metaObj
1264 );
1265 continue;
1266 }
1267 // CVV map keyed by raw or display code — accept both.
1268 $cvvInput = '';
1269 if ($rawCodeForGate !== '' && isset($cvvMap[$rawCodeForGate])) {
1270 $cvvInput = (string) $cvvMap[$rawCodeForGate];
1271 } elseif (isset($cvvMap[$displayCodeForGate])) {
1272 $cvvInput = (string) $cvvMap[$displayCodeForGate];
1273 }
1274 if ($cvvInput === '') {
1275 $ret["cvv_required"][] = $this->stripDetailsForCVVLocked(
1276 $stubTicketObj,
1277 null,
1278 $metaObj
1279 );
1280 continue;
1281 }
1282 $result = $this->MAIN->getCore()->verifyCVV($codeObj, $cvvInput);
1283 if (!$result['ok']) {
1284 $ret["cvv_required"][] = $this->stripDetailsForCVVLocked(
1285 $stubTicketObj,
1286 $result,
1287 $metaObj
1288 );
1289 continue;
1290 }
1291 // CVV verified — fall through to redeem_ticket call
1292 }
1293
1294 $r = $this->redeem_ticket("", $codeObj);
1295 $r["code"] = $code;
1296 if ($this->redeem_successfully) {
1297 $ret["redeemed"][] = $r;
1298 } else {
1299 $ret["not_redeemed"] = $r; // is not implemented yet - all not redeem operation are exceptions
1300 }
1301 } catch(Exception $e) {
1302 $ret["errors"][] = ["error"=>$e->getMessage(), "code"=>$code, "ticket_id"=>$public_ticket_id];
1303 }
1304 }
1305 }
1306 $ret = apply_filters( $this->MAIN->_add_filter_prefix.'ticket_redeem_order_ticket', $ret, $code );
1307 do_action( $this->MAIN->_do_action_prefix.'ticket_redeem_order_ticket', $code, $ret );
1308 return $ret;
1309 }
1310 private function redeem_ticket($code, $codeObj=null, string $cvv = '') {
1311 // CVV-locked pre-check: if this ticket was locked by 5 wrong CVV attempts,
1312 // return the structured locked response directly. We skip the normal
1313 // getCodeObj() call because it throws on aktiv=0, which a CVV-lock sets.
1314 if ($codeObj === null && !empty($code)) {
1315 try {
1316 $_preCheckRawCode = $this->getParts($code)['code'] ?? '';
1317 } catch (Exception $_e) {
1318 $_preCheckRawCode = '';
1319 }
1320 if (!empty($_preCheckRawCode)) {
1321 global $wpdb;
1322 $codesTable = $this->MAIN->getDB()->getTabelle('codes');
1323 $_preRow = $wpdb->get_row(
1324 $wpdb->prepare("SELECT meta FROM $codesTable WHERE code = %s LIMIT 1", $_preCheckRawCode),
1325 ARRAY_A
1326 );
1327 if ($_preRow !== null) {
1328 $_preMeta = json_decode($_preRow['meta'] ?? '', true);
1329 if (is_array($_preMeta) && !empty($_preMeta['cvv_attempts']['locked'])) {
1330 return [
1331 'requires_cvv' => true,
1332 'attempts_remaining' => 0,
1333 'locked' => true,
1334 'public_ticket_id' => (string) ($_preMeta['wc_ticket']['_public_ticket_id'] ?? ''),
1335 ];
1336 }
1337 }
1338 }
1339 }
1340
1341 if ($codeObj === null) {
1342 $codeObj = $this->getCodeObj(true, $code);
1343 }
1344 $metaObj = $codeObj['metaObj'];
1345
1346 // CVV gate: short-circuit with minimal info if the per-product option requires
1347 // a CVV and the request did not provide a correct one. Anti-information-leak —
1348 // only the public ticket id is exposed; no name/seat/order details until verified.
1349 // IMPORTANT: this gate runs BEFORE redeemTicket() so that the ticket cannot be
1350 // redeemed without a valid CVV on CVV-protected products.
1351 // Skip CVV gate for the order-ticket path (which passes $code='' + pre-resolved $codeObj).
1352 // Per-row CVV handling in order-ticket scans is Task 15.
1353 if (!empty($code) && $this->MAIN->getCore()->isCVVRequiredForScanner($codeObj)) {
1354 $publicId = (string) ($metaObj['wc_ticket']['_public_ticket_id'] ?? '');
1355 if ($cvv === '') {
1356 $attempts = $metaObj['cvv_attempts'] ?? ['count' => 0, 'locked' => false];
1357 return [
1358 'requires_cvv' => true,
1359 'attempts_remaining' => max(0, 5 - (int) ($attempts['count'] ?? 0)),
1360 'locked' => (bool) ($attempts['locked'] ?? false),
1361 'public_ticket_id' => $publicId,
1362 ];
1363 }
1364 $result = $this->MAIN->getCore()->verifyCVV($codeObj, $cvv);
1365 if (!$result['ok']) {
1366 return [
1367 'requires_cvv' => true,
1368 'attempts_remaining' => $result['attempts_remaining'],
1369 'locked' => $result['locked'],
1370 'public_ticket_id' => $publicId,
1371 ];
1372 }
1373 // CVV verified — fall through to actual redeem
1374 }
1375
1376 $order = $this->getOrderById($codeObj["order_id"]);
1377 $order_item = $this->getOrderItem($order, $metaObj);
1378 if ($order_item == null) return wp_send_json_error("#302 ".__("Order item not found", 'event-tickets-with-ticket-scanner'));
1379 $product = $order_item->get_product();
1380 if ($product == null) return wp_send_json_error("#303 ".esc_html__("product of the order and ticket not found!", 'event-tickets-with-ticket-scanner'));
1381
1382 $this->isProductAllowedByAuthToken([$product->get_id()]);
1383
1384 $this->redeemTicket($codeObj);
1385 $ticket_id = $this->MAIN->getCore()->getTicketId($codeObj, $metaObj);
1386
1387 $ret = ['redeem_successfully'=>$this->redeem_successfully, 'ticket_id'=>$ticket_id];
1388 $ret["_ret"] = $this->rest_helper_tickets_redeemed($codeObj);
1389
1390 $ret = apply_filters( $this->MAIN->_add_filter_prefix.'ticket_redeem_ticket', $ret, $code, $codeObj );
1391
1392 return $ret;
1393 }
1394
1395 public function getCalcDateStringAllowedRedeemFromCorrectProduct($product_id, $codeObj = null) {
1396 $product = $this->get_product( $product_id );
1397 if ($product == null) {
1398 return wp_send_json_error(esc_html__("#232 original product of the order and ticket not found!", 'event-tickets-with-ticket-scanner'));
1399 }
1400 $is_variation = false;
1401 try {
1402 $is_variation = $product->get_type() == "variation";
1403 } catch (Exception $e) {
1404 $this->MAIN->getAdmin()->logErrorToDB($e);
1405 }
1406 $tmp_prod = $product;
1407 if ($is_variation) {
1408 $product_parent_id = $product->get_parent_id();
1409 $product_parent_id_orig = $this->getWPMLProductId($product_parent_id);
1410 if ($product_parent_id_orig > 0) {
1411 $product_parent = $this->get_product( $product_parent_id_orig );
1412 $saso_eventtickets_is_date_for_all_variants = get_post_meta( $product_parent->get_id(), 'saso_eventtickets_is_date_for_all_variants', true ) == "yes";
1413 if ($saso_eventtickets_is_date_for_all_variants) {
1414 $tmp_prod = $product_parent;
1415 }
1416 }
1417 }
1418 return $this->calcDateStringAllowedRedeemFrom($tmp_prod->get_id(), $codeObj);
1419 }
1420 /**
1421 * Convert a local date+time (as entered by admin in WordPress timezone) to a UTC Unix timestamp.
1422 * WordPress sets PHP timezone to UTC, so strtotime() would wrongly interpret local dates as UTC.
1423 */
1424 private function localDateToTimestamp(string $date, string $time = ''): int {
1425 $datetime_str = trim($date . ' ' . $time);
1426 try {
1427 $dt = new \DateTime($datetime_str, wp_timezone());
1428 return $dt->getTimestamp();
1429 } catch (\Exception $e) {
1430 return (int) strtotime($datetime_str);
1431 }
1432 }
1433
1434 public function calcDateStringAllowedRedeemFrom($product_id, $codeObj = null) {
1435 // check if product id is from WPML plugin
1436 // get the original product id, because the event ticket information is stored in the original product
1437 $product_id_orig = $this->getWPMLProductId($product_id);
1438
1439 $ret = [];
1440 $ret['is_daychooser'] = get_post_meta( $product_id_orig, 'saso_eventtickets_is_daychooser', true ) == "yes" ? true : false;
1441 $ret['is_daychooser_value_set'] = false;
1442 $ret['is_date_for_all_variants'] = get_post_meta( $product_id_orig, 'saso_eventtickets_is_date_for_all_variants', true ) == "yes" ? true : false;
1443 $ret['is_date_set'] = true;
1444 $ret['is_end_date_set'] = true;
1445 $ret['is_end_time_set'] = false;
1446
1447 $ret['ticket_start_date'] = trim(get_post_meta( $product_id_orig, 'saso_eventtickets_ticket_start_date', true ));
1448 $ret['ticket_start_time'] = trim(get_post_meta( $product_id_orig, 'saso_eventtickets_ticket_start_time', true ));
1449 $ret['is_start_time_set'] = !empty($ret['ticket_start_time']) ? true : false;
1450 $ret['ticket_end_date'] = trim(get_post_meta( $product_id_orig, 'saso_eventtickets_ticket_end_date', true ));
1451 $ret['ticket_end_date_orig'] = $ret['ticket_end_date'];
1452 $ret['ticket_end_time'] = trim(get_post_meta( $product_id_orig, 'saso_eventtickets_ticket_end_time', true ));
1453
1454 $ret['daychooser_offset_start'] = intval(get_post_meta( $product_id_orig, 'saso_eventtickets_daychooser_offset_start', true ));
1455 $ret['daychooser_offset_end'] = intval(get_post_meta( $product_id_orig, 'saso_eventtickets_daychooser_offset_end', true ));
1456 $ret['daychooser_exclude_wdays'] = get_post_meta( $product_id_orig, 'saso_eventtickets_daychooser_exclude_wdays', true );
1457 if ($ret['daychooser_exclude_wdays'] == "") $ret['daychooser_exclude_wdays'] = [];
1458
1459 $ret['daychooser_exclude_dates'] = [];
1460 if ($this->MAIN->isPremium() && method_exists($this->MAIN->getPremiumFunctions(), 'getDayChooserExclusionDates')) {
1461 $ret['daychooser_exclude_dates'] = $this->MAIN->getPremiumFunctions()->getDayChooserExclusionDates($product_id_orig);
1462 if (!is_array($ret['daychooser_exclude_dates'])) $ret['daychooser_exclude_dates'] = [];
1463 }
1464
1465 if ($codeObj != null && $ret['is_daychooser']) {
1466 // use date of the codeObj
1467 $codeObj = $this->MAIN->getCore()->setMetaObj($codeObj);
1468 $metaObj = $codeObj['metaObj'];
1469 $is_daychooser = intval($metaObj["wc_ticket"]["is_daychooser"]);
1470 $day_per_ticket = $metaObj["wc_ticket"]["day_per_ticket"];
1471 if ($is_daychooser == 1 && !empty($day_per_ticket)) {
1472 if (empty($ret['ticket_start_time'])) {
1473 $ret['ticket_start_time'] = "00:00:00";
1474 }
1475 $ret['ticket_start_date'] = $day_per_ticket;
1476 $ret['ticket_end_date'] = $day_per_ticket;
1477 $ret['is_daychooser_value_set'] = true;
1478 }
1479 }
1480
1481 if (empty($ret['ticket_start_date']) && empty($ret['ticket_start_time'])) { // date not set
1482 $ret['is_date_set'] = false; // indicates that the ticket start date is not set, and the values are calculated
1483 }
1484 if (empty($ret['ticket_start_date'])) {
1485 $ret['ticket_start_date'] = wp_date("Y-m-d");
1486 }
1487 $ret['ticket_start_date_timestamp'] = $this->localDateToTimestamp($ret['ticket_start_date'], $ret['ticket_start_time']);
1488 $ret['ticket_start_p_date'] = wp_date("d", $ret['ticket_start_date_timestamp']);
1489 $ret['ticket_start_p_month'] = wp_date("m", $ret['ticket_start_date_timestamp']);
1490 $ret['ticket_start_p_year'] = wp_date("Y", $ret['ticket_start_date_timestamp']);
1491 $ret['ticket_start_p_hour'] = wp_date("H", $ret['ticket_start_date_timestamp']);
1492 $ret['ticket_start_p_min'] = wp_date("i", $ret['ticket_start_date_timestamp']);
1493 $ret['ticket_start_p_sec'] = wp_date("s", $ret['ticket_start_date_timestamp']);
1494
1495 if (empty($ret['ticket_end_date'])) {
1496 $ret['ticket_end_date'] = $ret['ticket_start_date'];
1497 $ret['is_end_date_set'] = false;
1498 }
1499
1500 if (empty($ret['ticket_end_time'])) {
1501 $ret['ticket_end_time'] = "23:59:59";
1502 } else {
1503 $ret['is_end_time_set'] = true;
1504 }
1505 $ret['ticket_end_date_timestamp'] = $this->localDateToTimestamp($ret['ticket_end_date'], $ret['ticket_end_time']);
1506 $ret['ticket_end_p_date'] = wp_date("d", $ret['ticket_end_date_timestamp']);
1507 $ret['ticket_end_p_month'] = wp_date("m", $ret['ticket_end_date_timestamp']);
1508 $ret['ticket_end_p_year'] = wp_date("Y", $ret['ticket_end_date_timestamp']);
1509 $ret['ticket_end_p_hour'] = wp_date("H", $ret['ticket_end_date_timestamp']);
1510 $ret['ticket_end_p_min'] = wp_date("i", $ret['ticket_end_date_timestamp']);
1511 $ret['ticket_end_p_sec'] = wp_date("s", $ret['ticket_end_date_timestamp']);
1512
1513 $redeem_allowed_from = time();
1514 if ($this->MAIN->getOptions()->isOptionCheckboxActive('wcTicketDontAllowRedeemTicketBeforeStart')) {
1515 $time_offset = intval($this->MAIN->getAdmin()->getOptionValue("wcTicketOffsetAllowRedeemTicketBeforeStart"));
1516 if ($time_offset < 0) $time_offset = 0;
1517 //$offset = (float) get_option( 'gmt_offset' ); // timezone offset
1518 //$redeem_allowed_from = $ret['ticket_start_date_timestamp'] - ($time_offset * 3600) - ($offset * 3600);
1519 //if ($offset > 0) $redeem_allowed_from -= ($offset * 3600);
1520 //else $redeem_allowed_from += ($offset * 3600);
1521 $redeem_allowed_from = $ret['ticket_start_date_timestamp'] - ($time_offset * 3600);
1522 }
1523 $ret['redeem_allowed_from'] = wp_date("Y-m-d H:i", $redeem_allowed_from);
1524 $ret['redeem_allowed_from_timestamp'] = $redeem_allowed_from;
1525 $ret['redeem_allowed_until'] = wp_date("Y-m-d H:i:s", $ret['ticket_end_date_timestamp']);
1526 $ret['redeem_allowed_until_timestamp'] = $ret['ticket_end_date_timestamp'];
1527 $ret['server_time_timestamp'] = time(); // real Unix timestamp
1528 $ret['redeem_allowed_too_late'] = $ret['ticket_end_date_timestamp'] < $ret['server_time_timestamp'];
1529 $ret['server_time'] = wp_date("Y-m-d H:i:s"); // formatted with timezone
1530 $ret = apply_filters( $this->MAIN->_add_filter_prefix.'ticket_calcDateStringAllowedRedeemFrom', $ret, $product_id, $codeObj, $product_id_orig );
1531 return $ret;
1532 }
1533
1534 /**
1535 * Get post meta with variation-to-parent fallback.
1536 * If variation has the meta key set, use it. Otherwise fall back to parent product.
1537 *
1538 * @param int $product_id Parent product ID
1539 * @param int $variation_id Variation ID (0 for simple products)
1540 * @param string $meta_key Meta key to look up
1541 * @param bool $single Return single value
1542 * @return mixed
1543 */
1544 public function getMetaWithVariationFallback(int $product_id, int $variation_id, string $meta_key, bool $single = true) {
1545 if ($variation_id > 0) {
1546 $val = get_post_meta($variation_id, $meta_key, $single);
1547 if (!empty($val)) {
1548 return $val;
1549 }
1550 }
1551 return get_post_meta($product_id, $meta_key, $single);
1552 }
1553
1554 public function getLabelNamePerTicket($product_id, int $variation_id = 0) {
1555 $product_id_orig = $this->getWPMLProductId($product_id);
1556 if ($variation_id > 0) {
1557 $t = trim(get_post_meta($variation_id, "saso_eventtickets_request_name_per_ticket_label", true));
1558 if (!empty($t)) return $t;
1559 }
1560 $t = trim(get_post_meta($product_id_orig, "saso_eventtickets_request_name_per_ticket_label", true));
1561 if (empty($t)) $t = "Name for the ticket #{count}:";
1562 return $t;
1563 }
1564 public function getLabelValuePerTicket($product_id, int $variation_id = 0) {
1565 $product_id_orig = $this->getWPMLProductId($product_id);
1566 if ($variation_id > 0) {
1567 $t = trim(get_post_meta($variation_id, "saso_eventtickets_request_value_per_ticket_label", true));
1568 if (!empty($t)) return $t;
1569 }
1570 $t = trim(get_post_meta($product_id_orig, "saso_eventtickets_request_value_per_ticket_label", true));
1571 if (empty($t)) $t = "Please choose a value #{count}:";
1572 return $t;
1573 }
1574 public function getLabelDaychooserPerTicket($product_id) {
1575 $product_id_orig = $this->getWPMLProductId($product_id);
1576 $t = trim(get_post_meta($product_id_orig, "saso_eventtickets_request_daychooser_per_ticket_label", true));
1577 if (empty($t)) $t = "Please choose a day #{count}:";
1578 return $t;
1579 }
1580
1581 /**
1582 * has to be explicitly called
1583 */
1584 public function initFilterAndActions() {
1585 add_filter('query_vars', function( $query_vars ){
1586 $query_vars[] = 'symbol';
1587 return $query_vars;
1588 });
1589 add_filter("pre_get_document_title", function($title){
1590 return __("Ticket Info", "event-tickets-with-ticket-scanner");
1591 }, 2000);
1592 add_action('wp_head', function() {
1593 include_once plugin_dir_path(__FILE__)."sasoEventtickets_Ticket.php";
1594 $sasoEventtickets_Ticket = sasoEventtickets_Ticket::Instance($_SERVER["REQUEST_URI"]);
1595 $sasoEventtickets_Ticket->addMetaTags();
1596 }, 1);
1597 add_action('template_redirect', function() {
1598 include_once plugin_dir_path(__FILE__)."sasoEventtickets_Ticket.php";
1599 $sasoEventtickets_Ticket = sasoEventtickets_Ticket::Instance($_SERVER["REQUEST_URI"]);
1600 $sasoEventtickets_Ticket->output();
1601 exit;
1602 }, 300);
1603 do_action( $this->MAIN->_do_action_prefix.'ticket_initFilterAndActions' );
1604 }
1605
1606 public function initFilterAndActionsTicketScanner() {
1607 add_filter('query_vars', function( $query_vars ){
1608 $query_vars[] = 'symbol';
1609 return $query_vars;
1610 });
1611 add_filter("pre_get_document_title", function($title){
1612 return __("Ticket Info", "event-tickets-with-ticket-scanner");
1613 }, 2000);
1614 add_action('template_redirect', function() {
1615 include_once plugin_dir_path(__FILE__)."sasoEventtickets_Ticket.php";
1616 $sasoEventtickets_Ticket = sasoEventtickets_Ticket::Instance($_SERVER["REQUEST_URI"]);
1617 $sasoEventtickets_Ticket->outputTicketScannerStandalone();
1618 exit;
1619 }, 100);
1620 do_action( $this->MAIN->_do_action_prefix.'ticket_initFilterAndActionsTicketScanner' );
1621 }
1622
1623 /** falls man direkt aufrufen muss. Wie beim /ticket/scanner/ */
1624 public function renderPage() {
1625 include_once plugin_dir_path(__FILE__)."sasoEventtickets_Ticket.php";
1626 $vollstart_Ticket = sasoEventtickets_Ticket::Instance($_SERVER["REQUEST_URI"]);
1627 $vollstart_Ticket->output();
1628 }
1629
1630 public function isScanner() {
1631 // /wp-content/plugins/event-tickets-with-ticket-scanner/ticket/scanner/
1632 if ($this->isScanner == null) {
1633 if ($this->isOnlyLoggedInScannerAllowed()) {
1634 if (!in_array('administrator', wp_get_current_user()->roles)) {
1635 return false;
1636 }
1637 }
1638
1639 $ret = false;
1640 $teile = explode("/", $this->request_uri);
1641 $teile = array_reverse($teile);
1642 if (count($teile) > 1) {
1643 if (substr(strtolower(trim($teile[1])), 0, 7) == "scanner") $ret = true;
1644 }
1645 $this->isScanner = $ret;
1646 }
1647 return $this->isScanner;
1648 }
1649
1650 public function setOrder($order) {
1651 $this->order = $order;
1652 }
1653
1654 private function getOrderById($order_id) {
1655 if (isset($this->orders_cache[$order_id])) {
1656 return $this->orders_cache[$order_id];
1657 }
1658 $order = null;
1659 if (function_exists("wc_get_order")) {
1660 $order = wc_get_order( $order_id );
1661 if (!$order) throw new Exception("#8009 Order not found by order id");
1662 }
1663 if (!isset($this->orders_cache[$order_id])) { // store also null, to prevent rechecks of this order_id
1664 $this->orders_cache[$order_id] = $order;
1665 }
1666 return $order;
1667 }
1668
1669 private function getOrder() {
1670 if ($this->order != null) return $this->order;
1671
1672 $codeObj = $this->getCodeObj();
1673 if (intval($codeObj['order_id']) == 0) throw new Exception("#8010 Order not available");
1674
1675 $this->order = $this->getOrderById($codeObj['order_id']);
1676 return $this->order;
1677 }
1678
1679 public function get_product($product_id) {
1680 $product = null;
1681 if (function_exists("wc_get_product")) {
1682 $product = wc_get_product( $product_id );
1683 }
1684 return $product;
1685 }
1686
1687 public function get_is_paid_statuses() {
1688 $def = ['processing', 'completed'];
1689 if (function_exists("wc_get_is_paid_statuses")) {
1690 $def = wc_get_is_paid_statuses();
1691 }
1692 $def = apply_filters( $this->MAIN->_add_filter_prefix.'ticket_get_is_paid_statuses', $def );
1693 return $def;
1694 }
1695
1696 private function getParts($code="") {
1697 if ($this->parts == null) {
1698 if ($this->isScanner()) {
1699 if (!SASO_EVENTTICKETS::issetRPara('code')) {
1700 throw new Exception("#8007 ticket number parameter missing");
1701 } else {
1702 if (empty($code)) {
1703 $code = SASO_EVENTTICKETS::getRequestPara('code', $def='');
1704 }
1705 $uri = trim($code);
1706 $this->parts = $this->MAIN->getCore()->getTicketURLComponents($uri);
1707 }
1708 } else {
1709 $this->parts = $this->MAIN->getCore()->getTicketURLComponents($this->request_uri);
1710 }
1711 }
1712 return $this->parts;
1713 }
1714
1715 public function generateICSFile($product, $codeObj = null) {
1716 $product_id = $product->get_id();
1717 $titel = $product->get_name();
1718 $short_desc = "";
1719
1720 $product_parent_id = $product->get_parent_id();
1721 $product_parent = $product;
1722 if ($product_parent_id > 0) {
1723 $product_parent = $this->get_product( $product_parent_id );
1724 }
1725
1726 $product_original = $product;
1727 $product_parent_original = $product_parent;
1728
1729 $product_original_id = $this->getWPMLProductId($product->get_id());
1730 if ($product_original_id != $product->get_id()) {
1731 $product_original = $this->get_product($product_original_id);
1732 }
1733 if ($product_parent_id > 0) {
1734 $product_parent_original_id = $this->getWPMLProductId($product_parent_id);
1735 if ($product_parent_original_id != $product_parent_id) {
1736 $product_parent_original = $this->get_product($product_parent_original_id);
1737 }
1738 }
1739
1740 if ($this->MAIN->getOptions()->isOptionCheckboxActive('wcTicketDisplayShortDesc')) {
1741 $short_desc .= trim($product_parent->get_short_description());
1742 }
1743
1744 $tzid = wp_timezone_string();
1745 //$tzid_text = empty($tzid) ? '' : ';TZID="'.wp_timezone_string().'":';
1746
1747 $ticket_info = wp_kses_post(nl2br(trim(get_post_meta( $product_original->get_id(), 'saso_eventtickets_ticket_is_ticket_info', true ))));
1748 if (!empty($short_desc) && !empty($ticket_info)) $short_desc .= "\n\n";
1749 $short_desc .= trim($ticket_info);
1750
1751 $ticket_times = $this->getCalcDateStringAllowedRedeemFromCorrectProduct($product_id, $codeObj);
1752 $ticket_start_date = $ticket_times['ticket_start_date'];
1753 $ticket_start_time = $ticket_times['ticket_start_time'];
1754 $ticket_end_date = $ticket_times['ticket_end_date'];
1755 $ticket_end_time = $ticket_times['ticket_end_time'];
1756
1757 if (empty($ticket_start_date) && !empty($ticket_start_time)) {
1758 $ticket_start_date = wp_date("Y-m-d");
1759 }
1760 if (empty($ticket_start_date)) throw new Exception("#8011 ".esc_html__("No date available", 'event-tickets-with-ticket-scanner'));
1761
1762 if (empty($ticket_end_date) && !empty($ticket_end_time)) {
1763 $ticket_end_date = $ticket_start_date;
1764 }
1765 if (empty($ticket_end_time)) $ticket_end_time = "23:59:59";
1766
1767 $start_timestamp = strtotime(trim($ticket_start_date." ".$ticket_start_time));
1768 $end_timestamp = strtotime(trim($ticket_end_date." ".$ticket_end_time));
1769
1770 $DTSTART_line = "DTSTART";
1771 $DTEND_line = "";
1772 if (empty($ticket_start_time)) {
1773 // Use date() - ticket dates are stored in local time, using wp_date() would double-convert
1774 $DTSTART_line .= ";VALUE=DATE:".date("Ymd", $start_timestamp);
1775 if (!empty($ticket_end_date)) {
1776 $DTEND_line .= ";VALUE=DATE:".date("Ymd", strtotime(trim($ticket_start_date)));
1777 }
1778 } else {
1779 $DTEND_line = "DTEND";
1780 // using utc to leave out the tzid
1781 //if (!empty($tzid)) {
1782 // $DTSTART_line .= ";TZID=".$tzid;
1783 // $DTEND_line .= ";TZID=".$tzid;
1784 //}
1785 // Use date() - ticket dates are stored in local time, using wp_date() would double-convert
1786 $DTSTART_line .= ":".date("Ymd\THis", $start_timestamp);
1787 $DTEND_line .= ":".date("Ymd\THis", $end_timestamp);
1788 }
1789
1790 $LOCATION = trim(get_post_meta( $product_parent_original->get_id(), 'saso_eventtickets_event_location', true ));
1791
1792 $temp = wp_kses_post(str_replace(array("\r\n", "<br>"),"\n",$short_desc));
1793 $lines = explode("\n",$temp);
1794 $new_lines =array();
1795 foreach($lines as $i => $line) {
1796 if(!empty($line))
1797 $new_lines[]=trim($line);
1798 }
1799 $desc = implode("\r\n ",$new_lines);
1800
1801 $event_url = get_permalink( $product->get_id() );
1802 $uid = $product_id."-".wp_date("Y-m-d-H-i-s")."-".get_site_url();
1803
1804 $wcTicketICSOrganizerEmail = trim($this->MAIN->getOptions()->getOptionValue("wcTicketICSOrganizerEmail"));
1805
1806 $ret = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//hacksw/handcal//NONSGML v1.0//EN\r\nBEGIN:VEVENT\r\n";
1807 $ret .= "UID:".$uid."\r\n";
1808 if ($wcTicketICSOrganizerEmail != "") {
1809 $ret .= "ORGANIZER;CN=".trim(wp_kses_post(str_replace(":", " ", get_bloginfo('name')))).":mailto:".$wcTicketICSOrganizerEmail."\r\n";
1810 }
1811 $ret .= "LOCATION:".htmlentities($LOCATION)."\r\n";
1812 //$ret .= "DTSTAMP:".gmdate("Ymd\THis")."\r\n";
1813 $ret .= "DTSTAMP:".wp_date("Ymd\THis")."\r\n";
1814 $ret .= $DTSTART_line."\r\n";
1815 if (!empty($DTEND_line)) $ret .= $DTEND_line."\r\n";
1816 $ret .= "SUMMARY:".$titel."\r\n";
1817 $ret .= "DESCRIPTION:".$desc."\r\n ".$event_url."\r\n";
1818 $ret .= "X-ALT-DESC;FMTTYPE=text/html:".$desc."<br>".$event_url."\r\n";
1819 $ret .= "URL:".trim($event_url)."\r\n";
1820 $ret .= "END:VEVENT\r\n";
1821 $ret .= "END:VCALENDAR";
1822 return $ret;
1823 }
1824
1825 public function setCodeObj($codeObj) {
1826 $this->codeObj = $codeObj;
1827 $this->order = null;
1828 }
1829 private function getCodeObj($dontFailPaid=false, $code="") {
1830 if ($this->codeObj != null) {
1831 $this->codeObj = $this->MAIN->getCore()->setMetaObj($this->codeObj);
1832 return $this->codeObj;
1833 }
1834 $codeObj = $this->MAIN->getCore()->retrieveCodeByCode($this->getParts($code)['code']);
1835 if ($codeObj['aktiv'] == 2) throw new Exception("#8005 ".esc_html($this->MAIN->getAdmin()->getOptionValue("wcTicketTransTicketIsStolen")));
1836 if ($codeObj['aktiv'] != 1) throw new Exception("#8006 ".esc_html($this->MAIN->getAdmin()->getOptionValue("wcTicketTransTicketNotValid")));
1837 $metaObj = $this->MAIN->getCore()->encodeMetaValuesAndFillObject($codeObj['meta'], $codeObj);
1838 $codeObj["metaObj"] = $metaObj;
1839
1840 // check ob order_id stimmen
1841 if ($this->getParts($code)['order_id'] != $codeObj['order_id']) throw new Exception("#8001 ".esc_html($this->MAIN->getAdmin()->getOptionValue("wcTicketTransTicketNumberWrong")));
1842 // check idcode
1843 if ($this->getParts($code)['idcode'] != $metaObj['wc_ticket']['idcode']) throw new Exception("#8006 ".esc_html($this->MAIN->getAdmin()->getOptionValue("wcTicketTransTicketNumberWrong")));
1844 // check ob serial ein ticket ist
1845 if ($metaObj['wc_ticket']['is_ticket'] != 1) throw new Exception("#8002 ".esc_html($this->MAIN->getAdmin()->getOptionValue("wcTicketTransTicketNotValid")));
1846 // check ob order bezahlt ist
1847 if ($dontFailPaid == false) {
1848 $order = $this->getOrderById($codeObj["order_id"]);
1849 $ok_order_statuses = $this->get_is_paid_statuses();
1850 if (!$dontFailPaid && !$this->isPaid($order)) throw new Exception("#8003 Ticket payment is not completed. The ticket order status has to be set to a paid status like ".join(" or ", $ok_order_statuses).".");
1851 }
1852
1853 $this->codeObj = $codeObj;
1854 return $codeObj;
1855 }
1856
1857 private function isPaid($order) {
1858 return SASO_EVENTTICKETS::isOrderPaid($order);
1859 }
1860
1861 public function getTicketScannerHTMLBoilerplate() {
1862 $t = '
1863 <div style="width: 100%; justify-content: center;align-items: center;position: relative;">
1864 <div class="ticket_content" style="background-color:white;color:black;padding:15px;display:block;position: relative;left: 0;right: 0;margin: auto;text-align:left;border:1px solid black;">
1865 <div id="ticket_scanner_info_area"></div>
1866 <div id="ticket_info_retrieved" style="padding-top:20px;padding-bottom:20px;"></div>
1867 <div id="reader_output"></div>
1868 <div id="reader" style="width:100%"></div>
1869 <div id="order_info"></div>
1870 <div id="ticket_info"></div>
1871 <div id="ticket_add_info"></div>
1872 <div id="ticket_info_btns" style="padding-top:20px;padding-bottom:20px;"></div>
1873 <div id="reader_options" style="width:100%"></div>
1874 </div>
1875 </div>
1876 ';
1877 $t = apply_filters( $this->MAIN->_add_filter_prefix.'ticket_getTicketScannerHTMLBoilerplate', $t );
1878 return trim($t);
1879 }
1880
1881 public function outputTicketScannerStandalone() {
1882 header('HTTP/1.1 200 OK');
1883 $this->MAIN->setTicketScannerJS();
1884 $pwaEnabled = $this->MAIN->getOptions()->isOptionCheckboxActive('ticketScannerPWA');
1885 $themeColor = $this->MAIN->getOptions()->getOptionValue('ticketScannerThemeColor', '#2e74b5');
1886 if (empty($themeColor)) $themeColor = '#2e74b5';
1887 echo '<!DOCTYPE html>';
1888 echo '<html lang="'.esc_attr(get_locale()).'">';
1889 echo '<head>';
1890 echo '<meta charset="UTF-8">';
1891 echo '<meta name="viewport" content="width=device-width, initial-scale=1">';
1892 if ($pwaEnabled) {
1893 echo '<meta name="theme-color" content="'.esc_attr($themeColor).'">';
1894 echo '<meta name="mobile-web-app-capable" content="yes">';
1895 echo '<link rel="manifest" href="'.esc_url(rest_url(SASO_EVENTTICKETS::getRESTPrefixURL().'/ticket/scanner/pwa-manifest')).'">';
1896 echo '<link rel="apple-touch-icon" href="'.esc_url(plugins_url('img/pwa-icon-192.png', __FILE__)).'">';
1897 }
1898 $tc = esc_attr($themeColor);
1899 ?>
1900 <style>
1901 body {font-family: Helvetica, Arial, sans-serif;}
1902 h3,h4,h5 {padding-bottom:0.5em;margin-bottom:0;}
1903 p {padding:0;margin:0;margin-bottom:1em;}
1904 div.ticket_content p {font-size:initial !important;margin-bottom:1em !important;}
1905 button {padding:10px;font-size: 1.5em;}
1906 .lds-dual-ring {display:inline-block;width:64px;height:64px;}
1907 .lds-dual-ring:after {content:" ";display:block;width:46px;height:46px;margin:1px;border-radius:50%;border:5px solid #fff;border-color:<?php echo $tc; ?> transparent <?php echo $tc; ?> transparent;animation:lds-dual-ring 0.6s linear infinite;}
1908 @keyframes lds-dual-ring {0% {transform: rotate(0deg);} 100% {transform: rotate(360deg);}}
1909 </style>
1910 <?php
1911 wp_head();
1912 ?>
1913 </head><body>
1914 <center>
1915 <h1>Ticket Scanner</h1>
1916 <div style="width:90%;max-width:800px;">
1917 <?php echo $this->getTicketScannerHTMLBoilerplate(); ?>
1918 </div>
1919 </center>
1920 <?php
1921 //echo determine_locale();
1922 //load_script_translations(__DIR__.'/languages/event-tickets-with-ticket-scanner-de_CH-ajax_script_ticket_scanner.json', 'ajax_script_ticket_scanner', 'event-tickets-with-ticket-scanner');
1923 get_footer();
1924 //wp_footer();
1925 //echo '</body></html>';
1926 }
1927
1928 public function outputTicketScanner() {
1929 echo '<center>';
1930 echo '<h3>'.__('Ticket scanner', 'event-tickets-with-ticket-scanner').'</h3>';
1931 echo '<div id="ticket_scanner_info_area">';
1932 if (isset($_GET['code']) && isset($_GET['redeemauto']) && $this->redeem_successfully == false) {
1933 echo '<h3 style="color:red;">'.esc_html__('TICKET NOT REDEEMED - see reason below', 'event-tickets-with-ticket-scanner').'</h3>';
1934 } else if (isset($_GET['code']) && isset($_GET['redeemauto']) && $this->redeem_successfully) {
1935 echo '<h3 style="color:green;">'.esc_html__('TICKET OK - Redeemed', 'event-tickets-with-ticket-scanner').'</h3>';
1936 }
1937 echo '</div>';
1938
1939 echo '</center>';
1940 echo '<div id="reader_output">';
1941 if (SASO_EVENTTICKETS::issetRPara("code")) {
1942 try {
1943 $codeObj = $this->getCodeObj();
1944 $metaObj = $codeObj["metaObj"];
1945
1946 $ticket_id = $this->MAIN->getCore()->getTicketId($codeObj, $metaObj);
1947
1948 $ticket_times = $this->getCalcDateStringAllowedRedeemFromCorrectProduct($metaObj['woocommerce']['product_id'], $codeObj);
1949 $ticket_end_date = $ticket_times['ticket_end_date'];
1950 $ticket_end_date_timestamp = $ticket_times['ticket_end_date_timestamp'];
1951 $color = 'green';
1952 if ($ticket_end_date != "" && $ticket_end_date_timestamp < time()) {
1953 $color = 'orange';
1954 }
1955 if (!empty($metaObj['wc_ticket']['redeemed_date'])) {
1956 $color = 'red';
1957 }
1958
1959 if (SASO_EVENTTICKETS::issetRPara('action') && SASO_EVENTTICKETS::getRequestPara('action') == "redeem") {
1960 $pfad = plugins_url( "img/",__FILE__ );
1961 if ($this->redeem_successfully) {
1962 echo '<p style="text-align:center;color:green"><img src="'.$pfad.'button_ok.png"><br><b>'.__("Successfully redeemed", 'event-tickets-with-ticket-scanner').'</b></p>';
1963 } else {
1964 echo '<p style="text-align:center;color:red;"><img src="'.$pfad.'button_cancel.png"><br><b>'.__("Failed to redeem", 'event-tickets-with-ticket-scanner').'</b></p>';
1965 }
1966 }
1967
1968 echo '<div style="border:5px solid '.esc_attr($color).';margin:10px;padding:10px;">';
1969 $this->outputTicketInfo();
1970 echo '</div>';
1971
1972 echo '<form id="f_reload" action="?" method="get">
1973 <input type="hidden" name="code" value="'.urlencode($ticket_id).'">
1974 </form>';
1975 echo '
1976 <script>
1977 function reload_ticket() {
1978 document.getElementById("f_reload").submit();
1979 }
1980 </script>
1981 ';
1982 if (empty($metaObj['wc_ticket']['redeemed_date'])) {
1983 echo '<form id="f_redeem" action="?" method="post">
1984 <input type="hidden" name="action" value="redeem">
1985 <input type="hidden" name="code" value="'.urlencode($ticket_id).'">
1986 </form></p></center>';
1987 echo '
1988 <script>
1989 function redeem_ticket() {
1990 document.getElementById("f_redeem").submit();
1991 }
1992 </script>
1993 ';
1994 }
1995 echo '<center><p><button onclick="reload_ticket()">'.esc_attr__("Reload Ticket", 'event-tickets-with-ticket-scanner').'</button>';
1996 if (empty($metaObj['wc_ticket']['redeemed_date'])) {
1997 echo '<button onclick="redeem_ticket()" style="background-color:green;color:white;">'.__("Redeem Ticket", 'event-tickets-with-ticket-scanner').'</button>';
1998 }
1999 echo '</p></center>';
2000 } catch (Exception $e) {
2001 echo '</div>';
2002 echo '<div style="color:red;">'.$e->getMessage().'</div>';
2003 echo $this->getParts()['code'];
2004 }
2005 }
2006 echo '</div>';
2007 echo '<center>';
2008 echo '<div id="reader" style="width:600px"></div>';
2009 echo '</center>';
2010 echo '<script>
2011 var serial_ticket_scanner_redeem = '.(isset($_GET['redeemauto']) ? 'true' : 'false').';
2012 var loadingticket = false;
2013 function setRedeemImmediately() {
2014 serial_ticket_scanner_redeem = !serial_ticket_scanner_redeem;
2015 }
2016 function onScanSuccess(decodedText, decodedResult) {
2017 if (loadingticket) return;
2018 loadingticket = true;
2019 // handle the scanned code as you like, for example:
2020 jQuery("#reader_output").html(decodedText+"<br>...'.__("loading", 'event-tickets-with-ticket-scanner').'...");
2021 window.location.href = "?code="+encodeURIComponent(decodedText) + (serial_ticket_scanner_redeem ? "&redeemauto=1" : "");
2022 window.setTimeout(()=>{
2023 html5QrcodeScanner.stop().then((ignore) => {
2024 // QR Code scanning is stopped.
2025 // reload the page with the ticket info and redeem button
2026 //console.log("stop success");
2027 }).catch((err) => {
2028 // Stop failed, handle it.
2029 //console.log("stop failed");
2030 });
2031 }, 250);
2032 }
2033 function onScanFailure(error) {
2034 // handle scan failure, usually better to ignore and keep scanning.
2035 // for example:
2036 console.warn("Code scan error = ${error}");
2037 }
2038 var html5QrcodeScanner = new Html5QrcodeScanner(
2039 "reader",
2040 { fps: 10, qrbox: {width: 250, height: 250} },
2041 /* verbose= */ false);
2042 </script>';
2043 echo '<script>
2044 function startScanner() {
2045 jQuery("#ticket_scanner_info_area").html("");
2046 jQuery("#reader_output").html("");
2047 html5QrcodeScanner.render(onScanSuccess, onScanFailure);
2048 }
2049 </script>';
2050
2051 if (SASO_EVENTTICKETS::issetRPara("code")) {
2052 echo "<center>";
2053 echo '<input type="checkbox" onclick="setRedeemImmediately()"'.(SASO_EVENTTICKETS::issetRPara("redeemauto") ? " ".'checked' :'').'> '.esc_html__('Scan and Redeem immediately', 'event-tickets-with-ticket-scanner').'<br>';
2054 echo '<button onclick="startScanner()">'.esc_attr__("Scan next Ticket", 'event-tickets-with-ticket-scanner').'</button>';
2055 echo "</center>";
2056
2057 // display the amount entered already
2058 $redeemed_tickets = $this->rest_helper_tickets_redeemed($codeObj);
2059 if ($redeemed_tickets['tickets_redeemed_show']) {
2060 echo "<center><h5>";
2061 echo $redeemed_tickets['tickets_redeemed']." ".__('ticket redeemed already', 'event-tickets-with-ticket-scanner');
2062 echo "</h5></center>";
2063 }
2064 } else {
2065 echo '<script>
2066 startScanner();
2067 </script>';
2068 }
2069 }
2070
2071 private function checkIfDownloadIsAllowed() {
2072 if ($this->MAIN->getOptions()->isOptionCheckboxActive('wcTicketAllowOnlyLoggedinToDownload')) {
2073 if (!is_user_logged_in()) {
2074 $url = $this->MAIN->getOptions()->getOptionValue("wcTicketAllowOnlyLoggedinToDownloadRedirectURL");
2075 if (!empty($url)) {
2076 wp_redirect( $url );
2077 } else {
2078 // send header not allowed
2079 header("HTTP/1.1 403 Forbidden");
2080 echo esc_html__("You are not allowed to download the ticket PDF. Please log in to your account.", 'event-tickets-with-ticket-scanner');
2081 //wp_redirect( home_url() );
2082 }
2083 exit;
2084 }
2085 }
2086 }
2087
2088 private function sendBadgeFile() {
2089 $codeObj = $this->getCodeObj(true);
2090 $badgeHandler = $this->MAIN->getTicketBadgeHandler();
2091 $badgeHandler->downloadPDFTicketBadge($codeObj);
2092 die();
2093 }
2094
2095 private function sendICSFile() {
2096 $codeObj = $this->getCodeObj(true);
2097 $metaObj = $codeObj['metaObj'];
2098 do_action( $this->MAIN->_do_action_prefix.'trackIPForICSDownload', $codeObj );
2099 $product_id = $metaObj['woocommerce']['product_id'];
2100 $this->sendICSFileByProductId($product_id, $codeObj);
2101 }
2102
2103 public function sendICSFileByProductId($product_id, $codeObj = null) { // null, because it could be called for the product
2104 $product = $this->get_product( $product_id );
2105 $contents = $this->generateICSFile($product, $codeObj);
2106 SASO_EVENTTICKETS::sendeDaten($contents, "ics_".$product_id.".ics", "text/calendar");
2107 }
2108
2109 /**
2110 * will generate all tickets PDF
2111 * then merge them together to one PDF
2112 */
2113 public function outputPDFTicketsForOrder($order, $filemode="I") {
2114 $tickets = $this->MAIN->getWC()->getTicketsFromOrder($order);
2115 if (count($tickets) > 0) {
2116 set_time_limit(0);
2117 $this->setOrder($order);
2118 if ($filemode == "I") {
2119 do_action( $this->MAIN->_do_action_prefix.'trackIPForPDFOneView', $order );
2120 }
2121 $filepaths = [];
2122 foreach($tickets as $key => $obj) {
2123 $codes = [];
2124 if (!empty($obj['codes'])) {
2125 $codes = explode(",", $obj['codes']);
2126 }
2127 foreach($codes as $code) {
2128 try {
2129 $codeObj = $this->MAIN->getCore()->retrieveCodeByCode($code);
2130 } catch (Exception $e) {
2131 continue;
2132 }
2133 $this->setCodeObj($codeObj);
2134 // attach PDF
2135 $filepaths[] = $this->outputPDF("F");
2136 }
2137 }
2138 $filename = "tickets_".$order->get_id().".pdf";
2139 // merge files
2140 $fullFilePath = $this->MAIN->getCore()->mergePDFs($filepaths, $filename, $filemode);
2141 return $fullFilePath; // if not already exit call was made
2142 }
2143 }
2144 public function generateOnePDFForCodes($codes=[], $filename=null, $filemode="I") {
2145 try {
2146 if (count($codes) > 0) {
2147 set_time_limit(0);
2148 $filepaths = [];
2149 foreach($codes as $code) {
2150 try {
2151 $codeObj = $this->MAIN->getCore()->retrieveCodeByCode($code);
2152 } catch (Exception $e) {
2153 continue;
2154 }
2155 $this->setCodeObj($codeObj);
2156 // attach PDF
2157 $filepaths[] = $this->outputPDF("F");
2158 }
2159 if ($filename == null) {
2160 $filename = "tickets_".wp_date("Ymd_Hi").".pdf";
2161 }
2162 // merge files
2163 $fullFilePath = $this->MAIN->getCore()->mergePDFs($filepaths, $filename, $filemode);
2164 return $fullFilePath; // if not already exit call was made
2165 }
2166 } catch (Exception $e) {
2167 $this->MAIN->getAdmin()->logErrorToDB($e);
2168 throw $e;
2169 }
2170 }
2171
2172 public function generateOneBadgePDFForCodes($codes=[], $filename=null, $filemode="I") {
2173 // set_time_limit(0); // should be set by the caller already
2174 try {
2175 if (count($codes) > 0) {
2176 $badgeHandler = $this->MAIN->getTicketBadgeHandler();
2177 $dirname = get_temp_dir(); // pfad zu den dateien
2178 if (wp_is_writable($dirname)) {
2179 $dirname .= trailingslashit($this->MAIN->getPrefix());
2180 if (!file_exists($dirname)) {
2181 wp_mkdir_p($dirname);
2182 }
2183 set_time_limit(0);
2184 $filepaths = [];
2185 foreach($codes as $code) {
2186 try {
2187 $codeObj = $this->MAIN->getCore()->retrieveCodeByCode($code);
2188 } catch (Exception $e) {
2189 continue;
2190 }
2191 $this->setCodeObj($codeObj);
2192 // attach PDF
2193 $filepaths[] = $badgeHandler->getPDFTicketBadgeFilepath($codeObj, $dirname);
2194 }
2195 if ($filename == null) {
2196 $filename = "ticketsbadges_".wp_date("Ymd_Hi").".pdf";
2197 }
2198 // merge files
2199 $fullFilePath = $this->MAIN->getCore()->mergePDFs($filepaths, $filename, $filemode);
2200 return $fullFilePath; // if not already exit call was made
2201 } else {
2202 $this->MAIN->getAdmin()->logErrorToDB(new Exception("#8012 cannot create badge pdf - no write access to ".$dirname));
2203 }
2204 }
2205 } catch (Exception $e) {
2206 $this->MAIN->getAdmin()->logErrorToDB($e);
2207 throw $e;
2208 }
2209 }
2210
2211 public function outputPDF($filemode="I") {
2212 $codeObj = $this->getCodeObj(true);
2213 $metaObj = $codeObj['metaObj'];
2214 $order = $this->getOrder();
2215 $ticket_id = $this->MAIN->getCore()->getTicketId($codeObj, $metaObj);
2216 $order_item = $this->getOrderItem($order, $metaObj);
2217 if ($order_item == null) throw new Exception("#8013 ".esc_html__("Order item not found for the PDF ticket", 'event-tickets-with-ticket-scanner'));
2218
2219 if ($filemode == "I") {
2220 do_action( $this->MAIN->_do_action_prefix.'trackIPForPDFView', $codeObj );
2221 $this->setOrderStatusAfterViewOperation($order);
2222 }
2223
2224 $ticket_template = apply_filters( $this->MAIN->_add_filter_prefix.'ticket_outputTicketInfo_template', null, $codeObj );
2225
2226 $product = $order_item->get_product();
2227 if ($product == null) throw new Exception("#8020 ".esc_html__("Product not found for the PDF ticket", 'event-tickets-with-ticket-scanner'));
2228
2229 $product_id = $product->get_id();
2230 $product_parent_id = $product->get_parent_id();
2231 $is_variation = $product->get_type() == "variation" ? true : false;
2232 if ($is_variation && $product_parent_id > 0) {
2233 $product_id = $product_parent_id;
2234 }
2235
2236 ob_start();
2237 try {
2238 $this->outputTicketInfo(true);
2239 $html = trim(ob_get_contents());
2240 } catch (Exception $e) {
2241 $this->MAIN->getAdmin()->logErrorToDB($e);
2242 $html = $e->getMessage();
2243 }
2244 ob_end_clean();
2245 ob_start();
2246
2247 $pdf = $this->MAIN->getNewPDFObject();
2248
2249 // RTL product approach
2250 $rtl = false;
2251 if ($ticket_template != null) {
2252 $rtl = $ticket_template['metaObj']['wcTicketPDFisRTL'] == true || intval($ticket_template['metaObj']['wcTicketPDFisRTL']) == 1;
2253 } else {
2254 //if (get_post_meta( $metaObj['woocommerce']['product_id'], 'saso_eventtickets_ticket_is_RTL', true ) == "yes") {
2255 //$rtl = true;
2256 //}
2257 if (SASO_EVENTTICKETS::issetRPara('testDesigner') && $this->MAIN->getOptions()->isOptionCheckboxActive('wcTicketPDFisRTLTest')) {
2258 $rtl = true;
2259 } else if($this->MAIN->getOptions()->isOptionCheckboxActive('wcTicketPDFisRTL')) {
2260 $rtl = true;
2261 }
2262 }
2263 $pdf->setRTL($rtl);
2264
2265 $pdf->setQRParams(['style'=>['position'=>'C'],'align'=>'N']);
2266 //$pdf->setQRParams(['style'=>['position'=>'R','vpadding'=>0,'hpadding'=>0], 'align'=>'C']);
2267 if ($pdf->isRTL()) {
2268 //$pdf->setQRParams(['style'=>['position'=>'L'], 'align'=>'C']);
2269 $lg = Array();
2270 $lg['a_meta_charset'] = 'UTF-8';
2271 $lg['a_meta_dir'] = 'rtl';
2272 $lg['a_meta_language'] = 'fa';
2273 $lg['w_page'] = 'page';
2274 // set some language-dependent strings (optional)
2275 $pdf->setLanguageArray($lg);
2276 $pdf->setQRParams(['style'=>['position'=>'T'],'align'=>'T']);
2277 }
2278
2279 $marginZero = false;
2280 if ($ticket_template != null) {
2281 $marginZero = $ticket_template['metaObj']['wcTicketPDFZeroMargin'] == true || intval($ticket_template['metaObj']['wcTicketPDFZeroMargin']) == 1;
2282 } else {
2283 if (SASO_EVENTTICKETS::issetRPara('testDesigner')) {
2284 if ($this->MAIN->getOptions()->isOptionCheckboxActive('wcTicketPDFZeroMarginTest')) {
2285 $marginZero = true;
2286 }
2287 } else {
2288 if ($this->MAIN->getOptions()->isOptionCheckboxActive('wcTicketPDFZeroMargin')) {
2289 $marginZero = true;
2290 }
2291 }
2292 }
2293 $pdf->marginsZero = $marginZero;
2294
2295 // Full bleed mode for edge-to-edge printing
2296 $fullBleed = false;
2297 if ($ticket_template != null) {
2298 $fullBleed = $ticket_template['metaObj']['wcTicketPDFFullBleed'] == true || intval($ticket_template['metaObj']['wcTicketPDFFullBleed']) == 1;
2299 } else {
2300 if (SASO_EVENTTICKETS::issetRPara('testDesigner')) {
2301 if ($this->MAIN->getOptions()->isOptionCheckboxActive('wcTicketPDFFullBleedTest')) {
2302 $fullBleed = true;
2303 }
2304 } else {
2305 if ($this->MAIN->getOptions()->isOptionCheckboxActive('wcTicketPDFFullBleed')) {
2306 $fullBleed = true;
2307 }
2308 }
2309 }
2310 $pdf->fullBleed = $fullBleed;
2311
2312 $width = 210;
2313 $height = 297;
2314 $qr_code_size = 0; // takes default then
2315 if ($ticket_template != null) {
2316 $width = intval($ticket_template['metaObj']['wcTicketSizeWidth']);
2317 $height = intval($ticket_template['metaObj']['wcTicketSizeHeight']);
2318 $qr_code_size = intval($ticket_template['metaObj']['wcTicketQRSize']);
2319 } else {
2320 if (SASO_EVENTTICKETS::issetRPara('testDesigner')) {
2321 $width = $this->MAIN->getOptions()->getOptionValue("wcTicketSizeWidthTest", 0);
2322 $height = $this->MAIN->getOptions()->getOptionValue("wcTicketSizeHeightTest", 0);
2323 $qr_code_size = intval($this->MAIN->getOptions()->getOptionValue("wcTicketQRSizeTest", 0));
2324 } else {
2325 $width = $this->MAIN->getOptions()->getOptionValue("wcTicketSizeWidth", 0);
2326 $height = $this->MAIN->getOptions()->getOptionValue("wcTicketSizeHeight", 0);
2327 $qr_code_size = intval($this->MAIN->getOptions()->getOptionValue("wcTicketQRSize", 0));
2328 }
2329 }
2330
2331 $width = $width > 0 ? $width : 210;
2332 $height = $height > 0 ? $height : 297;
2333 $pdf->setSize($width, $height);
2334
2335 if ($qr_code_size > 0) {
2336 $pdf->setQRParams(['size'=>['width'=>$qr_code_size, 'height'=>$qr_code_size]]);
2337 }
2338
2339 $pdf->setFilemode($filemode);
2340 if ($pdf->getFilemode() == "F") {
2341 $dirname = get_temp_dir();
2342 $dirname .= trailingslashit($this->MAIN->getPrefix());
2343 $filename = "ticket_".$order->get_id()."_".$ticket_id.".pdf";
2344 wp_mkdir_p($dirname);
2345 $pdf->setFilepath($dirname);
2346 } else {
2347 $filename = "ticket_".$order->get_id()."_".$ticket_id.".pdf";
2348 }
2349 $pdf->setFilename($filename);
2350
2351 $wcTicketTicketBanner = $this->MAIN->getAdmin()->getOptionValue('wcTicketTicketBanner');
2352 $wcTicketTicketBanner = apply_filters( $this->MAIN->_add_filter_prefix.'wcTicketTicketBanner', $wcTicketTicketBanner, $product_id);
2353 if (!empty($wcTicketTicketBanner) && intval($wcTicketTicketBanner) > 0) {
2354 //$option_wcTicketTicketBanner = $this->MAIN->getOptions()->getOption('wcTicketTicketBanner');
2355 $mediaData = SASO_EVENTTICKETS::getMediaData($wcTicketTicketBanner);
2356 /*$width = "600";
2357 if (isset($option_wcTicketTicketBanner['additional']) && isset($option_wcTicketTicketBanner['additional']['min']) && isset($option_wcTicketTicketBanner['additional']['min']['width'])) {
2358 $width = $option_wcTicketTicketBanner['additional']['min']['width'];
2359 }*/
2360 //if (!empty($mediaData['location']) && file_exists($mediaData['location'])) {
2361 $has_banner = false;
2362 if ($this->MAIN->getOptions()->isOptionCheckboxActive('wcTicketCompatibilityUseURL')) {
2363 if (!empty($mediaData['url'])) {
2364 $pdf->addPart('<div style="text-align:center;"><img src="'.$mediaData['url'].'"></div>');
2365 $has_banner = true;
2366 }
2367 } else {
2368 if (!empty($mediaData['for_pdf'])) {
2369 $pdf->addPart('<div style="text-align:center;"><img src="'.$mediaData['for_pdf'].'"></div>');
2370 $has_banner = true;
2371 }
2372 }
2373 if ($has_banner && isset($mediaData['meta']) && isset($mediaData['meta']['height']) && floatval($mediaData['meta']['height']) > 0) {
2374 $dpiY = 96;
2375 if (function_exists("getimagesize")) {
2376 $imageInfo = getimagesize($mediaData['location']);
2377 // DPI-Werte aus den EXIF-Daten extrahieren
2378 $dpiY = isset($imageInfo['dpi_y']) ? $imageInfo['dpi_y'] : $dpiY;
2379 }
2380 $units = $pdf->convertPixelIntoMm($mediaData['meta']['height'] + 10, $dpiY);
2381 $pdf->setQRParams(['pos'=>['y'=>$units]]);
2382 }
2383 }
2384
2385 /* old approach
2386 $pdf->addPart('<h1 style="font-size:20pt;text-align:center;">'.htmlentities($this->MAIN->getAdmin()->getOptionValue("wcTicketHeading")).'</h1>');
2387 $pdf->addPart('{QRCODE_INLINE}');
2388 $pdf->addPart("<style>h4{font-size:16pt;} table.ticket_content_upper {width:14cm;padding-top:10pt;} table.ticket_content_upper td {height:5cm;}</style>".$html);
2389 $pdf->addPart('<br><br><p style="text-align:center;">'.$ticket_id.'</p>');
2390 */
2391
2392 if (strpos(" ".$html,"{QRCODE_INLINE}") > 0 || strpos(" ".$html,"{QRCODE}") > 0) {
2393 } else {
2394 $pdf->addPart('{QRCODE}');
2395 }
2396
2397 $pdf->addPart($html);
2398
2399 $wcTicketDontDisplayBlogName = $this->MAIN->getOptions()->isOptionCheckboxActive('wcTicketDontDisplayBlogName');
2400 if (!$wcTicketDontDisplayBlogName) {
2401 $pdf->addPart('<br><br><div style="text-align:center;font-size:10pt;"><b>'.wp_kses_post(get_bloginfo("name")).'</b></div>');
2402 }
2403 $wcTicketDontDisplayBlogDesc = $this->MAIN->getOptions()->isOptionCheckboxActive('wcTicketDontDisplayBlogDesc');
2404 if (!$wcTicketDontDisplayBlogDesc) {
2405 if ($wcTicketDontDisplayBlogName) $pdf->addPart('<br>');
2406 $pdf->addPart('<div style="text-align:center;font-size:10pt;">'.wp_kses_post(get_bloginfo("description")).'</div>');
2407 }
2408 if (!$this->MAIN->getOptions()->isOptionCheckboxActive('wcTicketDontDisplayBlogURL')) {
2409 $pdf->addPart('<br><div style="text-align:center;font-size:10pt;">'.site_url().'</div>');
2410 }
2411
2412 $wcTicketTicketLogo = $this->MAIN->getAdmin()->getOptionValue('wcTicketTicketLogo');
2413 $wcTicketTicketLogo = apply_filters( $this->MAIN->_add_filter_prefix.'wcTicketTicketLogo', $wcTicketTicketLogo, $product_id);
2414 if (!empty($wcTicketTicketLogo) && intval($wcTicketTicketLogo) >0) {
2415 $option_wcTicketTicketLogo = $this->MAIN->getOptions()->getOption('wcTicketTicketLogo');
2416 $mediaData = SASO_EVENTTICKETS::getMediaData($wcTicketTicketLogo);
2417 $width = "200";
2418 if (isset($option_wcTicketTicketLogo['additional']) && isset($option_wcTicketTicketLogo['additional']['max']) && isset($option_wcTicketTicketLogo['additional']['max']['width'])) {
2419 $width = $option_wcTicketTicketLogo['additional']['max']['width'];
2420 }
2421 //if (!empty($mediaData['location']) && file_exists($mediaData['location'])) {
2422 if ($this->MAIN->getOptions()->isOptionCheckboxActive('wcTicketCompatibilityUseURL')) {
2423 if (!empty($mediaData['url'])) {
2424 $pdf->addPart('<br><br><p style="text-align:center;"><img width="'.$width.'" src="'.$mediaData['url'].'"></p>');
2425 }
2426 } else {
2427 if (!empty($mediaData['for_pdf'])) {
2428 $pdf->addPart('<br><br><p style="text-align:center;"><img width="'.$width.'" src="'.$mediaData['for_pdf'].'"></p>');
2429 }
2430 }
2431 }
2432 $brandingHidePluginBannerText = $this->MAIN->getOptions()->isOptionCheckboxActive('brandingHidePluginBannerText');
2433 if ($brandingHidePluginBannerText == false) {
2434 $pdf->addPart('<br><p style="text-align:center;font-size:6pt;">"Event Tickets With Ticket Scanner Plugin" for Wordpress</p>');
2435 }
2436
2437 $wcTicketTicketBG = $this->MAIN->getAdmin()->getOptionValue('wcTicketTicketBG');
2438 $wcTicketTicketBG = apply_filters( $this->MAIN->_add_filter_prefix.'wcTicketTicketBG', $wcTicketTicketBG, $product_id);
2439 if (!empty($wcTicketTicketBG) && intval($wcTicketTicketBG) >0) {
2440 $mediaData = SASO_EVENTTICKETS::getMediaData($wcTicketTicketBG);
2441 if ($this->MAIN->getOptions()->isOptionCheckboxActive('wcTicketCompatibilityUseURL')) {
2442 if (!empty($mediaData['url'])) {
2443 $pdf->setBackgroundImage($mediaData['url']);
2444 }
2445 } else {
2446 if (!empty($mediaData['for_pdf'])) {
2447 $pdf->setBackgroundImage($mediaData['for_pdf']);
2448 }
2449 }
2450 }
2451
2452 // Background color (as fallback when no image or to fill gaps)
2453 $bgColorOption = SASO_EVENTTICKETS::issetRPara('testDesigner')
2454 ? 'wcTicketPDFBackgroundColorTest'
2455 : 'wcTicketPDFBackgroundColor';
2456 $wcTicketPDFBackgroundColor = $this->MAIN->getOptions()->getOptionValue($bgColorOption);
2457 if (!empty($wcTicketPDFBackgroundColor)) {
2458 $pdf->setBackgroundColor($wcTicketPDFBackgroundColor);
2459 }
2460
2461 $wcTicketTicketAttachPDFOnTicket = $this->MAIN->getAdmin()->getOptionValue('wcTicketTicketAttachPDFOnTicket');
2462 if (!empty($wcTicketTicketAttachPDFOnTicket)) {
2463 $mediaData = SASO_EVENTTICKETS::getMediaData($wcTicketTicketAttachPDFOnTicket);
2464 if (!empty($mediaData['location']) && file_exists($mediaData['location'])) {
2465 $pdf->setAdditionalPDFsToAttachThem([$mediaData['location']]);
2466 }
2467 }
2468
2469 $qrCodeContent = $this->MAIN->getCore()->getQRCodeContent($codeObj);
2470 $qrTicketPDFPadding = intval($this->MAIN->getOptions()->getOptionValue('qrTicketPDFPadding'));
2471 $pdf->setQRCodeContent(["text"=>$qrCodeContent, "style"=>["vpadding"=>$qrTicketPDFPadding, "hpadding"=>$qrTicketPDFPadding]]);
2472
2473 ob_end_clean();
2474
2475 try {
2476 $pdf->render();
2477 } catch(Exception $e) {}
2478 if ($pdf->getFilemode() == "F") {
2479 return $pdf->getFullFilePath();
2480 } else {
2481 die("PDF render not possible. Please remove HTML tags from the product description and ticket info with the product detail view.");
2482 }
2483 }
2484
2485 public function displayDayChooserDateAsString($codeObj, $withTime=false) {
2486 if ($codeObj == null) return "";
2487
2488 // check of product is day chooser
2489 $codeObj = $this->MAIN->getCore()->setMetaObj($codeObj);
2490 $metaObj = $codeObj['metaObj'];
2491 $is_daychooser = intval($metaObj["wc_ticket"]["is_daychooser"]);
2492 if ($is_daychooser != 1) return "";
2493
2494 $day_per_ticket = $metaObj["wc_ticket"]["day_per_ticket"];
2495 if (empty($day_per_ticket)) return "";
2496
2497 $date_string = "";
2498
2499 if ($withTime) {
2500 $format = $this->MAIN->getOptions()->getOptionDateTimeFormat();
2501
2502 // get time from product
2503 $product_id = intval($metaObj['woocommerce']['product_id']);
2504 if ($product_id > 0) {
2505 $ticket_times = $this->getCalcDateStringAllowedRedeemFromCorrectProduct($product_id, $codeObj);
2506 if ($ticket_times['is_start_time_set']) {
2507 $time_str = $day_per_ticket." ".$ticket_times['ticket_start_time'];
2508 // Use date_i18n with gmt=true - input is already in local time, gmt=true prevents timezone conversion but translates month/day names
2509 $date_string = date_i18n($format, strtotime($time_str), true);
2510 }
2511 }
2512 }
2513
2514 if (empty($date_string)) {
2515 // format day_per_ticket - use date_i18n with gmt=true to translate month/day names without timezone conversion
2516 $date_format = $this->MAIN->getOptions()->getOptionDateFormat();
2517 $date_string = date_i18n($date_format, strtotime($day_per_ticket), true);
2518 }
2519
2520 return $date_string;
2521 }
2522
2523 public function displayTicketDateAsString($product_id, $date_format="Y/m/d", $time_format="H:i", $codeObj = null) {
2524 $product_id = intval($product_id);
2525 if ($product_id <= 0) throw new Exception("#8021 ".esc_html__("Product ID not valid for ticket date string", 'event-tickets-with-ticket-scanner'));
2526
2527 $ticket_times = $this->calcDateStringAllowedRedeemFrom($product_id, $codeObj);
2528 $ticket_start_date = $ticket_times['ticket_start_date'];
2529 $ticket_start_time = $ticket_times['ticket_start_time'];
2530 $ticket_end_date = $ticket_times['ticket_end_date'];
2531 $ticket_end_time = $ticket_times['ticket_end_time'];
2532 $is_daychooser = $ticket_times['is_daychooser'];
2533 $is_date_set = $ticket_times['is_date_set'];
2534 $is_end_time_set = $ticket_times['is_end_time_set'];
2535 $is_start_time_set = $ticket_times['is_start_time_set'];
2536 $ret = "";
2537
2538 // not start day and time set
2539 // then only display what is set
2540 // to avoid something like " - 2024-12-12 14:00"
2541 // or "2024-12-12 14:00 - "
2542 // or " - 14:00"
2543 // or "2024-12-12 - "
2544 // or " - 2024-12-12"
2545
2546 // Use date_i18n with gmt=true - input is already in local time, gmt=true prevents timezone conversion but translates month/day names
2547 if ($is_date_set) {
2548 $ret .= date_i18n($date_format." ".$time_format, strtotime($ticket_start_date." ".$ticket_start_time), true);
2549 } else if (!empty($ticket_start_date)) {
2550 $ret .= date_i18n($date_format, strtotime($ticket_start_date), true);
2551 } else if ($is_start_time_set) {
2552 $ret .= date_i18n($time_format, strtotime($ticket_start_time), true);
2553 }
2554 if (!empty($ret) && !empty($ticket_end_date) || $is_end_time_set) $ret .= " - ";
2555 if (!empty($ticket_end_date) && $is_end_time_set) {
2556 $ret .= date_i18n($date_format." ".$time_format, strtotime($ticket_end_date." ".$ticket_end_time), true);
2557 } else if (!empty($ticket_end_date)) {
2558 $ret .= date_i18n($date_format, strtotime($ticket_end_date), true);
2559 } else if ($is_end_time_set) {
2560 $ret .= date_i18n($time_format, strtotime($ticket_end_time), true);
2561 }
2562
2563 return $ret;
2564 }
2565
2566 public function getOrderItem($order, $metaObj) {
2567 $order_item = null;
2568 foreach ( $order->get_items() as $item_id => $item ) {
2569 if ($metaObj['woocommerce']['item_id'] == $item_id) {
2570 $order_item = $item;
2571 break;
2572 }
2573 }
2574 return $order_item;
2575 }
2576
2577 private function getOrderTicketsInfos($order_id, $my_idcode) {
2578 $order_id = intval($order_id);
2579 $order = wc_get_order($order_id);
2580 if ($order == null) return "Wrong ticket code id";
2581 $idcode = $order->get_meta('_saso_eventtickets_order_idcode');
2582 if (empty($idcode) || $idcode != $my_idcode) return "Wrong ticket code";
2583
2584 $option_displayDateTimeFormat = $this->MAIN->getOptions()->getOptionDateTimeFormat();
2585 $products = []; // to have the single items listed on the order view
2586 $ticket_infos = [];
2587 $tickets = $this->MAIN->getWC()->getTicketsFromOrder($order);
2588 if (count($tickets) > 0) {
2589 set_time_limit(0);
2590 $this->setOrder($order);
2591
2592 $wcTicketHideDateOnPDF = $this->MAIN->getOptions()->isOptionCheckboxActive('wcTicketHideDateOnPDF');
2593
2594 foreach($tickets as $key => $obj) {
2595 $codes = [];
2596 if (!empty($obj['codes'])) {
2597 $codes = explode(",", $obj['codes']);
2598 }
2599 foreach($codes as $code) {
2600 try {
2601 $codeObj = $this->MAIN->getCore()->retrieveCodeByCode($code);
2602 } catch (Exception $e) {
2603 continue;
2604 }
2605 $codeObj = $this->MAIN->getCore()->setMetaObj($codeObj);
2606 $metaObj = $codeObj['metaObj'];
2607
2608 $order_item = $this->getOrderItem($order, $metaObj);
2609 if ($order_item == null) throw new Exception("#8004 Order not found");
2610 $product = $order_item->get_product();
2611 $is_variation = $product->get_type() == "variation" ? true : false;
2612 $product_parent = $product;
2613 $product_parent_id = $product->get_parent_id();
2614
2615 if ($is_variation && $product_parent_id > 0) {
2616 $product_parent = $this->get_product( $product_parent_id );
2617 }
2618
2619 $product_original = $product;
2620 $product_parent_original = $product_parent;
2621
2622 $product_original_id = $this->getWPMLProductId($product->get_id());
2623 $product_parent_original_id = $this->getWPMLProductId($product_parent_id);
2624 if ($product_original_id != $product->get_id()) {
2625 $product_original = $this->get_product($product_original_id);
2626 }
2627 if ($product_parent_original_id > 0 && $product_parent_original_id != $product_parent->get_id()) {
2628 $product_parent_original = $this->get_product($product_parent_original_id);
2629 }
2630
2631 $saso_eventtickets_is_date_for_all_variants = true;
2632 if ($is_variation && $product_parent_id > 0) {
2633 $saso_eventtickets_is_date_for_all_variants = get_post_meta( $product_parent_original->get_id(), 'saso_eventtickets_is_date_for_all_variants', true ) == "yes" ? true : false;
2634 }
2635
2636 $this->isProductAllowedByAuthToken([$product->get_id()]);
2637
2638 $tmp_product = $product_parent;
2639 if (!$saso_eventtickets_is_date_for_all_variants) $tmp_product = $product; // unter Umständen die Variante
2640 $ticket_start_date = trim(get_post_meta( $tmp_product->get_id(), 'saso_eventtickets_ticket_start_date', true ));
2641 $ticket_start_time = trim(get_post_meta( $tmp_product->get_id(), 'saso_eventtickets_ticket_start_time', true ));
2642 if (empty($ticket_start_date) && !empty($ticket_start_time)) {
2643 $ticket_start_date = wp_date("Y-m-d");
2644 }
2645
2646 $ticket_id = $this->MAIN->getCore()->getTicketId($codeObj, $metaObj);
2647 $qrCodeContent = $this->MAIN->getCore()->getQRCodeContent($codeObj, $metaObj);
2648
2649 $ticketObj = [];
2650 $ticketObj['ticket_id'] = $ticket_id;
2651 $ticketObj['product_id'] = $product->get_id();
2652 $ticketObj['product_parent_id'] = $product_parent->get_id();
2653 $ticketObj['qrcode_content'] = $qrCodeContent;
2654 $ticketObj['code_public'] = $metaObj["wc_ticket"]["_public_ticket_id"];
2655 $ticketObj['code'] = $codeObj['code'];
2656 $ticketObj['code_display'] = $codeObj['code_display'];
2657 $ticketObj['product_name'] = esc_html($product_parent->get_Title());
2658 $ticketObj['product_name_variant'] = "";
2659 if ($is_variation && $this->MAIN->getOptions()->isOptionCheckboxActive('wcTicketPDFDisplayVariantName') && count($product->get_attributes()) > 0) {
2660 foreach($product->get_attributes() as $k => $v){
2661 $ticketObj['product_name_variant'] .= $v." ";
2662 }
2663 }
2664 $location = trim(get_post_meta( $product_parent_original->get_id(), 'saso_eventtickets_event_location', true ));
2665 $ticketObj['location'] = $location == "" ? "" : wp_kses_post($this->MAIN->getAdmin()->getOptionValue("wcTicketTransLocation"))." <b>".wp_kses_post($location)."</b>";
2666 $ticketObj['ticket_date'] = "";
2667 if ($wcTicketHideDateOnPDF == false && !empty($ticket_start_date)) {
2668 $ticketObj['ticket_date'] = $this->displayTicketDateAsString($tmp_product->get_id(), $this->MAIN->getOptions()->getOptionDateFormat(), $this->MAIN->getOptions()->getOptionTimeFormat(), $codeObj);
2669 }
2670 $ticketObj['name_per_ticket'] = "";
2671 if (!empty($metaObj['wc_ticket']['name_per_ticket'])) {
2672 $order_quantity = $order_item->get_quantity();
2673 $ticket_pos = "";
2674 if ($order_quantity > 1) {
2675 // ermittel ticket pos
2676 $codes = explode(",", $order_item->get_meta('_saso_eventtickets_product_code', true));
2677 $ticket_pos = $this->ermittelCodePosition($codeObj['code_display'], $codes);
2678 }
2679 $_vid2 = $is_variation ? $product_original->get_id() : 0;
2680 $label = esc_attr($this->getLabelNamePerTicket($product_parent_original->get_id(), $_vid2));
2681 $ticketObj['name_per_ticket'] = str_replace("{count}", $ticket_pos, $label)." ".esc_attr($metaObj['wc_ticket']['name_per_ticket']);
2682 }
2683 $ticketObj['value_per_ticket'] = "";
2684 if (!empty($metaObj['wc_ticket']['value_per_ticket'])) {
2685 $order_quantity = $order_item->get_quantity();
2686 $ticket_pos = "";
2687 if ($order_quantity > 1) {
2688 $codes = explode(",", $order_item->get_meta('_saso_eventtickets_product_code', true));
2689 $ticket_pos = $this->ermittelCodePosition($codeObj['code_display'], $codes);
2690 }
2691 $_vid2 = $is_variation ? $product_original->get_id() : 0;
2692 $label = esc_attr($this->getLabelValuePerTicket($product_parent_original->get_id(), $_vid2));
2693 $ticketObj['value_per_ticket'] = str_replace("{count}", $ticket_pos, $label)." ".esc_attr($metaObj['wc_ticket']['value_per_ticket']);
2694 }
2695
2696 // Seat information
2697 $ticketObj['seat_label'] = !empty($metaObj['wc_ticket']['seat_label']) ? esc_html($metaObj['wc_ticket']['seat_label']) : '';
2698 $ticketObj['seat_category'] = !empty($metaObj['wc_ticket']['seat_category']) ? esc_html($metaObj['wc_ticket']['seat_category']) : '';
2699 $ticketObj['seat_id'] = !empty($metaObj['wc_ticket']['seat_id']) ? intval($metaObj['wc_ticket']['seat_id']) : 0;
2700 $ticketObj['seating_plan_id'] = 0;
2701 $ticketObj['seating_plan_name'] = '';
2702 $hidePlanInScanner = $this->MAIN->getOptions()->isOptionCheckboxActive('seatingHidePlanNameInScanner');
2703 if ($ticketObj['seat_id'] > 0 && !$hidePlanInScanner) {
2704 $planId = $this->MAIN->getSeating()->getSeatManager()->getSeatingPlanIdForSeatId($ticketObj['seat_id']);
2705 if ($planId) {
2706 $ticketObj['seating_plan_id'] = intval($planId);
2707 $plan = $this->MAIN->getSeating()->getPlanManager()->getById($planId);
2708 if ($plan) {
2709 $ticketObj['seating_plan_name'] = esc_html($plan['name']);
2710 }
2711 }
2712 }
2713
2714 $ticket_infos[] = $ticketObj;
2715
2716 $products[$product->get_id()] = [
2717 "product_id"=>$product->get_id(),
2718 "product_parent_id"=>$product_parent->get_id(),
2719 "product_id_original"=>$product_original->get_id(),
2720 "product_parent_original_id"=>$product_parent_original->get_id(),
2721 "product_name"=>$ticketObj['product_name'],
2722 "product_name_variant"=>$ticketObj['product_name_variant'],
2723 ];
2724 }
2725 }
2726 }
2727
2728 $order_code = $this->getParts(trim(SASO_EVENTTICKETS::getRequestPara('code', $def='')))["foundcode"];
2729 $qrcode_content = $order_code;
2730 if ($this->MAIN->getOptions()->isOptionCheckboxActive('ticketQRUseURLToTicketScanner')) {
2731 $qrcode_content = $this->MAIN->getCore()->getTicketScannerURL($order_code);
2732 }
2733 $order_infos = [
2734 "id"=>$order_id,
2735 "is_order_ticket"=>true, // for the ticket scanner to recognize the answer
2736 "code"=>$order_code,
2737 "qrcode_content"=>$qrcode_content,
2738 "option_displayDateTimeFormat"=>$option_displayDateTimeFormat,
2739 "date_created"=>wp_date($option_displayDateTimeFormat, strtotime($order->get_date_created())),
2740 "date_paid"=> $order->get_date_paid() != null ? wp_date($option_displayDateTimeFormat, strtotime($order->get_date_paid())) : "-",
2741 "date_completed"=>$order->get_date_completed() != null ? wp_date($option_displayDateTimeFormat, strtotime($order->get_date_completed())) : "-",
2742 "total"=>$order->get_formatted_order_total(),
2743 "customer_id"=>$order->get_customer_id(),
2744 "billing_name"=>$order->get_formatted_billing_full_name(),
2745 "products"=>array_values($products)
2746 ];
2747
2748 $ret = ["order"=>$order, "order_infos"=>$order_infos, "ticket_infos"=>$ticket_infos];
2749 $ret = apply_filters( $this->MAIN->_add_filter_prefix.'ticket_getOrderTicketsInfos', $ret );
2750 return $ret;
2751 }
2752
2753 private function outputOrderTicketsInfos() {
2754 $parts = $this->getParts();
2755 if (count($parts) < 3) return "WRONG CODE";
2756
2757 wp_enqueue_style("wp-jquery-ui-dialog");
2758
2759 wp_enqueue_script(
2760 'ajax_script_order_ticket',
2761 plugins_url("order_details.js?_v=".$this->MAIN->getPluginVersion(), __FILE__),
2762 array('jquery', 'jquery-ui-dialog', 'wp-i18n')
2763 );
2764 wp_set_script_translations('ajax_script_order_ticket', 'event-tickets-with-ticket-scanner', __DIR__.'/languages');
2765
2766 $infos = $this->getOrderTicketsInfos($parts['order_id'], $parts['code']);
2767 $order = $infos["order"];
2768
2769 $this->setOrderStatusAfterViewOperation($order);
2770
2771 $order_infos = $infos["order_infos"];
2772 $ticket_infos = $infos["ticket_infos"];
2773
2774 if ($this->MAIN->getOptions()->isOptionCheckboxActive('wcTicketDisplayDownloadAllTicketsPDFButtonOnOrderdetail')) {
2775 $url = $this->MAIN->getCore()->getOrderTicketsURL($order);
2776 $dlnbtnlabel = $this->MAIN->getOptions()->getOptionValue('wcTicketLabelPDFDownload');
2777 $dlnbtnlabelHeading = $this->MAIN->getOptions()->getOptionValue('wcTicketLabelPDFDownloadHeading');
2778 $order_infos["wcTicketDisplayDownloadAllTicketsPDFButtonOnOrderdetail"] = 1;
2779 $order_infos["wcTicketLabelPDFDownloadHeading"] = esc_html($dlnbtnlabelHeading);
2780 $order_infos["url_order_tickets"] = esc_url($url);
2781 $order_infos["wcTicketLabelPDFDownload"] = esc_html($dlnbtnlabel);
2782 }
2783
2784 echo '<div id="'.$this->MAIN->getPrefix().'_order_detail_area"></div>';
2785 echo "\n<script>\n";
2786 echo 'let sasoEventtickets_order_detail_data = {"order":{},"tickets":[]};'."\n";
2787 echo 'sasoEventtickets_order_detail_data.order = '.json_encode($order_infos).';';
2788 echo 'sasoEventtickets_order_detail_data.tickets = '.json_encode($ticket_infos).';';
2789 echo 'sasoEventtickets_order_detail_data.system = '.json_encode(["base_url"=>plugin_dir_url(__FILE__), "divPrefix"=>$this->MAIN->getPrefix()]).';';
2790 echo '</script>';
2791 }
2792
2793 private function outputTicketInfo($forPDFOutput=false) {
2794 $codeObj = $this->getCodeObj();
2795 $codeObj = $this->MAIN->getCore()->setMetaObj($codeObj);
2796 $metaObj = $codeObj['metaObj'];
2797
2798 if ($forPDFOutput == false) {
2799 do_action( $this->MAIN->_do_action_prefix.'trackIPForTicketView', $codeObj );
2800 }
2801
2802 $display_the_ticket = apply_filters( $this->MAIN->_do_action_prefix.'ticket_outputTicketInfo', true, $codeObj, $forPDFOutput );
2803 do_action( $this->MAIN->_do_action_prefix.'ticket_outputTicketInfo_pre', $display_the_ticket, $codeObj, $forPDFOutput );
2804
2805 if ($display_the_ticket) {
2806 $ticketDesigner = $this->MAIN->getTicketDesignerHandler();
2807
2808 // !!! nonce test is not working, because this function is also called from the other methods
2809 //if (SASO_EVENTTICKETS::issetRPara('testDesigner') && current_user_can( 'manage_options' ) ) {
2810 //$a = SASO_EVENTTICKETS::getRequestPara('nonce');
2811 //$b = $this->MAIN->_js_nonce;
2812
2813 //$is_nonce_check_ok = wp_verify_nonce(SASO_EVENTTICKETS::getRequestPara('nonce'), $this->MAIN->_js_nonce);
2814 //if (SASO_EVENTTICKETS::issetRPara('testDesigner') && $this->MAIN->isUserAllowedToAccessAdminArea() ) {
2815 //if (SASO_EVENTTICKETS::issetRPara('testDesigner') && $is_nonce_check_ok ) {
2816
2817 $template = "";
2818 $ticket_template = apply_filters( $this->MAIN->_add_filter_prefix.'ticket_outputTicketInfo_template', null, $codeObj );
2819 if ($ticket_template != null) {
2820 $template = $ticket_template['template'];
2821 }
2822 if (SASO_EVENTTICKETS::issetRPara('testDesigner') ) { // TODO: quick fix, so that users can work
2823 if (empty($template)) {
2824 $template = $this->MAIN->getAdmin()->getOptionValue("wcTicketDesignerTemplateTest");
2825 }
2826 } else {
2827 if ($this->MAIN->getOptions()->isOptionCheckboxActive('wcTicketTemplateUseDefault') == false) {
2828 if (empty($template)) {
2829 $template = $this->MAIN->getAdmin()->getOptionValue("wcTicketDesignerTemplate");
2830 }
2831 }
2832 }
2833 $ticketDesigner->setTemplate($template);
2834 echo $ticketDesigner->renderHTML($codeObj, $forPDFOutput);
2835
2836 // buttons
2837 $vars = $ticketDesigner->getVariables();
2838 $ticket_times = $this->calcDateStringAllowedRedeemFrom($vars["PRODUCT"]->get_id(), $codeObj);
2839 if ($vars["forPDFOutput"] == false) {
2840 $is_expired = $this->MAIN->getCore()->checkCodeExpired($codeObj);
2841 if (!empty($vars["METAOBJ"]["wc_ticket"]["redeemed_date"])) {
2842 $redeem_counter = count($vars["METAOBJ"]["wc_ticket"]["stats_redeemed"]);
2843 $redeem_max = intval(get_post_meta( $vars["PRODUCT_PARENT"]->get_id(), 'saso_eventtickets_ticket_max_redeem_amount', true ));
2844 $color = "red";
2845 if ($redeem_max == 0) { // unlimited
2846 $color = "green";
2847 } elseif ($redeem_max > 1 && $redeem_counter <= $redeem_max) {
2848 $color = "green";
2849 }
2850 echo '<center>';
2851 echo '<h4 style="color:'.$color.';">'.wp_kses_post($vars["OPTIONS"]["wcTicketTransTicketRedeemed"]).'</h4>';
2852 // Use date_i18n with gmt=true - redeemed_date is stored in local time, gmt=true prevents timezone conversion but translates month/day names
2853 echo wp_kses_post($vars["OPTIONS"]["wcTicketTransRedeemDate"]).' '.date_i18n($vars["TICKET"]["date_time_format"], strtotime($vars["METAOBJ"]["wc_ticket"]["redeemed_date"]), true);
2854 if ($is_expired == false && $vars["isScanner"] == false && $ticket_times['ticket_end_date_timestamp'] > $ticket_times['server_time_timestamp']) {
2855 echo '<h5 style="font-weight:bold;color:green;">'.wp_kses_post($vars["OPTIONS"]["wcTicketTransTicketValid"]).'</h5>';
2856 echo '<form method="get"><input type="hidden" name="code" value="'.esc_attr($metaObj["wc_ticket"]["_public_ticket_id"]).'"><input type="submit" value="'.esc_attr($vars["OPTIONS"]["wcTicketTransRefreshPage"]).'"></form>';
2857 }
2858 echo '</center>';
2859 }
2860 if ($vars["isScanner"] == false) {
2861 if ($vars["OPTIONS"]["wcTicketShowRedeemBtnOnTicket"] == true) {
2862 $display_button = true;
2863 if ($is_expired) {
2864 $display_button = false;
2865 echo ' <center><h4 style="color:red;">'.wp_kses_post($vars["OPTIONS"]["wcTicketTransTicketExpired"]).'</h4></center>';
2866 } elseif ($ticket_times['is_date_set'] == true && $ticket_times['ticket_end_date_timestamp'] < $ticket_times['server_time_timestamp']) {
2867 $display_button = false;
2868 echo ' <center><h4 style="color:red;">'.wp_kses_post($vars["OPTIONS"]["wcTicketTransTicketNotValidToLate"]).'</h4></center>';
2869 } elseif ($ticket_times['is_date_set'] == true && $ticket_times['redeem_allowed_from_timestamp'] > $ticket_times['server_time_timestamp']) {
2870 $display_button = false;
2871 echo ' <center><h4 style="color:red;">'.wp_kses_post($vars["OPTIONS"]["wcTicketTransTicketNotValidToEarly"]).'</h4></center>';
2872 }
2873 if ($display_button) {
2874 echo '
2875 <script>
2876 function redeem_ticket() {
2877 if (confirm("'.$vars["OPTIONS"]["wcTicketTransRedeemQuestion"].'")) {
2878 return true;
2879 }
2880 return false;
2881 }
2882 </script>
2883 <div style="margin-top:30px;margin-bottom:30px;text-align:center;">
2884 <form onsubmit="return redeem_ticket()" method="post">
2885 <input type="hidden" name="action" value="redeem">
2886 <input type="submit" class="button-primary" value="'.wp_kses_post($vars["OPTIONS"]["wcTicketTransBtnRedeemTicket"]).'">
2887 </form>
2888 </div>';
2889 }
2890 }
2891 }
2892 if (SASO_EVENTTICKETS::issetRPara('displaytime')) {
2893 echo '<p>Server time: '.wp_date("Y-m-d H:i").'</p>';
2894 print_r($ticket_times);
2895 }
2896 if ($vars["OPTIONS"]["wcTicketDontDisplayPDFButtonOnDetail"] == false || $vars["OPTIONS"]["wcTicketLabelICSDownload"] == false || $vars["OPTIONS"]["wcTicketBadgeDisplayButtonOnDetail"]) {
2897 echo '<p style="text-align:center;">';
2898 if ($vars["OPTIONS"]["wcTicketDontDisplayPDFButtonOnDetail"] == false) {
2899 echo '<a class="button button-primary" target="_blank" href="'.$vars["METAOBJ"]["wc_ticket"]["_url"].'?pdf">'.wp_kses_post($vars["OPTIONS"]["wcTicketLabelPDFDownload"]).'</a> ';
2900 }
2901 if ($vars["OPTIONS"]["wcTicketDontDisplayICSButtonOnDetail"] == false) {
2902 echo '<a class="button button-primary" target="_blank" href="'.$vars["METAOBJ"]["wc_ticket"]["_url"].'?ics">'.wp_kses_post($vars["OPTIONS"]["wcTicketLabelICSDownload"]).'</a> ';
2903 }
2904 if ($vars["OPTIONS"]["wcTicketBadgeDisplayButtonOnDetail"] == true) {
2905 echo '<a class="button button-primary" target="_blank" href="'.$vars["METAOBJ"]["wc_ticket"]["_url"].'?badge">'.wp_kses_post($vars["OPTIONS"]["wcTicketBadgeLabelDownload"]).'</a>';
2906 }
2907 echo '</p>';
2908 }
2909 }
2910 }
2911
2912 do_action( $this->MAIN->_do_action_prefix.'ticket_outputTicketInfo_after', $codeObj, $forPDFOutput );
2913 }
2914
2915 /**
2916 * welche position in den erstellten tickets für das order item hat der code
2917 * @param $codes array mit den codes
2918 */
2919 public function ermittelCodePosition($code, $codes) {
2920 $pos = array_search($code, $codes);
2921 if ($pos === false) return 1;
2922 return $pos + 1;
2923 }
2924
2925 public function getMaxRedeemAmountOfTicket($codeObj) {
2926 $codeObj = $this->MAIN->getCore()->setMetaObj($codeObj);
2927 $metaObj = $codeObj['metaObj'];
2928 $max_redeem_amount = 1;
2929 if (isset($metaObj['woocommerce']) && isset($metaObj['woocommerce']['product_id'])) {
2930 $product_id = intval($metaObj['woocommerce']['product_id']);
2931 if ($product_id > 0) {
2932 $product = $this->get_product( $product_id );
2933 $is_variation = $product->get_type() == "variation" ? true : false;
2934 $product_parent_id = $product->get_parent_id();
2935 if ($is_variation && $product_parent_id > 0) {
2936 $product = $this->get_product( $product_parent_id );
2937 }
2938 $max_redeem_amount = intval(get_post_meta( $product->get_id(), 'saso_eventtickets_ticket_max_redeem_amount', true ));
2939 }
2940 }
2941 return $max_redeem_amount;
2942 }
2943
2944 public function getMaxRedeemPerDayOfTicket($codeObj): int {
2945 $codeObj = $this->MAIN->getCore()->setMetaObj($codeObj);
2946 $metaObj = $codeObj['metaObj'];
2947 $max_per_day = 0;
2948 if (isset($metaObj['woocommerce'], $metaObj['woocommerce']['product_id'])) {
2949 $product_id = intval($metaObj['woocommerce']['product_id']);
2950 if ($product_id > 0) {
2951 $product = $this->get_product($product_id);
2952 if ($product->get_type() == "variation" && $product->get_parent_id() > 0) {
2953 $product = $this->get_product($product->get_parent_id());
2954 }
2955 $max_per_day = intval(get_post_meta($product->get_id(), 'saso_eventtickets_ticket_max_redeem_per_day', true));
2956 }
2957 }
2958 return $max_per_day;
2959 }
2960
2961 /**
2962 * Count how many times a ticket was redeemed today
2963 *
2964 * @param array $statsRedeemed Array of redeem entries from stats_redeemed
2965 * @return int Number of redeems today
2966 */
2967 public function countRedeemsToday(array $statsRedeemed): int {
2968 $today = wp_date('Y-m-d');
2969 $count = 0;
2970 foreach ($statsRedeemed as $entry) {
2971 if (!empty($entry['redeemed_date']) && substr($entry['redeemed_date'], 0, 10) === $today) {
2972 $count++;
2973 }
2974 }
2975 return $count;
2976 }
2977
2978 public function getRedeemAmountText($codeObj, $metaObj, $forPDFOutput=false) {
2979 $text_redeem_amount = "";
2980 $max_redeem_amount = $this->getMaxRedeemAmountOfTicket($codeObj);
2981 if ($max_redeem_amount > 1) {
2982 if ($forPDFOutput) {
2983 $text_redeem_amount = wp_kses_post($this->MAIN->getOptions()->getOptionValue('wcTicketTransRedeemMaxAmount'));
2984 $text_redeem_amount = str_replace("{MAX_REDEEM_AMOUNT}", $max_redeem_amount, $text_redeem_amount);
2985 } else {
2986 $text_redeem_amount = wp_kses_post($this->MAIN->getOptions()->getOptionValue('wcTicketTransRedeemedAmount'));
2987 $text_redeem_amount = str_replace("{MAX_REDEEM_AMOUNT}", $max_redeem_amount, $text_redeem_amount);
2988 $text_redeem_amount = str_replace("{REDEEMED_AMOUNT}", count($metaObj['wc_ticket']['stats_redeemed']), $text_redeem_amount);
2989 }
2990 }
2991 return $text_redeem_amount;
2992 }
2993
2994 private function isRedeemOperationTooEarly($codeObj, $metaObj, $order) {
2995 // ermittel product
2996 $order_item = $this->getOrderItem($order, $metaObj);
2997 if ($order_item == null) throw new Exception("#8015 ".esc_html__("Can not find the product for this ticket.", 'event-tickets-with-ticket-scanner'));
2998 $product = $order_item->get_product();
2999 $product_id = $product->get_id();
3000 if ($product_id < 1) {
3001 throw new Exception("#236 product id could not be retrieved");
3002 }
3003 $ret = $this->getCalcDateStringAllowedRedeemFromCorrectProduct($product_id, $codeObj);
3004 return $ret['redeem_allowed_from_timestamp'] >= $ret['server_time_timestamp'];
3005 }
3006 private function isRedeemOperationTooLateEventEnded($codeObj, $metaObj, $order) {
3007 $order_item = $this->getOrderItem($order, $metaObj);
3008 if ($order_item == null) throw new Exception("#8015 ".esc_html__("Can not find the product for this ticket.", 'event-tickets-with-ticket-scanner'));
3009 $product = $order_item->get_product();
3010 $product_id = $product->get_id();
3011 if ($product_id < 1) {
3012 throw new Exception("#233 product id could not be retrieved");
3013 }
3014 $ret = $this->getCalcDateStringAllowedRedeemFromCorrectProduct($product_id, $codeObj);
3015 return $ret['ticket_end_date_timestamp'] <= $ret['server_time_timestamp'];
3016 }
3017 private function isRedeemOperationTooLate($codeObj, $metaObj, $order) {
3018 $order_item = $this->getOrderItem($order, $metaObj);
3019 if ($order_item == null) throw new Exception("#8018 ".esc_html__("Can not find the product for this ticket.", 'event-tickets-with-ticket-scanner'));
3020 $product = $order_item->get_product();
3021 $product_id = $product->get_id();
3022 if ($product_id < 1) {
3023 throw new Exception("#234 product id could not be retrieved");
3024 }
3025 $ret = $this->getCalcDateStringAllowedRedeemFromCorrectProduct($product_id, $codeObj);
3026 return $ret['is_date_set'] && $ret['ticket_start_date_timestamp'] < $ret['server_time_timestamp'];
3027 }
3028 private function checkEventStart($codeObj, $metaObj, $order) {
3029 if ($this->MAIN->getOptions()->isOptionCheckboxActive('wcTicketDontAllowRedeemTicketBeforeStart')) {
3030 if ($this->isRedeemOperationTooEarly($codeObj, $metaObj, $order)) {
3031 throw new Exception("#8016 ".esc_html__("Too early. Ticket cannot be redeemed yet.", 'event-tickets-with-ticket-scanner'));
3032 }
3033 }
3034 }
3035 private function checkEventEnd($codeObj, $metaObj, $order) {
3036 if ($this->MAIN->getOptions()->isOptionCheckboxActive('wcTicketAllowRedeemTicketAfterEnd') == false) {
3037 if ($this->isRedeemOperationTooLateEventEnded($codeObj, $metaObj, $order)) {
3038 throw new Exception("#8017 ".esc_html__("Too late, event finished. Ticket cannot be redeemed anymore.", 'event-tickets-with-ticket-scanner'));
3039 }
3040 }
3041 if ($this->MAIN->getOptions()->isOptionCheckboxActive('wsticketDenyRedeemAfterstart')) {
3042 if ($this->isRedeemOperationTooLate($codeObj, $metaObj, $order)) {
3043 throw new Exception("#8019 ".esc_html__("Too late, event started. Ticket cannot be redeemed anymore.", 'event-tickets-with-ticket-scanner'));
3044 }
3045 }
3046 }
3047 private function setStatusAfterRedeemOperation($order) {
3048 $ticketScannerSetOrderStatusAfterRedeem = $this->MAIN->getOptions()->getOptionValue("ticketScannerSetOrderStatusAfterRedeem");
3049 if (strlen($ticketScannerSetOrderStatusAfterRedeem) > 1) { // no status change = "1"
3050 if ($order != null) {
3051 if ($order->get_status() != $ticketScannerSetOrderStatusAfterRedeem) {
3052 $order->update_status($ticketScannerSetOrderStatusAfterRedeem);
3053 }
3054 }
3055 }
3056 return $order;
3057 }
3058 private function setOrderStatusAfterViewOperation($order) {
3059 $ticketScannerSetOrderStatusAfterTicketView = $this->MAIN->getOptions()->getOptionValue("ticketScannerSetOrderStatusAfterTicketView");
3060 if (strlen($ticketScannerSetOrderStatusAfterTicketView) > 1) { // no status change = "1"
3061 if ($order != null) {
3062 if ($order->get_status() != $ticketScannerSetOrderStatusAfterTicketView) {
3063 $order->update_status($ticketScannerSetOrderStatusAfterTicketView);
3064 }
3065 }
3066 }
3067 return $order;
3068 }
3069 private function redeemTicket($codeObj = null) {
3070 $this->redeem_successfully = false;
3071 if ($codeObj == null) {
3072 $codeObj = $this->getCodeObj();
3073 }
3074 $metaObj = $codeObj['metaObj'];
3075
3076 // check wird nochmal in adminsetting redeem gemacht, aber ohne eigenen Text
3077 $max_redeem_amount = $this->getMaxRedeemAmountOfTicket($codeObj);
3078
3079 if ($metaObj['wc_ticket']['redeemed_date'] == "" || $max_redeem_amount > 0) {
3080 $order = $this->getOrderById($codeObj["order_id"]);
3081 $is_paid = $this->isPaid($order);
3082 if (!$is_paid && $this->MAIN->getOptions()->isOptionCheckboxActive('wcTicketAllowRedeemOnlyPaid')) {
3083 throw new Exception("#8014 ".esc_html__("Order is not paid. And the option is active to allow only paid ticket to be redeemed is active.", 'event-tickets-with-ticket-scanner'));
3084 }
3085
3086 $this->checkEventStart($codeObj, $metaObj, $order);
3087 $this->checkEventEnd($codeObj, $metaObj, $order);
3088
3089 $user_id = $order->get_user_id();
3090 $user_id = intval($user_id);
3091 $data = [
3092 'code'=>$codeObj['code'],
3093 'userid'=>$user_id,
3094 'redeemed_by_admin'=>1
3095 ];
3096 if ($this->authtoken_id > 0) {
3097 $data['authtoken_id'] = $this->authtoken_id;
3098 }
3099 $this->MAIN->getAdmin()->executeJSON('redeemWoocommerceTicketForCode', $data, true);
3100
3101 $order = $this->setStatusAfterRedeemOperation($order);
3102
3103 $this->redeem_successfully = true;
3104 do_action( $this->MAIN->_do_action_prefix.'ticket_redeemTicket', $codeObj, $data );
3105 }
3106 }
3107
3108 private function executeRequestScanner() {
3109 if (SASO_EVENTTICKETS::issetRPara('action') && SASO_EVENTTICKETS::getRequestPara('action') == "redeem" || (SASO_EVENTTICKETS::issetRPara('redeemauto') && SASO_EVENTTICKETS::issetRPara('code'))) {
3110 if (!SASO_EVENTTICKETS::issetRPara('code')) throw new Exception("#8008 ".esc_html__('Ticket number to redeem is missing', 'event-tickets-with-ticket-scanner')); // hmm, seems that this will never be called
3111 $this->redeemTicket();
3112 $this->codeObj = null;
3113 }
3114 }
3115
3116 private function executeRequest() {
3117 // auswerten $this->getParts()['_request']
3118 //if ($this->getParts()['_request'] == "action=redeem") {
3119 if (SASO_EVENTTICKETS::issetRPara('action') && SASO_EVENTTICKETS::getRequestPara('action') == "redeem") {
3120 // redeem ausführen
3121 $order = $this->getOrder();
3122 if ($this->isPaid($order)) {
3123 $codeObj = $this->getCodeObj();
3124 $metaObj = $codeObj['metaObj'];
3125
3126 $user_id = get_current_user_id();
3127 if (empty($user_id)) {
3128 $user_id = $order->get_user_id();
3129 }
3130 $user_id = intval($user_id);
3131 $data = [
3132 'code'=>$codeObj['code'],
3133 'userid'=>$user_id
3134 ];
3135
3136 try {
3137 $this->checkEventStart($codeObj, $metaObj, $order);
3138 } catch (Exception $e) {
3139 throw new Exception(esc_html__("Redeem operation not yet possible.", 'event-tickets-with-ticket-scanner'));
3140 }
3141
3142 try {
3143 $this->checkEventEnd($codeObj, $metaObj, $order);
3144 } catch (Exception $e) {
3145 throw new Exception(esc_html__("Redeem operation not possible. Too late.", 'event-tickets-with-ticket-scanner'));
3146 }
3147
3148 $this->MAIN->getAdmin()->executeJSON('redeemWoocommerceTicketForCode', $data, true);
3149
3150 $order = $this->setStatusAfterRedeemOperation($order);
3151
3152 // check if ticket redirection is activated
3153 if ($this->MAIN->getOptions()->isOptionCheckboxActive('wcTicketRedirectUser')) {
3154 // redirect
3155 $url = $this->MAIN->getAdmin()->getOptionValue('wcTicketRedirectUserURL');
3156 $url = $this->MAIN->getCore()->replaceURLParameters($url, $codeObj);
3157 if (!empty($url)) {
3158 header('Location: '.$url);
3159 exit;
3160 }
3161 }
3162 // check if user redirect is activated - Big BS, did not realize it was already implemented :( , now we need it twice here (on the front end, only the user redirect will be used)
3163 if ($this->MAIN->getOptions()->isOptionCheckboxActive('userJSRedirectActiv')) {
3164 $url = $this->MAIN->getTicketHandler()->getUserRedirectURLForCode($codeObj);
3165 if (!empty($url)) {
3166 header('Location: '.$url);
3167 exit;
3168 }
3169 }
3170
3171 $this->codeObj = null;
3172
3173 } else {
3174 throw new Exception(esc_html__("Order not marked as paid. Ticket not redeemed.", 'event-tickets-with-ticket-scanner'));
3175 }
3176 }
3177 }
3178
3179 public function getUserRedirectURLForCode($codeObj) {
3180 $url = $this->MAIN->getOptions()->getOptionValue('userJSRedirectURL');
3181 // check if code list has url
3182 if ($codeObj['list_id'] != 0) {
3183 // hole code list
3184 $listObj = $this->MAIN->getCore()->getListById($codeObj['list_id']);
3185 $metaObj = $this->MAIN->getCore()->encodeMetaValuesAndFillObjectList($listObj['meta']);
3186 if (isset($metaObj['redirect']['url'])) {
3187 $_url = trim($metaObj['redirect']['url']);
3188 if (!empty($_url)) $url = $_url;
3189 }
3190 }
3191
3192 $url = apply_filters($this->MAIN->_add_filter_prefix.'getJSRedirectURL', $codeObj);
3193 if (is_array($_url)) $_url = ""; // codeobj kam zurück, da niemand auf den hook hört (premium missing/deaktiviert)
3194 if (!empty($_url)) $url = $_url;
3195
3196 // replace place holder
3197 $url = $this->MAIN->getCore()->replaceURLParameters($url, $codeObj);
3198 return $url;
3199 }
3200
3201 public function addMetaTags() {
3202 echo "\n<!-- Meta TICKET EVENT -->\n";
3203 echo '<meta property="og:title" content="'.esc_attr__("Ticket Info", 'event-tickets-with-ticket-scanner').'" />';
3204 echo '<meta property="og:type" content="article" />';
3205 //echo '<meta property="og:description" content="'.$this->getPageDescription().'" />';
3206 echo '<style>
3207 div.ticket_content p {font-size:initial !important;margin-bottom:1em !important;}
3208 </style>';
3209 echo "\n<!-- Ende Meta TICKET EVENT -->\n\n";
3210 }
3211
3212 /**
3213 * Per-view access toggle. Each output reachable through the ticket link
3214 * (view/pdf/ics/badge/onepdf/congress) can be switched off individually via
3215 * its wcTicketShow* option. Default is ENABLED, so an unknown view type or a
3216 * missing option never blocks an existing flow.
3217 */
3218 public function isViewEnabled(string $viewType): bool {
3219 $map = [
3220 'view' => 'wcTicketShowView',
3221 'pdf' => 'wcTicketShowPDFView',
3222 'ics' => 'wcTicketShowICSView',
3223 'badge' => 'wcTicketShowBadgeView',
3224 'onepdf' => 'wcTicketShowOnePDFView',
3225 'congress' => 'wcTicketShowCongressView',
3226 ];
3227 if (!isset($map[$viewType])) return true;
3228 return $this->MAIN->getOptions()->isOptionCheckboxActive($map[$viewType]);
3229 }
3230
3231 /**
3232 * Notice shown when a view was switched off via its wcTicketShow* option.
3233 */
3234 public function getViewDisabledMessage(): string {
3235 return __("This view has been deactivated.", 'event-tickets-with-ticket-scanner');
3236 }
3237
3238 /**
3239 * Download view (pdf/ics/badge/onepdf) is switched off → 403 + notice, exit.
3240 */
3241 private function sendViewDisabled403() {
3242 header("HTTP/1.1 403 Forbidden");
3243 echo esc_html($this->getViewDisabledMessage());
3244 exit;
3245 }
3246
3247 /**
3248 * HTML view (ticket detail / congress) is switched off → small notice page.
3249 */
3250 private function renderViewDisabledHtmlPage() {
3251 if ($this->MAIN->getOptions()->isOptionCheckboxActive('brandingHideHeader') == false) get_header();
3252 echo '<div style="max-width:640px;margin:40px auto;padding:15px;border:1px solid #ccc;text-align:center;">';
3253 echo '<p>'.esc_html($this->getViewDisabledMessage()).'</p>';
3254 echo '</div>';
3255 if ($this->MAIN->getOptions()->isOptionCheckboxActive('brandingHideFooter') == false) get_footer();
3256 }
3257
3258 private function isPDFRequest() {
3259 if (isset($_GET['pdf'])) return true;
3260 $this->getParts();
3261 if ($this->parts != null && isset($this->parts['_isPDFRequest'])) {
3262 return $this->parts['_isPDFRequest'];
3263 }
3264 return false;
3265 }
3266
3267 private function isICSRequest() {
3268 if (isset($_GET['ics'])) return true;
3269 $this->getParts();
3270 if ($this->parts != null && isset($this->parts['_isICSRequest'])) {
3271 return $this->parts['_isICSRequest'];
3272 }
3273 return false;
3274 }
3275
3276 private function isBadgeRequest() {
3277 if (isset($_GET['badge'])) return true;
3278 $this->getParts();
3279 if ($this->parts != null && isset($this->parts['_isBadgeRequest'])) {
3280 return $this->parts['_isBadgeRequest'];
3281 }
3282 return false;
3283 }
3284
3285 private function isCongressRequest() {
3286 if (isset($_GET['congress'])) return true;
3287 $this->getParts();
3288 if ($this->parts != null && isset($this->parts['_isCongressRequest'])) {
3289 return $this->parts['_isCongressRequest'];
3290 }
3291 return false;
3292 }
3293
3294 private function isOrderTicketInfo() {
3295 $parts = $this->getParts();
3296 // bsp ordertickets-395-3477288899
3297 if (isset($parts['idcode']) && $parts['idcode'] == "ordertickets") return true;
3298 return false;
3299 }
3300
3301 private function isOnePDFRequest() {
3302 $parts = $this->getParts();
3303 // bsp order-395-3477288899
3304 if (isset($parts['idcode']) && $parts['idcode'] == "order") return true;
3305 return false;
3306 }
3307
3308 private function initOnePDFOutput() {
3309 $parts = $this->getParts();
3310 if (count($parts) > 2) {
3311 $order_id = intval($parts['order_id']);
3312 $order = wc_get_order($order_id);
3313 $idcode = $order->get_meta('_saso_eventtickets_order_idcode');
3314 if (!empty($idcode) && $idcode == $parts['code']) {
3315 $this->setOrderStatusAfterViewOperation($order);
3316 $this->outputPDFTicketsForOrder($order);
3317 } else {
3318 echo "Wrong ticket code";
3319 }
3320 }
3321 }
3322
3323 /**
3324 * Render ticket detail for shortcode (returns HTML, no header/footer)
3325 */
3326 public function renderTicketDetailForShortcode(): string {
3327 if (!class_exists('WooCommerce')) {
3328 return '<p>' . esc_html__('No WooCommerce Support Found', 'event-tickets-with-ticket-scanner') . '</p>';
3329 }
3330
3331 wp_enqueue_style("wp-jquery-ui-dialog");
3332 $js_url = "jquery.qrcode.min.js?_v=" . $this->MAIN->getPluginVersion();
3333 wp_enqueue_script(
3334 'ajax_script',
3335 plugins_url("3rd/" . $js_url, __FILE__),
3336 array('jquery', 'jquery-ui-dialog', 'wp-i18n')
3337 );
3338 wp_set_script_translations('ajax_script', 'event-tickets-with-ticket-scanner', __DIR__ . '/languages');
3339
3340 ob_start();
3341 echo '<div class="ticket_content" style="background-color:white;color:black;padding:15px;display:block;position:relative;text-align:left;max-width:640px;border:1px solid black;margin:0 auto;">';
3342 try {
3343 if ($this->isOrderTicketInfo()) {
3344 $this->outputOrderTicketsInfos();
3345 } else {
3346 $this->outputTicketInfo();
3347 $order = $this->getOrder();
3348 if ($order != null) {
3349 $this->setOrderStatusAfterViewOperation($order);
3350 }
3351 }
3352 } catch (Exception $e) {
3353 echo '<h1 style="color:red;">' . esc_html__('Error', 'event-tickets-with-ticket-scanner') . '</h1>';
3354 echo '<p>' . esc_html($e->getMessage()) . '</p>';
3355 }
3356 echo '</div>';
3357 return ob_get_clean();
3358 }
3359
3360 public function output() {
3361 $hasError = false;
3362 header('HTTP/1.1 200 OK');
3363 if (class_exists( 'WooCommerce' )) {
3364
3365 try {
3366 if (!$this->isScanner()) {
3367 if ($this->isCongressRequest()) {
3368 if (!$this->isViewEnabled('congress')) { $this->renderViewDisabledHtmlPage(); exit; }
3369 $this->getParts();
3370 // Full public ticket id ({idcode}-{order}-{code}) so self-referential
3371 // URLs (manifest/start_url) stay valid ticket URLs.
3372 $congress_ticket_id = ($this->parts != null && isset($this->parts['code']))
3373 ? $this->parts['idcode'] . '-' . $this->parts['order_id'] . '-' . $this->parts['code']
3374 : '';
3375 $this->MAIN->getCongressPage()->renderForTicket($congress_ticket_id);
3376 exit;
3377 } elseif($this->isPDFRequest()) {
3378 if (!$this->isViewEnabled('pdf')) { $this->sendViewDisabled403(); }
3379 $this->checkIfDownloadIsAllowed();
3380 try {
3381 $this->outputPDF();
3382 exit;
3383 } catch (Exception $e) {}
3384 } elseif ($this->isICSRequest()) {
3385 if (!$this->isViewEnabled('ics')) { $this->sendViewDisabled403(); }
3386 $this->checkIfDownloadIsAllowed();
3387 $this->sendICSFile();
3388 exit;
3389 } elseif ($this->isBadgeRequest()) {
3390 if (!$this->isViewEnabled('badge')) { $this->sendViewDisabled403(); }
3391 $this->checkIfDownloadIsAllowed();
3392 $this->sendBadgeFile();
3393 exit;
3394 } elseif ($this->isOnePDFRequest()) {
3395 if (!$this->isViewEnabled('onepdf')) { $this->sendViewDisabled403(); }
3396 $this->checkIfDownloadIsAllowed();
3397 $this->initOnePDFOutput();
3398 exit;
3399 }
3400 }
3401 } catch(Exception $e) {
3402 $this->MAIN->getAdmin()->logErrorToDB($e);
3403 $hasError = true;
3404 get_header();
3405 echo '<div style="width: 100%; justify-content: center;align-items: center;position: relative;">';
3406 echo '<div class="ticket_content" style="background-color:white;color:black;padding:15px;display:block;position: relative;left: 0;right: 0;margin: auto;text-align:left;max-width:640px;border:1px solid black;">';
3407 echo '<h1 style="color:red;">'.esc_html__('Error', 'event-tickets-with-ticket-scanner').'</h1>';
3408 echo '<p>'.$e->getMessage().'</p>';
3409 }
3410
3411 if (!$hasError) {
3412 wp_enqueue_style("wp-jquery-ui-dialog");
3413
3414 $js_url = "jquery.qrcode.min.js?_v=".$this->MAIN->getPluginVersion();
3415 wp_enqueue_script(
3416 'ajax_script',
3417 plugins_url( "3rd/".$js_url,__FILE__ ),
3418 array('jquery', 'jquery-ui-dialog', 'wp-i18n')
3419 );
3420 wp_set_script_translations('ajax_script', 'event-tickets-with-ticket-scanner', __DIR__.'/languages');
3421
3422 if ($this->MAIN->getOptions()->isOptionCheckboxActive('brandingHideHeader') == false) {
3423 get_header();
3424 }
3425 echo '<div style="width: 100%; justify-content: center;align-items: center;position: relative;">';
3426 echo '<div class="ticket_content" style="background-color:white;color:black;padding:15px;display:block;position: relative;left: 0;right: 0;margin: auto;text-align:left;max-width:640px;border:1px solid black;">';
3427
3428 try {
3429 if ($this->isScanner()) { // old approach
3430 $this->executeRequestScanner();
3431 $this->outputTicketScanner();
3432 } elseif (!$this->isViewEnabled('view')) {
3433 echo '<p>'.esc_html($this->getViewDisabledMessage()).'</p>';
3434 } else {
3435 $this->executeRequest();
3436 if ($this->isOrderTicketInfo()) {
3437 $this->outputOrderTicketsInfos();
3438 } else {
3439 $this->outputTicketInfo();
3440 $order = $this->getOrder();
3441 if ($order != null) {
3442 $this->setOrderStatusAfterViewOperation($order);
3443 }
3444 }
3445 }
3446 } catch(Exception $e) {
3447 echo '<h1 style="color:red;">Error</h1>';
3448 echo $e->getMessage();
3449 }
3450 }
3451
3452 echo '</div>';
3453 echo '</div>';
3454
3455 if ($hasError || $this->MAIN->getOptions()->isOptionCheckboxActive('brandingHideFooter') == false) {
3456 get_footer();
3457 }
3458 } else {
3459 get_header();
3460 echo '<h1 style="color:red;">'.esc_html__('No WooCommerce Support Found', 'event-tickets-with-ticket-scanner').'</h1>';
3461 echo '<p>'.esc_html__('Please contact us for a solution.', 'event-tickets-with-ticket-scanner').'</p>';
3462 get_footer();
3463 }
3464 }
3465 }
3466 ?>