class-congress-admin.php
1 week ago
class-congress-api.php
1 week ago
class-congress-page.php
1 week ago
class-congress-repository.php
1 week ago
class-template-variables.php
1 week ago
class-congress-page.php
163 lines
| 1 | <?php |
| 2 | if (!defined('ABSPATH')) exit; |
| 3 | |
| 4 | class sasoEventtickets_CongressPage { |
| 5 | |
| 6 | private sasoEventtickets $MAIN; |
| 7 | |
| 8 | public function __construct(sasoEventtickets $main) { |
| 9 | $this->MAIN = $main; |
| 10 | } |
| 11 | |
| 12 | /** |
| 13 | * Render the congress page for a ticket. Called from the Ticket handler's output() |
| 14 | * when the "congress" marker is present (…/ticket/{TICKETID}?congress or ?code=…&congress) |
| 15 | * — so it reuses the whole ticket routing (path-based, compatibility-mode, query fallback). |
| 16 | * The congress is derived from the ticket's product; no slug in the URL → no slug collision. |
| 17 | */ |
| 18 | public function renderForTicket(string $ticket_id): void { |
| 19 | // Public congress page gated by the option (admin area stays available regardless). |
| 20 | if (!$this->MAIN->getOptions()->isOptionCheckboxActive('congressModeActive')) { |
| 21 | status_header(404); |
| 22 | $this->renderError(__('Congress not found.', 'event-tickets-with-ticket-scanner')); |
| 23 | return; |
| 24 | } |
| 25 | $repo = $this->MAIN->getCongressRepository(); |
| 26 | // getForTicket applies all access checks (order status, expiry, event window, linkage) |
| 27 | $congress = $repo->getForTicket($ticket_id); |
| 28 | if (!$congress) { |
| 29 | status_header(403); |
| 30 | $this->renderError(__('Access denied. Invalid ticket or no congress assigned to this ticket.', 'event-tickets-with-ticket-scanner')); |
| 31 | return; |
| 32 | } |
| 33 | |
| 34 | // Manifest request (PWA) |
| 35 | if (isset($_GET['manifest'])) { |
| 36 | $this->renderManifest($congress, $ticket_id); |
| 37 | return; |
| 38 | } |
| 39 | |
| 40 | $expired = !empty($congress['access_expires_at']) && strtotime($congress['access_expires_at']) < time(); |
| 41 | |
| 42 | // ETag / Last-Modified |
| 43 | $etag = '"' . strtotime($congress['updated_at']) . '"'; |
| 44 | $last_modified = gmdate('D, d M Y H:i:s', strtotime($congress['updated_at'])) . ' GMT'; |
| 45 | if (isset($_SERVER['HTTP_IF_NONE_MATCH']) && trim($_SERVER['HTTP_IF_NONE_MATCH']) === $etag) { |
| 46 | status_header(304); |
| 47 | return; |
| 48 | } |
| 49 | // Served from a plugin path that WP first resolves as 404 — force a real 200. |
| 50 | status_header(200); |
| 51 | header('ETag: ' . $etag); |
| 52 | header('Last-Modified: ' . $last_modified); |
| 53 | header('Cache-Control: private, no-cache'); |
| 54 | |
| 55 | // Sections are NOT rendered here — the minimal shell loads them via the REST API in JS. |
| 56 | $this->renderPage($congress, $ticket_id, $expired); |
| 57 | } |
| 58 | |
| 59 | /** |
| 60 | * Minimal page shell. NO UI is built in PHP — scripts/styles are enqueued via the WP |
| 61 | * framework (no echoed tags), config is passed via wp_localize_script, and the sections |
| 62 | * are fetched from the REST API and rendered by congress-frontend.js in the browser. |
| 63 | * Keeps PHP per-request work tiny (just an access check + shell) → cacheable, light. |
| 64 | */ |
| 65 | private function renderPage(array $congress, string $ticket_id, bool $expired): void { |
| 66 | $plugin_url = plugin_dir_url(dirname(dirname(__FILE__))); |
| 67 | $plugin_dir = dirname(dirname(__DIR__)); |
| 68 | $languages = $plugin_dir . '/languages'; |
| 69 | // filemtime cache-busting so frontend asset changes are picked up without a version bump. |
| 70 | $css_file = $plugin_dir . '/css/congress-frontend.css'; |
| 71 | $js_file = $plugin_dir . '/js/congress-frontend.js'; |
| 72 | $css_ver = SASO_EVENTTICKETS_PLUGIN_VERSION . '.' . (@filemtime($css_file) ?: '0'); |
| 73 | $js_ver = SASO_EVENTTICKETS_PLUGIN_VERSION . '.' . (@filemtime($js_file) ?: '0'); |
| 74 | $show_wallet = (bool) $this->MAIN->getOptions()->isOptionCheckboxActive('congressShowWalletLink'); |
| 75 | |
| 76 | // Ticket QR as a data-URI so the portal can always offer a "My ticket" entry — |
| 77 | // attendees without a physical badge can show it at the door. The handler caches |
| 78 | // the PNG on disk by filename, so this is cheap on repeat loads. |
| 79 | $ticketQr = ''; |
| 80 | try { |
| 81 | $qrh = $this->MAIN->getTicketQRHandler(); |
| 82 | $qrh->setFilepath(trailingslashit(get_temp_dir())); |
| 83 | $png = $qrh->renderPNG($ticket_id, 'F'); // returns a file path; content is PNG |
| 84 | if ($png && file_exists($png)) { |
| 85 | $ticketQr = 'data:image/png;base64,' . base64_encode((string) file_get_contents($png)); |
| 86 | } |
| 87 | } catch (\Throwable $e) { $ticketQr = ''; } |
| 88 | |
| 89 | // Depend on core 'dashicons' so the page-card icons actually render the glyph font — |
| 90 | // it isn't loaded on the public frontend for anonymous visitors by default. The |
| 91 | // dependency also makes wp_print_styles() below emit it alongside our stylesheet. |
| 92 | wp_register_style('saso-congress-frontend', $plugin_url . 'css/congress-frontend.css', ['dashicons'], $css_ver); |
| 93 | wp_enqueue_style('saso-congress-frontend'); |
| 94 | |
| 95 | wp_register_script('saso-congress-frontend', $plugin_url . 'js/congress-frontend.js', ['wp-i18n'], $js_ver, true); |
| 96 | wp_set_script_translations('saso-congress-frontend', 'event-tickets-with-ticket-scanner', $languages); |
| 97 | wp_localize_script('saso-congress-frontend', 'sasoEtCongressFrontend', [ |
| 98 | 'restBase' => rest_url('saso-et/v1/'), |
| 99 | 'slug' => $congress['slug'], |
| 100 | 'ticket' => $ticket_id, |
| 101 | 'nonce' => wp_create_nonce('wp_rest'), |
| 102 | 'title' => $congress['title'], |
| 103 | 'expired' => $expired ? 1 : 0, |
| 104 | 'showWalletLink' => $show_wallet ? 1 : 0, |
| 105 | 'walletUrl' => $show_wallet ? $this->MAIN->getCore()->getWalletImportURL($ticket_id) : '', |
| 106 | 'ticketQr' => $ticketQr, |
| 107 | 'ticketLabel' => $ticket_id, |
| 108 | ]); |
| 109 | wp_enqueue_script('saso-congress-frontend'); |
| 110 | |
| 111 | // Seam for premium (separate plugin) to register+enqueue its own assets on this |
| 112 | // self-contained document. Fires BEFORE any repo call that could throw. |
| 113 | do_action($this->MAIN->_do_action_prefix.'congress_enqueue', $congress, $ticket_id); |
| 114 | |
| 115 | // Premium can add its handle here so it is actually printed below (this is a |
| 116 | // standalone document — only handles in this list get emitted). Default is unchanged. |
| 117 | $print_handles = apply_filters($this->MAIN->_add_filter_prefix.'congress_print_handles', ['saso-congress-frontend']); |
| 118 | |
| 119 | $manifest_url = add_query_arg('manifest', '1', $this->MAIN->getCongressRepository()->getUrl($ticket_id)); |
| 120 | ?><!DOCTYPE html> |
| 121 | <html <?php language_attributes(); ?>> |
| 122 | <head> |
| 123 | <meta charset="<?php bloginfo('charset'); ?>"> |
| 124 | <meta name="viewport" content="width=device-width, initial-scale=1"> |
| 125 | <title><?php echo esc_html($congress['title']); ?></title> |
| 126 | <link rel="manifest" href="<?php echo esc_url($manifest_url); ?>"> |
| 127 | <?php wp_print_styles($print_handles); ?> |
| 128 | </head> |
| 129 | <body class="congress-page"> |
| 130 | <div id="congress-app" class="congress-app" data-loading="1"> |
| 131 | <div class="congress-loading"><?php esc_html_e('Loading…', 'event-tickets-with-ticket-scanner'); ?></div> |
| 132 | </div> |
| 133 | <?php wp_print_scripts($print_handles); ?> |
| 134 | </body> |
| 135 | </html><?php |
| 136 | } |
| 137 | |
| 138 | public function renderManifest(array $congress, string $ticket_id = ''): void { |
| 139 | header('Content-Type: application/manifest+json'); |
| 140 | $page_url = $this->MAIN->getCongressRepository()->getUrl($ticket_id); |
| 141 | echo wp_json_encode([ |
| 142 | 'name' => $congress['title'], |
| 143 | 'short_name' => mb_substr($congress['title'], 0, 12), |
| 144 | 'start_url' => $page_url, |
| 145 | 'scope' => $page_url, |
| 146 | 'display' => 'standalone', |
| 147 | 'background_color' => '#ffffff', |
| 148 | 'theme_color' => '#0073aa', |
| 149 | 'icons' => [ |
| 150 | ['src' => plugin_dir_url(dirname(dirname(__FILE__))) . 'img/pwa-icon-192.png', 'sizes' => '192x192', 'type' => 'image/png'], |
| 151 | ['src' => plugin_dir_url(dirname(dirname(__FILE__))) . 'img/pwa-icon-512.png', 'sizes' => '512x512', 'type' => 'image/png'], |
| 152 | ], |
| 153 | ]); |
| 154 | } |
| 155 | |
| 156 | private function renderError(string $message): void { |
| 157 | ?><!DOCTYPE html> |
| 158 | <html><head><meta charset="utf-8"><title>Fehler</title></head> |
| 159 | <body><p style="padding:40px;font-family:sans-serif"><?php echo esc_html($message); ?></p></body> |
| 160 | </html><?php |
| 161 | } |
| 162 | } |
| 163 |