PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.8.16
VikBooking Hotel Booking Engine & PMS v1.8.16
1.8.16 1.8.15 1.8.14 1.8.13 1.8.12 1.8.11 1.8.10 1.8.9 1.8.6 1.8.7 1.8.8 trunk 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 All 37 releases
← All changes | admin/helpers/conditional_rules.php +1206 -0 1.6.2 → 1.8.16 View file →
@@ -1,0 +1,1206 @@
1 +<?php
2 +/**
3 + * @package VikBooking
4 + * @subpackage com_vikbooking
5 + * @author Alessio Gaggii - e4j - Extensionsforjoomla.com
6 + * @copyright Copyright (C) 2018 e4j - Extensionsforjoomla.com. All rights reserved.
7 + * @license GNU General Public License version 2 or later; see LICENSE
8 + * @link https://vikwp.com
9 + */
10 +
11 +defined('ABSPATH') or die('No script kiddies please!');
12 +
13 +/**
14 + * Helper class for the conditional rules.
15 + *
16 + * @since 1.14 (J) - 1.4.0 (WP)
17 + */
18 +class VikBookingHelperConditionalRules
19 +{
20 + /**
21 + * The singleton instance of the class.
22 + *
23 + * @var VikBookingHelperConditionalRules
24 + */
25 + protected static $instance = null;
26 +
27 + /**
28 + * A flag that indicates the rules debug mode.
29 + *
30 + * @var bool
31 + */
32 + public static $debugRules = false;
33 +
34 + /**
35 + * An array to store some cached/static values.
36 + *
37 + * @var array
38 + */
39 + protected static $helper = null;
40 +
41 + /**
42 + * Logs the execution of all the complex template
43 + * editing methods that manipulate the HTML/PHP DOM.
44 + *
45 + * @var array
46 + */
47 + protected static $editingLog = null;
48 +
49 + /**
50 + * The database handler instance.
51 + *
52 + * @var object
53 + */
54 + protected $dbo;
55 +
56 + /**
57 + * The list of rules instances loaded.
58 + *
59 + * @var array
60 + */
61 + protected $rules;
62 +
63 + /**
64 + * The VikBooking translation object.
65 + *
66 + * @var object
67 + */
68 + protected $vbo_tn;
69 +
70 + /**
71 + * Class constructor is protected.
72 + *
73 + * @see getInstance()
74 + */
75 + protected function __construct()
76 + {
77 + static::$helper = array();
78 + $this->dbo = JFactory::getDbo();
79 + $this->rules = array();
80 + $this->vbo_tn = VikBooking::getTranslator();
81 + $this->load();
82 + }
83 +
84 + /**
85 + * Returns the global object, either
86 + * a new instance or the existing instance
87 + * if the class was already instantiated.
88 + *
89 + * @return self A new instance of the class.
90 + */
91 + public static function getInstance()
92 + {
93 + if (is_null(static::$instance)) {
94 + static::$instance = new static();
95 + }
96 +
97 + return static::$instance;
98 + }
99 +
100 + /**
101 + * Loads a list of all available conditional rules.
102 + *
103 + * @return self
104 + */
105 + protected function load()
106 + {
107 + // require main/parent conditional-rule class
108 + require_once(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'conditional_rule.php');
109 +
110 + $rules_base = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'conditionalrules' . DIRECTORY_SEPARATOR;
111 + $rules_files = glob($rules_base . '*.php');
112 +
113 + /**
114 + * Trigger event to let other plugins register additional rules.
115 + *
116 + * @return array A list of supported rules.
117 + */
118 + $list = VBOFactory::getPlatform()->getDispatcher()->filter('onLoadConditionalRules');
119 + foreach ($list as $chunk) {
120 + // merge default rule files with the returned ones
121 + $rules_files = array_merge($rules_files, (array)$chunk);
122 + }
123 +
124 + foreach ($rules_files as $rf) {
125 + try {
126 + // require rule class file
127 + if (is_file($rf)) {
128 + require_once($rf);
129 + }
130 +
131 + // instantiate rule object
132 + $classname = 'VikBookingConditionalRule' . str_replace(' ', '', ucwords(str_replace('_', ' ', basename($rf, '.php'))));
133 + if (class_exists($classname)) {
134 + $rule = new $classname();
135 + // push rule object
136 + array_push($this->rules, $rule);
137 + }
138 + } catch (Exception $e) {
139 + // do nothing
140 + }
141 + }
142 +
143 + return $this;
144 + }
145 +
146 + /**
147 + * Gets the list of conditional rules instantiated.
148 + *
149 + * @return array list of conditional rules objects.
150 + */
151 + public function getRules()
152 + {
153 + return $this->rules;
154 + }
155 +
156 + /**
157 + * Gets a single conditional rule instantiated.
158 + *
159 + * @param string $id the rule identifier.
160 + *
161 + * @return mixed the conditional rule object, false otherwise.
162 + */
163 + public function getRule($id)
164 + {
165 + foreach ($this->rules as $rule) {
166 + if ($rule->getIdentifier() != $id) {
167 + continue;
168 + }
169 + return $rule;
170 + }
171 +
172 + return false;
173 + }
174 +
175 + /**
176 + * Gets a list of sorted rule names, ids and descriptions.
177 + *
178 + * @return array associative and sorted rules list.
179 + */
180 + public function getRuleNames()
181 + {
182 + $names = array();
183 + $pool = array();
184 +
185 + foreach ($this->rules as $rule) {
186 + $id = $rule->getIdentifier();
187 + $name = $rule->getName();
188 + $descr = $rule->getDescription();
189 + $rdata = new stdClass;
190 + $rdata->id = $id;
191 + $rdata->name = $name;
192 + $rdata->descr = $descr;
193 + $names[$name] = $rdata;
194 + }
195 +
196 + // apply sorting by name
197 + ksort($names);
198 +
199 + // push sorted rules to pool
200 + foreach ($names as $rdata) {
201 + array_push($pool, $rdata);
202 + }
203 +
204 + return $pool;
205 + }
206 +
207 + /**
208 + * Tells whether the rule object overrides the method from its parent
209 + * class. Useful to distinguish action-rules from filter-rules.
210 + *
211 + * @param object $rule child class object of VikBookingConditionalRule.
212 + * @param string $method the name of the method to check if it was overridden.
213 + *
214 + * @return bool true if overridden, false otherwise.
215 + */
216 + public function supportsAction($rule, $method = 'callbackAction')
217 + {
218 + if (!class_exists('ReflectionMethod')) {
219 + return false;
220 + }
221 +
222 + $reflect = new ReflectionMethod($rule, $method);
223 +
224 + return ($reflect->getDeclaringClass()->getName() == get_class($rule));
225 + }
226 +
227 + /**
228 + * Helper method for the controller to compose the rules
229 + * of the conditional text by parsing all input values in the same order requested.
230 + *
231 + * @return array list of stdClass object with the various rules params.
232 + */
233 + public function composeRulesParamsFromRequest()
234 + {
235 + $rules_list = array();
236 + $raw_vals = JFactory::getApplication()->input->getArray();
237 +
238 + foreach ($raw_vals as $raw_inp_key => $rule_inp_vals) {
239 + foreach ($this->rules as $rule) {
240 + $rule_id = $rule->getIdentifier();
241 + $rule_inp_key = basename($rule_id, '.php');
242 + if ($rule_inp_key != $raw_inp_key) {
243 + continue;
244 + }
245 + // rule found, make sure the settings are not empty
246 + $has_vals = false;
247 + foreach ($rule_inp_vals as $rule_inp_val) {
248 + if (is_array($rule_inp_val) && count($rule_inp_val)) {
249 + $has_vals = true;
250 + break;
251 + }
252 + if (is_string($rule_inp_val) && strlen($rule_inp_val)) {
253 + $has_vals = true;
254 + break;
255 + }
256 + }
257 + if (!$has_vals) {
258 + // do not store empty rule params
259 + continue 2;
260 + }
261 + // compose rule object
262 + $rule_data = new stdClass;
263 + $rule_data->id = $rule_id;
264 + $rule_data->params = $rule_inp_vals;
265 + // push rule to list
266 + array_push($rules_list, $rule_data);
267 + }
268 + }
269 +
270 + return $rules_list;
271 + }
272 +
273 + /**
274 + * Helper method to load all special tags and related records.
275 + *
276 + * @param string $orby_col the column to order by.
277 + * @param string $orby_dir the order by direction.
278 + *
279 + * @return array associative list of special-tags (key) records (value).
280 + */
281 + public function getSpecialTags($orby_col = 'name', $orby_dir = 'ASC')
282 + {
283 + $special_tags = array();
284 +
285 + $q = "SELECT `ct`.* FROM `#__vikbooking_condtexts` AS `ct` ORDER BY `ct`.`{$orby_col}` {$orby_dir};";
286 + $this->dbo->setQuery($q);
287 + $records = $this->dbo->loadAssocList();
288 + if ($records) {
289 + $this->vbo_tn->translateContents($records, '#__vikbooking_condtexts');
290 + foreach ($records as $record) {
291 + // decode rules
292 + $record['rules'] = !empty($record['rules']) ? json_decode($record['rules']) : array();
293 + $record['rules'] = !is_array($record['rules']) ? array() : $record['rules'];
294 + // push record
295 + $special_tags[$record['token']] = $record;
296 + }
297 + }
298 +
299 + return $special_tags;
300 + }
301 +
302 + /**
303 + * Helper method to load one precise conditional text from the given special tag.
304 + *
305 + * @param string $token the special tag (token) to look for.
306 + *
307 + * @return array the record of the conditional text found or an empty array.
308 + */
309 + public function getBySpecialTag($token)
310 + {
311 + $q = "SELECT * FROM `#__vikbooking_condtexts` WHERE `token`=" . $this->dbo->quote($token) . ";";
312 + $this->dbo->setQuery($q);
313 + $cond_text = (array) $this->dbo->loadAssoc();
314 + if ($cond_text) {
315 + $this->vbo_tn->translateContents($cond_text, '#__vikbooking_condtexts');
316 + }
317 +
318 + return $cond_text;
319 + }
320 +
321 + /**
322 + * Helper method to store information in the helper array.
323 + *
324 + * @param mixed $key the key or array of keys to set.
325 + * @param mixed $val the value or array of values to set.
326 + *
327 + * @return self
328 + */
329 + public function set($key, $val = null)
330 + {
331 + if (!is_string($key) && !is_array($key)) {
332 + return $this;
333 + }
334 +
335 + if (is_string($key)) {
336 + $key = array($key);
337 + }
338 + if (!is_array($val)) {
339 + $val = array($val);
340 + }
341 +
342 + foreach ($key as $i => $prop) {
343 + if (!isset($val[$i])) {
344 + continue;
345 + }
346 + static::$helper[$prop] = $val[$i];
347 + }
348 +
349 + return $this;
350 + }
351 +
352 + /**
353 + * Helper method to get information from the helper array.
354 + *
355 + * @param string $key the key to get.
356 + * @param string $def the default value to get.
357 + *
358 + * @return mixed the requested key value.
359 + */
360 + public function get($key, $def = null)
361 + {
362 + return isset(static::$helper[$key]) ? static::$helper[$key] : $def;
363 + }
364 +
365 + /**
366 + * Parses all tokens in the given template string and applies the
367 + * conditional texts found if all rules are compliant.
368 + *
369 + * @param string $tmpl the template string to parse, passed by reference.
370 + *
371 + * @return mixed false if no tokens found, integer for how many tokens were applied.
372 + */
373 + public function parseTokens(&$tmpl)
374 + {
375 + preg_match_all('/\{condition: ?([a-zA-Z0-9_]+)\}/U', $tmpl, $matches);
376 +
377 + $tot_tokens = count($matches[0]);
378 +
379 + if (!$tot_tokens) {
380 + // no tokens to parse
381 + return false;
382 + }
383 +
384 + // default empty replacement
385 + $null_replace = '';
386 +
387 + // load all helper property keys and values
388 + $prop_keys = array_keys(static::$helper);
389 + $prop_vals = array_values(static::$helper);
390 +
391 + // load all conditional text records
392 + $cond_texts = $this->getSpecialTags();
393 +
394 + // iterate through all tokens found
395 + foreach ($matches[0] as $token) {
396 + if (!isset($cond_texts[$token])) {
397 + if (static::$debugRules) {
398 + // debuggining at this step cannot be enabled as the record was not found
399 + $null_replace = "{$token} was not found";
400 + }
401 + // remove token from template file
402 + $tmpl = str_replace($token, $null_replace, $tmpl);
403 + // decrease total tokens applied
404 + $tot_tokens--;
405 + // iterate to the next token, if any
406 + continue;
407 + }
408 +
409 + // set debug mode according to record
410 + static::$debugRules = (bool)$cond_texts[$token]['debug'];
411 +
412 + // set flag to know whether the token was compliant
413 + $compliant = false;
414 +
415 + // parse all rules for this conditional text
416 + foreach ($cond_texts[$token]['rules'] as $rule_data) {
417 + if (empty($rule_data->id)) {
418 + continue;
419 + }
420 + $rule = $this->getRule($rule_data->id);
421 + if ($rule === false) {
422 + continue;
423 + }
424 + // inject params and booking to rule, then check if compliant
425 + $compliant = $rule->setParams($rule_data->params)->setProperties($prop_keys, $prop_vals)->isCompliant();
426 + if (!$compliant) {
427 + if (static::$debugRules) {
428 + $null_replace = JText::sprintf('VBO_DEBUG_RULE_CONDTEXT', $rule->getName(), $token);
429 + }
430 + // all rules must be compliant with the booking
431 + break;
432 + }
433 + }
434 +
435 + if (!$compliant) {
436 + // remove token from template file
437 + $tmpl = str_replace($token, $null_replace, $tmpl);
438 + // decrease total tokens applied
439 + $tot_tokens--;
440 + // iterate to the next token, if any
441 + continue;
442 + }
443 +
444 + // all rules were compliant, trigger callback and manipulation actions
445 + foreach ($cond_texts[$token]['rules'] as $rule_data) {
446 + if (empty($rule_data->id)) {
447 + continue;
448 + }
449 + $rule = $this->getRule($rule_data->id);
450 + if ($rule === false) {
451 + continue;
452 + }
453 + // inject params and booking to rule
454 + $rule->setParams($rule_data->params)->setProperties($prop_keys, $prop_vals);
455 + // trigger callback action
456 + $rule->callbackAction();
457 + // allow rule to manipulate the actual message
458 + $cond_texts[$token]['msg'] = $rule->manipulateMessage($cond_texts[$token]['msg']);
459 + }
460 +
461 + /**
462 + * The message of the conditional text rules is usually written through a WYSIWYG editor
463 + * which may contain HTML tags. However, the context where these texts are being used is
464 + * unknown to VBO, and so we can detect from the content whether plain text messages are
465 + * necessary, maybe for sending an SMS message. We allow the use of special strings to
466 + * detect if no HTML should ever be included in the message, like [sms] or [plain text].
467 + *
468 + * @since 1.14.2 (J) - 1.4.3 (WP)
469 + */
470 + $requires_plain_text = false;
471 + if (preg_match_all("/(\[sms\]|\[plain_? ?text\])+/i", $cond_texts[$token]['msg'], $plt_matches)) {
472 + $requires_plain_text = true;
473 + foreach ($plt_matches[1] as $plt_match) {
474 + $cond_texts[$token]['msg'] = str_replace($plt_match, '', $cond_texts[$token]['msg']);
475 + }
476 + $cond_texts[$token]['msg'] = strip_tags($cond_texts[$token]['msg']);
477 + }
478 +
479 + /**
480 + * @wponly we need to let WordPress parse the paragraphs in the message.
481 + */
482 + if (VBOPlatformDetection::isWordPress() && !empty($cond_texts[$token]['msg']) && !$requires_plain_text) {
483 + $cond_texts[$token]['msg'] = wpautop($cond_texts[$token]['msg']);
484 + }
485 +
486 + /**
487 + * Make sure any src/href attribute does not contain relative URLs.
488 + *
489 + * @since 1.15 (J) - 1.5.0 (WP)
490 + * @since 1.18.8 (J) - 1.8.8 (WP) ignore attributes containing a special tag.
491 + */
492 + $cond_texts[$token]['msg'] = preg_replace_callback("/\s*(src|href)=([\"'])(.*?)[\"']/i", function($match) {
493 + // check if the URL starts with the base domain
494 + if (stripos($match[3], JUri::root()) !== 0 && !preg_match("/^(https?:\/\/|www\.|{)/i", $match[3])) {
495 + // safely prepend base domain to URL
496 + $match[0] = ' ' . $match[1] . '=' . $match[2] . JUri::root() . $match[3] . $match[2];
497 + }
498 + return $match[0];
499 + }, $cond_texts[$token]['msg']);
500 +
501 + // finally, apply the message to the template
502 + $tmpl = str_replace($token, $cond_texts[$token]['msg'], $tmpl);
503 + }
504 +
505 + return $tot_tokens;
506 + }
507 +
508 + /**
509 + * Toggles the rules debugging mode.
510 + *
511 + * @return self
512 + */
513 + public function toggleDebugging()
514 + {
515 + static::$debugRules = !static::$debugRules;
516 +
517 + return $this;
518 + }
519 +
520 + /**
521 + * Returns the list of the template files paths supporting the conditional text tags.
522 + *
523 + * @return array
524 + */
525 + public static function getTemplateFilesPaths()
526 + {
527 + return array(
528 + 'email_tmpl.php' => VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'email_tmpl.php',
529 + 'invoice_tmpl.php' => VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'invoices' . DIRECTORY_SEPARATOR . 'invoice_tmpl.php',
530 + 'checkin_tmpl.php' => VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'checkins' . DIRECTORY_SEPARATOR . 'checkin_tmpl.php',
531 + );
532 + }
533 +
534 + /**
535 + * Returns the list of the template files names supporting the conditional text tags.
536 + *
537 + * @return array
538 + */
539 + public static function getTemplateFilesNames()
540 + {
541 + return array(
542 + 'email_tmpl.php' => JText::translate('VBOCONFIGEMAILTEMPLATE'),
543 + 'invoice_tmpl.php' => JText::translate('VBOCONFIGINVOICETEMPLATE'),
544 + 'checkin_tmpl.php' => JText::translate('VBOCONFIGCHECKINTEMPLATE'),
545 + );
546 + }
547 +
548 + /**
549 + * Returns the list of the template files contents supporting the conditional text tags.
550 + *
551 + * @param string $file the basename of the template file to read.
552 + *
553 + * @return array
554 + */
555 + public static function getTemplateFilesContents($file = null)
556 + {
557 + $templates = self::getTemplateFilesPaths();
558 +
559 + if (!empty($file) && isset($templates[$file])) {
560 + $templates = array(
561 + $file => $templates[$file]
562 + );
563 + }
564 +
565 + $contents = array();
566 + foreach ($templates as $file => $path) {
567 + if (!is_file($path)) {
568 + continue;
569 + }
570 + switch ($file) {
571 + case 'email_tmpl.php':
572 + $contents[$file] = VikBooking::loadEmailTemplate();
573 + break;
574 + case 'invoice_tmpl.php':
575 + $data = VikBooking::loadInvoiceTmpl();
576 + $contents[$file] = $data[0];
577 + break;
578 + case 'checkin_tmpl.php':
579 + $data = VikBooking::loadCheckinDocTmpl();
580 + $contents[$file] = $data[0];
581 + break;
582 + default:
583 + break;
584 + }
585 + }
586 +
587 + return $contents;
588 + }
589 +
590 + /**
591 + * Returns the list of the template files code supporting the conditional text tags.
592 + *
593 + * @param string $file the basename of the template file to read.
594 + *
595 + * @return mixed array of files code, or just the code of the requested file.
596 + */
597 + public static function getTemplateFileCode($file = null)
598 + {
599 + $templates = self::getTemplateFilesPaths();
600 +
601 + if (!empty($file) && isset($templates[$file])) {
602 + $templates = array(
603 + $file => $templates[$file]
604 + );
605 + }
606 +
607 + $contents = array();
608 + foreach ($templates as $f => $path) {
609 + if (!is_file($path)) {
610 + continue;
611 + }
612 + switch ($f) {
613 + case 'email_tmpl.php':
614 + case 'invoice_tmpl.php':
615 + case 'checkin_tmpl.php':
616 + $fp = fopen($path, 'r');
617 + if (!$fp) {
618 + break;
619 + }
620 + $fcode = '';
621 + while (!feof($fp)) {
622 + $fcode .= fread($fp, 8192);
623 + }
624 + fclose($fp);
625 + if (empty($fcode)) {
626 + break;
627 + }
628 + $contents[$f] = $fcode;
629 + break;
630 + default:
631 + break;
632 + }
633 + }
634 +
635 + return !empty($file) && isset($contents[$file]) ? $contents[$file] : $contents;
636 + }
637 +
638 + /**
639 + * Updates the source code of the given template file name.
640 + *
641 + * @param string $file the basename of the template file to write.
642 + * @param string $code the new code of the template file to write.
643 + *
644 + * @return bool true on success, false otherwise.
645 + */
646 + public static function writeTemplateFileCode($file, $code)
647 + {
648 + $templates = self::getTemplateFilesPaths();
649 +
650 + if (!isset($templates[$file]) || empty($code)) {
651 + return false;
652 + }
653 +
654 + $fp = fopen($templates[$file], 'w+');
655 + if (!$fp) {
656 + return false;
657 + }
658 + $bytes = fwrite($fp, $code);
659 + fclose($fp);
660 +
661 + return ($bytes !== false);
662 + }
663 +
664 + /**
665 + * Tells whether the given special tag is used in the passed content.
666 + *
667 + * @param string $tag the special tag to look for.
668 + * @param string $content the content of the template file.
669 + *
670 + * @return bool
671 + */
672 + public static function isTagInContent($tag, $content)
673 + {
674 + if (empty($tag) || empty($content)) {
675 + return false;
676 + }
677 +
678 + if (strpos($tag, '{condition:') === false) {
679 + // invalid conditional text special tag
680 + return false;
681 + }
682 +
683 + return (strpos($content, $tag) !== false);
684 + }
685 +
686 + /**
687 + * Makes sure the path obtained to query the raw source code will produce
688 + * results in the raw php code. Some Libxml constants may skip html or
689 + * style tags, while the html source code may contain more tbody tags
690 + * than the php source code as it is parsed by the browser, where table
691 + * tags without a nested tbody tag will add it automatically.
692 + *
693 + * @param string $tag_path the node-path to the tag obtained
694 + * from the html source code.
695 + * @param DOMXpath $php_path DOMXpath object for the php code.
696 + *
697 + * @return string a valid query path to be used.
698 + *
699 + * @see addTagByComparingSources() and addStylesByComparingSources()
700 + */
701 + public static function adjustDOMXpathQuery($tag_path, $php_xpath)
702 + {
703 + // Xpath query expressions require two leading slashes
704 + if (substr($tag_path, 0, 2) !== '//' && substr($tag_path, 0, 1) == '/') {
705 + $tag_path = '/' . $tag_path;
706 + }
707 +
708 + // take care of any count mismatch of tbody tags
709 + $tbody_in_html = substr_count($tag_path, 'tbody');
710 + $tbody_in_php = $php_xpath->evaluate("count(//tbody)");
711 + if ($tbody_in_html > 0 && (int)$tbody_in_php < $tbody_in_html) {
712 + // raw php code has got less tbody nodes than html code
713 + $tbody_in_php = (int)$tbody_in_php;
714 + $parts = explode('/tbody', $tag_path);
715 + $new_tag_path = '';
716 + foreach ($parts as $k => $path_part) {
717 + // use only the amount of tbody found in php code
718 + $new_tag_path .= $path_part . ($k < $tbody_in_php ? '/tbody' : '');
719 + }
720 + // set new path to tag
721 + $tag_path = $new_tag_path;
722 + }
723 +
724 + // foresee the result of the Xpath query
725 + $testcase = $php_xpath->query($tag_path);
726 + if (!$testcase || !$testcase->length) {
727 + // this Xpath query is about to fail, try to do something
728 +
729 + if (strpos($tag_path, '/style') !== false) {
730 + // style tags found in the HTML source code may not be available in the PHP source code
731 + $tag_path = str_replace('/style', '', $tag_path);
732 + }
733 +
734 + /**
735 + * BC with old invoice template file structure where no table is ever inside another.
736 + *
737 + * @since any version prior to 1.14 (J) - 1.4.0 (WP)
738 + */
739 + if (strpos($tag_path, '//table/table[2]') !== false) {
740 + $tag_path = str_replace('//table/table[2]', '//table[3]', $tag_path);
741 + } elseif (strpos($tag_path, '//table/table[1]') !== false) {
742 + $tag_path = str_replace('//table/table[1]', '//table[2]', $tag_path);
743 + }
744 + }
745 +
746 +
747 + return $tag_path;
748 + }
749 +
750 + /**
751 + * Earlier versions of PHP and Libxml may not support to load code strings
752 + * without adding the DOCTYPE, the html+body tags, and any missing/malformed tag.
753 + * This will break the entire PHP source code of the file by getting HTML entities
754 + * like ?&gt; for the PHP closing tag, or =&gt; for the array key-val operator.
755 + *
756 + * @param string $php_code the raw source code generated by DOMDocument.
757 + * @param object $php_dom the DOMDocument object of the php code.
758 + *
759 + * @return string the clean PHP source code to write onto the file.
760 + */
761 + public static function cleanPHPSourceCode($php_code, $php_dom)
762 + {
763 + /**
764 + * The following constants will produce a different nodePath, and they are available
765 + * starting from PHP 5.4 and Libxml >= 2.7.8.
766 + */
767 + $libxml_updated = defined('LIBXML_HTML_NOIMPLIED') && defined('LIBXML_HTML_NODEFDTD');
768 + //
769 +
770 + // immediately restore the PHP tags that could have been converted to HTML entities
771 + $php_code = str_replace(array('&lt;?php', '?&gt;'), array('<?php', '?>'), $php_code);
772 +
773 + // take care of "unsafe" HTML detected for PHP opening and closing tags that may now be HTML comments
774 + $php_code = str_replace(array('<!--?php', '?-->'), array('<?php', '?>'), $php_code);
775 +
776 + // remove doctype
777 + $php_code = preg_replace("/(^<!DOCTYPE.*\R)/i", '', $php_code);
778 +
779 + /**
780 + * Grab anything between PHP tags to make sure there are no syntax errors due to HTML entities.
781 + *
782 + * @see In the callback we should NEVER user strip_tags as PHP can contain HTML.
783 + * We can at most look for some HTML tags mixed to PHP code to remove only them.
784 + */
785 + $php_code = preg_replace_callback("/<\?php(.*?)\?>/si", function($match) {
786 + // use just what's inside the PHP tags
787 + $pure_code = $match[1];
788 + /**
789 + * PHP comments in the check-in document template file may get for array declarations
790 + * one opening "<p>" tag next to the HTML entity for "=>". Therefore, we remove it.
791 + */
792 + if (strpos($pure_code, '=&gt;') !== false && strpos($pure_code, 'array') !== false && preg_match_all("/(<[a-zA-Z]+>)/", $pure_code, $extra_tags)) {
793 + foreach ($extra_tags[0] as $extra_tag) {
794 + // strip the tag as well as its closing version
795 + $pure_code = str_replace(array($extra_tag, str_replace('<', '</', $extra_tag)), '', $pure_code);
796 + }
797 + }
798 + // return the decoded HTML entities needed by PHP
799 + return '<?php' . html_entity_decode($pure_code) . '?>';
800 + }, $php_code);
801 +
802 + // get rid of html and body tags
803 + $php_code = str_replace(array('<html>', '<body>', '</html>', '</body>'), '', $php_code);
804 +
805 + // check if we have an HTML closing tag after the PHP closing tag due to previous manipulation of PHP code
806 + $php_code = preg_replace_callback("/\?>\R+(<\/?[a-zA-Z]+.*?>)/s", function($match) {
807 + if ($match[1] && strpos($match[1], '/') !== false) {
808 + return str_replace($match[1], '', $match[0]);
809 + }
810 + return $match[0];
811 + }, $php_code);
812 +
813 + /**
814 + * If libxml is not updated, we load the HTML by enclosing the whole source within a placeholder DIV tag.
815 + * This is to avoid getting the HTML and BODY tags started inside PHP code maybe, because it has a > or <.
816 + */
817 + if (!$libxml_updated) {
818 + // get rid of the wrapper div tag, added as a placeholder to avoid getting html and body inside php code
819 + $wrapper = $php_dom->getElementsByTagName('div')->item(0);
820 + if ($wrapper) {
821 + // remove all children and store the element
822 + $wrapper = $wrapper->parentNode->removeChild($wrapper);
823 + while ($php_dom->firstChild) {
824 + $php_dom->removeChild($php_dom->firstChild);
825 + }
826 + // append children again
827 + while ($wrapper->firstChild ) {
828 + $php_dom->appendChild($wrapper->firstChild);
829 + }
830 + // get new HTML without the wrapper
831 + $php_code = $php_dom->saveHTML();
832 + }
833 + }
834 + //
835 +
836 + return $php_code;
837 + }
838 +
839 + /**
840 + * Writes the source code of the template file onto a backup file.
841 + *
842 + * @param string $file the basename of the template file.
843 + * @param string $php_code the raw source code of the file.
844 + *
845 + * @return bool True on success, false otherwise.
846 + */
847 + public static function backupTemplateFileCode($file, $php_code)
848 + {
849 + $fp = fopen(dirname(__FILE__) . DIRECTORY_SEPARATOR . $file . '.bkp', 'w+');
850 + if (!$fp) {
851 + return false;
852 + }
853 +
854 + $bytes = fwrite($fp, $php_code);
855 + fclose($fp);
856 +
857 + return ($bytes !== false);
858 + }
859 +
860 + /**
861 + * Restores the source code of the template file from the backup file.
862 + *
863 + * @param string $file the basename of the template file.
864 + *
865 + * @return bool True on success, false otherwise.
866 + */
867 + public static function restoreTemplateFileCode($file)
868 + {
869 + $backup_fpath = dirname(__FILE__) . DIRECTORY_SEPARATOR . $file . '.bkp';
870 + if (!is_file($backup_fpath)) {
871 + return false;
872 + }
873 +
874 + $fp = fopen($backup_fpath, 'r');
875 + if (!$fp) {
876 + return false;
877 + }
878 +
879 + $fcode = '';
880 + while (!feof($fp)) {
881 + $fcode .= fread($fp, 8192);
882 + }
883 + fclose($fp);
884 +
885 + if (empty($fcode)) {
886 + return false;
887 + }
888 +
889 + return self::writeTemplateFileCode($file, $fcode);
890 + }
891 +
892 + /**
893 + * Compares the new HTML source code of the compiled template file
894 + * to the raw source code of the template file. Finds the newly added
895 + * tag in the HTML source code and adds it to the same position of the
896 + * raw source code of the same template file. Used to add a new tag to the code.
897 + *
898 + * @param string $tag the conditional text tag to add.
899 + * @param string $file the basename of the template file.
900 + * @param string $html_code the full HTML source code where the new tag is.
901 + * @param string $php_code the raw source code where the new tag should be added.
902 + *
903 + * @return string the new PHP source code to write onto the file.
904 + */
905 + public static function addTagByComparingSources($tag, $file, $html_code, $php_code)
906 + {
907 + if (!class_exists('DOMDocument') || !class_exists('DOMXpath')) {
908 + // this sucks, we just append the tag to the end of the file
909 + $php_code .= "\n{$tag}\n";
910 +
911 + // log the case
912 + self::setEditingLog("Classes DOMDocument or DOMXpath are not available (" . __LINE__ . ")");
913 +
914 + return $php_code;
915 + }
916 +
917 + // backup the file source code no matter what
918 + self::backupTemplateFileCode($file, $php_code);
919 +
920 + /**
921 + * The following constants will produce a different nodePath, and they are available
922 + * starting from PHP 5.4 and Libxml >= 2.7.8.
923 + */
924 + $libxml_updated = defined('LIBXML_HTML_NOIMPLIED') && defined('LIBXML_HTML_NODEFDTD');
925 + //
926 +
927 + // log data
928 + self::setEditingLog("Libxml support: " . (int)$libxml_updated);
929 +
930 + /**
931 + * Suppress warnings for bad markup by using libxml's error handling functions.
932 + * Errors could be retrieved by using print_r(libxml_get_errors(), true).
933 + */
934 + libxml_use_internal_errors(true);
935 + //
936 +
937 + // load HTML source code
938 + $html_dom = new DOMDocument();
939 + if ($libxml_updated) {
940 + $html_dom->loadHTML($html_code, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
941 + } else {
942 + $html_dom->loadHTML('<div>' . $html_code . '</div>');
943 + }
944 +
945 + // get DOMXPath instance of the html DOM Document
946 + $html_xpath = new DOMXpath($html_dom);
947 + // find DOMNodeList from given tag (there should be just one tag)
948 + $found_nodelist = $html_xpath->query("//*[text()[contains(., '{$tag}')]]");
949 +
950 + if (!$found_nodelist || !$found_nodelist->length) {
951 + // log the case
952 + self::setEditingLog("tag not found in html source code (" . __LINE__ . ")");
953 +
954 + // tag not found in html source code
955 + return $php_code;
956 + }
957 +
958 + // find the path to the first node occurrence of the given tag string
959 + $tag_path = $found_nodelist->item(0)->getNodePath();
960 + if (empty($tag_path)) {
961 + // log the case
962 + self::setEditingLog("unable to proceed without knowing the path to the tag (" . __LINE__ . ")");
963 +
964 + // unable to proceed without knowing the path to the tag
965 + return $php_code;
966 + }
967 +
968 + // log data
969 + self::setEditingLog("Node Path to tag in HTML source code: " . $tag_path);
970 +
971 + // import the raw php code to DOMDocument
972 + $php_dom = new DOMDocument();
973 + if ($libxml_updated) {
974 + $php_dom->loadHTML($php_code, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
975 + } else {
976 + $php_dom->loadHTML('<div>' . $php_code . '</div>');
977 + }
978 +
979 + // get DOMXPath instance of the php DOM Document
980 + $php_xpath = new DOMXpath($php_dom);
981 +
982 + // adjust html path to tag to comply with the expression for the raw code
983 + $tag_path = self::adjustDOMXpathQuery($tag_path, $php_xpath);
984 +
985 + // log data
986 + self::setEditingLog("Adjusted Node Path: " . $tag_path);
987 +
988 + // query the raw source code to find the same path as a DOMNodeList
989 + $found_nodelist = $php_xpath->query($tag_path);
990 +
991 + if (!$found_nodelist || !$found_nodelist->length) {
992 + // log the case
993 + self::setEditingLog("Node Path to tag not found in php source code: {$tag_path} must be invalid (" . __LINE__ . ")");
994 +
995 + // path not found in php source code: $tag_path must be invalid
996 + return $php_code;
997 + }
998 +
999 + // create a text node with the special tag string
1000 + $tag_element = $php_dom->createTextNode($tag);
1001 + // append the tag string to the first (and only) path found
1002 + $found_nodelist->item(0)->appendChild($tag_element);
1003 +
1004 + // obtain the new php source code
1005 + $php_code = $php_dom->saveHTML();
1006 +
1007 + // log data
1008 + self::setEditingLog("Tag appended to the given path. New template source code before cleaning:\n\n" . $php_code);
1009 +
1010 + // always clean up the PHP code to avoid breaking the file
1011 + $php_code = self::cleanPHPSourceCode($php_code, $php_dom);
1012 +
1013 + /**
1014 + * @see the following code can help debugging the source code and entire flow.
1015 + *
1016 + * $fp = fopen(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'debug.txt', 'w+');
1017 + * fwrite($fp, implode("\n", self::getEditingLog()) . "\n\n\nclean source is:\n\n-------------\n" . $php_code);
1018 + * fclose($fp);
1019 + */
1020 +
1021 + return $php_code;
1022 + }
1023 +
1024 + /**
1025 + * Compares the new HTML source code of the compiled template file
1026 + * to the raw source code of the template file. Finds the newly added
1027 + * class attributes in the HTML source code, gets it styling and adds it
1028 + * to the raw source code of the same template file. Used by the CSS inspector.
1029 + *
1030 + * @param array $classes list of custom/temporary CSS classes to look for.
1031 + * @param string $file the basename of the template file.
1032 + * @param string $html_code the full HTML source code where the new classes are.
1033 + * @param string $php_code the raw source code where the new styles should be added.
1034 + *
1035 + * @return string the new PHP source code to write onto the file.
1036 + *
1037 + * @throws Exception if DOMDocument is not supported as nothing could be done.
1038 + */
1039 + public static function addStylesByComparingSources($classes, $file, $html_code, $php_code)
1040 + {
1041 + if (!class_exists('DOMDocument') || !class_exists('DOMXpath')) {
1042 + // we cannot proceed without these classes
1043 + throw new Exception("DOMDocument or DOMXpath are missing in your PHP installation", 403);
1044 + }
1045 +
1046 + // backup the file source code no matter what
1047 + self::backupTemplateFileCode($file, $php_code);
1048 +
1049 + /**
1050 + * The following constants will produce a different nodePath, and they are available
1051 + * starting from PHP 5.4 and Libxml >= 2.7.8.
1052 + */
1053 + $libxml_updated = defined('LIBXML_HTML_NOIMPLIED') && defined('LIBXML_HTML_NODEFDTD');
1054 + //
1055 +
1056 + // log data
1057 + self::setEditingLog("Libxml support: " . (int)$libxml_updated);
1058 +
1059 + /**
1060 + * Suppress warnings for bad markup by using libxml's error handling functions.
1061 + * Errors could be retrieved by using print_r(libxml_get_errors(), true).
1062 + */
1063 + libxml_use_internal_errors(true);
1064 + //
1065 +
1066 + // load HTML source code
1067 + $html_dom = new DOMDocument();
1068 + if ($libxml_updated) {
1069 + $html_dom->loadHTML($html_code, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
1070 + } else {
1071 + $html_dom->loadHTML('<div>' . $html_code . '</div>');
1072 + }
1073 +
1074 + // get DOMXPath instance of the html DOM Document
1075 + $html_xpath = new DOMXpath($html_dom);
1076 +
1077 + // compose pool of styles
1078 + $styles_pool = array();
1079 +
1080 + foreach ($classes as $css_class) {
1081 + // find DOMNodeList from given CSS class (there should be just one node)
1082 + $found_nodelist = $html_xpath->query("//*[contains(@class, '" . $css_class . "')]");
1083 + if (!$found_nodelist || !$found_nodelist->length) {
1084 + // log the case
1085 + self::setEditingLog("given CSS class {$css_class} not found in html source code (" . __LINE__ . ")");
1086 +
1087 + // given CSS class not found in html source code
1088 + continue;
1089 + }
1090 +
1091 + // log data
1092 + self::setEditingLog("CSS class {$css_class} found in HTML source code");
1093 +
1094 + // get the first node
1095 + $node = $found_nodelist->item(0);
1096 + // make sure the node has a style attribute
1097 + if (!$node->hasAttribute('style')) {
1098 + // log the case
1099 + self::setEditingLog("style attribute not found in available tag with CSS class {$css_class} (" . __LINE__ . ")");
1100 +
1101 + // style attribute not found
1102 + continue;
1103 + }
1104 +
1105 + // make sure the style attribute is not empty
1106 + $style_attr = $node->getAttribute('style');
1107 + if (!$style_attr || empty($style_attr)) {
1108 + // log the case
1109 + self::setEditingLog("style attribute is empty in available tag with CSS class {$css_class} (" . __LINE__ . ")");
1110 +
1111 + // style attribute is empty
1112 + continue;
1113 + }
1114 +
1115 + // compose style information
1116 + $style = new stdClass;
1117 + $style->node_path = $node->getNodePath();
1118 + $style->attribute = $style_attr;
1119 +
1120 + // push style object to the pool
1121 + array_push($styles_pool, $style);
1122 + }
1123 +
1124 + if (!count($styles_pool)) {
1125 + // no style attributes found to add, unable to proceed
1126 + return $php_code;
1127 + }
1128 +
1129 + // import the raw php code to DOMDocument
1130 + $php_dom = new DOMDocument();
1131 + if ($libxml_updated) {
1132 + $php_dom->loadHTML($php_code, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
1133 + } else {
1134 + $php_dom->loadHTML('<div>' . $php_code . '</div>');
1135 + }
1136 +
1137 + // get DOMXPath instance of the php DOM Document
1138 + $php_xpath = new DOMXpath($php_dom);
1139 +
1140 + // iterate all styles to add to the various nodes
1141 + foreach ($styles_pool as $style) {
1142 + // log data
1143 + self::setEditingLog("Node Path to tag to be styled: " . $style->node_path);
1144 +
1145 + // adjust html path to node to comply with the expression for the raw code
1146 + $node_path = self::adjustDOMXpathQuery($style->node_path, $php_xpath);
1147 +
1148 + // log data
1149 + self::setEditingLog("Adjusted node path is: " . $node_path);
1150 +
1151 + // query the raw source code to find the same path as a DOMNodeList
1152 + $found_nodelist = $php_xpath->query($node_path);
1153 +
1154 + if (!$found_nodelist || !$found_nodelist->length) {
1155 + // log the case
1156 + self::setEditingLog("Node Path not found in php source code for styling: {$node_path} must be invalid (" . __LINE__ . ")");
1157 +
1158 + // path not found in php source code: $node_path must be invalid
1159 + continue;
1160 + }
1161 +
1162 + // get the first node
1163 + $node = $found_nodelist->item(0);
1164 +
1165 + // set the style attribute
1166 + $node->setAttribute('style', $style->attribute);
1167 +
1168 + // obtain the new php source code
1169 + $php_code = $php_dom->saveHTML();
1170 + }
1171 +
1172 + // log data
1173 + self::setEditingLog("Style(s) added to the source code. New template source code before cleaning:\n\n" . $php_code);
1174 +
1175 + // always clean up the PHP code to avoid breaking the file
1176 + $php_code = self::cleanPHPSourceCode($php_code, $php_dom);
1177 +
1178 + return $php_code;
1179 + }
1180 +
1181 + /**
1182 + * Appends an execution log to the execution log array.
1183 + *
1184 + * @param string $log the execution string to append.
1185 + *
1186 + * @return void
1187 + */
1188 + public static function setEditingLog($log)
1189 + {
1190 + if (static::$editingLog === null) {
1191 + static::$editingLog = array();
1192 + }
1193 +
1194 + array_push(static::$editingLog, $log);
1195 + }
1196 +
1197 + /**
1198 + * Gets the execution log array for all editing operations.
1199 + *
1200 + * @return mixed the current editing log array or null.
1201 + */
1202 + public static function getEditingLog()
1203 + {
1204 + return static::$editingLog;
1205 + }
1206 +}