PluginProbe
VikBooking Hotel Booking Engine & PMS / trunk
VikBooking Hotel Booking Engine & PMS vtrunk
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 trunk, at admin/helpers/conditional_rules.php

1,207 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 * @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 }
1207