PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.8.6
VikBooking Hotel Booking Engine & PMS v1.8.6
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 1.7.3 All 36 releases
vikbooking / admin / helpers / conditional_rules.php

conditional_rules.php in VikBooking Hotel Booking Engine & PMS 1.8.6, at admin/helpers/conditional_rules.php

1,207 lines 35.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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.4.0
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 $this->dbo->execute();
288 if ($this->dbo->getNumRows()) {
289 $records = $this->dbo->loadAssocList();
290 $this->vbo_tn->translateContents($records, '#__vikbooking_condtexts');
291 foreach ($records as $record) {
292 // decode rules
293 $record['rules'] = !empty($record['rules']) ? json_decode($record['rules']) : array();
294 $record['rules'] = !is_array($record['rules']) ? array() : $record['rules'];
295 // push record
296 $special_tags[$record['token']] = $record;
297 }
298 }
299
300 return $special_tags;
301 }
302
303 /**
304 * Helper method to load one precise conditional text from the given special tag.
305 *
306 * @param string $token the special tag (token) to look for.
307 *
308 * @return array the record of the conditional text found or an empty array.
309 */
310 public function getBySpecialTag($token)
311 {
312 $cond_text = array();
313
314 $q = "SELECT * FROM `#__vikbooking_condtexts` WHERE `token`=" . $this->dbo->quote($token) . ";";
315 $this->dbo->setQuery($q);
316 $this->dbo->execute();
317 if ($this->dbo->getNumRows()) {
318 $cond_text = $this->dbo->loadAssoc();
319 $this->vbo_tn->translateContents($cond_text, '#__vikbooking_condtexts');
320 }
321
322 return $cond_text;
323 }
324
325 /**
326 * Helper method to store information in the helper array.
327 *
328 * @param mixed $key the key or array of keys to set.
329 * @param mixed $val the value or array of values to set.
330 *
331 * @return self
332 */
333 public function set($key, $val = null)
334 {
335 if (!is_string($key) && !is_array($key)) {
336 return $this;
337 }
338
339 if (is_string($key)) {
340 $key = array($key);
341 }
342 if (!is_array($val)) {
343 $val = array($val);
344 }
345
346 foreach ($key as $i => $prop) {
347 if (!isset($val[$i])) {
348 continue;
349 }
350 static::$helper[$prop] = $val[$i];
351 }
352
353 return $this;
354 }
355
356 /**
357 * Helper method to get information from the helper array.
358 *
359 * @param string $key the key to get.
360 * @param string $def the default value to get.
361 *
362 * @return mixed the requested key value.
363 */
364 public function get($key, $def = null)
365 {
366 return isset(static::$helper[$key]) ? static::$helper[$key] : $def;
367 }
368
369 /**
370 * Parses all tokens in the given template string and applies the
371 * conditional texts found if all rules are compliant.
372 *
373 * @param string $tmpl the template string to parse, passed by reference.
374 *
375 * @return mixed false if no tokens found, integer for how many tokens were applied.
376 */
377 public function parseTokens(&$tmpl)
378 {
379 preg_match_all('/\{condition: ?([a-zA-Z0-9_]+)\}/U', $tmpl, $matches);
380
381 $tot_tokens = count($matches[0]);
382
383 if (!$tot_tokens) {
384 // no tokens to parse
385 return false;
386 }
387
388 // default empty replacement
389 $null_replace = '';
390
391 // load all helper property keys and values
392 $prop_keys = array_keys(static::$helper);
393 $prop_vals = array_values(static::$helper);
394
395 // load all conditional text records
396 $cond_texts = $this->getSpecialTags();
397
398 // iterate through all tokens found
399 foreach ($matches[0] as $token) {
400 if (!isset($cond_texts[$token])) {
401 if (static::$debugRules) {
402 // debuggining at this step cannot be enabled as the record was not found
403 $null_replace = "{$token} was not found";
404 }
405 // remove token from template file
406 $tmpl = str_replace($token, $null_replace, $tmpl);
407 // decrease total tokens applied
408 $tot_tokens--;
409 // iterate to the next token, if any
410 continue;
411 }
412
413 // set debug mode according to record
414 static::$debugRules = (bool)$cond_texts[$token]['debug'];
415
416 // set flag to know whether the token was compliant
417 $compliant = false;
418
419 // parse all rules for this conditional text
420 foreach ($cond_texts[$token]['rules'] as $rule_data) {
421 if (empty($rule_data->id)) {
422 continue;
423 }
424 $rule = $this->getRule($rule_data->id);
425 if ($rule === false) {
426 continue;
427 }
428 // inject params and booking to rule, then check if compliant
429 $compliant = $rule->setParams($rule_data->params)->setProperties($prop_keys, $prop_vals)->isCompliant();
430 if (!$compliant) {
431 if (static::$debugRules) {
432 $null_replace = JText::sprintf('VBO_DEBUG_RULE_CONDTEXT', $rule->getName(), $token);
433 }
434 // all rules must be compliant with the booking
435 break;
436 }
437 }
438
439 if (!$compliant) {
440 // remove token from template file
441 $tmpl = str_replace($token, $null_replace, $tmpl);
442 // decrease total tokens applied
443 $tot_tokens--;
444 // iterate to the next token, if any
445 continue;
446 }
447
448 // all rules were compliant, trigger callback and manipulation actions
449 foreach ($cond_texts[$token]['rules'] as $rule_data) {
450 if (empty($rule_data->id)) {
451 continue;
452 }
453 $rule = $this->getRule($rule_data->id);
454 if ($rule === false) {
455 continue;
456 }
457 // inject params and booking to rule
458 $rule->setParams($rule_data->params)->setProperties($prop_keys, $prop_vals);
459 // trigger callback action
460 $rule->callbackAction();
461 // allow rule to manipulate the actual message
462 $cond_texts[$token]['msg'] = $rule->manipulateMessage($cond_texts[$token]['msg']);
463 }
464
465 /**
466 * The message of the conditional text rules is usually written through a WYSIWYG editor
467 * which may contain HTML tags. However, the context where these texts are being used is
468 * unknown to VBO, and so we can detect from the content whether plain text messages are
469 * necessary, maybe for sending an SMS message. We allow the use of special strings to
470 * detect if no HTML should ever be included in the message, like [sms] or [plain text].
471 *
472 * @since 1.4.3
473 */
474 $requires_plain_text = false;
475 if (preg_match_all("/(\[sms\]|\[plain_? ?text\])+/i", $cond_texts[$token]['msg'], $plt_matches)) {
476 $requires_plain_text = true;
477 foreach ($plt_matches[1] as $plt_match) {
478 $cond_texts[$token]['msg'] = str_replace($plt_match, '', $cond_texts[$token]['msg']);
479 }
480 $cond_texts[$token]['msg'] = strip_tags($cond_texts[$token]['msg']);
481 }
482
483 /**
484 * @wponly we need to let WordPress parse the paragraphs in the message.
485 */
486 if (VBOPlatformDetection::isWordPress() && !empty($cond_texts[$token]['msg']) && !$requires_plain_text) {
487 $cond_texts[$token]['msg'] = wpautop($cond_texts[$token]['msg']);
488 }
489
490 /**
491 * Make sure any src/href attribute does not contain relative URLs.
492 *
493 * @since 1.5.0
494 */
495 $cond_texts[$token]['msg'] = preg_replace_callback("/\s*(src|href)=([\"'])(.*?)[\"']/i", function($match) {
496 // check if the URL starts with the base domain
497 if (stripos($match[3], JUri::root()) !== 0 && !preg_match("/^(https?:\/\/|www\.)/i", $match[3])) {
498 // prepend base domain to URL
499 $match[0] = ' ' . $match[1] . '=' . $match[2] . JUri::root() . $match[3] . $match[2];
500 }
501 return $match[0];
502 }, $cond_texts[$token]['msg']);
503
504 // finally, apply the message to the template
505 $tmpl = str_replace($token, $cond_texts[$token]['msg'], $tmpl);
506 }
507
508 return $tot_tokens;
509 }
510
511 /**
512 * Toggles the rules debugging mode.
513 *
514 * @return self
515 */
516 public function toggleDebugging()
517 {
518 static::$debugRules = !static::$debugRules;
519
520 return $this;
521 }
522
523 /**
524 * Returns the list of the template files paths supporting the conditional text tags.
525 *
526 * @return array
527 */
528 public static function getTemplateFilesPaths()
529 {
530 return array(
531 'email_tmpl.php' => VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'email_tmpl.php',
532 'invoice_tmpl.php' => VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'invoices' . DIRECTORY_SEPARATOR . 'invoice_tmpl.php',
533 'checkin_tmpl.php' => VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'checkins' . DIRECTORY_SEPARATOR . 'checkin_tmpl.php',
534 );
535 }
536
537 /**
538 * Returns the list of the template files names supporting the conditional text tags.
539 *
540 * @return array
541 */
542 public static function getTemplateFilesNames()
543 {
544 return array(
545 'email_tmpl.php' => JText::translate('VBOCONFIGEMAILTEMPLATE'),
546 'invoice_tmpl.php' => JText::translate('VBOCONFIGINVOICETEMPLATE'),
547 'checkin_tmpl.php' => JText::translate('VBOCONFIGCHECKINTEMPLATE'),
548 );
549 }
550
551 /**
552 * Returns the list of the template files contents supporting the conditional text tags.
553 *
554 * @param string $file the basename of the template file to read.
555 *
556 * @return array
557 */
558 public static function getTemplateFilesContents($file = null)
559 {
560 $templates = self::getTemplateFilesPaths();
561
562 if (!empty($file) && isset($templates[$file])) {
563 $templates = array(
564 $file => $templates[$file]
565 );
566 }
567
568 $contents = array();
569 foreach ($templates as $file => $path) {
570 if (!is_file($path)) {
571 continue;
572 }
573 switch ($file) {
574 case 'email_tmpl.php':
575 $contents[$file] = VikBooking::loadEmailTemplate();
576 break;
577 case 'invoice_tmpl.php':
578 $data = VikBooking::loadInvoiceTmpl();
579 $contents[$file] = $data[0];
580 break;
581 case 'checkin_tmpl.php':
582 $data = VikBooking::loadCheckinDocTmpl();
583 $contents[$file] = $data[0];
584 break;
585 default:
586 break;
587 }
588 }
589
590 return $contents;
591 }
592
593 /**
594 * Returns the list of the template files code supporting the conditional text tags.
595 *
596 * @param string $file the basename of the template file to read.
597 *
598 * @return mixed array of files code, or just the code of the requested file.
599 */
600 public static function getTemplateFileCode($file = null)
601 {
602 $templates = self::getTemplateFilesPaths();
603
604 if (!empty($file) && isset($templates[$file])) {
605 $templates = array(
606 $file => $templates[$file]
607 );
608 }
609
610 $contents = array();
611 foreach ($templates as $f => $path) {
612 if (!is_file($path)) {
613 continue;
614 }
615 switch ($f) {
616 case 'email_tmpl.php':
617 case 'invoice_tmpl.php':
618 case 'checkin_tmpl.php':
619 $fp = fopen($path, 'r');
620 if (!$fp) {
621 break;
622 }
623 $fcode = '';
624 while (!feof($fp)) {
625 $fcode .= fread($fp, 8192);
626 }
627 fclose($fp);
628 if (empty($fcode)) {
629 break;
630 }
631 $contents[$f] = $fcode;
632 break;
633 default:
634 break;
635 }
636 }
637
638 return !empty($file) && isset($contents[$file]) ? $contents[$file] : $contents;
639 }
640
641 /**
642 * Updates the source code of the given template file name.
643 *
644 * @param string $file the basename of the template file to write.
645 * @param string $code the new code of the template file to write.
646 *
647 * @return bool true on success, false otherwise.
648 */
649 public static function writeTemplateFileCode($file, $code)
650 {
651 $templates = self::getTemplateFilesPaths();
652
653 if (!isset($templates[$file]) || empty($code)) {
654 return false;
655 }
656
657 $fp = fopen($templates[$file], 'w+');
658 if (!$fp) {
659 return false;
660 }
661 $bytes = fwrite($fp, $code);
662 fclose($fp);
663
664 return ($bytes !== false);
665 }
666
667 /**
668 * Tells whether the given special tag is used in the passed content.
669 *
670 * @param string $tag the special tag to look for.
671 * @param string $content the content of the template file.
672 *
673 * @return bool
674 */
675 public static function isTagInContent($tag, $content)
676 {
677 if (empty($tag) || empty($content)) {
678 return false;
679 }
680
681 if (strpos($tag, '{condition:') === false) {
682 // invalid conditional text special tag
683 return false;
684 }
685
686 return (strpos($content, $tag) !== false);
687 }
688
689 /**
690 * Makes sure the path obtained to query the raw source code will produce
691 * results in the raw php code. Some Libxml constants may skip html or
692 * style tags, while the html source code may contain more tbody tags
693 * than the php source code as it is parsed by the browser, where table
694 * tags without a nested tbody tag will add it automatically.
695 *
696 * @param string $tag_path the node-path to the tag obtained
697 * from the html source code.
698 * @param DOMXpath $php_path DOMXpath object for the php code.
699 *
700 * @return string a valid query path to be used.
701 *
702 * @see addTagByComparingSources() and addStylesByComparingSources()
703 */
704 public static function adjustDOMXpathQuery($tag_path, $php_xpath)
705 {
706 // Xpath query expressions require two leading slashes
707 if (substr($tag_path, 0, 2) !== '//' && substr($tag_path, 0, 1) == '/') {
708 $tag_path = '/' . $tag_path;
709 }
710
711 // take care of any count mismatch of tbody tags
712 $tbody_in_html = substr_count($tag_path, 'tbody');
713 $tbody_in_php = $php_xpath->evaluate("count(//tbody)");
714 if ($tbody_in_html > 0 && (int)$tbody_in_php < $tbody_in_html) {
715 // raw php code has got less tbody nodes than html code
716 $tbody_in_php = (int)$tbody_in_php;
717 $parts = explode('/tbody', $tag_path);
718 $new_tag_path = '';
719 foreach ($parts as $k => $path_part) {
720 // use only the amount of tbody found in php code
721 $new_tag_path .= $path_part . ($k < $tbody_in_php ? '/tbody' : '');
722 }
723 // set new path to tag
724 $tag_path = $new_tag_path;
725 }
726
727 // foresee the result of the Xpath query
728 $testcase = $php_xpath->query($tag_path);
729 if (!$testcase || !$testcase->length) {
730 // this Xpath query is about to fail, try to do something
731
732 if (strpos($tag_path, '/style') !== false) {
733 // style tags found in the HTML source code may not be available in the PHP source code
734 $tag_path = str_replace('/style', '', $tag_path);
735 }
736
737 /**
738 * BC with old invoice template file structure where no table is ever inside another.
739 *
740 * @since any version prior to 1.14 (J) - 1.4.0 (WP)
741 */
742 if (strpos($tag_path, '//table/table[2]') !== false) {
743 $tag_path = str_replace('//table/table[2]', '//table[3]', $tag_path);
744 } elseif (strpos($tag_path, '//table/table[1]') !== false) {
745 $tag_path = str_replace('//table/table[1]', '//table[2]', $tag_path);
746 }
747 }
748
749
750 return $tag_path;
751 }
752
753 /**
754 * Earlier versions of PHP and Libxml may not support to load code strings
755 * without adding the DOCTYPE, the html+body tags, and any missing/malformed tag.
756 * This will break the entire PHP source code of the file by getting HTML entities
757 * like ?&gt; for the PHP closing tag, or =&gt; for the array key-val operator.
758 *
759 * @param string $php_code the raw source code generated by DOMDocument.
760 * @param object $php_dom the DOMDocument object of the php code.
761 *
762 * @return string the clean PHP source code to write onto the file.
763 */
764 public static function cleanPHPSourceCode($php_code, $php_dom)
765 {
766 /**
767 * The following constants will produce a different nodePath, and they are available
768 * starting from PHP 5.4 and Libxml >= 2.7.8.
769 */
770 $libxml_updated = defined('LIBXML_HTML_NOIMPLIED') && defined('LIBXML_HTML_NODEFDTD');
771 //
772
773 // immediately restore the PHP tags that could have been converted to HTML entities
774 $php_code = str_replace(array('&lt;?php', '?&gt;'), 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 }
1207