From 0007692c64883717ddaf66f5f8adf271a521b6bd Mon Sep 17 00:00:00 2001 From: Meik Sievertsen Date: Sun, 28 Dec 2008 10:51:39 +0000 Subject: [PATCH] add url class git-svn-id: file:///svn/phpbb/trunk@9237 89ea8834-ac86-4346-8a33-228a782c2dd0 --- phpBB/includes/core/url.php | 740 ++++++++++++++++++++++++++++++++++++ 1 file changed, 740 insertions(+) create mode 100644 phpBB/includes/core/url.php diff --git a/phpBB/includes/core/url.php b/phpBB/includes/core/url.php new file mode 100644 index 0000000000..c073c48606 --- /dev/null +++ b/phpBB/includes/core/url.php @@ -0,0 +1,740 @@ + + * @copyright 2006 Project Minerva Team + * @param string $path The path which we should attempt to resolve. + * @return mixed realpath + * @access private + */ + private function own_realpath($path) + { + // Switch to use UNIX slashes + $path = str_replace(DIRECTORY_SEPARATOR, '/', $path); + $path_prefix = ''; + + // Determine what sort of path we have + if ($this->is_absolute($path)) + { + $absolute = true; + + if ($path[0] == '/') + { + // Absolute path, *NIX style + $path_prefix = ''; + } + else + { + // Absolute path, Windows style + // Remove the drive letter and colon + $path_prefix = $path[0] . ':'; + $path = substr($path, 2); + } + } + else + { + // Relative Path + // Prepend the current working directory + if (function_exists('getcwd')) + { + // This is the best method, hopefully it is enabled! + $path = str_replace(DIRECTORY_SEPARATOR, '/', getcwd()) . '/' . $path; + $absolute = true; + if (preg_match('#^[a-z]:#i', $path)) + { + $path_prefix = $path[0] . ':'; + $path = substr($path, 2); + } + else + { + $path_prefix = ''; + } + } + else if (!empty($_SERVER['SCRIPT_FILENAME'])) + { + // Warning: If chdir() has been used this will lie! + // Warning: This has some problems sometime (CLI can create them easily) + $path = str_replace(DIRECTORY_SEPARATOR, '/', dirname($_SERVER['SCRIPT_FILENAME'])) . '/' . $path; + $absolute = true; + $path_prefix = ''; + } + else + { + // We have no way of getting the absolute path, just run on using relative ones. + $absolute = false; + $path_prefix = '.'; + } + } + + // Remove any repeated slashes + $path = preg_replace('#/{2,}#', '/', $path); + + // Remove the slashes from the start and end of the path + $path = trim($path, '/'); + + // Break the string into little bits for us to nibble on + $bits = explode('/', $path); + + // Remove any . in the path, renumber array for the loop below + $bits = array_values(array_diff($bits, array('.'))); + + // Lets get looping, run over and resolve any .. (up directory) + for ($i = 0, $max = sizeof($bits); $i < $max; $i++) + { + // @todo Optimise + if ($bits[$i] == '..' ) + { + if (isset($bits[$i - 1])) + { + if ($bits[$i - 1] != '..') + { + // We found a .. and we are able to traverse upwards, lets do it! + unset($bits[$i]); + unset($bits[$i - 1]); + $i -= 2; + $max -= 2; + $bits = array_values($bits); + } + } + else if ($absolute) // ie. !isset($bits[$i - 1]) && $absolute + { + // We have an absolute path trying to descend above the root of the filesystem + // ... Error! + return false; + } + } + } + + // Prepend the path prefix + array_unshift($bits, $path_prefix); + + $resolved = ''; + + $max = sizeof($bits) - 1; + + // Check if we are able to resolve symlinks, Windows cannot. + $symlink_resolve = (function_exists('readlink')) ? true : false; + + foreach ($bits as $i => $bit) + { + if (@is_dir("$resolved/$bit") || ($i == $max && @is_file("$resolved/$bit"))) + { + // Path Exists + if ($symlink_resolve && is_link("$resolved/$bit") && ($link = readlink("$resolved/$bit"))) + { + // Resolved a symlink. + $resolved = $link . (($i == $max) ? '' : '/'); + continue; + } + } + else + { + // Something doesn't exist here! + // This is correct realpath() behaviour but sadly open_basedir and safe_mode make this problematic + // return false; + } + $resolved .= $bit . (($i == $max) ? '' : '/'); + } + + // @todo If the file exists fine and open_basedir only has one path we should be able to prepend it + // because we must be inside that basedir, the question is where... + // @internal The slash in is_dir() gets around an open_basedir restriction + if (!@file_exists($resolved) || (!is_dir($resolved . '/') && !is_file($resolved))) + { + return false; + } + + // Put the slashes back to the native operating systems slashes + $resolved = str_replace('/', DIRECTORY_SEPARATOR, $resolved); + + // Check for DIRECTORY_SEPARATOR at the end (and remove it!) + if (substr($resolved, -1) == DIRECTORY_SEPARATOR) + { + return substr($resolved, 0, -1); + } + + // We got here, in the end! + return $resolved; + } + + /** + * A wrapper for realpath + * + * @param string $path The path which we should attempt to resolve. + * @staticvar string $_phpbb_realpath_exist This is set to false if the PHP function realpath() is not accessible or returns incorrect results + * + * @return string Real path + * @access public + */ + public function realpath($path) + { + static $_phpbb_realpath_exist; + + if (!isset($_phpbb_realpath_exist)) + { + $_phpbb_realpath_exist = (!function_exists('realpath')) ? false : true; + } + + if (!$_phpbb_realpath_exist) + { + return $this->own_realpath($path); + } + + $realpath = realpath($path); + + // Strangely there are provider not disabling realpath but returning strange values. :o + // We at least try to cope with them. + if ($realpath === $path || $realpath === false) + { + $_phpbb_realpath_exist = false; + return $this->own_realpath($path); + } + + // Check for DIRECTORY_SEPARATOR at the end (and remove it!) + if (substr($realpath, -1) == DIRECTORY_SEPARATOR) + { + $realpath = substr($realpath, 0, -1); + } + + return $realpath; + } + + /** + * URL wrapper + * All urls are run through this... either after {@link append_sid() append_sid} or directly + * + * @param string $url URL to process + * @return string URL + * @access public + */ + public function get($url) + { + return $url; + } + + /** + * Append session id to url. + * + * Examples: + * + * append_sid(PHPBB_ROOT_PATH . 'viewtopic.' . PHP_EXT . '?t=1&f=2'); // VALID + * append_sid(PHPBB_ROOT_PATH . 'viewtopic.' . PHP_EXT, 't=1&f=2'); // VALID + * append_sid('viewtopic', 't=1&f=2'); // short notation of the above example - VALID + * append_sid('viewtopic', 't=1&f=2', false); // Instead of & use & + * append_sid('viewtopic', array('t' => 1, 'f' => 2)); // Instead of parameter in string notation, use an array + * + * + * @param string $url The url the session id needs to be appended to (without parameter) + * @param string|array $params String or array of additional url parameter. + * @param bool $is_amp Is url using & (true) or & (false) + * @param string $session_id Possibility to use a custom session id instead of the global one. This also forces the use of a session id. + * + * @plugin-support default, return + * @return string URL + * @access public + */ + public function append_sid($url, $params = false, $is_amp = true, $session_id = false) + { + static $parsed_urls = array(); + + // The following code is used to make sure such calls like append_sid('viewtopic') (ommitting phpbb_root_path and php_ext) work as intended + if (isset($parsed_urls[$url])) + { + // Set an url like 'viewtopic' to PHPBB_ROOT_PATH . 'viewtopic.' . PHP_EXT + $url = $parsed_urls[$url]; + } + else + { + // If we detect an url without root path and extension, and also not a relative or absolute path, we add it and put it to the parsed urls + if (strpos($url, '.' . PHP_EXT) === false && $url[0] != '.' && $url[0] != '/') + { + $parsed_urls[$url] = $url = PHPBB_ROOT_PATH . $url . '.' . PHP_EXT; + } + } + + if (empty($params)) + { + $params = false; + } + + $params_is_array = is_array($params); + + // Get anchor + $anchor = ''; + if (strpos($url, '#') !== false) + { + list($url, $anchor) = explode('#', $url, 2); + $anchor = '#' . $anchor; + } + else if (!$params_is_array && strpos($params, '#') !== false) + { + list($params, $anchor) = explode('#', $params, 2); + $anchor = '#' . $anchor; + } + + // Handle really simple cases quickly + if ($session_id === false && !phpbb::$user->need_sid && empty(phpbb::$user->extra_url) && !$params_is_array && !$anchor) + { + if ($params === false) + { + return $this->get($url); + } + + $url_delim = (strpos($url, '?') === false) ? '?' : (($is_amp) ? '&' : '&'); + return $this->get($url . ($params !== false ? $url_delim . $params : '')); + } + + // Assign sid if session id is not specified + if (phpbb::$user->need_sid && $session_id === false) + { + $session_id = phpbb::$user->session_id; + } + + $amp_delim = ($is_amp) ? '&' : '&'; + $url_delim = (strpos($url, '?') === false) ? '?' : $amp_delim; + + // Appending custom url parameter? + $append_url = (!empty(phpbb::$user->extra_url)) ? implode($amp_delim, phpbb::$user->extra_url) : ''; + + if ($this->method_inject(__FUNCTION__)) $this->call_inject(__FUNCTION__, array('default', &$url, &$params, &$session_id, &$append_url, &$anchor, &$amp_delim, &$url_delim)); + + if ($this->method_inject(__FUNCTION__, 'return')) + { + $url = $this->call_inject(__FUNCTION__, array('return', $url, $params, $session_id, $append_url, $anchor, $amp_delim, $url_delim)); + return $this->get($url); + } + + // Use the short variant if possible ;) + if ($params === false) + { + // Append session id + if (!$session_id) + { + return $this->get($url . (($append_url) ? $url_delim . $append_url : '') . $anchor); + } + else + { + return $this->get($url . (($append_url) ? $url_delim . $append_url . $amp_delim : $url_delim) . 'sid=' . $session_id . $anchor); + } + } + + // Build string if parameters are specified as array + if ($params_is_array) + { + $output = array(); + + foreach ($params as $key => $item) + { + if ($item === NULL) + { + continue; + } + + if ($key == '#') + { + $anchor = '#' . $item; + continue; + } + + $output[] = $key . '=' . $item; + } + + $params = implode($amp_delim, $output); + } + + // Append session id and parameter + return $this->get($url . (($append_url) ? $url_delim . $append_url : '') . (($params) ? (($append_url) ? $amp_delim : $url_delim) . $params : '') . ((!$session_id) ? '' : $amp_delim . 'sid=' . $session_id) . $anchor); + } + + /** + * Generate board url (example: http://www.example.com/phpBB) + * + * @param bool $without_script_path If set to true the script path gets not appended (example: http://www.example.com instead of http://www.example.com/phpBB) + * @return string Board URL + * @access public + */ + public function generate_board_url($without_script_path = false) + { + $server_name = phpbb::$user->system['host']; + $server_port = phpbb::$user->system['port']; + + // Forcing server vars is the only way to specify/override the protocol + if (phpbb::$config['force_server_vars'] || !$server_name) + { + $server_protocol = (phpbb::$config['server_protocol']) ? phpbb::$config['server_protocol'] : ((phpbb::$config['cookie_secure']) ? 'https://' : 'http://'); + $server_name = phpbb::$config['server_name']; + $server_port = (int) phpbb::$config['server_port']; + $script_path = phpbb::$config['script_path']; + + $url = $server_protocol . $server_name; + $cookie_secure = phpbb::$config['cookie_secure']; + } + else + { + // Do not rely on cookie_secure, users seem to think that it means a secured cookie instead of an encrypted connection + $cookie_secure = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ? 1 : 0; + $url = (($cookie_secure) ? 'https://' : 'http://') . $server_name; + + $script_path = phpbb::$user->system['page']['root_script_path']; + } + + if ($server_port && (($cookie_secure && $server_port <> 443) || (!$cookie_secure && $server_port <> 80))) + { + // HTTP HOST can carry a port number (we fetch $user->host, but for old versions this may be true) + if (strpos($server_name, ':') === false) + { + $url .= ':' . $server_port; + } + } + + if (!$without_script_path) + { + $url .= $script_path; + } + + // Strip / from the end + if (substr($url, -1, 1) == '/') + { + $url = substr($url, 0, -1); + } + + return $url; + } + + /** + * Redirects the user to another page then exits the script nicely + * This function is intended for urls within the board. It's not meant to redirect to cross-domains. + * + * @param string $url The url to redirect to + * @param bool $return If true, do not redirect but return the sanitized URL. + * @param bool $disable_cd_check If true, redirect() will support redirects to an external domain. + * If false, the redirect points to the boards url if it does not match the current domain. + * + * @return mixed Sanitized URL if $return is true + * @access public + */ + public function redirect($url, $return = false, $disable_cd_check = false) + { + if (empty(phpbb::$user->lang)) + { + phpbb::$user->add_lang('common'); + } + + if (!$return) + { + garbage_collection(); + } + + // Make sure no &'s are in, this will break the redirect + $url = str_replace('&', '&', $url); + + // Determine which type of redirect we need to handle... + $url_parts = parse_url($url); + + if ($url_parts === false) + { + // Malformed url, redirect to current page... + $url = $this->generate_board_url() . '/' . phpbb::$user->system['page']['page']; + } + else if (!empty($url_parts['scheme']) && !empty($url_parts['host'])) + { + // Attention: only able to redirect within the same domain if $disable_cd_check is false (yourdomain.com -> www.yourdomain.com will not work) + if (!$disable_cd_check && $url_parts['host'] !== phpbb::$user->system['host']) + { + $url = $this->generate_board_url(); + } + } + else if ($url[0] == '/') + { + // Absolute uri, prepend direct url... + $url = $this->generate_board_url(true) . $url; + } + else + { + // Relative uri + $pathinfo = pathinfo($url); + + // Is the uri pointing to the current directory? + if ($pathinfo['dirname'] == '.') + { + $url = str_replace('./', '', $url); + + // Strip / from the beginning + if ($url && substr($url, 0, 1) == '/') + { + $url = substr($url, 1); + } + + if (phpbb::$user->system['page']['page_dir']) + { + $url = $this->generate_board_url() . '/' . phpbb::$user->system['page']['page_dir'] . '/' . $url; + } + else + { + $url = $this->generate_board_url() . '/' . $url; + } + } + else + { + // Used ./ before, but PHPBB_ROOT_PATH is working better with urls within another root path + $root_dirs = explode('/', str_replace('\\', '/', $this->realpath(PHPBB_ROOT_PATH))); + $page_dirs = explode('/', str_replace('\\', '/', $this->realpath($pathinfo['dirname']))); + $intersection = array_intersect_assoc($root_dirs, $page_dirs); + + $root_dirs = array_diff_assoc($root_dirs, $intersection); + $page_dirs = array_diff_assoc($page_dirs, $intersection); + + $dir = str_repeat('../', sizeof($root_dirs)) . implode('/', $page_dirs); + + // Strip / from the end + if ($dir && substr($dir, -1, 1) == '/') + { + $dir = substr($dir, 0, -1); + } + + // Strip / from the beginning + if ($dir && substr($dir, 0, 1) == '/') + { + $dir = substr($dir, 1); + } + + $url = str_replace($pathinfo['dirname'] . '/', '', $url); + + // Strip / from the beginning + if (substr($url, 0, 1) == '/') + { + $url = substr($url, 1); + } + + $url = (!empty($dir) ? $dir . '/' : '') . $url; + $url = $this->generate_board_url() . '/' . $url; + } + } + + // Make sure no linebreaks are there... to prevent http response splitting for PHP < 4.4.2 + if (strpos(urldecode($url), "\n") !== false || strpos(urldecode($url), "\r") !== false || strpos($url, ';') !== false) + { + trigger_error('Tried to redirect to potentially insecure url.', E_USER_ERROR); + } + + // Now, also check the protocol and for a valid url the last time... + $allowed_protocols = array('http', 'https', 'ftp', 'ftps'); + $url_parts = parse_url($url); + + if ($url_parts === false || empty($url_parts['scheme']) || !in_array($url_parts['scheme'], $allowed_protocols)) + { + trigger_error('Tried to redirect to potentially insecure url.', E_USER_ERROR); + } + + if ($return) + { + return $url; + } + + // Redirect via an HTML form for PITA webservers + if (@preg_match('#Microsoft|WebSTAR|Xitami#', getenv('SERVER_SOFTWARE'))) + { + header('Refresh: 0; URL=' . $url); + + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo '' . phpbb::$user->lang['REDIRECT'] . ''; + echo ''; + echo ''; + echo '
' . phpbb::$user->lang('URL_REDIRECT', '', '') . '
'; + echo ''; + echo ''; + + exit; + } + + // Behave as per HTTP/1.1 spec for others + header('Location: ' . $url); + exit; + } + + /** + * Meta refresh assignment + * + * If the template object is present, the META template variable holds the meta refresh, else a normal redirect is done. + * + * @param int $time The time in seconds when to redirect + * @param string $url The URL to redirect to + * @param bool $disable_cd_check If true, redirect() will support redirects to an external domain. + * If false, the redirect points to the boards url if it does not match the current domain. + * + * @return string Sanitized URL + * @plugin-support return + * @access public + */ + public function meta_refresh($time, $url, $disable_cd_check = false) + { + if (phpbb::registered('template')) + { + $result_url = $this->redirect($url, true, $disable_cd_check); + $result_url = str_replace('&', '&', $result_url); + + // For XHTML compatibility we change back & to & + phpbb::$template->assign_var('META', ''); + } + else + { + $this->redirect($url, false, $disable_cd_check); + } + + return ($this->method_inject(__FUNCTION__, 'return')) ? $this->call_inject(__FUNCTION__, array('return', $result_url, $time, $url, $disable_cd_check)) : $result_url; + } + + /** + * Re-Apply session id after page reloads + * + * @param string $url URL to re-apply session id to + * @return string URL with re-applied session id + * @access public + */ + public function reapply_sid($url) + { + if ($url === 'index.' . PHP_EXT) + { + return $this->append_sid('index.' . PHP_EXT); + } + else if ($url === PHPBB_ROOT_PATH . 'index.' . PHP_EXT) + { + return $this->append_sid('index'); + } + + // Remove previously added sid + if (strpos($url, '?sid=') !== false) + { + $url = preg_replace('/(\?)sid=[a-z0-9]+(&|&)?/', '\1', $url); + } + else if (strpos($url, '&sid=') !== false) + { + $url = preg_replace('/&sid=[a-z0-9]+(&)?/', '\1', $url); + } + else if (strpos($url, '&sid=') !== false) + { + $url = preg_replace('/&sid=[a-z0-9]+(&)?/', '\1', $url); + } + + return $this->append_sid($url); + } + + /** + * Returns url from the session/current page with an re-appended SID with optionally stripping vars from the url + * + * @param array|string $strip_vars An array containing variables to be stripped from the URL. + * @return string Current page URL with re-applied SID and optionally stripped parameter + * @access public + */ + public function build_url($strip_vars = false) + { + // Append SID + $redirect = $this->append_sid(phpbb::$user->system['page']['page'], false, false); + + // Add delimiter if not there... + if (strpos($redirect, '?') === false) + { + $redirect .= '?'; + } + + // Strip vars... + if ($strip_vars !== false && strpos($redirect, '?') !== false) + { + if (!is_array($strip_vars)) + { + $strip_vars = array($strip_vars); + } + + $query = $_query = array(); + + $args = substr($redirect, strpos($redirect, '?') + 1); + $args = ($args) ? explode('&', $args) : array(); + $redirect = substr($redirect, 0, strpos($redirect, '?')); + + foreach ($args as $argument) + { + $arguments = explode('=', $argument); + $key = $arguments[0]; + unset($arguments[0]); + + $query[$key] = implode('=', $arguments); + } + + // Strip the vars off + foreach ($strip_vars as $strip) + { + if (isset($query[$strip])) + { + unset($query[$strip]); + } + } + + // Glue the remaining parts together... already urlencoded + foreach ($query as $key => $value) + { + $_query[] = $key . '=' . $value; + } + $query = implode('&', $_query); + + $redirect .= ($query) ? '?' . $query : ''; + } + + return PHPBB_ROOT_PATH . str_replace('&', '&', $redirect); + } +} + +?> \ No newline at end of file