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 << '.yourls_s( 'Please click here', $location ).'
" . 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 deprecated since version %2$s! Use %3$s instead.'), $function, $version, $replacement ) ); else trigger_error( sprintf( yourls__('%1$s is deprecated 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 )) { require_once $file; yourls_debug_log("loaded $file"); return true; } else { throw new \Exception('File not readable'); } } catch ( \Throwable $e ) { yourls_debug_log("could not load $file"); return sprintf("%s (%s : %s)", $e->getMessage() , $e->getFile() , $e->getLine() ); } }