PluginProbe ʕ •ᴥ•ʔ
Event Tickets with Ticket Scanner / 3.1.2
Event Tickets with Ticket Scanner v3.1.2
3.1.2 3.1.1 3.1.0 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.4 trunk 2.6.0 2.7.0 2.7.1 2.7.10 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 2.7.8 2.7.9 2.8.0 2.8.1 2.8.10 2.8.2 2.8.3 2.8.4 2.8.5 2.8.6 2.8.7 2.8.8 2.8.9 2.9.0 2.9.2 2.9.3 2.9.4 2.9.5 2.9.6 2.9.7 2.9.8 2.9.9 3.0.0 3.0.1 3.0.2 3.0.3
event-tickets-with-ticket-scanner / sasoEventtickets_Core.php
event-tickets-with-ticket-scanner Last commit date
3rd 1 week ago css 1 week ago img 1 week ago includes 1 week ago js 1 week ago languages 1 week ago ticket 1 week ago vendors 1 week ago SASO_EVENTTICKETS.php 1 week ago backend.js 1 week ago changelog-features.json 1 week ago changelog.txt 1 week ago db.php 1 week ago index.php 1 week ago init_file.php 1 week ago order_details.js 1 week ago pwa-sw.js 1 week ago readme.txt 1 week ago saso-eventtickets-validator.js 1 week ago sasoEventtickets_AdminSettings.php 1 week ago sasoEventtickets_Authtoken.php 1 week ago sasoEventtickets_Base.php 1 week ago sasoEventtickets_Core.php 1 week ago sasoEventtickets_Frontend.php 1 week ago sasoEventtickets_Messenger.php 1 week ago sasoEventtickets_Options.php 1 week ago sasoEventtickets_PDF.php 1 week ago sasoEventtickets_Seating.php 1 week ago sasoEventtickets_Ticket.php 1 week ago sasoEventtickets_TicketBadge.php 1 week ago sasoEventtickets_TicketDesigner.php 1 week ago sasoEventtickets_TicketQR.php 1 week ago ticket_events.js 1 week ago ticket_scanner.js 1 week ago validator.js 1 week ago version-notices.json 1 week ago vollstart-cross-promo.php 1 week ago wc_backend.js 1 week ago wc_frontend.js 1 week ago woocommerce-hooks.php 1 week ago
sasoEventtickets_Core.php
1217 lines
1 <?php
2 include_once(plugin_dir_path(__FILE__)."init_file.php");
3 class sasoEventtickets_Core {
4 private $MAIN;
5
6 private $_CACHE_list = [];
7 private $_CACHE_authtokenNames = [];
8
9 public $ticket_url_path_part = "ticket";
10
11 public function __construct($MAIN) {
12 if ($MAIN->getDB() == null) throw new Exception("#9999 DB needed");
13 $this->MAIN = $MAIN;
14 }
15
16 public function clearCode($code) {
17 $ret = trim(urldecode(strip_tags(str_replace(" ","",str_replace(":","",str_replace("-", "", $code))))));
18 $ret = apply_filters( $this->MAIN->_add_filter_prefix.'core_clearCode', $ret );
19 return $ret;
20 }
21
22 public function getListById($id) {
23 $sql = "select * from ".$this->MAIN->getDB()->getTabelle("lists")." where id = ".intval($id);
24 $ret = $this->MAIN->getDB()->_db_datenholen($sql);
25 if (count($ret) == 0) throw new Exception("#9232 ticket list not found");
26 return $ret[0];
27 }
28
29 public function getCodesByRegUserId($user_id) {
30 $user_id = intval($user_id);
31 if ($user_id <= 0) return [];
32 $sql = "select a.* from ".$this->MAIN->getDB()->getTabelle("codes")." a where user_id = ".$user_id;
33 return $this->MAIN->getDB()->_db_datenholen($sql);
34 }
35
36 /**
37 * Get all ticket codes for a specific WooCommerce order
38 *
39 * @param int $order_id WooCommerce order ID
40 * @return array Array of code records
41 */
42 public function getCodesByOrderId(int $order_id): array {
43 $order_id = intval($order_id);
44 if ($order_id <= 0) return [];
45 $sql = "select a.* from ".$this->MAIN->getDB()->getTabelle("codes")." a where order_id = ".$order_id;
46 return $this->MAIN->getDB()->_db_datenholen($sql);
47 }
48
49 public function retrieveCodeByCode($code, $mitListe=false) {
50 $code = $this->clearCode($code);
51 $code = $this->MAIN->getDB()->reinigen_in($code);
52 if (empty($code)) throw new Exception("#203 tiket number empty");
53 if ($mitListe) {
54 $sql = "select a.*, b.name as list_name from ".$this->MAIN->getDB()->getTabelle("codes")." a
55 left join ".$this->MAIN->getDB()->getTabelle("lists")." b on a.list_id = b.id
56 where code = '".$code."'";
57 } else {
58 $sql = "select a.* from ".$this->MAIN->getDB()->getTabelle("codes")." a where code = '".$code."'";
59 }
60 $ret = $this->MAIN->getDB()->_db_datenholen($sql);
61 if (count($ret) == 0) throw new Exception("#204 ticket with ".$code." not found");
62 return $ret[0];
63 }
64
65 /**
66 * Look up a single ticket row by its plain printed ticket number, matching
67 * either the internal `code` or the human `code_display`. Used only by the
68 * opt-in "redeem by plain ticket number" path. Throws when nothing matches,
69 * and — deliberately — when more than one row matches (ambiguous, e.g. a
70 * recycled number under the reuse option): we never guess which ticket.
71 */
72 public function retrieveCodeByTicketNumber($number) {
73 $number = $this->clearCode($number);
74 $number = $this->MAIN->getDB()->reinigen_in($number);
75 if (empty($number)) throw new Exception("#260 ticket number empty");
76 $table = $this->MAIN->getDB()->getTabelle("codes");
77 $sql = "select * from ".$table." where code = '".$number."' or code_display = '".$number."'";
78 $ret = $this->MAIN->getDB()->_db_datenholen($sql);
79 if (count($ret) == 0) throw new Exception("#261 ticket number ".$number." not found");
80 if (count($ret) > 1) throw new Exception("#262 ticket number ".$number." is ambiguous");
81 return $ret[0];
82 }
83
84 /**
85 * Rebuild the full public ticket id ({idcode}-{order_id}-{code}) from a plain
86 * ticket number. Returns '' if it cannot be resolved to exactly one ticket.
87 * Because the rebuilt id carries the real stored idcode/order_id, every
88 * downstream copy-protection check passes unchanged.
89 */
90 public function reconstructPublicTicketIdFromNumber($number) {
91 $codeObj = $this->retrieveCodeByTicketNumber($number);
92 $metaObj = $this->encodeMetaValuesAndFillObject($codeObj['meta'], $codeObj);
93 return $this->getTicketId($codeObj, $metaObj);
94 }
95
96 /**
97 * If the opt-in option is active and $foundcode is a bare ticket number
98 * (not already a 3-part public id and not an order-ticket id), expand it to
99 * the full public ticket id. Any "?request" suffix is preserved. On any
100 * failure the original value is returned untouched, so the normal flow (and
101 * its existing #9303 error) is never disturbed.
102 */
103 public function maybeExpandPlainTicketNumber($foundcode) {
104 if (empty($foundcode)) return $foundcode;
105 if (!$this->MAIN->getOptions()->isOptionCheckboxActive('wcTicketAllowRedeemByTicketNumber')) return $foundcode;
106 $fc = trim($foundcode);
107 $core = explode('?', $fc)[0];
108 if (substr($core, 0, 13) === 'ordertickets-') return $foundcode; // order ticket, not in scope
109 if (count(explode('-', $core)) >= 3) return $foundcode; // already a full public id
110 try {
111 $full = $this->reconstructPublicTicketIdFromNumber($core);
112 if (!empty($full)) {
113 $suffix = (strpos($fc, '?') !== false) ? substr($fc, strpos($fc, '?')) : '';
114 return $full . $suffix;
115 }
116 } catch (Exception $e) {
117 // not resolvable → leave untouched; normal parsing throws #9303 as before
118 }
119 return $foundcode;
120 }
121
122 public function checkCodesSize() {
123 if ($this->isCodeSizeExceeded()) throw new Exception("#208 too many tickets. Unlimited tickets only with premium");
124 }
125 public function isCodeSizeExceeded() {
126 return $this->MAIN->getBase()->_isMaxReachedForTickets($this->MAIN->getDB()->getCodesSize()) == false;
127 }
128
129 // helpful if meta information is changed by function and the following function might retrieve the inform from the database
130 public function saveMetaObject($codeObj, $metaObj) {
131 // convert meta object to json and save it
132 $codeObj['meta'] = $this->json_encode_with_error_handling($metaObj);
133 $this->MAIN->getDB()->update("codes", ["meta"=>$codeObj['meta']], ['id'=>$codeObj['id']]);
134 return $codeObj;
135 }
136
137 public function retrieveCodeById($id, $mitListe=false) {
138 $id = intval($id);
139 if ($id == 0) throw new Exception("#220 id is wrong");
140 if ($mitListe) {
141 $sql = "select a.*, b.name as list_name from ".$this->MAIN->getDB()->getTabelle("codes")." a
142 left join ".$this->MAIN->getDB()->getTabelle("lists")." b on a.list_id = b.id
143 where a.id = ".$id;
144 } else {
145 $sql = "select a.* from ".$this->MAIN->getDB()->getTabelle("codes")." a where a.id = ".$id;
146 }
147 $ret = $this->MAIN->getDB()->_db_datenholen($sql);
148 if (count($ret) == 0) throw new Exception("#221 ticket not found");
149 return $ret[0];
150 }
151
152 public function getMetaObject() {
153 $metaObj = [
154 'validation'=>[
155 'first_success'=>'',
156 'first_success_tz'=>'',
157 'first_ip'=>'',
158 'last_success'=>'',
159 'last_success_tz'=>'',
160 'last_ip'=>''
161 ]
162 ,'user'=>[
163 'reg_approved'=>0,
164 'reg_request'=>'',
165 'reg_request_tz'=>'',
166 'value'=>'',
167 'reg_ip'=>'',
168 'reg_userid'=>0,
169 '_reg_username'=>'']
170 ,'used'=>[
171 'reg_ip'=>'',
172 'reg_request'=>'',
173 'reg_request_tz'=>'',
174 'reg_userid'=>0,
175 '_reg_username'=>'']
176 ,'confirmedCount'=>0
177 ,'woocommerce'=>[
178 'order_id'=>0,
179 'product_id'=>0,
180 'creation_date'=>0,
181 'creation_date_tz'=>'',
182 'item_id'=>0,
183 'user_id'=>0
184 ] // product code for sale
185 ,'wc_rp'=>[
186 'order_id'=>0,
187 'product_id'=>0,
188 'creation_date'=>0,
189 'creation_date_tz'=>'',
190 'item_id'=>0
191 ] // restriction purchase used
192 ,'wc_ticket'=>[
193 'is_ticket'=>0,
194 'ip'=>'',
195 'userid'=>0,
196 '_username'=>'',
197 'redeemed_date'=>'',
198 'redeemed_date_tz'=>'',
199 'redeemed_by_admin'=>0,
200 'redeemed_via_authtoken_id'=>0,
201 'set_by_admin'=>0,
202 'set_by_admin_date'=>'',
203 'set_by_admin_date_tz'=>'',
204 'idcode'=>'',
205 '_url'=>'',
206 '_wallet_url'=>'',
207 '_public_ticket_id'=>'',
208 'stats_redeemed'=>[],
209 'name_per_ticket'=>'',
210 'value_per_ticket'=>'',
211 'is_daychooser'=>0,
212 'day_per_ticket'=>'',
213 'subs'=>$this->getDefaultMetaValueOfSubs(),
214 '_qr_content'=>''
215 ] // ticket purchase ; stats_redeemed is only used if the ticket can be redeemed more than once
216 ,'cvv_attempts'=>[
217 'count' => 0,
218 'last_at' => '',
219 'locked' => false,
220 ] // rate-limiting state for CVV verification at the ticket scanner; locked=true after 5 wrong attempts
221 ];
222
223 if ($this->MAIN->isPremium() && method_exists($this->MAIN->getPremiumFunctions(), 'getMetaObject')) {
224 $metaObj = $this->MAIN->getPremiumFunctions()->getMetaObject($metaObj);
225 }
226
227 return $metaObj;
228 }
229 public function getDefaultMetaValueOfSubs() {
230 return [];
231 }
232
233 public function encodeMetaValuesAndFillObject($metaValuesString, $codeObj=null) {
234 // Decode + merge with defaults
235 $metaObj = $this->decodeAndMergeMeta($metaValuesString, $this->getMetaObject());
236
237 // Fill computed values (usernames, URLs, etc.)
238 if (isset($metaObj['user']['reg_userid']) && $metaObj['user']['reg_userid'] > 0) {
239 $u = get_userdata($metaObj['user']['reg_userid']);
240 if ($u === false) {
241 $metaObj['user']['_reg_username'] = esc_html__("USERID DO NOT EXISTS", 'event-tickets-with-ticket-scanner');
242 } else {
243 $metaObj['user']['_reg_username'] = $u->first_name." ".$u->last_name." (".$u->user_login.")";
244 }
245 } else {
246 $metaObj['user']['_reg_username'] = "";
247 }
248 if (isset($metaObj['used']['reg_userid']) && $metaObj['used']['reg_userid'] > 0) {
249 $u = get_userdata($metaObj['used']['reg_userid']);
250 if ($u === false) {
251 $metaObj['used']['_reg_username'] = esc_html__("USERID DO NOT EXISTS", 'event-tickets-with-ticket-scanner');
252 } else {
253 $metaObj['used']['_reg_username'] = $u->first_name." ".$u->last_name." (".$u->user_login.")";
254 }
255 } else {
256 $metaObj['used']['_reg_username'] = "";
257 }
258 if (isset($metaObj['wc_ticket']['userid']) && $metaObj['wc_ticket']['userid'] > 0) {
259 $u = get_userdata($metaObj['wc_ticket']['userid']);
260 if ($u === false) {
261 $metaObj['wc_ticket']['_username'] = esc_html__("USERID DO NOT EXISTS", 'event-tickets-with-ticket-scanner');
262 } else {
263 $metaObj['wc_ticket']['_username'] = $u->first_name." ".$u->last_name." (".$u->user_login.")";
264 }
265 } else {
266 $metaObj['wc_ticket']['_username'] = "";
267 }
268 if (isset($metaObj['wc_ticket']['redeemed_by_admin']) && $metaObj['wc_ticket']['redeemed_by_admin'] > 0) {
269 $u = get_userdata($metaObj['wc_ticket']['redeemed_by_admin']);
270 if ($u === false) {
271 $metaObj['wc_ticket']['_redeemed_by_admin_username'] = esc_html__("USERID DO NOT EXISTS", 'event-tickets-with-ticket-scanner');
272 } else {
273 $metaObj['wc_ticket']['_redeemed_by_admin_username'] = $u->first_name." ".$u->last_name." (".$u->user_login.")";
274 }
275 } else {
276 $metaObj['wc_ticket']['_redeemed_by_admin_username'] = "";
277 }
278 // Authtoken-Name live-Lookup mit Cache (mirrors getCustomerName pattern).
279 // We store only the ID in the JSON; the name is fetched on each render so a
280 // renamed token shows the current name everywhere — backend table, detail
281 // view and CSV export.
282 if (isset($metaObj['wc_ticket']['redeemed_via_authtoken_id']) && $metaObj['wc_ticket']['redeemed_via_authtoken_id'] > 0) {
283 $_t_id = (int) $metaObj['wc_ticket']['redeemed_via_authtoken_id'];
284 if (!isset($this->_CACHE_authtokenNames[$_t_id])) {
285 $tokenObj = $this->MAIN->getAuthtokenHandler()->getAuthtoken(['id' => $_t_id]);
286 $this->_CACHE_authtokenNames[$_t_id] = ($tokenObj && !empty($tokenObj['name']))
287 ? $tokenObj['name']
288 : esc_html__("AUTHTOKEN DELETED", 'event-tickets-with-ticket-scanner');
289 }
290 $metaObj['wc_ticket']['_redeemed_via_authtoken_name'] = $this->_CACHE_authtokenNames[$_t_id];
291 } else {
292 $metaObj['wc_ticket']['_redeemed_via_authtoken_name'] = "";
293 }
294 if (isset($metaObj['wc_ticket']['set_by_admin']) && $metaObj['wc_ticket']['set_by_admin'] > 0) {
295 $u = get_userdata($metaObj['wc_ticket']['set_by_admin']);
296 if ($u === false) {
297 $metaObj['wc_ticket']['_set_by_admin_username'] = esc_html__("USERID DO NOT EXISTS", 'event-tickets-with-ticket-scanner');
298 } else {
299 $metaObj['wc_ticket']['_set_by_admin_username'] = $u->first_name." ".$u->last_name." (".$u->user_login.")";
300 }
301 } else {
302 $metaObj['wc_ticket']['_set_by_admin_username'] = "";
303 }
304 if ($metaObj['wc_ticket']['is_ticket'] == 1 && $codeObj != null && is_array($codeObj)) {
305 if (empty($metaObj['wc_ticket']['idcode'])) $metaObj['wc_ticket']['idcode'] = crc32($codeObj['id']."-".time());
306 if (empty($metaObj['wc_ticket']['_public_ticket_id'])) $metaObj['wc_ticket']['_public_ticket_id'] = $this->getTicketId($codeObj, $metaObj);
307 if (empty($metaObj['wc_ticket']['_qr_content'])) $metaObj['wc_ticket']['_qr_content'] = $this->getQRCodeContent($codeObj, $metaObj);
308 $metaObj['wc_ticket']['_url'] = $this->getTicketURL($codeObj, $metaObj);
309 $metaObj['wc_ticket']['_wallet_url'] = $this->getWalletImportURL($metaObj['wc_ticket']['_public_ticket_id']);
310 }
311
312 // update validation fields
313 if ($metaObj['confirmedCount'] > 0) {
314 if (empty($metaObj['validation']['first_success'])) {
315 // check used wert
316 if ( !empty($metaObj['used']['reg_request']) ) {
317 if (empty($metaObj['validation']['first_success'])) $metaObj['validation']['first_success'] = $metaObj['used']['reg_request'];
318 if (empty($metaObj['validation']['first_success_tz'])) $metaObj['validation']['first_success_tz'] = $metaObj['used']['reg_request_tz'];
319 if (empty($metaObj['validation']['first_ip'])) $metaObj['validation']['first_ip'] = $metaObj['used']['reg_ip'];
320 } elseif (!empty($metaObj['user']['reg_request'])) { // check user reg wert
321 if (empty($metaObj['validation']['first_success'])) $metaObj['validation']['first_success'] = $metaObj['user']['reg_request'];
322 if (empty($metaObj['validation']['first_success_tz'])) $metaObj['validation']['first_success_tz'] = $metaObj['user']['reg_request_tz'];
323 if (empty($metaObj['validation']['first_ip'])) $metaObj['validation']['first_ip'] = $metaObj['user']['reg_ip'];
324 }
325 }
326 }
327
328 if ($this->MAIN->isPremium() && method_exists($this->MAIN->getPremiumFunctions(), 'encodeMetaValuesAndFillObject')) {
329 $metaObj = $this->MAIN->getPremiumFunctions()->encodeMetaValuesAndFillObject($metaObj, $codeObj);
330 }
331 return $metaObj;
332 }
333
334 public function getMetaObjectKeyList($metaObj, $prefix="META_") {
335 $keys = [];
336 $prefix = strtoupper(trim($prefix));
337 foreach(array_keys($metaObj) as $key) {
338 $tag = $prefix.strtoupper($key);
339 if (is_array($metaObj[$key])) {
340 $_keys = $this->getMetaObjectKeyList($metaObj[$key], $tag."_");
341 $keys = array_merge($keys, $_keys);
342 } else {
343 $keys[] = $tag;
344 }
345 }
346 return $keys;
347 }
348
349 public function getMetaObjectAllowedReplacementTags() {
350 $tags = [];
351 $allowed_tags = [
352 "USER_VALUE"=>esc_html__("Value given by the user during the code registration.", 'event-tickets-with-ticket-scanner'),
353 "USER_REG_IP"=>esc_html__("IP address of the user, register to a code.", 'event-tickets-with-ticket-scanner'),
354 "USER_REG_USERID"=>esc_html__("User id of the registered user to a code. Default will be 0.", 'event-tickets-with-ticket-scanner'),
355 "USED_REG_IP"=>esc_html__("IP addres of the user that used the code.", 'event-tickets-with-ticket-scanner'),
356 "CONFIRMEDCOUNT"=>esc_html__("Amount of how many times the code was validated successfully.", 'event-tickets-with-ticket-scanner'),
357 "WOOCOMMERCE_ORDER_ID"=>esc_html__("WooCommerce order id assigned to the code.", 'event-tickets-with-ticket-scanner'),
358 "WOOCOMMERCE_PRODUCT_ID"=>esc_html__("WooCommerce product id assigned to the code.", 'event-tickets-with-ticket-scanner'),
359 "WOOCOMMERCE_CREATION_DATE"=>esc_html__("Creation date of the WooCommerce sales date.", 'event-tickets-with-ticket-scanner'),
360 "WOOCOMMERCE_CREATION_DATE_TZ"=>esc_html__("Creation date of the WooCommerce sales date timezone.", 'event-tickets-with-ticket-scanner'),
361 "WOOCOMMERCE_USER_ID"=>esc_html__("User id of the WooCommerce sales.", 'event-tickets-with-ticket-scanner'),
362 "WC_RP_ORDER_ID"=>esc_html__("WooCommerce order id, that was purchases using this code as an allowance to purchase a restricted product.", 'event-tickets-with-ticket-scanner'),
363 "WC_RP_PRODUCT_ID"=>esc_html__("WooCommerce product id that was restricted with this code.", 'event-tickets-with-ticket-scanner'),
364 "WC_RP_CREATION_DATE"=>esc_html__("Creation date of the WooCommerce purchase using the allowance code.", 'event-tickets-with-ticket-scanner'),
365 "WC_RP_CREATION_DATE_TZ"=>esc_html__("Creation date timezone of the WooCommerce purchase using the allowance code.", 'event-tickets-with-ticket-scanner'),
366 "WC_TICKET__PUBLIC_TICKET_ID"=>esc_html__("The public ticket number", 'event-tickets-with-ticket-scanner')
367 ];
368 $allowed_tags = apply_filters( $this->MAIN->_add_filter_prefix.'core_getMetaObjectAllowedReplacementTags', $allowed_tags );
369 foreach($allowed_tags as $key => $value) {
370 $tags[] = ["key"=>$key, "label"=>$value];
371 }
372 return $tags;
373 }
374
375 public function getMetaObjectList() {
376 $metaObj = [
377 'desc'=>'',
378 'redirect'=>['url'=>''],
379 'formatter'=>[
380 'active'=>1,
381 'format'=>'' // JSON mit den Format Werten
382 ],
383 'webhooks'=>[
384 'webhookURLaddwcticketsold'=>''
385 ],
386 'messages'=>[
387 'format_limit_threshold_warning'=>[
388 'attempts'=>0,
389 'last_email'=>''
390 ],
391 'format_end_warning'=>[
392 'attempts'=>0,
393 'last_email'=>''
394 ]
395 ]
396 ];
397 if ($this->MAIN->isPremium() && method_exists($this->MAIN->getPremiumFunctions(), 'getMetaObjectList')) {
398 $metaObj = $this->MAIN->getPremiumFunctions()->getMetaObjectList($metaObj);
399 }
400 return $metaObj;
401 }
402
403 public function encodeMetaValuesAndFillObjectList($metaValuesString) {
404 return $this->decodeAndMergeMeta($metaValuesString, $this->getMetaObjectList());
405 }
406
407 public function setMetaObj($codeObj) {
408 if (!isset($codeObj["metaObj"])) {
409 $metaObj = $this->encodeMetaValuesAndFillObject($codeObj['meta'], $codeObj);
410 $codeObj["metaObj"] = $metaObj;
411 }
412 return $codeObj;
413 }
414
415 /**
416 * Generate a random 4-character CVV.
417 * Uppercase alphanumeric, excludes ambiguous characters (O, 0, I, 1).
418 *
419 * @return string 4-char CVV
420 */
421 public function generateCVV(): string {
422 $charset = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; // no O, 0, I, 1
423 $cvv = '';
424 $max = strlen($charset) - 1;
425 for ($i = 0; $i < 4; $i++) {
426 $cvv .= $charset[random_int(0, $max)];
427 }
428 return $cvv;
429 }
430
431 /**
432 * Returns true if the product behind this code has the per-product
433 * "Require CVV at scanner" option active. Honors variation fallback.
434 *
435 * @param array $codeObj Code with materialized metaObj
436 * @return bool
437 */
438 public function isCVVRequiredForScanner(array $codeObj): bool {
439 $metaObj = $codeObj['metaObj'] ?? null;
440 if (!is_array($metaObj)) return false;
441
442 // product_id is persisted by addWoocommerceInfoToCode at metaObj.woocommerce.product_id
443 // (NOT wc_ticket). Variation_id is not persisted in meta — pass 0 so
444 // getMetaWithVariationFallback reads parent-level meta only.
445 $product_id = (int) ($metaObj['woocommerce']['product_id'] ?? 0);
446 $variation_id = 0;
447 if ($product_id < 1) return false;
448
449 $th = $this->MAIN->getTicketHandler();
450 $value = $th->getMetaWithVariationFallback($product_id, $variation_id, 'saso_eventtickets_require_cvv_at_scanner');
451 return $value === 'yes';
452 }
453
454 /**
455 * Verify a CVV input against a code. Tracks attempts in meta and locks
456 * the code (aktiv=0, cvv_attempts.locked=true) after 5 wrong attempts.
457 *
458 * @param array $codeObj Code with materialized metaObj
459 * @param string $cvvInput Input from scanner (case-insensitive match)
460 * @return array ['ok'=>bool, 'attempts_remaining'=>int, 'locked'=>bool]
461 */
462 public function verifyCVV(array $codeObj, string $cvvInput): array {
463 $codeId = (int) ($codeObj['id'] ?? 0);
464 $stored = (string) ($codeObj['cvv'] ?? '');
465 $metaObj = $codeObj['metaObj'] ?? [];
466
467 // No CVV configured on this code (e.g. legacy ticket created before the
468 // per-product option existed, or an admin cleared the CVV). Caller already
469 // verified isCVVRequiredForScanner is true for the product. Rather than
470 // silently locking out the customer by failing every comparison against
471 // an empty string, treat as "nothing to verify — pass through" so the
472 // legitimate customer can still redeem. The configuration inconsistency
473 // (option=yes but cvv='') is recoverable by the admin and must not be
474 // grounds for destroying a valid ticket.
475 if ($stored === '') {
476 return ['ok' => true, 'attempts_remaining' => 5, 'locked' => false];
477 }
478
479 $attempts = $metaObj['cvv_attempts'] ?? ['count' => 0, 'last_at' => '', 'locked' => false];
480 $count = (int) ($attempts['count'] ?? 0);
481 $locked = (bool) ($attempts['locked'] ?? false);
482
483 if ($locked) {
484 return ['ok' => false, 'attempts_remaining' => 0, 'locked' => true];
485 }
486
487 if (strtoupper($cvvInput) === strtoupper($stored)) {
488 return ['ok' => true, 'attempts_remaining' => max(0, 5 - $count), 'locked' => false];
489 }
490
491 // Wrong — increment, persist, possibly lock
492 $count++;
493 $attempts['count'] = $count;
494 $attempts['last_at'] = current_time('mysql');
495 $attempts['locked'] = ($count >= 5);
496
497 $metaObj['cvv_attempts'] = $attempts;
498 $this->persistCVVAttempts($codeId, $metaObj, $attempts['locked']);
499
500 return [
501 'ok' => false,
502 'attempts_remaining' => max(0, 5 - $count),
503 'locked' => $attempts['locked'],
504 ];
505 }
506
507 /**
508 * Reset the CVV attempt counter on a code. If the code was
509 * specifically CVV-locked (cvv_attempts.locked=true), reactivate it.
510 * Does NOT reactivate codes that were made inactive for other reasons.
511 *
512 * @param int $codeId Code id
513 */
514 public function resetCVVAttempts(int $codeId): void {
515 if ($codeId < 1) return;
516 global $wpdb;
517 $table = $this->MAIN->getDB()->getTabelle('codes');
518 $row = $this->MAIN->getDB()->_db_datenholen($wpdb->prepare("SELECT meta, aktiv FROM $table WHERE id = %d", $codeId));
519 if (empty($row)) return;
520 $row = $row[0];
521
522 $metaObj = $this->encodeMetaValuesAndFillObject($row['meta'], $row);
523 $wasCVVLocked = !empty($metaObj['cvv_attempts']['locked']);
524
525 $metaObj['cvv_attempts'] = [
526 'count' => 0,
527 'last_at' => '',
528 'locked' => false,
529 ];
530
531 $updates = ['meta' => $this->json_encode_with_error_handling($metaObj)];
532 $formats = ['%s'];
533 if ($wasCVVLocked && (int)$row['aktiv'] === 0) {
534 $updates['aktiv'] = 1;
535 $formats[] = '%d';
536 }
537 $wpdb->update($table, $updates, ['id' => $codeId], $formats, ['%d']);
538 }
539
540 /**
541 * If the product has the "Require CVV at scanner" option active and the
542 * given code does not yet have a CVV, generate one and persist to codes.cvv.
543 *
544 * Called from addCodeFromListForOrder during WC-order code creation.
545 * Applies to both newly generated and reused codes — the reuse path
546 * (wcassignmentReuseNotusedCodes option) hits the same call site.
547 * Variation-aware via getMetaWithVariationFallback (variation_id=0 reads
548 * parent-product meta).
549 *
550 * @param int $codeId Code row id (just inserted/resolved)
551 * @param int $productId Product id (parent product for variable products)
552 */
553 public function maybeGenerateCVVForCode(int $codeId, int $productId): void {
554 if ($codeId < 1 || $productId < 1) return;
555
556 $requireCVV = $this->MAIN->getTicketHandler()->getMetaWithVariationFallback(
557 $productId, 0, 'saso_eventtickets_require_cvv_at_scanner'
558 ) === 'yes';
559 if (!$requireCVV) return;
560
561 global $wpdb;
562 $table = $this->MAIN->getDB()->getTabelle('codes');
563 $existingCVV = $wpdb->get_var($wpdb->prepare("SELECT cvv FROM $table WHERE id = %d", $codeId));
564 if (!empty($existingCVV)) return;
565
566 $wpdb->update(
567 $table,
568 ['cvv' => $this->generateCVV()],
569 ['id' => $codeId],
570 ['%s'],
571 ['%d']
572 );
573 }
574
575 /**
576 * Internal: persist cvv_attempts meta to DB, and set aktiv=0 if just locked.
577 */
578 private function persistCVVAttempts(int $codeId, array $metaObj, bool $lockNow): void {
579 if ($codeId < 1) return;
580 global $wpdb;
581 $updates = ['meta' => $this->json_encode_with_error_handling($metaObj)];
582 $formats = ['%s'];
583 if ($lockNow) {
584 $updates['aktiv'] = 0;
585 $formats[] = '%d';
586 }
587 $wpdb->update($this->MAIN->getDB()->getTabelle('codes'), $updates, ['id' => $codeId], $formats, ['%d']);
588 }
589
590 public function getQRCodeContent($codeObj, $metaObj=null) {
591 if (!isset($codeObj['metaObj']) || $codeObj['metaObj'] == null) {
592 if ($metaObj != null) {
593 $codeObj['metaObj'] = $metaObj;
594 } else {
595 $codeObj = $this->setMetaObj($codeObj);
596 }
597 }
598 $metaObj = $codeObj['metaObj'];
599 $ticket_id = $this->getTicketId($codeObj, $metaObj);
600 $qrCodeContent = $ticket_id;
601 if ($this->MAIN->getOptions()->isOptionCheckboxActive('ticketQRUseURLToTicketScanner')) {
602 $qrCodeContent = $this->getTicketScannerURL($ticket_id);
603 }
604 if ($this->MAIN->getOptions()->isOptionCheckboxActive('qrUseOwnQRContent')) {
605 $qr_content = $this->MAIN->getAdmin()->getOptionValue('qrOwnQRContent');
606 if (!empty($qr_content)) {
607 $qrCodeContent = $this->replaceURLParameters($qr_content, $codeObj);
608 }
609 }
610 $qrCodeContent = apply_filters( $this->MAIN->_add_filter_prefix.'core_getQRCodeContent', $qrCodeContent );
611 return $qrCodeContent;
612 }
613
614 public function getMetaObjectAuthtoken() {
615 $metaObj = [
616 'desc'=>'',
617 'ticketscanner'=>["bound_to_products"=>""]
618 ];
619 if ($this->MAIN->isPremium() && method_exists($this->MAIN->getPremiumFunctions(), 'getMetaObjectAuthtoken')) {
620 $metaObj = $this->MAIN->getPremiumFunctions()->getMetaObjectAuthtoken($metaObj);
621 }
622 return $metaObj;
623 }
624
625 public function encodeMetaValuesAndFillObjectAuthtoken($metaValuesString) {
626 return $this->decodeAndMergeMeta($metaValuesString, $this->getMetaObjectAuthtoken());
627 }
628
629 public function alignArrays(&$array1, &$array2) {
630 // Füge fehlende Schlüssel von array1 zu array2 hinzu
631 foreach ($array1 as $key => $value) {
632 if (!array_key_exists($key, $array2)) {
633 $array2[$key] = is_array($value) ? [] : null;
634 }
635 }
636
637 // Entferne überschüssige Schlüssel aus array2
638 foreach ($array2 as $key => $value) {
639 if (!array_key_exists($key, $array1)) {
640 unset($array2[$key]);
641 }
642 }
643
644 // Rekursiver Aufruf für Subarrays
645 foreach ($array1 as $key => &$value) {
646 if (is_array($value) && array_key_exists($key, $array2) && is_array($array2[$key])) {
647 $this->alignArrays($value, $array2[$key]);
648 }
649 }
650 unset($value); // Referenz aufheben
651 }
652
653 /**
654 * Search for customers by name and return matching user_ids and order_ids
655 *
656 * @param string $search_query Search term
657 * @return array ['user_ids' => [...], 'order_ids' => [...]]
658 */
659 public function getUserIdsForCustomerName($search_query): array {
660 $ret = ['user_ids' => [], 'order_ids' => []];
661 $search_query = trim($search_query);
662 if (empty($search_query)) return $ret;
663
664 // Search in WordPress standard meta AND WooCommerce billing/shipping meta
665 $args = array(
666 'meta_query' => array(
667 'relation' => 'OR',
668 // WordPress standard
669 array(
670 'key' => 'first_name',
671 'value' => $search_query,
672 'compare' => 'LIKE',
673 ),
674 array(
675 'key' => 'last_name',
676 'value' => $search_query,
677 'compare' => 'LIKE',
678 ),
679 // WooCommerce billing
680 array(
681 'key' => 'billing_first_name',
682 'value' => $search_query,
683 'compare' => 'LIKE',
684 ),
685 array(
686 'key' => 'billing_last_name',
687 'value' => $search_query,
688 'compare' => 'LIKE',
689 ),
690 // WooCommerce shipping
691 array(
692 'key' => 'shipping_first_name',
693 'value' => $search_query,
694 'compare' => 'LIKE',
695 ),
696 array(
697 'key' => 'shipping_last_name',
698 'value' => $search_query,
699 'compare' => 'LIKE',
700 ),
701 ),
702 );
703
704 $user_query = new WP_User_Query($args);
705 if (!empty($user_query->get_results())) {
706 foreach ($user_query->get_results() as $user) {
707 $ret['user_ids'][] = $user->ID;
708 }
709 }
710
711 // Also search by display_name and user_email
712 $args2 = array(
713 'search' => '*' . $search_query . '*',
714 'search_columns' => array('display_name', 'user_email', 'user_login'),
715 );
716 $user_query2 = new WP_User_Query($args2);
717 if (!empty($user_query2->get_results())) {
718 foreach ($user_query2->get_results() as $user) {
719 if (!in_array($user->ID, $ret['user_ids'])) {
720 $ret['user_ids'][] = $user->ID;
721 }
722 }
723 }
724
725 // Search in WooCommerce orders (HPOS compatible) - includes guest orders
726 if (class_exists('WooCommerce')) {
727 $this->searchWooCommerceOrdersForCustomer($search_query, $ret);
728 }
729
730 return $ret;
731 }
732
733 /**
734 * Search WooCommerce orders by customer name and add matching user_ids and order_ids
735 * Uses WooCommerce API (wc_get_orders) with field_query for LIKE searches
736 * Works with both HPOS and legacy storage
737 * For registered users: adds user_id
738 * For guest orders: adds order_id
739 *
740 * @param string $search_query Search term
741 * @param array &$ret Reference to result array ['user_ids' => [...], 'order_ids' => [...]]
742 */
743 private function searchWooCommerceOrdersForCustomer(string $search_query, array &$ret): void {
744 if (!function_exists('wc_get_orders')) {
745 return;
746 }
747
748 global $wpdb;
749 $search_like = '%' . $wpdb->esc_like($search_query) . '%';
750
751 // Check if HPOS is enabled
752 if (class_exists('Automattic\WooCommerce\Utilities\OrderUtil')
753 && \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled()) {
754
755 // HPOS: Search in wc_order_addresses table
756 $addresses_table = $wpdb->prefix . 'wc_order_addresses';
757 $orders_table = $wpdb->prefix . 'wc_orders';
758
759 $sql = $wpdb->prepare(
760 "SELECT DISTINCT o.id as order_id, o.customer_id
761 FROM {$orders_table} o
762 INNER JOIN {$addresses_table} a ON o.id = a.order_id
763 WHERE (a.first_name LIKE %s
764 OR a.last_name LIKE %s
765 OR a.email LIKE %s
766 OR a.company LIKE %s)",
767 $search_like, $search_like, $search_like, $search_like
768 );
769
770 $results = $wpdb->get_results($sql);
771 foreach ($results as $row) {
772 $customer_id = (int) $row->customer_id;
773 $order_id = (int) $row->order_id;
774
775 if ($customer_id > 0) {
776 if (!in_array($customer_id, $ret['user_ids'])) {
777 $ret['user_ids'][] = $customer_id;
778 }
779 } else {
780 if (!in_array($order_id, $ret['order_ids'])) {
781 $ret['order_ids'][] = $order_id;
782 }
783 }
784 }
785 } else {
786 // Legacy: Search in post meta
787 $sql = $wpdb->prepare(
788 "SELECT DISTINCT pm.post_id as order_id, COALESCE(pm_cust.meta_value, 0) as customer_id
789 FROM {$wpdb->postmeta} pm
790 LEFT JOIN {$wpdb->postmeta} pm_cust ON pm.post_id = pm_cust.post_id AND pm_cust.meta_key = '_customer_user'
791 WHERE pm.meta_key IN ('_billing_first_name', '_billing_last_name', '_billing_email', '_billing_company')
792 AND pm.meta_value LIKE %s",
793 $search_like
794 );
795
796 $results = $wpdb->get_results($sql);
797 foreach ($results as $row) {
798 $customer_id = (int) $row->customer_id;
799 $order_id = (int) $row->order_id;
800
801 if ($customer_id > 0) {
802 if (!in_array($customer_id, $ret['user_ids'])) {
803 $ret['user_ids'][] = $customer_id;
804 }
805 } else {
806 if (!in_array($order_id, $ret['order_ids'])) {
807 $ret['order_ids'][] = $order_id;
808 }
809 }
810 }
811 }
812 }
813
814 public function json_encode_with_error_handling($object, $depth=512) {
815 $json = json_encode($object, JSON_NUMERIC_CHECK, $depth);
816 if (json_last_error() !== JSON_ERROR_NONE) {
817 throw new Exception(json_last_error_msg());
818 }
819 return $json;
820 }
821
822 /**
823 * Generic decode and merge meta with defaults
824 *
825 * Use this for any entity type by passing the default meta object.
826 * Pattern: Decode stored JSON, merge over defaults, return complete object.
827 *
828 * Benefits:
829 * - Old stored data automatically gets new fields
830 * - No data loss (stored values preserved)
831 * - Single source of truth for merge logic
832 *
833 * @param string|null $metaJson JSON string from database
834 * @param array $defaultMetaObj Default meta object with all fields
835 * @return array Merged meta object with all fields guaranteed
836 */
837 public function decodeAndMergeMeta(?string $metaJson, array $defaultMetaObj): array {
838 if (!empty($metaJson)) {
839 $decoded = json_decode($metaJson, true);
840 if (is_array($decoded)) {
841 $defaultMetaObj = array_replace_recursive($defaultMetaObj, $decoded);
842 }
843 }
844 return $defaultMetaObj;
845 }
846
847 public function getRealIpAddr() {
848 if (!empty($_SERVER['HTTP_CLIENT_IP'])) //check ip from share internet
849 {
850 $ip=sanitize_text_field($_SERVER['HTTP_CLIENT_IP']);
851 }
852 elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) //to check ip is pass from proxy
853 {
854 $ip=sanitize_text_field($_SERVER['HTTP_X_FORWARDED_FOR']);
855 }
856 else
857 {
858 $ip=sanitize_text_field($_SERVER['REMOTE_ADDR']);
859 }
860 return $ip;
861 }
862
863 public function triggerWebhooks($status, $codeObj) {
864 $options = $this->MAIN->getOptions();
865 if ($options->isOptionCheckboxActive('webhooksActiv')) {
866 $statusToOption = [
867 0 => "webhookURLinvalid",
868 1 => "webhookURLvalid",
869 2 => "webhookURLinactive",
870 3 => "webhookURLisregistered",
871 4 => "webhookURLexpired",
872 5 => "webhookURLmarkedused",
873 6 => "webhookURLsetused",
874 7 => "webhookURLregister",
875 8 => "webhookURLipblocking",
876 9 => "webhookURLipblocked",
877 10 => "webhookURLaddwcinfotocode",
878 11 => "webhookURLwcremove",
879 12 => "webhookURLaddwcticketinfoset",
880 13 => "webhookURLaddwcticketredeemed",
881 14 => "webhookURLaddwcticketunredeemed",
882 15 => "webhookURLaddwcticketinforemoved",
883 16 => "webhookURLrestrictioncodeused",
884 17 => "webhookURLaddwcticketsold",
885 ];
886 $optionname = $statusToOption[$status] ?? "";
887 if (!empty($optionname)) {
888 $url = $options->getOption($optionname)['value'];
889
890 if ($optionname == "webhookURLaddwcticketsold") {
891 $list_id = intval($codeObj['list_id']);
892 if ($list_id > 0) {
893 try {
894 $listObj = $this->MAIN->getAdmin()->getList(['id'=>$list_id]);
895 $metaObj = $this->encodeMetaValuesAndFillObjectList($listObj['meta']);
896 if (isset($metaObj['webhooks']) && isset($metaObj['webhooks']['webhookURLaddwcticketsold'])) {
897 if (!empty(trim($metaObj['webhooks']['webhookURLaddwcticketsold']))) {
898 $url = trim($metaObj['webhooks']['webhookURLaddwcticketsold']);
899 }
900 }
901 } catch(Exception $e) {
902 $this->MAIN->getAdmin()->logErrorToDB($e);
903 }
904 }
905 }
906
907 if (!empty($url)) {
908 $url = $this->replaceURLParameters($url, $codeObj);
909 wp_remote_get($url);
910 do_action( $this->MAIN->_do_action_prefix.'core_triggerWebhooks', $status, $codeObj, $url );
911 }
912 }
913 }
914 }
915
916 private function _getCachedList($list_id) {
917 if (isset($this->_CACHE_list[$list_id])) return $this->_CACHE_list[$list_id];
918 $this->_CACHE_list[$list_id] = $this->getListById($list_id);
919 return $this->_CACHE_list[$list_id];
920 }
921
922 public function replaceURLParameters($url, $codeObj) {
923 $url = str_replace("{CODE}", isset($codeObj['code']) ? $codeObj['code'] : '', $url);
924 $url = str_replace("{CODEDISPLAY}", isset($codeObj['code_display']) ? $codeObj['code_display'] : '', $url);
925 $url = str_replace("{IP}", $this->getRealIpAddr(), $url);
926 $userid = '';
927 if (is_user_logged_in()) {
928 $userid = get_current_user_id();
929 }
930 $url = str_replace("{USERID}", $userid, $url);
931
932 $listname = "";
933 if (isset($codeObj['list_id']) && $codeObj['list_id'] > 0 && strpos(" ".$url, "{LIST}") !== false) {
934 try {
935 $listObj = $this->_getCachedList($codeObj['list_id']);
936 $listname = $listObj['name'];
937 } catch (Exception $e) {
938 }
939 }
940 $url = str_replace("{LIST}", urlencode($listname), $url);
941
942 $listdesc = "";
943 if (isset($codeObj['list_id']) && $codeObj['list_id'] > 0 && strpos(" ".$url, "{LIST_DESC}") !== false) {
944 try {
945 $listObj = $this->_getCachedList($codeObj['list_id']);
946 $metaObj = [];
947 if (!empty($listObj['meta'])) $metaObj = $this->encodeMetaValuesAndFillObjectList($listObj['meta']);
948 if (isset($metaObj['desc'])) $listdesc = $metaObj['desc'];
949 } catch (Exception $e) {
950 }
951 }
952 $url = str_replace("{LIST_DESC}", urlencode($listdesc), $url);
953
954 $metaObj = [];
955 if (!isset($codeObj['metaObj'])) {
956 if (!empty($codeObj['meta'])) $metaObj = $this->encodeMetaValuesAndFillObject($codeObj['meta'], $codeObj);
957 } else {
958 $metaObj = $codeObj['metaObj'];
959 }
960 if (count($metaObj) > 0) $url = $this->_replaceTagsInTextWithMetaObjectsValues($url, $metaObj, "META_");
961 if (count($metaObj) > 0) $url = $this->_replaceTagsInTextWithMetaObjectsValues($url, $metaObj, "");
962
963 $url = apply_filters( $this->MAIN->_add_filter_prefix.'core_replaceURLParameters', $url, $codeObj, $metaObj );
964
965 return $url;
966 }
967
968 private function _replaceTagsInTextWithMetaObjectsValues($text, $metaObj, $prefix="") {
969 $prefix = strtoupper(trim($prefix));
970 foreach(array_keys($metaObj) as $key) {
971 $tag = $prefix.strtoupper($key);
972 if (is_array($metaObj[$key])) {
973 $text = $this->_replaceTagsInTextWithMetaObjectsValues($text, $metaObj[$key], $tag."_");
974 } else {
975 $text = str_replace("{".$tag."}", urlencode($metaObj[$key]), $text);
976 }
977 }
978 return $text;
979 }
980
981 public function checkCodeExpired($codeObj) {
982 if ($this->MAIN->isPremium() && method_exists($this->MAIN->getPremiumFunctions(), 'checkCodeExpired')) {
983 if ($this->MAIN->getPremiumFunctions()->checkCodeExpired($codeObj)) {
984 return true;
985 }
986 }
987 return false;
988 }
989 public function isCodeIsRegistered($codeObj) {
990 $meta = [];
991 if (!empty($codeObj['meta'])) $meta = $this->encodeMetaValuesAndFillObject($codeObj['meta'], $codeObj);
992 if (isset($meta['user']) && isset($meta['user']['value']) && !empty($meta['user']['value'])) {
993 return true;
994 }
995 return false;
996 }
997
998 public function getTicketURLBase($defaultPath=false) {
999 $path = plugin_dir_url(__FILE__).$this->ticket_url_path_part;
1000 if ($defaultPath == false) {
1001 $wcTicketCompatibilityModeURLPath = trim($this->MAIN->getOptions()->getOptionValue('wcTicketCompatibilityModeURLPath'));
1002 $wcTicketCompatibilityModeURLPath = trim(trim($wcTicketCompatibilityModeURLPath, "/"));
1003 if (!empty($wcTicketCompatibilityModeURLPath)) {
1004 $path = site_url()."/".$wcTicketCompatibilityModeURLPath;
1005 }
1006 }
1007 $ret = $path."/";
1008 $ret = apply_filters( $this->MAIN->_add_filter_prefix.'core_getTicketURLBase', $ret );
1009 return $ret;
1010 }
1011 public function getTicketId($codeObj, $metaObj) {
1012 $ret = "";
1013 if (isset($codeObj['code']) && isset($codeObj['order_id']) && isset($metaObj['wc_ticket']['idcode'])) {
1014 $ret = $metaObj['wc_ticket']['idcode']."-".$codeObj['order_id']."-".$codeObj['code'];
1015 }
1016 $ret = apply_filters( $this->MAIN->_add_filter_prefix.'core_getTicketId', $ret, $codeObj, $metaObj );
1017 return $ret;
1018 }
1019 public function getTicketURL($codeObj, $metaObj) {
1020 $ticket_id = $this->getTicketId($codeObj, $metaObj);
1021 $baseURL = $this->getTicketURLBase();
1022 $url = $baseURL.$ticket_id;
1023 if ($this->MAIN->getOptions()->isOptionCheckboxActive('wcTicketCompatibilityMode')) {
1024 $url = $baseURL."?code=".$ticket_id;
1025 }
1026 $url = apply_filters( $this->MAIN->_add_filter_prefix.'core_getTicketURL', $url, $codeObj, $metaObj );
1027 return $url;
1028 }
1029 public function getOrderTicketIDCode($order) {
1030 $order_id = $order->get_id();
1031 $idcode = $order->get_meta('_saso_eventtickets_order_idcode');
1032 if (empty($idcode)) {
1033 $idcode = strtoupper(md5($order_id."-".time()."-".uniqid()));
1034 $order->update_meta_data( '_saso_eventtickets_order_idcode', $idcode );
1035 $order->save();
1036 }
1037 return $idcode;
1038 }
1039 public function getOrderTicketId($order, $ticket_id_prefix="order-") {
1040 $order_id = $order->get_id();
1041 $idcode = $this->getOrderTicketIDCode($order);
1042 $ticket_id = trim($ticket_id_prefix).$order_id."-".$idcode;
1043 return $ticket_id;
1044 }
1045 public function getOrderTicketsURL($order, $ticket_id_prefix="order-") {
1046 if ($order == null) throw new Exception("Order empty - no order tickets PDF url created");
1047 $ticket_id = $this->getOrderTicketId($order, $ticket_id_prefix);
1048 $baseURL = $this->getTicketURLBase();
1049 $url = $baseURL.$ticket_id;
1050 if ($this->MAIN->getOptions()->isOptionCheckboxActive('wcTicketCompatibilityMode')) {
1051 $url = $baseURL."?code=".$ticket_id;
1052 }
1053 $url = apply_filters( $this->MAIN->_add_filter_prefix.'core_getOrderTicketsURL', $url, $order, $ticket_id_prefix );
1054 return $url;
1055 }
1056 public function getTicketScannerURL($ticket_id) {
1057 $baseURL = $this->getTicketURLBase();
1058 $url = $baseURL."scanner/?code=".urlencode($ticket_id);
1059 $url = apply_filters( $this->MAIN->_add_filter_prefix.'core_getTicketScannerURL', $url, $ticket_id );
1060 return $url;
1061 }
1062 public function getWalletImportURL(string $publicTicketId): string {
1063 $url = 'https://wallet.vollstart.com/add?' . http_build_query([
1064 'shop' => site_url(),
1065 'ticket' => $publicTicketId,
1066 ]);
1067 return apply_filters($this->MAIN->_add_filter_prefix . 'core_getWalletImportURL', $url, $publicTicketId);
1068 }
1069 public function getTicketURLPath($defaultPath=false) {
1070 $p = $this->getTicketURLBase($defaultPath);
1071 $teile = parse_url($p);
1072 $ret = $teile['path'];
1073 $ret = apply_filters( $this->MAIN->_add_filter_prefix.'core_getTicketURLPath', $ret, $defaultPath );
1074 return $ret;
1075 }
1076 public function getTicketURLComponents($url) {
1077 $teile = explode("/", $url);
1078 $teile = array_reverse($teile);
1079 $request = "";
1080 $is_pdf_request = false;
1081 $is_ics_request = false;
1082 $is_badge_request = false;
1083 $is_congress_request = false;
1084 $foundcode = "";
1085 foreach($teile as $teil) {
1086 $teil = trim($teil);
1087 if (empty($teil)) continue;
1088 if (strtolower($teil) == "?pdf") continue;
1089 if (strtolower($teil) == "?ics") continue;
1090 if ($teil == $this->ticket_url_path_part) break;
1091 $foundcode = $teil;
1092 break;
1093 }
1094 if (SASO_EVENTTICKETS::issetRPara('code')) { // overwrites any found code, if parameter is available
1095 $foundcode = trim(SASO_EVENTTICKETS::getRequestPara('code'));
1096 $foundcode = $this->maybeExpandPlainTicketNumber($foundcode); // opt-in: bare ticket number → full public id
1097 if (strpos($foundcode, "'") === false) {
1098 $parts = explode("-", $foundcode);
1099 } else {
1100 $parts = explode("'", $foundcode);
1101 }
1102 $t = explode("?", $url);
1103 if (count($t) > 1) {
1104 unset($t[0]);
1105 $tt = [];
1106 foreach($t as $tp){
1107 $ttt = explode("&", $tp);
1108 $tt = array_merge($tt, $ttt);
1109 }
1110 $t = $tt;
1111 $request = join("&", $t);
1112 }
1113 $is_pdf_request = in_array("pdf", $t);
1114 $is_ics_request = in_array("ics", $t);
1115 $is_badge_request = in_array("badge", $t);
1116 $is_congress_request = in_array("congress", $t);
1117 } else {
1118 if (empty($foundcode)) throw new Exception("#9301 ticket id not found from ticket url");
1119 $foundcode = $this->maybeExpandPlainTicketNumber($foundcode); // opt-in: bare ticket number → full public id
1120 $parts = explode("-", $foundcode);
1121 if (count($parts) < 3) throw new Exception("#9303 ticket id is wrong");
1122 $t = explode("?", $parts[2]);
1123 $parts[2] = $t[0];
1124 if (count($t) > 1) {
1125 unset($t[0]);
1126 $request = join("&", $t);
1127 }
1128 $is_pdf_request = in_array("pdf", $t) || SASO_EVENTTICKETS::issetRPara('pdf');
1129 $is_ics_request = in_array("ics", $t) || SASO_EVENTTICKETS::issetRPara('ics');
1130 $is_badge_request = in_array("badge", $t) || SASO_EVENTTICKETS::issetRPara('badge');
1131 $is_congress_request = in_array("congress", $t) || SASO_EVENTTICKETS::issetRPara('congress');
1132 }
1133 if (count($parts) != 3) throw new Exception("#9302 ticket id not correct - cannot create ticket url components");
1134 $parts[2] = str_replace("?pdf", "", $parts[2]);
1135 $parts[2] = str_replace("?ics", "", $parts[2]);
1136 $parts[2] = str_replace("?congress", "", $parts[2]);
1137 $parts_assoc = [
1138 "foundcode"=>$foundcode,
1139 "idcode"=>$parts[0],
1140 "order_id"=>$parts[1],
1141 "code"=>$parts[2],
1142 "_request"=>$request,
1143 "_isPDFRequest"=>$is_pdf_request,
1144 "_isICSRequest"=>$is_ics_request,
1145 "_isBadgeRequest"=>$is_badge_request,
1146 "_isCongressRequest"=>$is_congress_request
1147 ];
1148 $parts_assoc = apply_filters( $this->MAIN->_add_filter_prefix.'core_getTicketURLComponents', $parts_assoc, $url );
1149 return $parts_assoc;
1150 }
1151
1152 public function mergePDFs($filepaths, $filename, $filemode="I", $deleteFilesAfterMerge=true) {
1153 if (count($filepaths) > 0) {
1154 $pdf = $this->MAIN->getNewPDFObject();
1155 $pdf->setFilemode($filemode);
1156 $pdf->setFilename($filename);
1157 try {
1158 $pdf->mergeFiles($filepaths); // send file to browser if,filemode is I
1159 } catch(Exception $e) {
1160 $this->MAIN->getAdmin()->logErrorToDB($e, null, "tried to merge PDFs together. Filepaths: (".join(", ", $filepaths).")");
1161 }
1162
1163 // clean up temp files
1164 if ($deleteFilesAfterMerge) {
1165 foreach($filepaths as $filepath) {
1166 if (file_exists($filepath)) {
1167 @unlink($filepath);
1168 }
1169 }
1170 }
1171 if ($pdf->getFilemode() == "F") {
1172 return $pdf->getFullFilePath();
1173 } else {
1174 exit;
1175 }
1176 }
1177 }
1178
1179 public function parser_search_loop($text) {
1180 // search for loop
1181 // {{LOOP ORDER.items AS item}} loop-content {{LOOPEND}}
1182 if (empty($text)) return false;
1183 $pos = strpos($text, "{{LOOP ");
1184 if ($pos !== false) {
1185 $pos_end = strpos($text, "{{LOOPEND}}", $pos);
1186 if ($pos_end !== false) {
1187 $pos_end += 11;
1188 $html_part = substr($text, $pos, $pos_end - $pos);
1189 //echo $html_part;
1190
1191 $matches = [];
1192
1193 $collection = null;
1194 $item_var = null;
1195 $loop_part = null;
1196 // finde loop collection and item var
1197 $pattern = '/{{\s?LOOP\s(.*?)\sAS\s(.*?)\s?}}(.*?){{\s?LOOPEND\s?}}/is';
1198 if (preg_match($pattern, $html_part, $matches)) {
1199 $collection = trim($matches[1]);
1200 $item_var = trim($matches[2]);
1201 $loop_part = trim($matches[3]);
1202 }
1203
1204 return [
1205 "collection"=>$collection,
1206 "item_var"=>$item_var,
1207 "loop_part"=>$loop_part,
1208 "pos_start"=>$pos,
1209 "pos_end"=>$pos_end,
1210 "found_str"=>$matches[0]
1211 ];
1212 }
1213 }
1214 return false;
1215 }
1216 }
1217 ?>