<?php /** * @package VikWP - Libraries * @subpackage adapter.application * @author E4J s.r.l. * @copyright Copyright (C) 2023 E4J s.r.l. All Rights Reserved. * @license http://www.gnu.org/licenses/gpl-2.0.html GNU/GPL * @link https://vikwp.com */ // No direct access defined('ABSPATH') or die('No script kiddies please!'); /** * Route adapter class. * * @since 10.0 */ class JRoute { /** * A list of component shortcodes. * * @var array */ protected static $shortcodes = array(); /** * Translates an internal URL to a human-readable URL. * * @param string $url Absolute or Relative URI. * @param boolean $xhtml Replace & by &amp; for XML compliance. * @param integer $ssl Secure state for the resolved URI. * 0: (default) No change, use the protocol currently used in the request. * 1: Make URI secure using global secure site URI. * 2: Make URI unsecure using the global unsecure site URI. * * @return string The translated humanly readable URL. * * @uses getPermalink() * @uses replace() */ public static function _($url, $xhtml = true, $ssl = null) { // parse URL to obtain query args (true: as array) $args = JUri::getInstance($url)->getQuery(true); // prepend current URI if starts with 'index.php' if (strpos($url, 'index.php') === 0) { $url = static::getPermalink(); $url .= (strpos($url, '?') !== false ? '&' : '?') . http_build_query($args); } // if 'option' and 'view' arguments are not empty, try to route URLs if (!empty($args['option']) && (!empty($args['view']) || !empty($args['Itemid']))) { $url = static::replace($args, $url); } /** * Prepend base URL in case the system * was not able to find a permalink. * * @since 10.1.29 */ if (preg_match("/^\?/", $url)) { $url = JUri::root() . $url; } // replace & with &amp; for XML compliance if ($xhtml) { $url = str_replace('&', '&amp;', $url); } if ($ssl == 1) { // force HTTPS $url = str_replace('http://', 'https://', $url); } else if ($ssl == 2) { // force HTTP $url = str_replace('https://', 'http://', $url); } return $url; } /** * Proxy for underscore method. * Needed to bypass the issue reported here: * @link https://meta.trac.wordpress.org/ticket/3601 * * @since 10.1.32 Renamed from `u`. */ public static function rewrite($url, $xhtml = true, $ssl = null) { return static::_($url, $xhtml, $ssl); } /** * Replaces the plain URL with the rewritten one. * * @param array $args The URL parts. * @param string $url The URL to rewrite. * * @return string The rewritten URL, if possible. * * @uses getPermalink() * @uses matchShortcode() * @uses withQueryString() */ protected static function replace(array $args, $url) { // make safe by replacing initial 'com_' (if any) $option = preg_replace("/^com_/", "", $args['option']); unset($args['option']); // check if the shortcodes have been already cached if (!isset(static::$shortcodes[$option])) { // get model to access shortcodes $model = JModel::getInstance($option, 'shortcodes', 'admin'); /** * Make sure the model exists for the requested option. * * @since 10.1.19 */ if ($model) { // cache the component shortcodes static::$shortcodes[$option] = $model->all(); } else { // create an empty list static::$shortcodes[$option] = array(); } } $post_id = false; if (!empty($args['view'])) { // try to rewrite the URL with a matching shortcode $post_id = static::matchShortcode(static::$shortcodes[$option], $args); } if (!$post_id && !empty($args['Itemid'])) { // if the Itemid is set, get the POST permalink directly $post_id = (int) $args['Itemid']; } if ($post_id) { $url = static::getPermalink($post_id); $url = static::withQueryString($url, $post_id, static::$shortcodes[$option], $args); } /** * Try to instantiate an external router. * * @since 10.1.19 */ $router = JFactory::getApplication()->getRouter($option); if ($router) { // route URL using the specified arguments $url = $router->build($url); } return $url; } /** * Wrapper method to extend the function used to obtain * the current permalink or the post link. * * @param mixed $post_id The post ID. Null to obtain the current link. * * @return string The permalink. */ protected static function getPermalink($post_id = null) { // if the post ID is set, get the permalink directly if ($post_id) { /** * Added support to WPML, since it is unable to properly obtain the * correct permalink depending the requested language tag. * * @since 10.1.55 */ $wpml_post_lang = apply_filters('wpml_post_language_details', null, $post_id); /** * Make sure we haven't received an error object from WPML. * * @since 10.1.60 */ if ($wpml_post_lang && !is_wp_error($wpml_post_lang)) { // WPML supported, go ahead return apply_filters('wpml_permalink', get_permalink($post_id), $wpml_post_lang['language_code']); } // WPML not supported, fallback to the default behavior return get_permalink($post_id); } // otherwise get the current permalink $link = get_permalink(); // return the permalink only if it is not empty if ($link) { return $link; } // obtain the post ID from the current URL $post_id = url_to_postid(JUri::current()); // if the current URL doesn't match any post, return an empty string if (!$post_id) { return ''; } // recursive call to obtain the permalink of the post found return static::getPermalink($post_id); } /** * Returns a URL containing the specified query string. * * @param string $url The URL to which append the query string. * @param integer $post_id The post ID. * @param array $shortcodes The shortcodes list. * @param array $args The args to build the query string. * * @return string The URL with the query string. */ protected static function withQueryString($url, $post_id, array $shortcodes, array $args = array()) { // unset always the Item ID unset($args['Itemid']); // iterate the shortcodes foreach ($shortcodes as $shortcode) { // process only if the given post ID matches the one assigned to the shortcode if ($post_id == $shortcode->post_id) { // get shortcodes parameters, view and language $json = (array) json_decode($shortcode->json, true); $json['view'] = $shortcode->type; $json['lang'] = $shortcode->lang; // get the keys that the shortcode doesn't contain $diff = self::diff($args, $json); // if there are differences between the args, append them to the URL if (count($diff)) { $url .= (strpos($url, '?') !== false ? '&' : '?') . http_build_query($diff); } // return the URL with the query string (do not proceed) return $url; } } if (count($args)) { /** * if no shortcodes found, attach the whole query string * * @since 10.1.8 */ $url .= (strpos($url, '?') !== false ? '&' : '?') . http_build_query($args); } // nothing found, return plain URL return $url; } /** * Checks if there is a shortcode object that matches the given URL parts. * In addition, the shortcode MUST own an existing post ID. * * @param array $shortcodes The shortcode objects list. * @param array $args The URL query parts. * * @return mixed The post ID found, false on failure. */ protected static function matchShortcode($shortcodes, array $args) { $post_id = false; $last_count = 0; if (empty($args['lang'])) { // inject current langtag within URL arguments, only if not specified // by query string $args['lang'] = JFactory::getLanguage()->getTag(); } foreach ($shortcodes as $shortcode) { $count = 0; // decode JSON data $json = json_decode($shortcode->json, true); // inject VIEW name $json['view'] = $shortcode->type; // inject shortcode LANGTAG $json['lang'] = $shortcode->lang; // proceed only if the view is the same if ($json['view'] == $args['view']) { // iterate the URL arguments foreach ($args as $k => $v) { // the property should be matched only if the shortcode // owns a parameter with the same name (defined in xml view) // and value. if (isset($json[$k])) { /** * Improved the comparison of the language tag, which might be * handled in different ways from different sections of WP. * * @since 10.1.38 */ if ($k === 'lang') { // check whether at least a language code doesn't mention the country code if (!preg_match("/[_\-]/", $json[$k]) || !preg_match("/[_\-]/", $v)) { // remove country code from locale assigned to the shortcode $json[$k] = preg_split("/[_\-]/", $json[$k]); $json[$k] = array_shift($json[$k]); // remove country code from URL locale $v = preg_split("/[_\-]/", $v); $v = array_shift($v); } // normalize locale separators $json[$k] = str_replace('-', '_', $json[$k]); $v = str_replace('-', '_', $v); } // make sure both the values are scalar if (is_scalar($json[$k]) && is_scalar($v)) { if (!strcmp($json[$k], $v)) { // if the shortcode owns the parameter and it is equals // to the one specified in the URL, increase the counter $count++; } } /** * Added support for arrays in query string, such as cid[]. * * @since 10.1.27 */ else { // treat both the arguments as arrays $json[$k] = (array) $json[$k]; $v = (array) $v; // sort them to maximize the possibility of having matching URLs sort($json[$k]); sort($v); if ($json[$k] == $v) { // increase the counter as we have a matching parameter $count++; } } } } // if the count of matches is higher than the last one, // update the flags with the shortcode post ID and the current count if ($count > $last_count) { $post_id = $shortcode->post_id; $last_count = $count; } } } return $post_id; } /** * Implementation of array_diff_assoc with recursion. * * @param array array1 The array to compare from. * @param array array2 An array to compare against. * * @return array Returns an array containing all the values from array1 * that are not present in the second array. * * @since 10.1.27 */ private static function diff($array1, $array2) { $difference = array(); foreach ($array1 as $key => $value) { /** * Improved the comparison of the language tag, which might be * handled in different ways from different sections of WP. * * @since 10.1.49 */ if ($key === 'lang') { // check whether at least a language code doesn't mention the country code if (!preg_match("/[_\-]/", $value) || !preg_match("/[_\-]/", $array2[$key])) { // remove country code from locale assigned to the shortcode $array2[$key] = preg_split("/[_\-]/", $array2[$key]); $array2[$key] = array_shift($array2[$key]); // remove country code from URL locale $value = preg_split("/[_\-]/", $value); $value = array_shift($value); } // normalize locale separators $array2[$key] = str_replace('-', '_', $array2[$key]); $value = str_replace('-', '_', $value); } if (is_array($value)) { if (!isset($array2[$key]) || !is_array($array2[$key])) { $difference[$key] = $value; } else { $new_diff = self::diff($value, $array2[$key]); if (!empty($new_diff)) { $difference[$key] = $new_diff; } } } else if (!array_key_exists($key, $array2) || $array2[$key] !== $value) { $difference[$key] = $value; } } return $difference; } }