Create New Item
Item Type
File
Folder
Item Name
Search file in folder and subfolders...
Are you sure want to rename?
File Manager
/
wp-content
/
plugins
/
vikbooking
/
admin
/
helpers
:
conditional_rules.php
Advanced Search
Upload
New Item
Settings
Back
Back Up
Advanced Editor
Save
<?php /** * @package VikBooking * @subpackage com_vikbooking * @author Alessio Gaggii - e4j - Extensionsforjoomla.com * @copyright Copyright (C) 2018 e4j - Extensionsforjoomla.com. All rights reserved. * @license GNU General Public License version 2 or later; see LICENSE * @link https://vikwp.com */ defined('ABSPATH') or die('No script kiddies please!'); /** * Helper class for the conditional rules. * * @since 1.4.0 */ class VikBookingHelperConditionalRules { /** * The singleton instance of the class. * * @var VikBookingHelperConditionalRules */ protected static $instance = null; /** * A flag that indicates the rules debug mode. * * @var bool */ public static $debugRules = false; /** * An array to store some cached/static values. * * @var array */ protected static $helper = null; /** * Logs the execution of all the complex template * editing methods that manipulate the HTML/PHP DOM. * * @var array */ protected static $editingLog = null; /** * The database handler instance. * * @var object */ protected $dbo; /** * The list of rules instances loaded. * * @var array */ protected $rules; /** * The VikBooking translation object. * * @var object */ protected $vbo_tn; /** * Class constructor is protected. * * @see getInstance() */ protected function __construct() { static::$helper = array(); $this->dbo = JFactory::getDbo(); $this->rules = array(); $this->vbo_tn = VikBooking::getTranslator(); $this->load(); } /** * Returns the global object, either * a new instance or the existing instance * if the class was already instantiated. * * @return self A new instance of the class. */ public static function getInstance() { if (is_null(static::$instance)) { static::$instance = new static(); } return static::$instance; } /** * Loads a list of all available conditional rules. * * @return self */ protected function load() { // require main/parent conditional-rule class require_once(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'conditional_rule.php'); $rules_base = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'conditionalrules' . DIRECTORY_SEPARATOR; $rules_files = glob($rules_base . '*.php'); /** * Trigger event to let other plugins register additional rules. * * @return array A list of supported rules. */ $list = VBOFactory::getPlatform()->getDispatcher()->filter('onLoadConditionalRules'); foreach ($list as $chunk) { // merge default rule files with the returned ones $rules_files = array_merge($rules_files, (array)$chunk); } foreach ($rules_files as $rf) { try { // require rule class file if (is_file($rf)) { require_once($rf); } // instantiate rule object $classname = 'VikBookingConditionalRule' . str_replace(' ', '', ucwords(str_replace('_', ' ', basename($rf, '.php')))); if (class_exists($classname)) { $rule = new $classname(); // push rule object array_push($this->rules, $rule); } } catch (Exception $e) { // do nothing } } return $this; } /** * Gets the list of conditional rules instantiated. * * @return array list of conditional rules objects. */ public function getRules() { return $this->rules; } /** * Gets a single conditional rule instantiated. * * @param string $id the rule identifier. * * @return mixed the conditional rule object, false otherwise. */ public function getRule($id) { foreach ($this->rules as $rule) { if ($rule->getIdentifier() != $id) { continue; } return $rule; } return false; } /** * Gets a list of sorted rule names, ids and descriptions. * * @return array associative and sorted rules list. */ public function getRuleNames() { $names = array(); $pool = array(); foreach ($this->rules as $rule) { $id = $rule->getIdentifier(); $name = $rule->getName(); $descr = $rule->getDescription(); $rdata = new stdClass; $rdata->id = $id; $rdata->name = $name; $rdata->descr = $descr; $names[$name] = $rdata; } // apply sorting by name ksort($names); // push sorted rules to pool foreach ($names as $rdata) { array_push($pool, $rdata); } return $pool; } /** * Tells whether the rule object overrides the method from its parent * class. Useful to distinguish action-rules from filter-rules. * * @param object $rule child class object of VikBookingConditionalRule. * @param string $method the name of the method to check if it was overridden. * * @return bool true if overridden, false otherwise. */ public function supportsAction($rule, $method = 'callbackAction') { if (!class_exists('ReflectionMethod')) { return false; } $reflect = new ReflectionMethod($rule, $method); return ($reflect->getDeclaringClass()->getName() == get_class($rule)); } /** * Helper method for the controller to compose the rules * of the conditional text by parsing all input values in the same order requested. * * @return array list of stdClass object with the various rules params. */ public function composeRulesParamsFromRequest() { $rules_list = array(); $raw_vals = JFactory::getApplication()->input->getArray(); foreach ($raw_vals as $raw_inp_key => $rule_inp_vals) { foreach ($this->rules as $rule) { $rule_id = $rule->getIdentifier(); $rule_inp_key = basename($rule_id, '.php'); if ($rule_inp_key != $raw_inp_key) { continue; } // rule found, make sure the settings are not empty $has_vals = false; foreach ($rule_inp_vals as $rule_inp_val) { if (is_array($rule_inp_val) && count($rule_inp_val)) { $has_vals = true; break; } if (is_string($rule_inp_val) && strlen($rule_inp_val)) { $has_vals = true; break; } } if (!$has_vals) { // do not store empty rule params continue 2; } // compose rule object $rule_data = new stdClass; $rule_data->id = $rule_id; $rule_data->params = $rule_inp_vals; // push rule to list array_push($rules_list, $rule_data); } } return $rules_list; } /** * Helper method to load all special tags and related records. * * @param string $orby_col the column to order by. * @param string $orby_dir the order by direction. * * @return array associative list of special-tags (key) records (value). */ public function getSpecialTags($orby_col = 'name', $orby_dir = 'ASC') { $special_tags = array(); $q = "SELECT `ct`.* FROM `#__vikbooking_condtexts` AS `ct` ORDER BY `ct`.`{$orby_col}` {$orby_dir};"; $this->dbo->setQuery($q); $this->dbo->execute(); if ($this->dbo->getNumRows()) { $records = $this->dbo->loadAssocList(); $this->vbo_tn->translateContents($records, '#__vikbooking_condtexts'); foreach ($records as $record) { // decode rules $record['rules'] = !empty($record['rules']) ? json_decode($record['rules']) : array(); $record['rules'] = !is_array($record['rules']) ? array() : $record['rules']; // push record $special_tags[$record['token']] = $record; } } return $special_tags; } /** * Helper method to load one precise conditional text from the given special tag. * * @param string $token the special tag (token) to look for. * * @return array the record of the conditional text found or an empty array. */ public function getBySpecialTag($token) { $cond_text = array(); $q = "SELECT * FROM `#__vikbooking_condtexts` WHERE `token`=" . $this->dbo->quote($token) . ";"; $this->dbo->setQuery($q); $this->dbo->execute(); if ($this->dbo->getNumRows()) { $cond_text = $this->dbo->loadAssoc(); $this->vbo_tn->translateContents($cond_text, '#__vikbooking_condtexts'); } return $cond_text; } /** * Helper method to store information in the helper array. * * @param mixed $key the key or array of keys to set. * @param mixed $val the value or array of values to set. * * @return self */ public function set($key, $val = null) { if (!is_string($key) && !is_array($key)) { return $this; } if (is_string($key)) { $key = array($key); } if (!is_array($val)) { $val = array($val); } foreach ($key as $i => $prop) { if (!isset($val[$i])) { continue; } static::$helper[$prop] = $val[$i]; } return $this; } /** * Helper method to get information from the helper array. * * @param string $key the key to get. * @param string $def the default value to get. * * @return mixed the requested key value. */ public function get($key, $def = null) { return isset(static::$helper[$key]) ? static::$helper[$key] : $def; } /** * Parses all tokens in the given template string and applies the * conditional texts found if all rules are compliant. * * @param string $tmpl the template string to parse, passed by reference. * * @return mixed false if no tokens found, integer for how many tokens were applied. */ public function parseTokens(&$tmpl) { preg_match_all('/\{condition: ?([a-zA-Z0-9_]+)\}/U', $tmpl, $matches); $tot_tokens = count($matches[0]); if (!$tot_tokens) { // no tokens to parse return false; } // default empty replacement $null_replace = ''; // load all helper property keys and values $prop_keys = array_keys(static::$helper); $prop_vals = array_values(static::$helper); // load all conditional text records $cond_texts = $this->getSpecialTags(); // iterate through all tokens found foreach ($matches[0] as $token) { if (!isset($cond_texts[$token])) { if (static::$debugRules) { // debuggining at this step cannot be enabled as the record was not found $null_replace = "{$token} was not found"; } // remove token from template file $tmpl = str_replace($token, $null_replace, $tmpl); // decrease total tokens applied $tot_tokens--; // iterate to the next token, if any continue; } // set debug mode according to record static::$debugRules = (bool)$cond_texts[$token]['debug']; // set flag to know whether the token was compliant $compliant = false; // parse all rules for this conditional text foreach ($cond_texts[$token]['rules'] as $rule_data) { if (empty($rule_data->id)) { continue; } $rule = $this->getRule($rule_data->id); if ($rule === false) { continue; } // inject params and booking to rule, then check if compliant $compliant = $rule->setParams($rule_data->params)->setProperties($prop_keys, $prop_vals)->isCompliant(); if (!$compliant) { if (static::$debugRules) { $null_replace = JText::sprintf('VBO_DEBUG_RULE_CONDTEXT', $rule->getName(), $token); } // all rules must be compliant with the booking break; } } if (!$compliant) { // remove token from template file $tmpl = str_replace($token, $null_replace, $tmpl); // decrease total tokens applied $tot_tokens--; // iterate to the next token, if any continue; } // all rules were compliant, trigger callback and manipulation actions foreach ($cond_texts[$token]['rules'] as $rule_data) { if (empty($rule_data->id)) { continue; } $rule = $this->getRule($rule_data->id); if ($rule === false) { continue; } // inject params and booking to rule $rule->setParams($rule_data->params)->setProperties($prop_keys, $prop_vals); // trigger callback action $rule->callbackAction(); // allow rule to manipulate the actual message $cond_texts[$token]['msg'] = $rule->manipulateMessage($cond_texts[$token]['msg']); } /** * The message of the conditional text rules is usually written through a WYSIWYG editor * which may contain HTML tags. However, the context where these texts are being used is * unknown to VBO, and so we can detect from the content whether plain text messages are * necessary, maybe for sending an SMS message. We allow the use of special strings to * detect if no HTML should ever be included in the message, like [sms] or [plain text]. * * @since 1.4.3 */ $requires_plain_text = false; if (preg_match_all("/(\[sms\]|\[plain_? ?text\])+/i", $cond_texts[$token]['msg'], $plt_matches)) { $requires_plain_text = true; foreach ($plt_matches[1] as $plt_match) { $cond_texts[$token]['msg'] = str_replace($plt_match, '', $cond_texts[$token]['msg']); } $cond_texts[$token]['msg'] = strip_tags($cond_texts[$token]['msg']); } /** * @wponly we need to let WordPress parse the paragraphs in the message. */ if (VBOPlatformDetection::isWordPress() && !empty($cond_texts[$token]['msg']) && !$requires_plain_text) { $cond_texts[$token]['msg'] = wpautop($cond_texts[$token]['msg']); } /** * Make sure any src/href attribute does not contain relative URLs. * * @since 1.5.0 */ $cond_texts[$token]['msg'] = preg_replace_callback("/\s*(src|href)=([\"'])(.*?)[\"']/i", function($match) { // check if the URL starts with the base domain if (stripos($match[3], JUri::root()) !== 0 && !preg_match("/^(https?:\/\/|www\.)/i", $match[3])) { // prepend base domain to URL $match[0] = ' ' . $match[1] . '=' . $match[2] . JUri::root() . $match[3] . $match[2]; } return $match[0]; }, $cond_texts[$token]['msg']); // finally, apply the message to the template $tmpl = str_replace($token, $cond_texts[$token]['msg'], $tmpl); } return $tot_tokens; } /** * Toggles the rules debugging mode. * * @return self */ public function toggleDebugging() { static::$debugRules = !static::$debugRules; return $this; } /** * Returns the list of the template files paths supporting the conditional text tags. * * @return array */ public static function getTemplateFilesPaths() { return array( 'email_tmpl.php' => VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'email_tmpl.php', 'invoice_tmpl.php' => VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'invoices' . DIRECTORY_SEPARATOR . 'invoice_tmpl.php', 'checkin_tmpl.php' => VBO_SITE_PATH . DIRECTORY_SEPARATOR . 'helpers' . DIRECTORY_SEPARATOR . 'checkins' . DIRECTORY_SEPARATOR . 'checkin_tmpl.php', ); } /** * Returns the list of the template files names supporting the conditional text tags. * * @return array */ public static function getTemplateFilesNames() { return array( 'email_tmpl.php' => JText::translate('VBOCONFIGEMAILTEMPLATE'), 'invoice_tmpl.php' => JText::translate('VBOCONFIGINVOICETEMPLATE'), 'checkin_tmpl.php' => JText::translate('VBOCONFIGCHECKINTEMPLATE'), ); } /** * Returns the list of the template files contents supporting the conditional text tags. * * @param string $file the basename of the template file to read. * * @return array */ public static function getTemplateFilesContents($file = null) { $templates = self::getTemplateFilesPaths(); if (!empty($file) && isset($templates[$file])) { $templates = array( $file => $templates[$file] ); } $contents = array(); foreach ($templates as $file => $path) { if (!is_file($path)) { continue; } switch ($file) { case 'email_tmpl.php': $contents[$file] = VikBooking::loadEmailTemplate(); break; case 'invoice_tmpl.php': $data = VikBooking::loadInvoiceTmpl(); $contents[$file] = $data[0]; break; case 'checkin_tmpl.php': $data = VikBooking::loadCheckinDocTmpl(); $contents[$file] = $data[0]; break; default: break; } } return $contents; } /** * Returns the list of the template files code supporting the conditional text tags. * * @param string $file the basename of the template file to read. * * @return mixed array of files code, or just the code of the requested file. */ public static function getTemplateFileCode($file = null) { $templates = self::getTemplateFilesPaths(); if (!empty($file) && isset($templates[$file])) { $templates = array( $file => $templates[$file] ); } $contents = array(); foreach ($templates as $f => $path) { if (!is_file($path)) { continue; } switch ($f) { case 'email_tmpl.php': case 'invoice_tmpl.php': case 'checkin_tmpl.php': $fp = fopen($path, 'r'); if (!$fp) { break; } $fcode = ''; while (!feof($fp)) { $fcode .= fread($fp, 8192); } fclose($fp); if (empty($fcode)) { break; } $contents[$f] = $fcode; break; default: break; } } return !empty($file) && isset($contents[$file]) ? $contents[$file] : $contents; } /** * Updates the source code of the given template file name. * * @param string $file the basename of the template file to write. * @param string $code the new code of the template file to write. * * @return bool true on success, false otherwise. */ public static function writeTemplateFileCode($file, $code) { $templates = self::getTemplateFilesPaths(); if (!isset($templates[$file]) || empty($code)) { return false; } $fp = fopen($templates[$file], 'w+'); if (!$fp) { return false; } $bytes = fwrite($fp, $code); fclose($fp); return ($bytes !== false); } /** * Tells whether the given special tag is used in the passed content. * * @param string $tag the special tag to look for. * @param string $content the content of the template file. * * @return bool */ public static function isTagInContent($tag, $content) { if (empty($tag) || empty($content)) { return false; } if (strpos($tag, '{condition:') === false) { // invalid conditional text special tag return false; } return (strpos($content, $tag) !== false); } /** * Makes sure the path obtained to query the raw source code will produce * results in the raw php code. Some Libxml constants may skip html or * style tags, while the html source code may contain more tbody tags * than the php source code as it is parsed by the browser, where table * tags without a nested tbody tag will add it automatically. * * @param string $tag_path the node-path to the tag obtained * from the html source code. * @param DOMXpath $php_path DOMXpath object for the php code. * * @return string a valid query path to be used. * * @see addTagByComparingSources() and addStylesByComparingSources() */ public static function adjustDOMXpathQuery($tag_path, $php_xpath) { // Xpath query expressions require two leading slashes if (substr($tag_path, 0, 2) !== '//' && substr($tag_path, 0, 1) == '/') { $tag_path = '/' . $tag_path; } // take care of any count mismatch of tbody tags $tbody_in_html = substr_count($tag_path, 'tbody'); $tbody_in_php = $php_xpath->evaluate("count(//tbody)"); if ($tbody_in_html > 0 && (int)$tbody_in_php < $tbody_in_html) { // raw php code has got less tbody nodes than html code $tbody_in_php = (int)$tbody_in_php; $parts = explode('/tbody', $tag_path); $new_tag_path = ''; foreach ($parts as $k => $path_part) { // use only the amount of tbody found in php code $new_tag_path .= $path_part . ($k < $tbody_in_php ? '/tbody' : ''); } // set new path to tag $tag_path = $new_tag_path; } // foresee the result of the Xpath query $testcase = $php_xpath->query($tag_path); if (!$testcase || !$testcase->length) { // this Xpath query is about to fail, try to do something if (strpos($tag_path, '/style') !== false) { // style tags found in the HTML source code may not be available in the PHP source code $tag_path = str_replace('/style', '', $tag_path); } /** * BC with old invoice template file structure where no table is ever inside another. * * @since any version prior to 1.14 (J) - 1.4.0 (WP) */ if (strpos($tag_path, '//table/table[2]') !== false) { $tag_path = str_replace('//table/table[2]', '//table[3]', $tag_path); } elseif (strpos($tag_path, '//table/table[1]') !== false) { $tag_path = str_replace('//table/table[1]', '//table[2]', $tag_path); } } return $tag_path; } /** * Earlier versions of PHP and Libxml may not support to load code strings * without adding the DOCTYPE, the html+body tags, and any missing/malformed tag. * This will break the entire PHP source code of the file by getting HTML entities * like ?> for the PHP closing tag, or => for the array key-val operator. * * @param string $php_code the raw source code generated by DOMDocument. * @param object $php_dom the DOMDocument object of the php code. * * @return string the clean PHP source code to write onto the file. */ public static function cleanPHPSourceCode($php_code, $php_dom) { /** * The following constants will produce a different nodePath, and they are available * starting from PHP 5.4 and Libxml >= 2.7.8. */ $libxml_updated = defined('LIBXML_HTML_NOIMPLIED') && defined('LIBXML_HTML_NODEFDTD'); // // immediately restore the PHP tags that could have been converted to HTML entities $php_code = str_replace(array('<?php', '?>'), array('<?php', '?>'), $php_code); // remove doctype $php_code = preg_replace("/(^<!DOCTYPE.*\R)/i", '', $php_code); /** * Grab anything between PHP tags to make sure there are no syntax errors due to HTML entities. * * @see In the callback we should NEVER user strip_tags as PHP can contain HTML. * We can at most look for some HTML tags mixed to PHP code to remove only them. */ $php_code = preg_replace_callback("/<\?php(.*?)\?>/si", function($match) { // use just what's inside the PHP tags $pure_code = $match[1]; /** * PHP comments in the check-in document template file may get for array declarations * one opening "<p>" tag next to the HTML entity for "=>". Therefore, we remove it. */ if (strpos($pure_code, '=>') !== false && strpos($pure_code, 'array') !== false && preg_match_all("/(<[a-zA-Z]+>)/", $pure_code, $extra_tags)) { foreach ($extra_tags[0] as $extra_tag) { // strip the tag as well as its closing version $pure_code = str_replace(array($extra_tag, str_replace('<', '</', $extra_tag)), '', $pure_code); } } // return the decoded HTML entities needed by PHP return '<?php' . html_entity_decode($pure_code) . '?>'; }, $php_code); // get rid of html and body tags $php_code = str_replace(array('<html>', '<body>', '</html>', '</body>'), '', $php_code); // check if we have an HTML closing tag after the PHP closing tag due to previous manipulation of PHP code $php_code = preg_replace_callback("/\?>\R+(<\/?[a-zA-Z]+.*?>)/s", function($match) { if ($match[1] && strpos($match[1], '/') !== false) { return str_replace($match[1], '', $match[0]); } return $match[0]; }, $php_code); /** * If libxml is not updated, we load the HTML by enclosing the whole source within a placeholder DIV tag. * This is to avoid getting the HTML and BODY tags started inside PHP code maybe, because it has a > or <. */ if (!$libxml_updated) { // get rid of the wrapper div tag, added as a placeholder to avoid getting html and body inside php code $wrapper = $php_dom->getElementsByTagName('div')->item(0); if ($wrapper) { // remove all children and store the element $wrapper = $wrapper->parentNode->removeChild($wrapper); while ($php_dom->firstChild) { $php_dom->removeChild($php_dom->firstChild); } // append children again while ($wrapper->firstChild ) { $php_dom->appendChild($wrapper->firstChild); } // get new HTML without the wrapper $php_code = $php_dom->saveHTML(); } } // return $php_code; } /** * Writes the source code of the template file onto a backup file. * * @param string $file the basename of the template file. * @param string $php_code the raw source code of the file. * * @return bool True on success, false otherwise. */ public static function backupTemplateFileCode($file, $php_code) { $fp = fopen(dirname(__FILE__) . DIRECTORY_SEPARATOR . $file . '.bkp', 'w+'); if (!$fp) { return false; } $bytes = fwrite($fp, $php_code); fclose($fp); return ($bytes !== false); } /** * Restores the source code of the template file from the backup file. * * @param string $file the basename of the template file. * * @return bool True on success, false otherwise. */ public static function restoreTemplateFileCode($file) { $backup_fpath = dirname(__FILE__) . DIRECTORY_SEPARATOR . $file . '.bkp'; if (!is_file($backup_fpath)) { return false; } $fp = fopen($backup_fpath, 'r'); if (!$fp) { return false; } $fcode = ''; while (!feof($fp)) { $fcode .= fread($fp, 8192); } fclose($fp); if (empty($fcode)) { return false; } return self::writeTemplateFileCode($file, $fcode); } /** * Compares the new HTML source code of the compiled template file * to the raw source code of the template file. Finds the newly added * tag in the HTML source code and adds it to the same position of the * raw source code of the same template file. Used to add a new tag to the code. * * @param string $tag the conditional text tag to add. * @param string $file the basename of the template file. * @param string $html_code the full HTML source code where the new tag is. * @param string $php_code the raw source code where the new tag should be added. * * @return string the new PHP source code to write onto the file. */ public static function addTagByComparingSources($tag, $file, $html_code, $php_code) { if (!class_exists('DOMDocument') || !class_exists('DOMXpath')) { // this sucks, we just append the tag to the end of the file $php_code .= "\n{$tag}\n"; // log the case self::setEditingLog("Classes DOMDocument or DOMXpath are not available (" . __LINE__ . ")"); return $php_code; } // backup the file source code no matter what self::backupTemplateFileCode($file, $php_code); /** * The following constants will produce a different nodePath, and they are available * starting from PHP 5.4 and Libxml >= 2.7.8. */ $libxml_updated = defined('LIBXML_HTML_NOIMPLIED') && defined('LIBXML_HTML_NODEFDTD'); // // log data self::setEditingLog("Libxml support: " . (int)$libxml_updated); /** * Suppress warnings for bad markup by using libxml's error handling functions. * Errors could be retrieved by using print_r(libxml_get_errors(), true). */ libxml_use_internal_errors(true); // // load HTML source code $html_dom = new DOMDocument(); if ($libxml_updated) { $html_dom->loadHTML($html_code, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); } else { $html_dom->loadHTML('<div>' . $html_code . '</div>'); } // get DOMXPath instance of the html DOM Document $html_xpath = new DOMXpath($html_dom); // find DOMNodeList from given tag (there should be just one tag) $found_nodelist = $html_xpath->query("//*[text()[contains(., '{$tag}')]]"); if (!$found_nodelist || !$found_nodelist->length) { // log the case self::setEditingLog("tag not found in html source code (" . __LINE__ . ")"); // tag not found in html source code return $php_code; } // find the path to the first node occurrence of the given tag string $tag_path = $found_nodelist->item(0)->getNodePath(); if (empty($tag_path)) { // log the case self::setEditingLog("unable to proceed without knowing the path to the tag (" . __LINE__ . ")"); // unable to proceed without knowing the path to the tag return $php_code; } // log data self::setEditingLog("Node Path to tag in HTML source code: " . $tag_path); // import the raw php code to DOMDocument $php_dom = new DOMDocument(); if ($libxml_updated) { $php_dom->loadHTML($php_code, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); } else { $php_dom->loadHTML('<div>' . $php_code . '</div>'); } // get DOMXPath instance of the php DOM Document $php_xpath = new DOMXpath($php_dom); // adjust html path to tag to comply with the expression for the raw code $tag_path = self::adjustDOMXpathQuery($tag_path, $php_xpath); // log data self::setEditingLog("Adjusted Node Path: " . $tag_path); // query the raw source code to find the same path as a DOMNodeList $found_nodelist = $php_xpath->query($tag_path); if (!$found_nodelist || !$found_nodelist->length) { // log the case self::setEditingLog("Node Path to tag not found in php source code: {$tag_path} must be invalid (" . __LINE__ . ")"); // path not found in php source code: $tag_path must be invalid return $php_code; } // create a text node with the special tag string $tag_element = $php_dom->createTextNode($tag); // append the tag string to the first (and only) path found $found_nodelist->item(0)->appendChild($tag_element); // obtain the new php source code $php_code = $php_dom->saveHTML(); // log data self::setEditingLog("Tag appended to the given path. New template source code before cleaning:\n\n" . $php_code); // always clean up the PHP code to avoid breaking the file $php_code = self::cleanPHPSourceCode($php_code, $php_dom); /** * @see the following code can help debugging the source code and entire flow. * * $fp = fopen(dirname(__FILE__) . DIRECTORY_SEPARATOR . 'debug.txt', 'w+'); * fwrite($fp, implode("\n", self::getEditingLog()) . "\n\n\nclean source is:\n\n-------------\n" . $php_code); * fclose($fp); */ return $php_code; } /** * Compares the new HTML source code of the compiled template file * to the raw source code of the template file. Finds the newly added * class attributes in the HTML source code, gets it styling and adds it * to the raw source code of the same template file. Used by the CSS inspector. * * @param array $classes list of custom/temporary CSS classes to look for. * @param string $file the basename of the template file. * @param string $html_code the full HTML source code where the new classes are. * @param string $php_code the raw source code where the new styles should be added. * * @return string the new PHP source code to write onto the file. * * @throws Exception if DOMDocument is not supported as nothing could be done. */ public static function addStylesByComparingSources($classes, $file, $html_code, $php_code) { if (!class_exists('DOMDocument') || !class_exists('DOMXpath')) { // we cannot proceed without these classes throw new Exception("DOMDocument or DOMXpath are missing in your PHP installation", 403); } // backup the file source code no matter what self::backupTemplateFileCode($file, $php_code); /** * The following constants will produce a different nodePath, and they are available * starting from PHP 5.4 and Libxml >= 2.7.8. */ $libxml_updated = defined('LIBXML_HTML_NOIMPLIED') && defined('LIBXML_HTML_NODEFDTD'); // // log data self::setEditingLog("Libxml support: " . (int)$libxml_updated); /** * Suppress warnings for bad markup by using libxml's error handling functions. * Errors could be retrieved by using print_r(libxml_get_errors(), true). */ libxml_use_internal_errors(true); // // load HTML source code $html_dom = new DOMDocument(); if ($libxml_updated) { $html_dom->loadHTML($html_code, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); } else { $html_dom->loadHTML('<div>' . $html_code . '</div>'); } // get DOMXPath instance of the html DOM Document $html_xpath = new DOMXpath($html_dom); // compose pool of styles $styles_pool = array(); foreach ($classes as $css_class) { // find DOMNodeList from given CSS class (there should be just one node) $found_nodelist = $html_xpath->query("//*[contains(@class, '" . $css_class . "')]"); if (!$found_nodelist || !$found_nodelist->length) { // log the case self::setEditingLog("given CSS class {$css_class} not found in html source code (" . __LINE__ . ")"); // given CSS class not found in html source code continue; } // log data self::setEditingLog("CSS class {$css_class} found in HTML source code"); // get the first node $node = $found_nodelist->item(0); // make sure the node has a style attribute if (!$node->hasAttribute('style')) { // log the case self::setEditingLog("style attribute not found in available tag with CSS class {$css_class} (" . __LINE__ . ")"); // style attribute not found continue; } // make sure the style attribute is not empty $style_attr = $node->getAttribute('style'); if (!$style_attr || empty($style_attr)) { // log the case self::setEditingLog("style attribute is empty in available tag with CSS class {$css_class} (" . __LINE__ . ")"); // style attribute is empty continue; } // compose style information $style = new stdClass; $style->node_path = $node->getNodePath(); $style->attribute = $style_attr; // push style object to the pool array_push($styles_pool, $style); } if (!count($styles_pool)) { // no style attributes found to add, unable to proceed return $php_code; } // import the raw php code to DOMDocument $php_dom = new DOMDocument(); if ($libxml_updated) { $php_dom->loadHTML($php_code, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); } else { $php_dom->loadHTML('<div>' . $php_code . '</div>'); } // get DOMXPath instance of the php DOM Document $php_xpath = new DOMXpath($php_dom); // iterate all styles to add to the various nodes foreach ($styles_pool as $style) { // log data self::setEditingLog("Node Path to tag to be styled: " . $style->node_path); // adjust html path to node to comply with the expression for the raw code $node_path = self::adjustDOMXpathQuery($style->node_path, $php_xpath); // log data self::setEditingLog("Adjusted node path is: " . $node_path); // query the raw source code to find the same path as a DOMNodeList $found_nodelist = $php_xpath->query($node_path); if (!$found_nodelist || !$found_nodelist->length) { // log the case self::setEditingLog("Node Path not found in php source code for styling: {$node_path} must be invalid (" . __LINE__ . ")"); // path not found in php source code: $node_path must be invalid continue; } // get the first node $node = $found_nodelist->item(0); // set the style attribute $node->setAttribute('style', $style->attribute); // obtain the new php source code $php_code = $php_dom->saveHTML(); } // log data self::setEditingLog("Style(s) added to the source code. New template source code before cleaning:\n\n" . $php_code); // always clean up the PHP code to avoid breaking the file $php_code = self::cleanPHPSourceCode($php_code, $php_dom); return $php_code; } /** * Appends an execution log to the execution log array. * * @param string $log the execution string to append. * * @return void */ public static function setEditingLog($log) { if (static::$editingLog === null) { static::$editingLog = array(); } array_push(static::$editingLog, $log); } /** * Gets the execution log array for all editing operations. * * @return mixed the current editing log array or null. */ public static function getEditingLog() { return static::$editingLog; } }