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 | ?> |