HTTP_X_FORWARDED_FOR > HTTP_CLIENT_IP > HTTP_VIA > REMOTE_ADDR $headers = [ 'X-Forwarded-For', 'HTTP_X_FORWARDED_FOR', 'HTTP_CLIENT_IP', 'HTTP_VIA', 'REMOTE_ADDR' ]; foreach( $headers as $header ) { if ( !empty( $_SERVER[ $header ] ) ) { $ip = $_SERVER[ $header ]; break; } } // headers can contain multiple IPs (X-Forwarded-For = client, proxy1, proxy2). Take first one. if ( strpos( $ip, ',' ) !== false ) $ip = substr( $ip, 0, strpos( $ip, ',' ) ); return (string)yourls_apply_filter( 'get_IP', yourls_sanitize_ip( $ip ) ); } /** * Get next id a new link will have if no custom keyword provided * * @since 1.0 * @return int id of next link */ function yourls_get_next_decimal() { return (int)yourls_apply_filter( 'get_next_decimal', (int)yourls_get_option( 'next_id' ) ); } /** * Update id for next link with no custom keyword * * Note: this function relies upon yourls_update_option(), which will return either true or false * depending upon if there has been an actual MySQL query updating the DB. * In other words, this function may return false yet this would not mean it has functionally failed * In other words I'm not sure if we really need this function to return something :face_with_eyes_looking_up: * See issue 2621 for more on this. * * @since 1.0 * @param integer $int id for next link * @return bool true or false depending on if there has been an actual MySQL query. See note above. */ function yourls_update_next_decimal( $int = 0 ) { $int = ( $int == 0 ) ? yourls_get_next_decimal() + 1 : (int)$int ; $update = yourls_update_option( 'next_id', $int ); yourls_do_action( 'update_next_decimal', $int, $update ); return $update; } /** * Return XML output. * * @param array $array * @return string */ function yourls_xml_encode( $array ) { return (\Spatie\ArrayToXml\ArrayToXml::convert($array, '', true, 'UTF-8')); } /** * Update click count on a short URL. Return 0/1 for error/success. * * @param string $keyword * @param false|int $clicks * @return int 0 or 1 for error/success */ function yourls_update_clicks( $keyword, $clicks = false ) { // Allow plugins to short-circuit the whole function $pre = yourls_apply_filter( 'shunt_update_clicks', false, $keyword, $clicks ); if ( false !== $pre ) { return $pre; } $keyword = yourls_sanitize_keyword( $keyword ); $table = YOURLS_DB_TABLE_URL; if ( $clicks !== false && is_int( $clicks ) && $clicks >= 0 ) { $update = "UPDATE `$table` SET `clicks` = :clicks WHERE `keyword` = :keyword"; $values = [ 'clicks' => $clicks, 'keyword' => $keyword ]; } else { $update = "UPDATE `$table` SET `clicks` = clicks + 1 WHERE `keyword` = :keyword"; $values = [ 'keyword' => $keyword ]; } // Try and update click count. An error probably means a concurrency problem : just skip the update try { $result = yourls_get_db()->fetchAffected($update, $values); } catch (Exception $e) { $result = 0; } yourls_do_action( 'update_clicks', $keyword, $result, $clicks ); return $result; } /** * Return array of stats. (string)$filter is 'bottom', 'last', 'rand' or 'top'. (int)$limit is the number of links to return * * @param string $filter 'bottom', 'last', 'rand' or 'top' * @param int $limit Number of links to return * @param int $start Offset to start from * @return array Array of links */ function yourls_get_stats($filter = 'top', $limit = 10, $start = 0) { switch( $filter ) { case 'bottom': $sort_by = '`clicks`'; $sort_order = 'asc'; break; case 'last': $sort_by = '`timestamp`'; $sort_order = 'desc'; break; case 'rand': case 'random': $sort_by = 'RAND()'; $sort_order = ''; break; case 'top': default: $sort_by = '`clicks`'; $sort_order = 'desc'; break; } // Fetch links $limit = intval( $limit ); $start = intval( $start ); if ( $limit > 0 ) { $table_url = YOURLS_DB_TABLE_URL; $results = yourls_get_db()->fetchObjects( "SELECT * FROM `$table_url` WHERE 1=1 ORDER BY $sort_by $sort_order LIMIT $start, $limit;" ); $return = []; $i = 1; foreach ( (array)$results as $res ) { $return['links']['link_'.$i++] = [ 'shorturl' => yourls_link($res->keyword), 'url' => $res->url, 'title' => $res->title, 'timestamp'=> $res->timestamp, 'ip' => $res->ip, 'clicks' => $res->clicks, ]; } } $return['stats'] = yourls_get_db_stats(); $return['statusCode'] = 200; return yourls_apply_filter( 'get_stats', $return, $filter, $limit, $start ); } /** * Get total number of URLs and sum of clicks. Input: optional "AND WHERE" clause. Returns array * * The $where parameter will contain additional SQL arguments: * $where['sql'] will concatenate SQL clauses: $where['sql'] = ' AND something = :value AND otherthing < :othervalue'; * $where['binds'] will hold the (name => value) placeholder pairs: $where['binds'] = array('value' => $value, 'othervalue' => $value2) * * @param array $where See comment above * @return array */ function yourls_get_db_stats( $where = [ 'sql' => '', 'binds' => [] ] ) { $table_url = YOURLS_DB_TABLE_URL; $totals = yourls_get_db()->fetchObject( "SELECT COUNT(keyword) as count, SUM(clicks) as sum FROM `$table_url` WHERE 1=1 " . $where['sql'] , $where['binds'] ); $return = [ 'total_links' => $totals->count, 'total_clicks' => $totals->sum ]; return yourls_apply_filter( 'get_db_stats', $return, $where ); } /** * Returns a sanitized a user agent string. Given what I found on http://www.user-agents.org/ it should be OK. * * @return string */ function yourls_get_user_agent() { $ua = '-'; if ( isset( $_SERVER['HTTP_USER_AGENT'] ) ) { $ua = strip_tags( html_entity_decode( $_SERVER['HTTP_USER_AGENT'] )); $ua = preg_replace('![^0-9a-zA-Z\':., /{}\(\)\[\]\+@&\!\?;_\-=~\*\#]!', '', $ua ); } return yourls_apply_filter( 'get_user_agent', substr( $ua, 0, 255 ) ); } /** * Returns the sanitized referrer submitted by the browser. * * @return string HTTP Referrer or 'direct' if no referrer was provided */ function yourls_get_referrer() { $referrer = isset( $_SERVER['HTTP_REFERER'] ) ? yourls_sanitize_url_safe( $_SERVER['HTTP_REFERER'] ) : 'direct'; return yourls_apply_filter( 'get_referrer', substr( $referrer, 0, 200 ) ); } /** * Redirect to another page * * YOURLS redirection, either to internal or external URLs. If headers have not been sent, redirection * is achieved with PHP's header(). If headers have been sent already and we're not in a command line * client, redirection occurs with Javascript. * * Note: yourls_redirect() does not exit automatically, and should almost always be followed by a call to exit() * to prevent the script from continuing. * * @since 1.4 * @param string $location URL to redirect to * @param int $code HTTP status code to send * @return int 1 for header redirection, 2 for js redirection, 3 otherwise (CLI) */ function yourls_redirect( $location, $code = 301 ) { yourls_do_action( 'pre_redirect', $location, $code ); $location = yourls_apply_filter( 'redirect_location', $location, $code ); $code = yourls_apply_filter( 'redirect_code', $code, $location ); // Redirect, either properly if possible, or via Javascript otherwise if( !headers_sent() ) { yourls_status_header( $code ); header( "Location: $location" ); return 1; } // Headers sent : redirect with JS if not in CLI if( php_sapi_name() !== 'cli') { yourls_redirect_javascript( $location ); return 2; } // We're in CLI return 3; } /** * Redirect to an existing short URL * * Redirect client to an existing short URL (no check performed) and execute misc tasks: update * clicks for short URL, update logs, and send an X-Robots-Tag header to control indexing of a page. * * @since 1.7.3 * @param string $url * @param string $keyword * @return void */ function yourls_redirect_shorturl($url, $keyword) { yourls_do_action( 'redirect_shorturl', $url, $keyword ); // Attempt to update click count in main table yourls_update_clicks( $keyword ); // Update detailed log for stats yourls_log_redirect( $keyword ); // Send an X-Robots-Tag header yourls_robots_tag_header(); yourls_redirect( $url, 301 ); } /** * Send an X-Robots-Tag header. See #3486 * * @since 1.9.2 * @return void */ function yourls_robots_tag_header() { // Allow plugins to short-circuit the whole function $pre = yourls_apply_filter( 'shunt_robots_tag_header', false ); if ( false !== $pre ) { return $pre; } // By default, we're sending a 'noindex' header $tag = yourls_apply_filter( 'robots_tag_header', 'noindex' ); $replace = yourls_apply_filter( 'robots_tag_header_replace', true ); if ( !headers_sent() ) { header( "X-Robots-Tag: $tag", $replace ); } } /** * Send headers to explicitly tell browser not to cache content or redirection * * @since 1.7.10 * @return void */ function yourls_no_cache_headers() { if( !headers_sent() ) { header( 'Expires: Thu, 23 Mar 1972 07:00:00 GMT' ); header( 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s' ) . ' GMT' ); header( 'Cache-Control: no-cache, must-revalidate, max-age=0' ); header( 'Pragma: no-cache' ); } } /** * Send header to prevent display within a frame from another site (avoid clickjacking) * * This header makes it impossible for an external site to display YOURLS admin within a frame, * which allows for clickjacking. * See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options * This said, the whole function is shuntable : legit uses of iframes should be still possible. * * @since 1.8.1 * @return void|mixed */ function yourls_no_frame_header() { // Allow plugins to short-circuit the whole function $pre = yourls_apply_filter( 'shunt_no_frame_header', false ); if ( false !== $pre ) { return $pre; } if( !headers_sent() ) { header( 'X-Frame-Options: SAMEORIGIN' ); } } /** * Send a filterable content type header * * @since 1.7 * @param string $type content type ('text/html', 'application/json', ...) * @return bool whether header was sent */ function yourls_content_type_header( $type ) { yourls_do_action( 'content_type_header', $type ); if( !headers_sent() ) { $charset = yourls_apply_filter( 'content_type_header_charset', 'utf-8' ); header( "Content-Type: $type; charset=$charset" ); return true; } return false; } /** * Set HTTP status header * * @since 1.4 * @param int $code status header code * @return bool whether header was sent */ function yourls_status_header( $code = 200 ) { yourls_do_action( 'status_header', $code ); if( headers_sent() ) return false; $protocol = $_SERVER['SERVER_PROTOCOL']; if ( 'HTTP/1.1' != $protocol && 'HTTP/1.0' != $protocol ) $protocol = 'HTTP/1.0'; $code = intval( $code ); $desc = yourls_get_HTTP_status( $code ); @header ("$protocol $code $desc"); // This causes problems on IIS and some FastCGI setups return true; } /** * Redirect to another page using Javascript. * Set optional (bool)$dontwait to false to force manual redirection (make sure a message has been read by user) * * @param string $location * @param bool $dontwait * @return void */ function yourls_redirect_javascript( $location, $dontwait = true ) { yourls_do_action( 'pre_redirect_javascript', $location, $dontwait ); $location = yourls_apply_filter( 'redirect_javascript', $location, $dontwait ); if ( $dontwait ) { $message = yourls_s( 'if you are not redirected after 10 seconds, please click here', $location ); echo << window.location="$location"; ($message) REDIR; } else { echo '

'.yourls_s( 'Please click here', $location ).'

'; } yourls_do_action( 'post_redirect_javascript', $location ); } /** * Return an HTTP status code * * @param int $code * @return string */ function yourls_get_HTTP_status( $code ) { $code = intval( $code ); $headers_desc = [ 100 => 'Continue', 101 => 'Switching Protocols', 102 => 'Processing', 200 => 'OK', 201 => 'Created', 202 => 'Accepted', 203 => 'Non-Authoritative Information', 204 => 'No Content', 205 => 'Reset Content', 206 => 'Partial Content', 207 => 'Multi-Status', 226 => 'IM Used', 300 => 'Multiple Choices', 301 => 'Moved Permanently', 302 => 'Found', 303 => 'See Other', 304 => 'Not Modified', 305 => 'Use Proxy', 306 => 'Reserved', 307 => 'Temporary Redirect', 400 => 'Bad Request', 401 => 'Unauthorized', 402 => 'Payment Required', 403 => 'Forbidden', 404 => 'Not Found', 405 => 'Method Not Allowed', 406 => 'Not Acceptable', 407 => 'Proxy Authentication Required', 408 => 'Request Timeout', 409 => 'Conflict', 410 => 'Gone', 411 => 'Length Required', 412 => 'Precondition Failed', 413 => 'Request Entity Too Large', 414 => 'Request-URI Too Long', 415 => 'Unsupported Media Type', 416 => 'Requested Range Not Satisfiable', 417 => 'Expectation Failed', 422 => 'Unprocessable Entity', 423 => 'Locked', 424 => 'Failed Dependency', 426 => 'Upgrade Required', 500 => 'Internal Server Error', 501 => 'Not Implemented', 502 => 'Bad Gateway', 503 => 'Service Unavailable', 504 => 'Gateway Timeout', 505 => 'HTTP Version Not Supported', 506 => 'Variant Also Negotiates', 507 => 'Insufficient Storage', 510 => 'Not Extended' ]; return $headers_desc[$code] ?? ''; } /** * Log a redirect (for stats) * * This function does not check for the existence of a valid keyword, in order to save a query. Make sure the keyword * exists before calling it. * * @since 1.4 * @param string $keyword short URL keyword * @return mixed Result of the INSERT query (1 on success) */ function yourls_log_redirect( $keyword ) { // Allow plugins to short-circuit the whole function $pre = yourls_apply_filter( 'shunt_log_redirect', false, $keyword ); if ( false !== $pre ) { return $pre; } if (!yourls_do_log_redirect()) { return true; } $table = YOURLS_DB_TABLE_LOG; $ip = yourls_get_IP(); $binds = [ 'now' => date( 'Y-m-d H:i:s' ), 'keyword' => yourls_sanitize_keyword($keyword), 'referrer' => substr( yourls_get_referrer(), 0, 200 ), 'ua' => substr(yourls_get_user_agent(), 0, 255), 'ip' => $ip, 'location' => yourls_geo_ip_to_countrycode($ip), ]; // Try and log. An error probably means a concurrency problem : just skip the logging try { $result = yourls_get_db()->fetchAffected("INSERT INTO `$table` (click_time, shorturl, referrer, user_agent, ip_address, country_code) VALUES (:now, :keyword, :referrer, :ua, :ip, :location)", $binds ); } catch (Exception $e) { $result = 0; } return $result; } /** * Check if we want to not log redirects (for stats) * * @return bool */ function yourls_do_log_redirect() { return ( !defined( 'YOURLS_NOSTATS' ) || YOURLS_NOSTATS != true ); } /** * Check if an upgrade is needed * * @return bool */ function yourls_upgrade_is_needed() { // check YOURLS_DB_VERSION exist && match values stored in YOURLS_DB_TABLE_OPTIONS list( $currentver, $currentsql ) = yourls_get_current_version_from_sql(); if ( $currentsql < YOURLS_DB_VERSION ) { return true; } // Check if YOURLS_VERSION exist && match value stored in YOURLS_DB_TABLE_OPTIONS, update DB if required if ( $currentver < YOURLS_VERSION ) { yourls_update_option( 'version', YOURLS_VERSION ); } return false; } /** * Get current version & db version as stored in the options DB. Prior to 1.4 there's no option table. * * @return array */ function yourls_get_current_version_from_sql() { $currentver = yourls_get_option( 'version' ); $currentsql = yourls_get_option( 'db_version' ); // Values if version is 1.3 if ( !$currentver ) { $currentver = '1.3'; } if ( !$currentsql ) { $currentsql = '100'; } return [ $currentver, $currentsql ]; } /** * Determine if the current page is private * * @return bool */ function yourls_is_private() { $private = defined( 'YOURLS_PRIVATE' ) && YOURLS_PRIVATE; if ( $private ) { // Allow overruling for particular pages: // API if ( yourls_is_API() && defined( 'YOURLS_PRIVATE_API' ) ) { $private = YOURLS_PRIVATE_API; } // Stat pages elseif ( yourls_is_infos() && defined( 'YOURLS_PRIVATE_INFOS' ) ) { $private = YOURLS_PRIVATE_INFOS; } // Others future cases ? } return yourls_apply_filter( 'is_private', $private ); } /** * Allow several short URLs for the same long URL ? * * @return bool */ function yourls_allow_duplicate_longurls() { // special treatment if API to check for WordPress plugin requests if ( yourls_is_API() && isset( $_REQUEST[ 'source' ] ) && $_REQUEST[ 'source' ] == 'plugin' ) { return false; } return yourls_apply_filter('allow_duplicate_longurls', defined('YOURLS_UNIQUE_URLS') && !YOURLS_UNIQUE_URLS); } /** * Check if an IP shortens URL too fast to prevent DB flood. Return true, or die. * * @param string $ip * @return bool|mixed|string */ function yourls_check_IP_flood( $ip = '' ) { // Allow plugins to short-circuit the whole function $pre = yourls_apply_filter( 'shunt_check_IP_flood', false, $ip ); if ( false !== $pre ) return $pre; yourls_do_action( 'pre_check_ip_flood', $ip ); // at this point $ip can be '', check it if your plugin hooks in here // Raise white flag if installing or if no flood delay defined if( ( defined('YOURLS_FLOOD_DELAY_SECONDS') && YOURLS_FLOOD_DELAY_SECONDS === 0 ) || !defined('YOURLS_FLOOD_DELAY_SECONDS') || yourls_is_installing() ) return true; // Don't throttle logged in users if( yourls_is_private() ) { if( yourls_is_valid_user() === true ) return true; } // Don't throttle whitelist IPs if( defined( 'YOURLS_FLOOD_IP_WHITELIST' ) && YOURLS_FLOOD_IP_WHITELIST ) { $whitelist_ips = explode( ',', YOURLS_FLOOD_IP_WHITELIST ); foreach( (array)$whitelist_ips as $whitelist_ip ) { $whitelist_ip = trim( $whitelist_ip ); if ( $whitelist_ip == $ip ) return true; } } $ip = ( $ip ? yourls_sanitize_ip( $ip ) : yourls_get_IP() ); yourls_do_action( 'check_ip_flood', $ip ); $table = YOURLS_DB_TABLE_URL; $lasttime = yourls_get_db()->fetchValue( "SELECT `timestamp` FROM $table WHERE `ip` = :ip ORDER BY `timestamp` DESC LIMIT 1", [ 'ip' => $ip ] ); if( $lasttime ) { $now = date( 'U' ); $then = date( 'U', strtotime( $lasttime ) ); if( ( $now - $then ) <= YOURLS_FLOOD_DELAY_SECONDS ) { // Flood! yourls_do_action( 'ip_flood', $ip, $now - $then ); yourls_die( yourls__( 'Too many URLs added too fast. Slow down please.' ), yourls__( 'Too Many Requests' ), 429 ); } } return true; } /** * Check if YOURLS is installing * * @since 1.6 * @return bool */ function yourls_is_installing() { return (bool)yourls_apply_filter( 'is_installing', defined( 'YOURLS_INSTALLING' ) && YOURLS_INSTALLING ); } /** * Check if YOURLS is upgrading * * @since 1.6 * @return bool */ function yourls_is_upgrading() { return (bool)yourls_apply_filter( 'is_upgrading', defined( 'YOURLS_UPGRADING' ) && YOURLS_UPGRADING ); } /** * Check if YOURLS is installed * * Checks property $ydb->installed that is created by yourls_get_all_options() * * See inline comment for updating from 1.3 or prior. * * @return bool */ function yourls_is_installed() { return (bool)yourls_apply_filter( 'is_installed', yourls_get_db()->is_installed() ); } /** * Set installed state * * @since 1.7.3 * @param bool $bool whether YOURLS is installed or not * @return void */ function yourls_set_installed( $bool ) { yourls_get_db()->set_installed( $bool ); } /** * Generate random string of (int)$length length and type $type (see function for details) * * @param int $length * @param int $type * @param string $charlist * @return mixed|string */ function yourls_rnd_string ( $length = 5, $type = 0, $charlist = '' ) { $length = intval( $length ); // define possible characters switch ( $type ) { // no vowels to make no offending word, no 0/1/o/l to avoid confusion between letters & digits. Perfect for passwords. case '1': $possible = "23456789bcdfghjkmnpqrstvwxyz"; break; // Same, with lower + upper case '2': $possible = "23456789bcdfghjkmnpqrstvwxyzBCDFGHJKMNPQRSTVWXYZ"; break; // all letters, lowercase case '3': $possible = "abcdefghijklmnopqrstuvwxyz"; break; // all letters, lowercase + uppercase case '4': $possible = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; break; // all digits & letters lowercase case '5': $possible = "0123456789abcdefghijklmnopqrstuvwxyz"; break; // all digits & letters lowercase + uppercase case '6': $possible = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; break; // custom char list, or comply to charset as defined in config default: case '0': $possible = $charlist ? $charlist : yourls_get_shorturl_charset(); break; } $str = substr( str_shuffle( $possible ), 0, $length ); return yourls_apply_filter( 'rnd_string', $str, $length, $type, $charlist ); } /** * Check if we're in API mode. * * @return bool */ function yourls_is_API() { return (bool)yourls_apply_filter( 'is_API', defined( 'YOURLS_API' ) && YOURLS_API ); } /** * Check if we're in Ajax mode. * * @return bool */ function yourls_is_Ajax() { return (bool)yourls_apply_filter( 'is_Ajax', defined( 'YOURLS_AJAX' ) && YOURLS_AJAX ); } /** * Check if we're in GO mode (yourls-go.php). * * @return bool */ function yourls_is_GO() { return (bool)yourls_apply_filter( 'is_GO', defined( 'YOURLS_GO' ) && YOURLS_GO ); } /** * Check if we're displaying stats infos (yourls-infos.php). Returns bool * * @return bool */ function yourls_is_infos() { return (bool)yourls_apply_filter( 'is_infos', defined( 'YOURLS_INFOS' ) && YOURLS_INFOS ); } /** * Check if we're in the admin area. Returns bool. Does not relate with user rights. * * @return bool */ function yourls_is_admin() { return (bool)yourls_apply_filter( 'is_admin', defined( 'YOURLS_ADMIN' ) && YOURLS_ADMIN ); } /** * Check if the server seems to be running on Windows. Not exactly sure how reliable this is. * * @return bool */ function yourls_is_windows() { return defined( 'DIRECTORY_SEPARATOR' ) && DIRECTORY_SEPARATOR == '\\'; } /** * Check if SSL is required. * * @return bool */ function yourls_needs_ssl() { return (bool)yourls_apply_filter( 'needs_ssl', defined( 'YOURLS_ADMIN_SSL' ) && YOURLS_ADMIN_SSL ); } /** * Check if SSL is used. Stolen from WP. * * @return bool */ function yourls_is_ssl() { $is_ssl = false; if ( isset( $_SERVER[ 'HTTPS' ] ) ) { if ( 'on' == strtolower( $_SERVER[ 'HTTPS' ] ) ) { $is_ssl = true; } if ( '1' == $_SERVER[ 'HTTPS' ] ) { $is_ssl = true; } } elseif ( isset( $_SERVER[ 'HTTP_X_FORWARDED_PROTO' ] ) ) { if ( 'https' == strtolower( $_SERVER[ 'HTTP_X_FORWARDED_PROTO' ] ) ) { $is_ssl = true; } } elseif ( isset( $_SERVER[ 'SERVER_PORT' ] ) && ( '443' == $_SERVER[ 'SERVER_PORT' ] ) ) { $is_ssl = true; } return (bool)yourls_apply_filter( 'is_ssl', $is_ssl ); } /** * Get a remote page title * * This function returns a string: either the page title as defined in HTML, or the URL if not found * The function tries to convert funky characters found in titles to UTF8, from the detected charset. * Charset in use is guessed from HTML meta tag, or if not found, from server's 'content-type' response. * * @param string $url URL * @return string Title (sanitized) or the URL if no title found */ function yourls_get_remote_title( $url ) { // Allow plugins to short-circuit the whole function $pre = yourls_apply_filter( 'shunt_get_remote_title', false, $url ); if ( false !== $pre ) { return $pre; } $url = yourls_sanitize_url( $url ); // Only deal with http(s):// if ( !in_array( yourls_get_protocol( $url ), [ 'http://', 'https://' ] ) ) { return $url; } $title = $charset = false; $max_bytes = yourls_apply_filter( 'get_remote_title_max_byte', 32768 ); // limit data fetching to 32K in order to find a tag $response = yourls_http_get( $url, [], [], [ 'max_bytes' => $max_bytes ] ); // can be a Request object or an error string if ( is_string( $response ) ) { return $url; } // Page content. No content? Return the URL $content = $response->body; if ( !$content ) { return $url; } // look for <title>. No title found? Return the URL if ( preg_match( '/<title>(.*?)<\/title>/is', $content, $found ) ) { $title = $found[ 1 ]; unset( $found ); } if ( !$title ) { return $url; } // Now we have a title. We'll try to get proper utf8 from it. // Get charset as (and if) defined by the HTML meta tag. We should match // <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> // or <meta charset='utf-8'> and all possible variations: see https://gist.github.com/ozh/7951236 if ( preg_match( '/<meta[^>]*charset\s*=["\' ]*([a-zA-Z0-9\-_]+)/is', $content, $found ) ) { $charset = $found[ 1 ]; unset( $found ); } else { // No charset found in HTML. Get charset as (and if) defined by the server response $_charset = current( $response->headers->getValues( 'content-type' ) ); if ( preg_match( '/charset=(\S+)/', $_charset, $found ) ) { $charset = trim( $found[ 1 ], ';' ); unset( $found ); } } // Conversion to utf-8 if what we have is not utf8 already if ( strtolower( $charset ) != 'utf-8' && function_exists( 'mb_convert_encoding' ) ) { // We use @ to remove warnings because mb_ functions are easily bitching about illegal chars if ( $charset ) { $title = @mb_convert_encoding( $title, 'UTF-8', $charset ); } else { $title = @mb_convert_encoding( $title, 'UTF-8' ); } } // Remove HTML entities $title = html_entity_decode( $title, ENT_QUOTES, 'UTF-8' ); // Strip out evil things $title = yourls_sanitize_title( $title, $url ); return (string)yourls_apply_filter( 'get_remote_title', $title, $url ); } /** * Quick UA check for mobile devices. * * @return bool */ function yourls_is_mobile_device() { // Strings searched $mobiles = [ 'android', 'blackberry', 'blazer', 'compal', 'elaine', 'fennec', 'hiptop', 'iemobile', 'iphone', 'ipod', 'ipad', 'iris', 'kindle', 'opera mobi', 'opera mini', 'palm', 'phone', 'pocket', 'psp', 'symbian', 'treo', 'wap', 'windows ce', 'windows phone' ]; // Current user-agent $current = strtolower( $_SERVER['HTTP_USER_AGENT'] ); // Check and return $is_mobile = ( str_replace( $mobiles, '', $current ) != $current ); return (bool)yourls_apply_filter( 'is_mobile_device', $is_mobile ); } /** * Get request in YOURLS base (eg in 'http://sho.rt/yourls/abcd' get 'abdc') * * With no parameter passed, this function will guess current page and consider * it is the requested page. * For testing purposes, parameters can be passed. * * @since 1.5 * @param string $yourls_site Optional, YOURLS installation URL (default to constant YOURLS_SITE) * @param string $uri Optional, page requested (default to $_SERVER['REQUEST_URI'] eg '/yourls/abcd' ) * @return string request relative to YOURLS base (eg 'abdc') */ function yourls_get_request($yourls_site = '', $uri = '') { // Allow plugins to short-circuit the whole function $pre = yourls_apply_filter( 'shunt_get_request', false ); if ( false !== $pre ) { return $pre; } yourls_do_action( 'pre_get_request', $yourls_site, $uri ); // Default values if ( '' === $yourls_site ) { $yourls_site = yourls_get_yourls_site(); } if ( '' === $uri ) { $uri = $_SERVER[ 'REQUEST_URI' ]; } // Even though the config sample states YOURLS_SITE should be set without trailing slash... $yourls_site = rtrim( $yourls_site, '/' ); // Now strip the YOURLS_SITE path part out of the requested URI, and get the request relative to YOURLS base // +---------------------------+-------------------------+---------------------+--------------+ // | if we request | and YOURLS is hosted on | YOURLS path part is | "request" is | // +---------------------------+-------------------------+---------------------+--------------+ // | http://sho.rt/abc | http://sho.rt | / | abc | // | https://SHO.rt/subdir/abc | https://shor.rt/subdir/ | /subdir/ | abc | // +---------------------------+-------------------------+---------------------+--------------+ // and so on. You can find various test cases in /tests/tests/utilities/get_request.php // Take only the URL_PATH part of YOURLS_SITE (ie "https://sho.rt:1337/path/to/yourls" -> "/path/to/yourls") $yourls_site = parse_url( $yourls_site, PHP_URL_PATH ).'/'; // Strip path part from request if exists $request = $uri; if ( substr( $uri, 0, strlen( $yourls_site ) ) == $yourls_site ) { $request = ltrim( substr( $uri, strlen( $yourls_site ) ), '/' ); } // Unless request looks like a full URL (ie request is a simple keyword) strip query string if ( !preg_match( "@^[a-zA-Z]+://.+@", $request ) ) { $request = current( explode( '?', $request ) ); } $request = yourls_sanitize_url( $request ); return (string)yourls_apply_filter( 'get_request', $request ); } /** * Fix $_SERVER['REQUEST_URI'] variable for various setups. Stolen from WP. * * We also strip $_COOKIE from $_REQUEST to allow our lazy using $_REQUEST without 3rd party cookie interfering. * See #3383 for explanation. * * @since 1.5.1 * @return void */ function yourls_fix_request_uri() { $default_server_values = [ 'SERVER_SOFTWARE' => '', 'REQUEST_URI' => '', ]; $_SERVER = array_merge( $default_server_values, $_SERVER ); // Make $_REQUEST with only $_GET and $_POST, not $_COOKIE. See #3383. $_REQUEST = array_merge( $_GET, $_POST ); // Fix for IIS when running with PHP ISAPI if ( empty( $_SERVER[ 'REQUEST_URI' ] ) || ( php_sapi_name() != 'cgi-fcgi' && preg_match( '/^Microsoft-IIS\//', $_SERVER[ 'SERVER_SOFTWARE' ] ) ) ) { // IIS Mod-Rewrite if ( isset( $_SERVER[ 'HTTP_X_ORIGINAL_URL' ] ) ) { $_SERVER[ 'REQUEST_URI' ] = $_SERVER[ 'HTTP_X_ORIGINAL_URL' ]; } // IIS Isapi_Rewrite elseif ( isset( $_SERVER[ 'HTTP_X_REWRITE_URL' ] ) ) { $_SERVER[ 'REQUEST_URI' ] = $_SERVER[ 'HTTP_X_REWRITE_URL' ]; } else { // Use ORIG_PATH_INFO if there is no PATH_INFO if ( !isset( $_SERVER[ 'PATH_INFO' ] ) && isset( $_SERVER[ 'ORIG_PATH_INFO' ] ) ) { $_SERVER[ 'PATH_INFO' ] = $_SERVER[ 'ORIG_PATH_INFO' ]; } // Some IIS + PHP configurations puts the script-name in the path-info (No need to append it twice) if ( isset( $_SERVER[ 'PATH_INFO' ] ) ) { if ( $_SERVER[ 'PATH_INFO' ] == $_SERVER[ 'SCRIPT_NAME' ] ) { $_SERVER[ 'REQUEST_URI' ] = $_SERVER[ 'PATH_INFO' ]; } else { $_SERVER[ 'REQUEST_URI' ] = $_SERVER[ 'SCRIPT_NAME' ].$_SERVER[ 'PATH_INFO' ]; } } // Append the query string if it exists and isn't null if ( !empty( $_SERVER[ 'QUERY_STRING' ] ) ) { $_SERVER[ 'REQUEST_URI' ] .= '?'.$_SERVER[ 'QUERY_STRING' ]; } } } } /** * Check for maintenance mode. If yes, die. See yourls_maintenance_mode(). Stolen from WP. * * @return void */ function yourls_check_maintenance_mode() { $dot_file = YOURLS_ABSPATH . '/.maintenance' ; if ( !file_exists( $dot_file ) || yourls_is_upgrading() || yourls_is_installing() ) { return; } global $maintenance_start; yourls_include_file_sandbox( $dot_file ); // If the $maintenance_start timestamp is older than 10 minutes, don't die. if ( ( time() - $maintenance_start ) >= 600 ) { return; } // Use any /user/maintenance.php file $file = YOURLS_USERDIR . '/maintenance.php'; if(file_exists($file)) { if(yourls_include_file_sandbox( $file ) == true) { die(); } } // Or use the default messages $title = yourls__('Service temporarily unavailable'); $message = yourls__('Our service is currently undergoing scheduled maintenance.') . "</p>\n<p>" . yourls__('Things should not last very long, thank you for your patience and please excuse the inconvenience'); yourls_die( $message, $title, 503 ); } /** * Check if a URL protocol is allowed * * Checks a URL against a list of whitelisted protocols. Protocols must be defined with * their complete scheme name, ie 'stuff:' or 'stuff://' (for instance, 'mailto:' is a valid * protocol, 'mailto://' isn't, and 'http:' with no double slashed isn't either * * @since 1.6 * @see yourls_get_protocol() * * @param string $url URL to be check * @param array $protocols Optional. Array of protocols, defaults to global $yourls_allowedprotocols * @return bool true if protocol allowed, false otherwise */ function yourls_is_allowed_protocol( $url, $protocols = [] ) { if ( empty( $protocols ) ) { global $yourls_allowedprotocols; $protocols = $yourls_allowedprotocols; } return yourls_apply_filter( 'is_allowed_protocol', in_array( yourls_get_protocol( $url ), $protocols ), $url, $protocols ); } /** * Get protocol from a URL (eg mailto:, http:// ...) * * What we liberally call a "protocol" in YOURLS is the scheme name + colon + double slashes if present of a URI. Examples: * "something://blah" -> "something://" * "something:blah" -> "something:" * "something:/blah" -> "something:" * * Unit Tests for this function are located in tests/format/urls.php * * @since 1.6 * * @param string $url URL to be check * @return string Protocol, with slash slash if applicable. Empty string if no protocol */ function yourls_get_protocol( $url ) { /* http://en.wikipedia.org/wiki/URI_scheme#Generic_syntax The scheme name consists of a sequence of characters beginning with a letter and followed by any combination of letters, digits, plus ("+"), period ("."), or hyphen ("-"). Although schemes are case-insensitive, the canonical form is lowercase and documents that specify schemes must do so with lowercase letters. It is followed by a colon (":"). */ preg_match( '!^[a-zA-Z][a-zA-Z0-9+.-]+:(//)?!', $url, $matches ); return (string)yourls_apply_filter( 'get_protocol', isset( $matches[0] ) ? $matches[0] : '', $url ); } /** * Get relative URL (eg 'abc' from 'http://sho.rt/abc') * * Treat indifferently http & https. If a URL isn't relative to the YOURLS install, return it as is * or return empty string if $strict is true * * @since 1.6 * @param string $url URL to relativize * @param bool $strict if true and if URL isn't relative to YOURLS install, return empty string * @return string URL */ function yourls_get_relative_url( $url, $strict = true ) { $url = yourls_sanitize_url( $url ); // Remove protocols to make it easier $noproto_url = str_replace( 'https:', 'http:', $url ); $noproto_site = str_replace( 'https:', 'http:', yourls_get_yourls_site() ); // Trim URL from YOURLS root URL : if no modification made, URL wasn't relative $_url = str_replace( $noproto_site.'/', '', $noproto_url ); if ( $_url == $noproto_url ) { $_url = ( $strict ? '' : $url ); } return yourls_apply_filter( 'get_relative_url', $_url, $url ); } /** * Marks a function as deprecated and informs that it has been used. Stolen from WP. * * There is a hook deprecated_function that will be called that can be used * to get the backtrace up to what file and function called the deprecated * function. * * The current behavior is to trigger a user error if YOURLS_DEBUG is true. * * This function is to be used in every function that is deprecated. * * @since 1.6 * @uses yourls_do_action() Calls 'deprecated_function' and passes the function name, what to use instead, * and the version the function was deprecated in. * @uses yourls_apply_filter() Calls 'deprecated_function_trigger_error' and expects boolean value of true to do * trigger or false to not trigger error. * * @param string $function The function that was called * @param string $version The version of WordPress that deprecated the function * @param string $replacement Optional. The function that should have been called * @return void */ function yourls_deprecated_function( $function, $version, $replacement = null ) { yourls_do_action( 'deprecated_function', $function, $replacement, $version ); // Allow plugin to filter the output error trigger if ( yourls_get_debug_mode() && yourls_apply_filter( 'deprecated_function_trigger_error', true ) ) { if ( ! is_null( $replacement ) ) trigger_error( sprintf( yourls__('%1$s is <strong>deprecated</strong> since version %2$s! Use %3$s instead.'), $function, $version, $replacement ) ); else trigger_error( sprintf( yourls__('%1$s is <strong>deprecated</strong> since version %2$s with no alternative available.'), $function, $version ) ); } } /** * Explode a URL in an array of ( 'protocol' , 'slashes if any', 'rest of the URL' ) * * Some hosts trip up when a query string contains 'http://' - see http://git.io/j1FlJg * The idea is that instead of passing the whole URL to a bookmarklet, eg index.php?u=http://blah.com, * we pass it by pieces to fool the server, eg index.php?proto=http:&slashes=//&rest=blah.com * * Known limitation: this won't work if the rest of the URL itself contains 'http://', for example * if rest = blah.com/file.php?url=http://foo.com * * Sample returns: * * with 'mailto:jsmith@example.com?subject=hey' : * array( 'protocol' => 'mailto:', 'slashes' => '', 'rest' => 'jsmith@example.com?subject=hey' ) * * with 'http://example.com/blah.html' : * array( 'protocol' => 'http:', 'slashes' => '//', 'rest' => 'example.com/blah.html' ) * * @since 1.7 * @param string $url URL to be parsed * @param array $array Optional, array of key names to be used in returned array * @return array|false false if no protocol found, array of ('protocol' , 'slashes', 'rest') otherwise */ function yourls_get_protocol_slashes_and_rest( $url, $array = [ 'protocol', 'slashes', 'rest' ] ) { $proto = yourls_get_protocol( $url ); if ( !$proto or count( $array ) != 3 ) { return false; } list( $null, $rest ) = explode( $proto, $url, 2 ); list( $proto, $slashes ) = explode( ':', $proto ); return [ $array[ 0 ] => $proto.':', $array[ 1 ] => $slashes, $array[ 2 ] => $rest ]; } /** * Set URL scheme (HTTP or HTTPS) to a URL * * @since 1.7.1 * @param string $url URL * @param string $scheme scheme, either 'http' or 'https' * @return string URL with chosen scheme */ function yourls_set_url_scheme( $url, $scheme = '' ) { if ( in_array( $scheme, [ 'http', 'https' ] ) ) { $url = preg_replace( '!^[a-zA-Z0-9+.-]+://!', $scheme.'://', $url ); } return $url; } /** * Tell if there is a new YOURLS version * * This function checks, if needed, if there's a new version of YOURLS and, if applicable, displays * an update notice. * * @since 1.7.3 * @return void */ function yourls_tell_if_new_version() { yourls_debug_log( 'Check for new version: '.( yourls_maybe_check_core_version() ? 'yes' : 'no' ) ); yourls_new_core_version_notice(YOURLS_VERSION); } /** * File include sandbox * * Attempt to include a PHP file, fail with an error message if the file isn't valid PHP code. * This function does not check first if the file exists : depending on use case, you may check first. * * @since 1.9.2 * @param string $file filename (full path) * @return string|bool string if error, true if success */ function yourls_include_file_sandbox($file) { try { if (is_readable( $file )) { include_once $file; yourls_debug_log("loaded $file"); return true; } } catch ( \Throwable $e ) { yourls_debug_log("could not load $file"); return sprintf("%s (%s : %s)", $e->getMessage() , $e->getFile() , $e->getLine() ); } }