PluginProbe
VikBooking Hotel Booking Engine & PMS / 1.8.7
VikBooking Hotel Booking Engine & PMS v1.8.7
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
vikbooking / admin / helpers / conditional_rules.php

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

1,206 lines 35.2 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.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 */
491 $cond_texts[$token]['msg'] = preg_replace_callback("/\s*(src|href)=([\"'])(.*?)[\"']/i", function($match) {
492 // check if the URL starts with the base domain
493 if (stripos($match[3], JUri::root()) !== 0 && !preg_match("/^(https?:\/\/|www\.)/i", $match[3])) {
494 // prepend base domain to URL
495 $match[0] = ' ' . $match[1] . '=' . $match[2] . JUri::root() . $match[3] . $match[2];
496 }
497 return $match[0];
498 }, $cond_texts[$token]['msg']);
499
500 // finally, apply the message to the template
501 $tmpl = str_replace($token, $cond_texts[$token]['msg'], $tmpl);
502 }
503
504 return $tot_tokens;
505 }
506
507 /**
508 * Toggles the rules debugging mode.
509 *
510 * @return self
511 */
512 public function toggleDebugging()
513 {
514 static::$debugRules = !static::$debugRules;
515
516 return $this;
517 }
518
519 /**
520 * Returns the list of the template files paths supporting the conditional text tags.
521 *
522 * @return array
523 */
524 public static function getTemplateFilesPaths()
525 {
526 return array(
527 'email_tmpl.php' => VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'email_tmpl.php',
528 'invoice_tmpl.php' => VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'invoices' . DIRECTORY_SEPARATOR . 'invoice_tmpl.php',
529 'checkin_tmpl.php' => VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'checkins' . DIRECTORY_SEPARATOR . 'checkin_tmpl.php',
530 );
531 }
532
533 /**
534 * Returns the list of the template files names supporting the conditional text tags.
535 *
536 * @return array
537 */
538 public static function getTemplateFilesNames()
539 {
540 return array(
541 'email_tmpl.php' => JText::translate('VBOCONFIGEMAILTEMPLATE'),
542 'invoice_tmpl.php' => JText::translate('VBOCONFIGINVOICETEMPLATE'),
543 'checkin_tmpl.php' => JText::translate('VBOCONFIGCHECKINTEMPLATE'),
544 );
545 }
546
547 /**
548 * Returns the list of the template files contents supporting the conditional text tags.
549 *
550 * @param string $file the basename of the template file to read.
551 *
552 * @return array
553 */
554 public static function getTemplateFilesContents($file = null)
555 {
556 $templates = self::getTemplateFilesPaths();
557
558 if (!empty($file) && isset($templates[$file])) {
559 $templates = array(
560 $file => $templates[$file]
561 );
562 }
563
564 $contents = array();
565 foreach ($templates as $file => $path) {
566 if (!is_file($path)) {
567 continue;
568 }
569 switch ($file) {
570 case 'email_tmpl.php':
571 $contents[$file] = VikBooking::loadEmailTemplate();
572 break;
573 case 'invoice_tmpl.php':
574 $data = VikBooking::loadInvoiceTmpl();
575 $contents[$file] = $data[0];
576 break;
577 case 'checkin_tmpl.php':
578 $data = VikBooking::loadCheckinDocTmpl();
579 $contents[$file] = $data[0];
580 break;
581 default:
582 break;
583 }
584 }
585
586 return $contents;
587 }
588
589 /**
590 * Returns the list of the template files code supporting the conditional text tags.
591 *
592 * @param string $file the basename of the template file to read.
593 *
594 * @return mixed array of files code, or just the code of the requested file.
595 */
596 public static function getTemplateFileCode($file = null)
597 {
598 $templates = self::getTemplateFilesPaths();
599
600 if (!empty($file) && isset($templates[$file])) {
601 $templates = array(
602 $file => $templates[$file]
603 );
604 }
605
606 $contents = array();
607 foreach ($templates as $f => $path) {
608 if (!is_file($path)) {
609 continue;
610 }
611 switch ($f) {
612 case 'email_tmpl.php':
613 case 'invoice_tmpl.php':
614 case 'checkin_tmpl.php':
615 $fp = fopen($path, 'r');
616 if (!$fp) {
617 break;
618 }
619 $fcode = '';
620 while (!feof($fp)) {
621 $fcode .= fread($fp, 8192);
622 }
623 fclose($fp);
624 if (empty($fcode)) {
625 break;
626 }
627 $contents[$f] = $fcode;
628 break;
629 default:
630 break;
631 }
632 }
633
634 return !empty($file) && isset($contents[$file]) ? $contents[$file] : $contents;
635 }
636
637 /**
638 * Updates the source code of the given template file name.
639 *
640 * @param string $file the basename of the template file to write.
641 * @param string $code the new code of the template file to write.
642 *
643 * @return bool true on success, false otherwise.
644 */
645 public static function writeTemplateFileCode($file, $code)
646 {
647 $templates = self::getTemplateFilesPaths();
648
649 if (!isset($templates[$file]) || empty($code)) {
650 return false;
651 }
652
653 $fp = fopen($templates[$file], 'w+');
654 if (!$fp) {
655 return false;
656 }
657 $bytes = fwrite($fp, $code);
658 fclose($fp);
659
660 return ($bytes !== false);
661 }
662
663 /**
664 * Tells whether the given special tag is used in the passed content.
665 *
666 * @param string $tag the special tag to look for.
667 * @param string $content the content of the template file.
668 *
669 * @return bool
670 */
671 public static function isTagInContent($tag, $content)
672 {
673 if (empty($tag) || empty($content)) {
674 return false;
675 }
676
677 if (strpos($tag, '{condition:') === false) {
678 // invalid conditional text special tag
679 return false;
680 }
681
682 return (strpos($content, $tag) !== false);
683 }
684
685 /**
686 * Makes sure the path obtained to query the raw source code will produce
687 * results in the raw php code. Some Libxml constants may skip html or
688 * style tags, while the html source code may contain more tbody tags
689 * than the php source code as it is parsed by the browser, where table
690 * tags without a nested tbody tag will add it automatically.
691 *
692 * @param string $tag_path the node-path to the tag obtained
693 * from the html source code.
694 * @param DOMXpath $php_path DOMXpath object for the php code.
695 *
696 * @return string a valid query path to be used.
697 *
698 * @see addTagByComparingSources() and addStylesByComparingSources()
699 */
700 public static function adjustDOMXpathQuery($tag_path, $php_xpath)
701 {
702 // Xpath query expressions require two leading slashes
703 if (substr($tag_path, 0, 2) !== '//' && substr($tag_path, 0, 1) == '/') {
704 $tag_path = '/' . $tag_path;
705 }
706
707 // take care of any count mismatch of tbody tags
708 $tbody_in_html = substr_count($tag_path, 'tbody');
709 $tbody_in_php = $php_xpath->evaluate("count(//tbody)");
710 if ($tbody_in_html > 0 && (int)$tbody_in_php < $tbody_in_html) {
711 // raw php code has got less tbody nodes than html code
712 $tbody_in_php = (int)$tbody_in_php;
713 $parts = explode('/tbody', $tag_path);
714 $new_tag_path = '';
715 foreach ($parts as $k => $path_part) {
716 // use only the amount of tbody found in php code
717 $new_tag_path .= $path_part . ($k < $tbody_in_php ? '/tbody' : '');
718 }
719 // set new path to tag
720 $tag_path = $new_tag_path;
721 }
722
723 // foresee the result of the Xpath query
724 $testcase = $php_xpath->query($tag_path);
725 if (!$testcase || !$testcase->length) {
726 // this Xpath query is about to fail, try to do something
727
728 if (strpos($tag_path, '/style') !== false) {
729 // style tags found in the HTML source code may not be available in the PHP source code
730 $tag_path = str_replace('/style', '', $tag_path);
731 }
732
733 /**
734 * BC with old invoice template file structure where no table is ever inside another.
735 *
736 * @since any version prior to 1.14 (J) - 1.4.0 (WP)
737 */
738 if (strpos($tag_path, '//table/table[2]') !== false) {
739 $tag_path = str_replace('//table/table[2]', '//table[3]', $tag_path);
740 } elseif (strpos($tag_path, '//table/table[1]') !== false) {
741 $tag_path = str_replace('//table/table[1]', '//table[2]', $tag_path);
742 }
743 }
744
745
746 return $tag_path;
747 }
748
749 /**
750 * Earlier versions of PHP and Libxml may not support to load code strings
751 * without adding the DOCTYPE, the html+body tags, and any missing/malformed tag.
752 * This will break the entire PHP source code of the file by getting HTML entities
753 * like ?&gt; for the PHP closing tag, or =&gt; for the array key-val operator.
754 *
755 * @param string $php_code the raw source code generated by DOMDocument.
756 * @param object $php_dom the DOMDocument object of the php code.
757 *
758 * @return string the clean PHP source code to write onto the file.
759 */
760 public static function cleanPHPSourceCode($php_code, $php_dom)
761 {
762 /**
763 * The following constants will produce a different nodePath, and they are available
764 * starting from PHP 5.4 and Libxml >= 2.7.8.
765 */
766 $libxml_updated = defined('LIBXML_HTML_NOIMPLIED') && defined('LIBXML_HTML_NODEFDTD');
767 //
768
769 // immediately restore the PHP tags that could have been converted to HTML entities
770 $php_code = str_replace(array('&lt;?php', '?&gt;'), array('<?php', '?>'), $php_code);
771
772 // take care of "unsafe" HTML detected for PHP opening and closing tags that may now be HTML comments
773 $php_code = str_replace(array('<!--?php', '?-->'), array('<?php', '?>'), $php_code);
774
775 // remove doctype
776 $php_code = preg_replace("/(^<!DOCTYPE.*\R)/i", '', $php_code);
777
778 /**
779 * Grab anything between PHP tags to make sure there are no syntax errors due to HTML entities.
780 *
781 * @see In the callback we should NEVER user strip_tags as PHP can contain HTML.
782 * We can at most look for some HTML tags mixed to PHP code to remove only them.
783 */
784 $php_code = preg_replace_callback("/<\?php(.*?)\?>/si", function($match) {
785 // use just what's inside the PHP tags
786 $pure_code = $match[1];
787 /**
788 * PHP comments in the check-in document template file may get for array declarations
789 * one opening "<p>" tag next to the HTML entity for "=>". Therefore, we remove it.
790 */
791 if (strpos($pure_code, '=&gt;') !== false && strpos($pure_code, 'array') !== false && preg_match_all("/(<[a-zA-Z]+>)/", $pure_code, $extra_tags)) {
792 foreach ($extra_tags[0] as $extra_tag) {
793 // strip the tag as well as its closing version
794 $pure_code = str_replace(array($extra_tag, str_replace('<', '</', $extra_tag)), '', $pure_code);
795 }
796 }
797 // return the decoded HTML entities needed by PHP
798 return '<?php' . html_entity_decode($pure_code) . '?>';
799 }, $php_code);
800
801 // get rid of html and body tags
802 $php_code = str_replace(array('<html>', '<body>', '</html>', '</body>'), '', $php_code);
803
804 // check if we have an HTML closing tag after the PHP closing tag due to previous manipulation of PHP code
805 $php_code = preg_replace_callback("/\?>\R+(<\/?[a-zA-Z]+.*?>)/s", function($match) {
806 if ($match[1] && strpos($match[1], '/') !== false) {
807 return str_replace($match[1], '', $match[0]);
808 }
809 return $match[0];
810 }, $php_code);
811
812 /**
813 * If libxml is not updated, we load the HTML by enclosing the whole source within a placeholder DIV tag.
814 * This is to avoid getting the HTML and BODY tags started inside PHP code maybe, because it has a > or <.
815 */
816 if (!$libxml_updated) {
817 // get rid of the wrapper div tag, added as a placeholder to avoid getting html and body inside php code
818 $wrapper = $php_dom->getElementsByTagName('div')->item(0);
819 if ($wrapper) {
820 // remove all children and store the element
821 $wrapper = $wrapper->parentNode->removeChild($wrapper);
822 while ($php_dom->firstChild) {
823 $php_dom->removeChild($php_dom->firstChild);
824 }
825 // append children again
826 while ($wrapper->firstChild ) {
827 $php_dom->appendChild($wrapper->firstChild);
828 }
829 // get new HTML without the wrapper
830 $php_code = $php_dom->saveHTML();
831 }
832 }
833 //
834
835 return $php_code;
836 }
837
838 /**
839 * Writes the source code of the template file onto a backup file.
840 *
841 * @param string $file the basename of the template file.
842 * @param string $php_code the raw source code of the file.
843 *
844 * @return bool True on success, false otherwise.
845 */
846 public static function backupTemplateFileCode($file, $php_code)
847 {
848 $fp = fopen(dirname(__FILE__) . DIRECTORY_SEPARATOR . $file . '.bkp', 'w+');
849 if (!$fp) {
850 return false;
851 }
852
853 $bytes = fwrite($fp, $php_code);
854 fclose($fp);
855
856 return ($bytes !== false);
857 }
858
859 /**
860 * Restores the source code of the template file from the backup file.
861 *
862 * @param string $file the basename of the template file.
863 *
864 * @return bool True on success, false otherwise.
865 */
866 public static function restoreTemplateFileCode($file)
867 {
868 $backup_fpath = dirname(__FILE__) . DIRECTORY_SEPARATOR . $file . '.bkp';
869 if (!is_file($backup_fpath)) {
870 return false;
871 }
872
873 $fp = fopen($backup_fpath, 'r');
874 if (!$fp) {
875 return false;
876 }
877
878 $fcode = '';
879 while (!feof($fp)) {
880 $fcode .= fread($fp, 8192);
881 }
882 fclose($fp);
883
884 if (empty($fcode)) {
885 return false;
886 }
887
888 return self::writeTemplateFileCode($file, $fcode);
889 }
890
891 /**
892 * Compares the new HTML source code of the compiled template file
893 * to the raw source code of the template file. Finds the newly added
894 * tag in the HTML source code and adds it to the same position of the
895 * raw source code of the same template file. Used to add a new tag to the code.
896 *
897 * @param string $tag the conditional text tag to add.
898 * @param string $file the basename of the template file.
899 * @param string $html_code the full HTML source code where the new tag is.
900 * @param string $php_code the raw source code where the new tag should be added.
901 *
902 * @return string the new PHP source code to write onto the file.
903 */
904 public static function addTagByComparingSources($tag, $file, $html_code, $php_code)
905 {
906 if (!class_exists('DOMDocument') || !class_exists('DOMXpath')) {
907 // this sucks, we just append the tag to the end of the file
908 $php_code .= "\n{$tag}\n";
909
910 // log the case
911 self::setEditingLog("Classes DOMDocument or DOMXpath are not available (" . __LINE__ . ")");
912
913 return $php_code;
914 }
915
916 // backup the file source code no matter what
917 self::backupTemplateFileCode($file, $php_code);
918
919 /**
920 * The following constants will produce a different nodePath, and they are available
921 * starting from PHP 5.4 and Libxml >= 2.7.8.
922 */
923 $libxml_updated = defined('LIBXML_HTML_NOIMPLIED') && defined('LIBXML_HTML_NODEFDTD');
924 //
925
926 // log data
927 self::setEditingLog("Libxml support: " . (int)$libxml_updated);
928
929 /**
930 * Suppress warnings for bad markup by using libxml's error handling functions.
931 * Errors could be retrieved by using print_r(libxml_get_errors(), true).
932 */
933 libxml_use_internal_errors(true);
934 //
935
936 // load HTML source code
937 $html_dom = new DOMDocument();
938 if ($libxml_updated) {
939 $html_dom->loadHTML($html_code, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
940 } else {
941 $html_dom->loadHTML('<div>' . $html_code . '</div>');
942 }
943
944 // get DOMXPath instance of the html DOM Document
945 $html_xpath = new DOMXpath($html_dom);
946 // find DOMNodeList from given tag (there should be just one tag)
947 $found_nodelist = $html_xpath->query("//*[text()[contains(., '{$tag}')]]");
948
949 if (!$found_nodelist || !$found_nodelist->length) {
950 // log the case
951 self::setEditingLog("tag not found in html source code (" . __LINE__ . ")");
952
953 // tag not found in html source code
954 return $php_code;
955 }
956
957 // find the path to the first node occurrence of the given tag string
958 $tag_path = $found_nodelist->item(0)->getNodePath();
959 if (empty($tag_path)) {
960 // log the case
961 self::setEditingLog("unable to proceed without knowing the path to the tag (" . __LINE__ . ")");
962
963 // unable to proceed without knowing the path to the tag
964 return $php_code;
965 }
966
967 // log data
968 self::setEditingLog("Node Path to tag in HTML source code: " . $tag_path);
969
970 // import the raw php code to DOMDocument
971 $php_dom = new DOMDocument();
972 if ($libxml_updated) {
973 $php_dom->loadHTML($php_code, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
974 } else {
975 $php_dom->loadHTML('<div>' . $php_code . '</div>');
976 }
977
978 // get DOMXPath instance of the php DOM Document
979 $php_xpath = new DOMXpath($php_dom);
980
981 // adjust html path to tag to comply with the expression for the raw code
982 $tag_path = self::adjustDOMXpathQuery($tag_path, $php_xpath);
983
984 // log data
985 self::setEditingLog("Adjusted Node Path: " . $tag_path);
986
987 // query the raw source code to find the same path as a DOMNodeList
988 $found_nodelist = $php_xpath->query($tag_path);
989
990 if (!$found_nodelist || !$found_nodelist->length) {
991 // log the case
992 self::setEditingLog("Node Path to tag not found in php source code: {$tag_path} must be invalid (" . __LINE__ . ")");
993
994 // path not found in php source code: $tag_path must be invalid
995 return $php_code;
996 }
997
998 // create a text node with the special tag string
999 $tag_element = $php_dom->createTextNode($tag);
1000 // append the tag string to the first (and only) path found
1001 $found_nodelist->item(0)->appendChild($tag_element);
1002
1003 // obtain the new php source code
1004 $php_code = $php_dom->saveHTML();
1005
1006 // log data
1007 self::setEditingLog("Tag appended to the given path. New template source code before cleaning:\n\n" . $php_code);
1008
1009 // always clean up the PHP code to avoid breaking the file
1010 $php_code = self::cleanPHPSourceCode($php_code, $php_dom);
1011
1012 /**
1013 * @see the following code can help debugging the source code and entire flow.
1014 *
1015 * $fp = fopen(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'debug.txt', 'w+');
1016 * fwrite($fp, implode("\n", self::getEditingLog()) . "\n\n\nclean source is:\n\n-------------\n" . $php_code);
1017 * fclose($fp);
1018 */
1019
1020 return $php_code;
1021 }
1022
1023 /**
1024 * Compares the new HTML source code of the compiled template file
1025 * to the raw source code of the template file. Finds the newly added
1026 * class attributes in the HTML source code, gets it styling and adds it
1027 * to the raw source code of the same template file. Used by the CSS inspector.
1028 *
1029 * @param array $classes list of custom/temporary CSS classes to look for.
1030 * @param string $file the basename of the template file.
1031 * @param string $html_code the full HTML source code where the new classes are.
1032 * @param string $php_code the raw source code where the new styles should be added.
1033 *
1034 * @return string the new PHP source code to write onto the file.
1035 *
1036 * @throws Exception if DOMDocument is not supported as nothing could be done.
1037 */
1038 public static function addStylesByComparingSources($classes, $file, $html_code, $php_code)
1039 {
1040 if (!class_exists('DOMDocument') || !class_exists('DOMXpath')) {
1041 // we cannot proceed without these classes
1042 throw new Exception("DOMDocument or DOMXpath are missing in your PHP installation", 403);
1043 }
1044
1045 // backup the file source code no matter what
1046 self::backupTemplateFileCode($file, $php_code);
1047
1048 /**
1049 * The following constants will produce a different nodePath, and they are available
1050 * starting from PHP 5.4 and Libxml >= 2.7.8.
1051 */
1052 $libxml_updated = defined('LIBXML_HTML_NOIMPLIED') && defined('LIBXML_HTML_NODEFDTD');
1053 //
1054
1055 // log data
1056 self::setEditingLog("Libxml support: " . (int)$libxml_updated);
1057
1058 /**
1059 * Suppress warnings for bad markup by using libxml's error handling functions.
1060 * Errors could be retrieved by using print_r(libxml_get_errors(), true).
1061 */
1062 libxml_use_internal_errors(true);
1063 //
1064
1065 // load HTML source code
1066 $html_dom = new DOMDocument();
1067 if ($libxml_updated) {
1068 $html_dom->loadHTML($html_code, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
1069 } else {
1070 $html_dom->loadHTML('<div>' . $html_code . '</div>');
1071 }
1072
1073 // get DOMXPath instance of the html DOM Document
1074 $html_xpath = new DOMXpath($html_dom);
1075
1076 // compose pool of styles
1077 $styles_pool = array();
1078
1079 foreach ($classes as $css_class) {
1080 // find DOMNodeList from given CSS class (there should be just one node)
1081 $found_nodelist = $html_xpath->query("//*[contains(@class, '" . $css_class . "')]");
1082 if (!$found_nodelist || !$found_nodelist->length) {
1083 // log the case
1084 self::setEditingLog("given CSS class {$css_class} not found in html source code (" . __LINE__ . ")");
1085
1086 // given CSS class not found in html source code
1087 continue;
1088 }
1089
1090 // log data
1091 self::setEditingLog("CSS class {$css_class} found in HTML source code");
1092
1093 // get the first node
1094 $node = $found_nodelist->item(0);
1095 // make sure the node has a style attribute
1096 if (!$node->hasAttribute('style')) {
1097 // log the case
1098 self::setEditingLog("style attribute not found in available tag with CSS class {$css_class} (" . __LINE__ . ")");
1099
1100 // style attribute not found
1101 continue;
1102 }
1103
1104 // make sure the style attribute is not empty
1105 $style_attr = $node->getAttribute('style');
1106 if (!$style_attr || empty($style_attr)) {
1107 // log the case
1108 self::setEditingLog("style attribute is empty in available tag with CSS class {$css_class} (" . __LINE__ . ")");
1109
1110 // style attribute is empty
1111 continue;
1112 }
1113
1114 // compose style information
1115 $style = new stdClass;
1116 $style->node_path = $node->getNodePath();
1117 $style->attribute = $style_attr;
1118
1119 // push style object to the pool
1120 array_push($styles_pool, $style);
1121 }
1122
1123 if (!count($styles_pool)) {
1124 // no style attributes found to add, unable to proceed
1125 return $php_code;
1126 }
1127
1128 // import the raw php code to DOMDocument
1129 $php_dom = new DOMDocument();
1130 if ($libxml_updated) {
1131 $php_dom->loadHTML($php_code, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
1132 } else {
1133 $php_dom->loadHTML('<div>' . $php_code . '</div>');
1134 }
1135
1136 // get DOMXPath instance of the php DOM Document
1137 $php_xpath = new DOMXpath($php_dom);
1138
1139 // iterate all styles to add to the various nodes
1140 foreach ($styles_pool as $style) {
1141 // log data
1142 self::setEditingLog("Node Path to tag to be styled: " . $style->node_path);
1143
1144 // adjust html path to node to comply with the expression for the raw code
1145 $node_path = self::adjustDOMXpathQuery($style->node_path, $php_xpath);
1146
1147 // log data
1148 self::setEditingLog("Adjusted node path is: " . $node_path);
1149
1150 // query the raw source code to find the same path as a DOMNodeList
1151 $found_nodelist = $php_xpath->query($node_path);
1152
1153 if (!$found_nodelist || !$found_nodelist->length) {
1154 // log the case
1155 self::setEditingLog("Node Path not found in php source code for styling: {$node_path} must be invalid (" . __LINE__ . ")");
1156
1157 // path not found in php source code: $node_path must be invalid
1158 continue;
1159 }
1160
1161 // get the first node
1162 $node = $found_nodelist->item(0);
1163
1164 // set the style attribute
1165 $node->setAttribute('style', $style->attribute);
1166
1167 // obtain the new php source code
1168 $php_code = $php_dom->saveHTML();
1169 }
1170
1171 // log data
1172 self::setEditingLog("Style(s) added to the source code. New template source code before cleaning:\n\n" . $php_code);
1173
1174 // always clean up the PHP code to avoid breaking the file
1175 $php_code = self::cleanPHPSourceCode($php_code, $php_dom);
1176
1177 return $php_code;
1178 }
1179
1180 /**
1181 * Appends an execution log to the execution log array.
1182 *
1183 * @param string $log the execution string to append.
1184 *
1185 * @return void
1186 */
1187 public static function setEditingLog($log)
1188 {
1189 if (static::$editingLog === null) {
1190 static::$editingLog = array();
1191 }
1192
1193 array_push(static::$editingLog, $log);
1194 }
1195
1196 /**
1197 * Gets the execution log array for all editing operations.
1198 *
1199 * @return mixed the current editing log array or null.
1200 */
1201 public static function getEditingLog()
1202 {
1203 return static::$editingLog;
1204 }
1205 }
1206