<?php
/*========================================================================*\
|| ###################################################################### ||
|| # vBulletin 6.2.3 - Licence Number L3138846
|| # ------------------------------------------------------------------ # ||
|| # Copyright 2000-2026 MH Sub I, LLC dba vBulletin. All Rights Reserved.  # ||
|| # This file may not be redistributed in whole or significant part.   # ||
|| # ----------------- VBULLETIN IS NOT FREE SOFTWARE ----------------- # ||
|| # http://www.vbulletin.com | http://www.vbulletin.com/license.html   # ||
|| ###################################################################### ||
\*========================================================================*/

class vB5_Template_Url
{
	const PLACEHOLDER_PREFIX = '!!VB:URL';
	const PLACEHOLDER_SUFIX = '!!';

	/**
	 * Singleton instance of this class.
	 *
	 * @var	vB5_Template_Url
	 */
	protected static $instance;

	/**
	 * Array of URL definitions:
	 * [route] - Route identifier (routeid or name)
	 * [$data] - Data for building route
	 * [extra] - Additional data to be added
	 * [options] - Options for building URL
	 *	- noBaseUrl: skips adding the baseurl
	 *
	 * @var	array
	 */
	protected $delayedUrlInfo = [];

	/**
	 * Array of already built URLs.
	 * Each element is a key:value pair of {String Hashkey}:{String Built URL}
	 *
	 * @var array
	 */
	protected $builtUrlList = [];

	/**
	 * Flag, whether finalBuildUrls() called route API's preloadRoutes() or not
	 *
	 * @var bool
	 */
	protected $loadedUrlKeys = false;

	/**
	 * Singleton instance getter
	 *
	 * @return vB5_Template_Url
	 */
	public static function instance()
	{
		if (!isset(self::$instance))
		{
			$c = __CLASS__;
			self::$instance = new $c;
		}

		return self::$instance;
	}

	/**
	 * Returns the full URL placeholder based on the passed hash
	 *
	 * @param	string	The hash
	 *
	 * @return	string	The full placeholder
	 */
	protected function getPlaceholder($hash)
	{
		return self::PLACEHOLDER_PREFIX . $hash . self::PLACEHOLDER_SUFIX;
	}

	/**
	 * Registers the information needed (route, data, etc) to create a URL and returns a placeholder
	 * so that template rendering can continue. The placeholders will be replaced in one go at the
	 * end of the rendering process.
	 *
	 * @param int|string $route -- The textual name of a route, along with optional modifiers. If
	 *				modifiers are added they are separated with a pipe (|). OR the
	 *				integer routeid of a route.
	 * @param array $data -- Data needed to generate the URL. Normally the node array is passed. Fields
	 *			potentially used are:
	 *				'title', 'startertitle', 'channeltitle', 'urlident','starterurlident',
	 *				'channelurlident', 'page', 'channelroute','starterroute' ,
	 *				'contenttypeid', 'starter', 'channelid', 'nodeid', 'routeid', 'userid',
	 *				'authorname', 'contentpagenum'
	 * @param array $extra -- Array of key:value pairs to be added to the URL as query parameters.
	 * @param array $options -- Array of options to modify the generated URL. Currently supports:
	 *				(this list may not be complete)
	 *				'anchor' => The anchor name for the #anchor hash at the end of the URL.
	 *
	 * @return string -- A string placeholder which is temporarily inserted in the rendered template.
	 */
	public function register($route, $data = [], $extra = [], $options = [])
	{
		if (empty($data))
		{
			$data = [];
		}
		else if (!is_array($data))
		{
			throw new vB5_Exception_Api('route', 'getUrl', $data, 'Invalid data for URL creation');
		}

		if (empty($extra))
		{
			$extra = [];
		}
		else if (!is_array($extra))
		{
			throw new vB5_Exception_Api('route', 'getUrl', $extra, 'Invalid extra data for URL creation');
		}
		//Most of the time we have a node record. Let's keep just what we need;
		if (!empty($data['nodeid']) AND isset($data['contenttypeid']))
		{
			$include = [
				'title',
				'startertitle',
				'channeltitle',
				'urlident',
				'starterurlident',
				'channelurlident',
				'page',
				'channelroute',
				'starterroute',
				'contenttypeid',
				'starter',
				'channelid',
				'nodeid',
				'routeid',
				'userid',
				'authorname',
				'contentpagenum',
			];

			//Let's unset the values we don't need
			foreach ($data AS $key => $dummy)
			{
				if (!in_array($key, $include))
				{
					unset($data[$key]);
				}
			}
		}

		$hash = md5($route . serialize($data). serialize($extra) . serialize($options));

		//We often call for the same url more than once on the page. Don't do this more than once.
		$replaceStr = $this->getPlaceholder($hash);
		if (empty($this->builtUrlList[$replaceStr]) AND empty($this->delayedUrlInfo[$replaceStr]))
		{
			$this->delayedUrlInfo[$replaceStr] = [
				'route' => $route ?? '',
				'data' => $data,
				'extra' => $extra,
				'options' => $options
			];
		}

		if (count($this->delayedUrlInfo) >= 3000)
		{
			$this->finalBuildUrls(array_keys($this->delayedUrlInfo));
		}

		return $replaceStr;
	}

	/**
	 * Returns the URLs for the routes with the passed parameters
	 *
	 * @param String[] 	$delayedUrlKeys 	HashKeys that exist in the URL definitions list class var $this->delayedUrlInfo
	 *
	 * @return String[]
	 *		hashKey => replacement URL
	 */
	public function finalBuildUrls($delayedUrlKeys)
	{
		// the only reason for this method to be public is that it is required in vB5_Frontend_Controller_Page::index
		// todo: check if we can avoid this

		if (!$this->loadedUrlKeys)
		{
			$urlIds = [];
			foreach($this->delayedUrlInfo as $urlInfo)
			{
				$options = explode('|', $urlInfo['route']);
				$urlIds[] = $options[0];
			}
			$check = Api_InterfaceAbstract::instance()->callApi('route', 'preloadRoutes', ['routeid' => array_unique($urlIds)]);
			$this->loadedUrlKeys = true;
		}

		$missing_replacements = [];
		$replacements = [];

		$info = [];
		$baseUrl =  vB5_Template_Options::instance()->get('options.frontendurl');
		foreach ($delayedUrlKeys AS $hashKey)
		{
			// I honestly have NO idea when this happens, but I'm reluctant to change old error handling code.
			// This might not be the best way to go about this, since *if* this happens, we will be constantly
			// expanding the replacements list when every node parse calls vb5_template_url->replacePlaceholders()
			// At the moment I'm assuming that this actually does NOT happen, while being too afraid to chop it out.
			if (!isset($this->delayedUrlInfo[$hashKey]))
			{
				$missing_replacements[$hashKey] = $this->addBaseUrl($baseUrl, '#', false);;
				continue; // if it's missing the required information, do not bother trying to build the URL
			}

			$info[$hashKey]['route'] = $this->delayedUrlInfo[$hashKey]['route'];
			$info[$hashKey]['data'] = $this->delayedUrlInfo[$hashKey]['data'];
			$info[$hashKey]['extra'] = $this->delayedUrlInfo[$hashKey]['extra'];
			$info[$hashKey]['options'] = $this->delayedUrlInfo[$hashKey]['options'];

			unset($this->delayedUrlInfo[$hashKey]);
		}

		if (!empty($info))
		{
			$replacements = Api_InterfaceAbstract::instance()->callApi('route', 'getUrls', ['info' => $info]);
			if (isset($replacements['errors']))
			{
				// VBV-9940 suggests that sometimes the API might return errors, and in that case, we should just not do the replacement.
				// In order to ensure that the replacements array will only contain strings, not other arrays, we just ditch the errors array if we get one.
				$replacements = [];
			}

			foreach ($info AS $hashKey => $item)
			{
				$routeoptions = array_flip(explode('|', $item['route']));

				//we need to append the base url because the routes incorrect return relative urls that
				//start with '/'.  Some of the routes have been fixed, but others haven't.  We'll
				//handle both for the time being.
				if (empty($item['options']['noBaseUrl']) AND empty($routeoptions['fullurl']) AND empty($routeoptions['bburl']))
				{
					$replacements[$hashKey] = $this->addBaseUrl($baseUrl, $replacements[$hashKey], !empty($routeoptions['urlencode']));
				}
			}
		}

		if (!empty($missing_replacements))
		{
			$replacements += $missing_replacements;
		}

		if (!empty($replacements))
		{
			$this->builtUrlList += $replacements;
		}

		return $this->builtUrlList;
	}

	private function addBaseUrl($baseUrl, $url, $encoded)
	{
		$slash = '/';
		$result = $baseUrl;

		if ($encoded)
		{
			$slash = urlencode($slash);
			$result = urlencode($result);
		}

		if (!$url OR $url[0] != $slash)
		{
			$result .= $slash;
		}

		$result .= $url;

		return $result;
	}

	/**
	 * Replaces the placeholders with the actual URL in the passed content.
	 * Builds any URLs that have not been built yet.
	 *
	 * @param	string	The content containing the placeholders (Passed by reference)
	 *
	 * @return	null
	 */
	public function replacePlaceholders(&$content)
	{
		if (!empty($this->delayedUrlInfo))
		{
			$this->finalBuildUrls(array_keys($this->delayedUrlInfo));
		}

		if (!empty($this->builtUrlList))
		{
			$content = str_replace(array_keys($this->builtUrlList), $this->builtUrlList, $content);
		}
	}
}

/*=========================================================================*\
|| #######################################################################
|| # Downloaded: 13:27, Sun Aug 30th 2026
|| # CVS: $RCSfile$ - $Revision: 122384 $
|| #######################################################################
\*=========================================================================*/
