PluginProbe ʕ •ᴥ•ʔ
VikAppointments Services Booking Calendar / 1.2.21
VikAppointments Services Booking Calendar v1.2.21
1.2.21 1.2.20 trunk 1.2.17 1.2.18 1.2.19
vikappointments / site / helpers / libraries / models / conversion.php
vikappointments / site / helpers / libraries / models Last commit date
conversion.php 3 days ago customer.php 3 days ago customfields.php 3 days ago index.html 3 days ago locations.php 3 days ago orderstatus.php 3 days ago restrictions.php 3 days ago specialrates.php 3 days ago statistics.php 3 days ago subscriptions.php 3 days ago
conversion.php
494 lines
1 <?php
2 /**
3 * @package VikAppointments
4 * @subpackage core
5 * @author E4J s.r.l.
6 * @copyright Copyright (C) 2021 E4J s.r.l. All Rights Reserved.
7 * @license http://www.gnu.org/licenses/gpl-2.0.html GNU/GPL
8 * @link https://vikwp.com
9 */
10
11 // No direct access
12 defined('ABSPATH') or die('No script kiddies please!');
13
14 /**
15 * VikAppointments conversion code class handler.
16 * In order to work, the configuration must own the
17 * following settings:
18 * - conversion_track boolean Flag to check if the conversion track is enabled.
19 *
20 * @since 1.6
21 */
22 class VAPConversion
23 {
24 /**
25 * A list of instances.
26 *
27 * @var array
28 */
29 protected static $instances = array();
30
31 /**
32 * A list of supported conversion rules.
33 *
34 * @var array
35 */
36 protected $list = array();
37
38 /**
39 * The database table.
40 *
41 * @var string
42 */
43 protected $table;
44
45 /**
46 * The page that we are visiting.
47 *
48 * @var string
49 */
50 protected $page;
51
52 /**
53 * Returns the list of all the pages that support conversions.
54 *
55 * @return array
56 */
57 public static function getSupportedPages()
58 {
59 $pages = array(
60 'confirmapp',
61 'order',
62 );
63
64 /**
65 * Loads a list of supported pages that can be used while creating/editing a conversion code.
66 *
67 * The name of the page must be equals to the view name in the front-end. In example, the page
68 * displaying the list of employees is called "employeeslist".
69 *
70 * @return array An array of supported pages.
71 *
72 * @since 1.7
73 */
74 $results = VAPFactory::getEventDispatcher()->trigger('onLoadSupportedConversionPages');
75
76 // join resulting pages with the default ones
77 foreach ($results as $list)
78 {
79 $pages = array_merge($pages, $list);
80 }
81
82 // get rid of duplicates
83 return array_values(array_unique($pages));
84 }
85
86 /**
87 * Returns the list of all the types (db tables) that support conversions.
88 *
89 * @return array
90 */
91 public static function getSupportedTypes()
92 {
93 $types = array(
94 'reservation',
95 );
96
97 /**
98 * Loads a list of supported types that can be used while creating/editing a conversion code.
99 *
100 * The name of the type must be equals to the database table name. In example, the table holding
101 * the orders of the packages is called "package_order". The selected table must own the following
102 * columns for a correct tracking:
103 * - `id` int the primary key;
104 * - `conversion` varchar(64) holds the conversion cookie signature.
105 *
106 *
107 * @return array An array of supported types.
108 *
109 * @since 1.7
110 */
111 $results = VAPFactory::getEventDispatcher()->trigger('onLoadSupportedConversionTypes');
112
113 // join resulting types with the default ones
114 foreach ($results as $list)
115 {
116 $types = array_merge($types, $list);
117 }
118
119 // get rid of duplicates
120 return array_values(array_unique($types));
121 }
122
123 /**
124 * Returns a new instance of this object, only creating it
125 * if it doesn't already exist.
126 *
127 * @param mixed $options The database table or an array of options.
128 *
129 * @return self A new instance of this object.
130 *
131 * @see __construct() for further details about the $options array.
132 */
133 public static function getInstance($options = null)
134 {
135 $sign = serialize($options);
136
137 if (!isset(static::$instances[$sign]))
138 {
139 static::$instances[$sign] = new static($options);
140 }
141
142 return static::$instances[$sign];
143 }
144
145 /**
146 * Class constructor.
147 *
148 * @param mixed $options The database table or an array of options.
149 * The options array can contain the values below
150 * - table the database table name ("reservations" by default).
151 * Since we are using a class of VikAppointments,
152 * the prefix "#__vikappointments_" must be omitted;
153 *
154 * @uses loadConversionRules()
155 */
156 public function __construct($options = null)
157 {
158 if (!is_array($options))
159 {
160 // string given, create an array of options
161 $options = array('table' => $options);
162 }
163
164 if (empty($options['table']))
165 {
166 // the table attribute is empty, use the default table
167 $options['table'] = '#__vikappointments_reservation';
168 }
169 else
170 {
171 // prepend the table prefix to the existing value
172 $options['table'] = '#__vikappointments_' . $options['table'];
173 }
174
175 if (empty($options['page']))
176 {
177 // the page attribute is empty, ignore this filter
178 $options['page'] = '*';
179 }
180
181 $this->table = $options['table'];
182 $this->page = $options['page'];
183
184 $this->loadConversionRules();
185 }
186
187 /**
188 * Loads all the conversion rules supported
189 * by the specified table.
190 *
191 * @return void
192 */
193 protected function loadConversionRules()
194 {
195 $dbo = JFactory::getDbo();
196
197 $q = $dbo->getQuery(true)
198 ->select('*')
199 ->from($dbo->qn('#__vikappointments_conversion'))
200 ->where(array(
201 $dbo->qn('published') . ' = 1',
202 $dbo->qn('type') . ' = ' . $dbo->q(preg_replace("/^#__vikappointments_/i", '', $this->table)),
203 ));
204
205 if ($this->page != '*')
206 {
207 $q->where($dbo->qn('page') . ' = ' . $dbo->q($this->page));
208 }
209
210 $dbo->setQuery($q);
211
212 foreach ($dbo->loadObjectList() as $obj)
213 {
214 // decode statuses array
215 $obj->statuses = (array) json_decode($obj->statuses);
216 // decode file attributes
217 $obj->attributes = (array) ($obj->attributes ? json_decode($obj->attributes, true) : []);
218 // push the record within the list
219 $this->list[] = $obj;
220 }
221 }
222
223 /**
224 * Attaches the script used for the conversion code.
225 * The script will be printed only if it is configured
226 * and the order hasn't been tracked yet.
227 *
228 * @param VAPOrderWrapper $order The order details.
229 *
230 * @return void
231 *
232 * @uses shouldBeTracked()
233 * @uses registerOrder()
234 * @uses parseSnippet()
235 */
236 public function trackCode($order = null)
237 {
238 $config = VAPFactory::getConfig();
239
240 // check if conversion is enabled
241 $enabled = $config->getBool('conversion_track', 0);
242
243 if (!$enabled)
244 {
245 // disabled conversion
246 return;
247 }
248
249 // cast order to object
250 $order = (object) $order;
251
252 // get compliant conversion object
253 $conversion = $this->shouldBeTracked($order);
254
255 if (!$conversion)
256 {
257 // conversion code disabled or not compliant
258 return;
259 }
260
261 // register order
262 $this->registerOrder($order);
263
264 if ($conversion->jsfile)
265 {
266 // append JS file
267 JHtml::fetch('script', $conversion->jsfile, [], $conversion->attributes);
268 }
269
270 // extract <script> and <noscript> from snippet
271 $script = $this->parseSnippet($conversion, $order, $noscript);
272
273 if ($script)
274 {
275 // attach the script to the <head> of the document
276 JFactory::getDocument()->addScriptDeclaration($script);
277 }
278
279 if ($noscript)
280 {
281 // display <noscript> as soon as possible
282 echo $noscript;
283 }
284 }
285
286 /**
287 * Checks if the given order should be tracked.
288 *
289 * @param mixed $order The order that should be tracked.
290 *
291 * @return mixed The conversion record object if found, otherwise false.
292 */
293 public function shouldBeTracked($order)
294 {
295 if (!$this->list)
296 {
297 // no conversion track found
298 return false;
299 }
300
301 if (isset($order->conversion))
302 {
303 $conversion = $order->conversion;
304 }
305 else
306 {
307 $cookie = JFactory::getApplication()->input->cookie;
308 // try to get the last conversion used from the cookie
309 $conversion = $cookie->get('vapconversion', '', 'string');
310 }
311
312 $status = isset($order->status) ? $order->status : '*';
313
314 $new_code = $this->page . '.' . strtolower($status);
315
316 // iterate the records list
317 foreach ($this->list as $code)
318 {
319 if (($status == '*' || in_array($status, $code->statuses)) && strcasecmp($new_code, $conversion))
320 {
321 // the status changed, track it
322 $order->conversion = $new_code;
323
324 // return the tracking record
325 return $code;
326 }
327 }
328
329 // type not supported or same conversion type
330 return false;
331 }
332
333 /**
334 * Updates the order in the database to register the conversion code.
335 * In case the ID is not provided, the conversion will be registered in
336 * the cookie of the browser.
337 *
338 * @param mixed $order The order to track.
339 *
340 * @return void
341 */
342 protected function registerOrder($order)
343 {
344 if (isset($order->id))
345 {
346 $dbo = JFactory::getDbo();
347
348 $q = $dbo->getQuery(true)
349 ->update($dbo->qn($this->table))
350 ->set($dbo->qn('conversion') . ' = ' . $dbo->q($order->conversion))
351 ->where($dbo->qn('id') . ' = ' . (int) $order->id);
352
353 $dbo->setQuery($q);
354 $dbo->execute();
355 }
356 else
357 {
358 $cookie = JFactory::getApplication()->input->cookie;
359
360 // keep the tracking cookie only for 15 minutes
361 $cookie->set('vapconversion', $order->conversion, time() + (15 * 60), '/');
362 }
363 }
364
365 /**
366 * Parses the snippet to inject some information about
367 * the order, such as the total amount paid.
368 *
369 * @param object $conversion The conversion code.
370 * @param mixed $order The order that should be tracked.
371 * @param string &$noscript The <noscript> declaration in case it
372 * was specified within the code snippet.
373 *
374 * @param string The resulting snippet.
375 */
376 public function parseSnippet($conversion, $order, &$noscript = '')
377 {
378 // extract snippet from conversion code
379 $snippet = $conversion->snippet;
380
381 /**
382 * Check if the snippet specifies the <script> declaration,
383 * so that we can support <noscript> tag if specified.
384 *
385 * @since 1.6.5
386 */
387 if (preg_match("/\s*<\s*?script[\s0-9a-zA-Z=\"'\/]*>/", $snippet))
388 {
389 // tags found, try to extract <script> and <noscript> from snippet
390 if (preg_match("/(?:^\s*<\s*?script[\s0-9a-zA-Z=\"'\/]*>)(.*?)(?:<\/script>)/is", $snippet, $match))
391 {
392 // extract pure JavaScript from matching results
393 $script = trim(end($match));
394 }
395 else
396 {
397 // script not found
398 $script = '';
399 }
400
401 if (preg_match("/\s*<\s*?noscript[\s0-9a-zA-Z=\"'\/]*>.*?<\/noscript>/is", $snippet, $match))
402 {
403 // extract full <noscript> declaration
404 $noscript = trim(end($match));
405 }
406 else
407 {
408 // script not found
409 $noscript = '';
410 }
411 }
412 else
413 {
414 // no <script> tag found, a pure JavaScript code is assumed
415 $script = $snippet;
416 }
417
418 // create lookup array for placeholders injection
419 $lookup = array();
420
421 VAPLoader::import('libraries.order.wrapper');
422
423 if ($order instanceof VAPOrderWrapper)
424 {
425 // extract basic details from order
426 $lookup['id'] = $order->id;
427 $lookup['total_cost'] = $order->totals->gross;
428 $lookup['status'] = $order->statusRole;
429
430 if ($order instanceof VAPOrderAppointment)
431 {
432 $lookup['service'] = $lookup['employee'] = array();
433
434 // extract booked services and employees
435 foreach ($order->appointments as $appointment)
436 {
437 $lookup['service'][] = $appointment->service->name;
438
439 // register employee only if selected by the user
440 if ($appointment->viewEmp)
441 {
442 $lookup['employee'][] = $appointment->employee->name;
443 }
444 }
445
446 // stringify selected services and employees
447 $lookup['service'] = implode(', ', array_unique($lookup['service']));
448 $lookup['employee'] = implode(', ', array_unique($lookup['employee']));
449 }
450 }
451 else
452 {
453 // use the whole order
454 $lookup = $order;
455 }
456
457 /**
458 * Trigger hook before parsing the placeholders contained within a snippet of
459 * a conversion/tracking code. It is possible to use this hook to inject or
460 * change the attributes of the $lookup array.
461 *
462 * Here's how to add support for a new placeholder:
463 * $lookup['tax'] = 12.50;
464 * And here's how to include that value within the JS snippet:
465 * var taxAmount = {tax};
466 *
467 * @param array &$lookup An array of placeholders.
468 * @param mixed $order An array/object holding the order details.
469 * @param object $conversion The conversion details.
470 *
471 * @return void
472 *
473 * @since 1.7
474 */
475 VAPFactory::getEventDispatcher()->trigger('onBeforeParseConversionSnippet', array(&$lookup, $order, $conversion));
476
477 foreach ($lookup as $k => $v)
478 {
479 if (is_scalar($v))
480 {
481 /**
482 * Replace specified placeholders with the order vars.
483 *
484 * @since 1.6.5 Placeholders are properly escaped.
485 */
486 $script = preg_replace('/{' . addslashes($k) . '}/i', $v, (string) $script);
487 $noscript = preg_replace('/{' . addslashes($k) . '}/i', $v, (string) $noscript);
488 }
489 }
490
491 return $script;
492 }
493 }
494