Upload files to ''

This commit is contained in:
Sophia Atkinson 2022-10-30 14:05:15 -07:00
parent e92aacd4e9
commit 037da0a554
23 changed files with 10658 additions and 0 deletions

63
auth.php Normal file
View File

@ -0,0 +1,63 @@
<?php
// No direct call
if( !defined( 'YOURLS_ABSPATH' ) ) die();
$auth = yourls_is_valid_user();
if( $auth !== true ) {
// API mode,
if ( yourls_is_API() ) {
$format = ( isset($_REQUEST['format']) ? $_REQUEST['format'] : 'xml' );
$callback = ( isset($_REQUEST['callback']) ? $_REQUEST['callback'] : '' );
yourls_api_output( $format, array(
'simple' => $auth,
'message' => $auth,
'errorCode' => 403,
'callback' => $callback,
) );
// Regular mode
} else {
yourls_login_screen( $auth );
}
die();
}
yourls_do_action( 'auth_successful' );
/*
* The following code is a shim that helps users store passwords securely in config.php
* by storing a password hash and removing the plaintext.
*
* TODO: Remove this once real user management is implemented
*/
// Did we just fail at encrypting passwords ?
if ( isset( $_GET['dismiss'] ) && $_GET['dismiss'] == 'hasherror' ) {
yourls_update_option( 'defer_hashing_error', time() + 86400 * 7 ); // now + 1 week
} else {
// Encrypt passwords that are clear text
if ( yourls_maybe_hash_passwords() ) {
$hash = yourls_hash_passwords_now( YOURLS_CONFIGFILE );
if ( $hash === true ) {
// Hashing succesful. Remove flag from DB if any.
if( yourls_get_option( 'defer_hashing_error' ) )
yourls_delete_option( 'defer_hashing_error' );
} else {
// It failed, display message for first time or if last time was a week ago
if ( time() > yourls_get_option( 'defer_hashing_error' ) or !yourls_get_option( 'defer_hashing_error' ) ) {
$message = yourls_s( 'Could not auto-encrypt passwords. Error was: "%s".', $hash );
$message .= ' ';
$message .= yourls_s( '<a href="%s">Get help</a>.', 'http://yourls.org/userpassword' );
$message .= '</p><p>';
$message .= yourls_s( '<a href="%s">Click here</a> to dismiss this message for one week.', '?dismiss=hasherror' );
yourls_add_notice( $message );
}
}
}
}

115
class-mysql.php Normal file
View File

@ -0,0 +1,115 @@
<?php
/**
* Connect to DB
*
* @since 1.0
* @return \YOURLS\Database\YDB
*/
function yourls_db_connect() {
global $ydb;
if ( !defined( 'YOURLS_DB_USER' )
or !defined( 'YOURLS_DB_PASS' )
or !defined( 'YOURLS_DB_NAME' )
or !defined( 'YOURLS_DB_HOST' )
) {
yourls_die( yourls__( 'Incorrect DB config, please refer to documentation' ), yourls__( 'Fatal error' ), 503 );
}
$dbhost = YOURLS_DB_HOST;
$user = YOURLS_DB_USER;
$pass = YOURLS_DB_PASS;
$dbname = YOURLS_DB_NAME;
// This action is deprecated
yourls_do_action( 'set_DB_driver', 'deprecated' );
// Get custom port if any
if ( false !== strpos( $dbhost, ':' ) ) {
list( $dbhost, $dbport ) = explode( ':', $dbhost );
$dbhost = sprintf( '%1$s;port=%2$d', $dbhost, $dbport );
}
$charset = yourls_apply_filter( 'db_connect_charset', 'utf8mb4' );
/**
* Data Source Name (dsn) used to connect the DB
*
* DSN with PDO is something like:
* 'mysql:host=123.4.5.6;dbname=test_db;port=3306'
* 'sqlite:/opt/databases/mydb.sq3'
* 'pgsql:host=192.168.13.37;port=5432;dbname=omgwtf'
*/
$dsn = sprintf( 'mysql:host=%s;dbname=%s;charset=%s', $dbhost, $dbname, $charset );
$dsn = yourls_apply_filter( 'db_connect_custom_dsn', $dsn );
/**
* PDO driver options and attributes
*
* The PDO constructor is something like:
* new PDO( string $dsn, string $username, string $password [, array $options ] )
* The driver options are passed to the PDO constructor, eg array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION)
* The attribute options are then set in a foreach($attr as $k=>$v){$db->setAttribute($k, $v)} loop
*/
$driver_options = yourls_apply_filter( 'db_connect_driver_option', [] ); // driver options as key-value pairs
$attributes = yourls_apply_filter( 'db_connect_attributes', [] ); // attributes as key-value pairs
$ydb = new \YOURLS\Database\YDB( $dsn, $user, $pass, $driver_options, $attributes );
$ydb->init();
// Past this point, we're connected
yourls_debug_log( sprintf( 'Connected to database %s on %s ', $dbname, $dbhost ) );
yourls_debug_mode( YOURLS_DEBUG );
return $ydb;
}
/**
* Helper function : return instance of the DB
*
* Instead of:
* global $ydb;
* $ydb->do_stuff()
* Prefer :
* yourls_get_db()->do_stuff()
*
* @since 1.7.10
* @return \YOURLS\Database\YDB
*/
function yourls_get_db() {
// Allow plugins to short-circuit the whole function
$pre = yourls_apply_filter( 'shunt_get_db', false );
if ( false !== $pre ) {
return $pre;
}
global $ydb;
$ydb = ( isset( $ydb ) ) ? $ydb : yourls_db_connect();
return yourls_apply_filter('get_db', $ydb);
}
/**
* Helper function : set instance of DB, or unset it
*
* Instead of:
* global $ydb;
* $ydb = stuff
* Prefer :
* yourls_set_db( stuff )
* (This is mostly used in the test suite)
*
* @since 1.7.10
* @param mixed $db Either a \YOURLS\Database\YDB instance, or anything. If null, the function will unset $ydb
* @return void
*/
function yourls_set_db($db) {
global $ydb;
if (is_null($db)) {
unset($ydb);
} else {
$ydb = $db;
}
}

241
functions-api.php Normal file
View File

@ -0,0 +1,241 @@
<?php
/*
* YOURLS
* Functions for the API
*
* Note about translation : this file should NOT be translation ready
* API messages and returns are supposed to be programmatically tested, so default English is expected
*
*/
/**
* API function wrapper: Shorten a URL
*
* @since 1.6
* @return array Result of API call
*/
function yourls_api_action_shorturl() {
$url = ( isset( $_REQUEST['url'] ) ? $_REQUEST['url'] : '' );
$keyword = ( isset( $_REQUEST['keyword'] ) ? $_REQUEST['keyword'] : '' );
$title = ( isset( $_REQUEST['title'] ) ? $_REQUEST['title'] : '' );
$return = yourls_add_new_link( $url, $keyword, $title );
$return['simple'] = ( isset( $return['shorturl'] ) ? $return['shorturl'] : '' ); // This one will be used in case output mode is 'simple'
unset( $return['html'] ); // in API mode, no need for our internal HTML output
return yourls_apply_filter( 'api_result_shorturl', $return );
}
/**
* API function wrapper: Stats about links (XX top, bottom, last, rand)
*
* @since 1.6
* @return array Result of API call
*/
function yourls_api_action_stats() {
$filter = ( isset( $_REQUEST['filter'] ) ? $_REQUEST['filter'] : '' );
$limit = ( isset( $_REQUEST['limit'] ) ? $_REQUEST['limit'] : '' );
$start = ( isset( $_REQUEST['start'] ) ? $_REQUEST['start'] : '' );
return yourls_apply_filter( 'api_result_stats', yourls_api_stats( $filter, $limit, $start ) );
}
/**
* API function wrapper: Just the global counts of shorturls and clicks
*
* @since 1.6
* @return array Result of API call
*/
function yourls_api_action_db_stats() {
return yourls_apply_filter( 'api_result_db_stats', yourls_api_db_stats() );
}
/**
* API function wrapper: Stats for a shorturl
*
* @since 1.6
* @return array Result of API call
*/
function yourls_api_action_url_stats() {
$shorturl = ( isset( $_REQUEST['shorturl'] ) ? $_REQUEST['shorturl'] : '' );
return yourls_apply_filter( 'api_result_url_stats', yourls_api_url_stats( $shorturl ) );
}
/**
* API function wrapper: Expand a short link
*
* @since 1.6
* @return array Result of API call
*/
function yourls_api_action_expand() {
$shorturl = ( isset( $_REQUEST['shorturl'] ) ? $_REQUEST['shorturl'] : '' );
return yourls_apply_filter( 'api_result_expand', yourls_api_expand( $shorturl ) );
}
/**
* API function wrapper: return version numbers
*
* @since 1.6
* @return array Result of API call
*/
function yourls_api_action_version() {
$return['version'] = $return['simple'] = YOURLS_VERSION;
if( isset( $_REQUEST['db'] ) && $_REQUEST['db'] == 1 )
$return['db_version'] = YOURLS_DB_VERSION;
return yourls_apply_filter( 'api_result_version', $return );
}
/**
* Output and return API result
*
* This function will echo (or only return if asked) an array as JSON, JSONP or XML. If the array has a
* 'simple' key, it can also output that key as unformatted text if expected output mode is 'simple'
*
* Most likely, script should not do anything after outputting this
*
* @since 1.6
*
* @param string $mode Expected output mode ('json', 'jsonp', 'xml', 'simple')
* @param array $output Array of things to output
* @param bool $send_headers Optional, default true: Whether a headers (status, content type) should be sent or not
* @param bool $echo Optional, default true: Whether the output should be outputted or just returned
* @return string API output, as an XML / JSON / JSONP / raw text string
*/
function yourls_api_output( $mode, $output, $send_headers = true, $echo = true ) {
if( isset( $output['simple'] ) ) {
$simple = $output['simple'];
unset( $output['simple'] );
}
yourls_do_action( 'pre_api_output', $mode, $output, $send_headers, $echo );
if( $send_headers ) {
if( isset( $output['statusCode'] ) ) {
$code = $output['statusCode'];
} elseif ( isset( $output['errorCode'] ) ) {
$code = $output['errorCode'];
} else {
$code = 200;
}
yourls_status_header( $code );
}
$result = '';
switch ( $mode ) {
case 'jsonp':
if( $send_headers )
yourls_content_type_header( 'application/javascript' );
$callback = isset( $output['callback'] ) ? $output['callback'] : '';
$result = $callback . '(' . json_encode( $output ) . ')';
break;
case 'json':
if( $send_headers )
yourls_content_type_header( 'application/json' );
$result = json_encode( $output );
break;
case 'xml':
if( $send_headers )
yourls_content_type_header( 'application/xml' );
$result = yourls_xml_encode( $output );
break;
case 'simple':
default:
if( $send_headers )
yourls_content_type_header( 'text/plain' );
$result = isset( $simple ) ? $simple : '';
break;
}
if( $echo ) {
echo $result;
}
yourls_do_action( 'api_output', $mode, $output, $send_headers, $echo );
return $result;
}
/**
* Return array for API stat requests
*
* @param string $filter either "top", "bottom" , "rand" or "last"
* @param int $limit maximum number of links to return
* @param int $start offset
* @return array
*/
function yourls_api_stats($filter = 'top', $limit = 10, $start = 0 ) {
$return = yourls_get_stats( $filter, $limit, $start );
$return['simple'] = 'Need either XML or JSON format for stats';
$return['message'] = 'success';
return yourls_apply_filter( 'api_stats', $return, $filter, $limit, $start );
}
/**
* Return array for counts of shorturls and clicks
*
* @return array
*/
function yourls_api_db_stats() {
$return = array(
'db-stats' => yourls_get_db_stats(),
'statusCode' => 200,
'simple' => 'Need either XML or JSON format for stats',
'message' => 'success',
);
return yourls_apply_filter( 'api_db_stats', $return );
}
/**
* Return array for API stat requests
*
* @param string $shorturl Short URL to check
* @return array
*/
function yourls_api_url_stats( $shorturl ) {
$keyword = str_replace( yourls_get_yourls_site() . '/' , '', $shorturl ); // accept either 'http://ozh.in/abc' or 'abc'
$keyword = yourls_sanitize_keyword( $keyword );
$return = yourls_get_keyword_stats( $keyword );
$return['simple'] = 'Need either XML or JSON format for stats';
return yourls_apply_filter( 'api_url_stats', $return, $shorturl );
}
/**
* Expand short url to long url
*
* @param string $shorturl Short URL to expand
* @return array
*/
function yourls_api_expand( $shorturl ) {
$keyword = str_replace( yourls_get_yourls_site() . '/' , '', $shorturl ); // accept either 'http://ozh.in/abc' or 'abc'
$keyword = yourls_sanitize_keyword( $keyword );
$longurl = yourls_get_keyword_longurl( $keyword );
if( $longurl ) {
$return = array(
'keyword' => $keyword,
'shorturl' => yourls_link($keyword),
'longurl' => $longurl,
'title' => yourls_get_keyword_title( $keyword ),
'simple' => $longurl,
'message' => 'success',
'statusCode' => 200,
);
} else {
$return = array(
'keyword' => $keyword,
'simple' => 'not found',
'message' => 'Error: short URL not found',
'errorCode' => 404,
);
}
return yourls_apply_filter( 'api_expand', $return, $shorturl );
}

747
functions-auth.php Normal file
View File

@ -0,0 +1,747 @@
<?php
/**
* Function related to authentication functions and nonces
*/
/**
* Show login form if required
*
* @return void
*/
function yourls_maybe_require_auth() {
if( yourls_is_private() ) {
yourls_do_action( 'require_auth' );
require_once( YOURLS_INC.'/auth.php' );
} else {
yourls_do_action( 'require_no_auth' );
}
}
/**
* Check for valid user via login form or stored cookie. Returns true or an error message
*
* @return bool|string|mixed true if valid user, error message otherwise. Can also call yourls_die() or redirect to login page. Oh my.
*/
function yourls_is_valid_user() {
// Allow plugins to short-circuit the whole function
$pre = yourls_apply_filter( 'shunt_is_valid_user', null );
if ( null !== $pre ) {
return $pre;
}
// $unfiltered_valid : are credentials valid? Boolean value. It's "unfiltered" to allow plugins to eventually filter it.
$unfiltered_valid = false;
// Logout request
if( isset( $_GET['action'] ) && $_GET['action'] == 'logout' && isset( $_REQUEST['nonce'] ) ) {
// The logout nonce is associated to fake user 'logout' since at this point we don't know the real user
yourls_verify_nonce('admin_logout', $_REQUEST['nonce'], 'logout');
yourls_do_action( 'logout' );
yourls_store_cookie( '' );
return yourls__( 'Logged out successfully' );
}
// Check cookies or login request. Login form has precedence.
yourls_do_action( 'pre_login' );
// Determine auth method and check credentials
if
// API only: Secure (no login or pwd) and time limited token
// ?timestamp=12345678&signature=md5(totoblah12345678)
( yourls_is_API() &&
isset( $_REQUEST['timestamp'] ) && !empty($_REQUEST['timestamp'] ) &&
isset( $_REQUEST['signature'] ) && !empty($_REQUEST['signature'] )
)
{
yourls_do_action( 'pre_login_signature_timestamp' );
$unfiltered_valid = yourls_check_signature_timestamp();
}
elseif
// API only: Secure (no login or pwd)
// ?signature=md5(totoblah)
( yourls_is_API() &&
!isset( $_REQUEST['timestamp'] ) &&
isset( $_REQUEST['signature'] ) && !empty( $_REQUEST['signature'] )
)
{
yourls_do_action( 'pre_login_signature' );
$unfiltered_valid = yourls_check_signature();
}
elseif
// API or normal: login with username & pwd
( isset( $_REQUEST['username'] ) && isset( $_REQUEST['password'] )
&& !empty( $_REQUEST['username'] ) && !empty( $_REQUEST['password'] ) )
{
yourls_do_action( 'pre_login_username_password' );
$unfiltered_valid = yourls_check_username_password();
}
elseif
// Normal only: cookies
( !yourls_is_API() &&
isset( $_COOKIE[ yourls_cookie_name() ] ) )
{
yourls_do_action( 'pre_login_cookie' );
$unfiltered_valid = yourls_check_auth_cookie();
}
// Regardless of validity, allow plugins to filter the boolean and have final word
$valid = yourls_apply_filter( 'is_valid_user', $unfiltered_valid );
// Login for the win!
if ( $valid ) {
yourls_do_action( 'login' );
// (Re)store encrypted cookie if needed
if ( !yourls_is_API() ) {
yourls_store_cookie( YOURLS_USER );
// Login form : redirect to requested URL to avoid re-submitting the login form on page reload
if( isset( $_REQUEST['username'] ) && isset( $_REQUEST['password'] ) && isset( $_SERVER['REQUEST_URI'] ) ) {
// The return makes sure we exit this function before waiting for redirection.
// See #3189 and note in yourls_redirect()
return yourls_redirect( yourls_sanitize_url_safe($_SERVER['REQUEST_URI']) );
}
}
// Login successful
return true;
}
// Login failed
yourls_do_action( 'login_failed' );
if ( isset( $_REQUEST['username'] ) || isset( $_REQUEST['password'] ) ) {
return yourls__( 'Invalid username or password' );
} else {
return yourls__( 'Please log in' );
}
}
/**
* Check auth against list of login=>pwd. Sets user if applicable, returns bool
*
* @return bool true if login/pwd pair is valid (and sets user if applicable), false otherwise
*/
function yourls_check_username_password() {
global $yourls_user_passwords;
// If login form (not API), check for nonce
if(!yourls_is_API()) {
yourls_verify_nonce('admin_login');
}
if( isset( $yourls_user_passwords[ $_REQUEST['username'] ] ) && yourls_check_password_hash( $_REQUEST['username'], $_REQUEST['password'] ) ) {
yourls_set_user( $_REQUEST['username'] );
return true;
}
return false;
}
/**
* Check a submitted password sent in plain text against stored password which can be a salted hash
*
* @param string $user
* @param string $submitted_password
* @return bool
*/
function yourls_check_password_hash($user, $submitted_password ) {
global $yourls_user_passwords;
if( !isset( $yourls_user_passwords[ $user ] ) )
return false;
if ( yourls_has_phpass_password( $user ) ) {
// Stored password is hashed
list( , $hash ) = explode( ':', $yourls_user_passwords[ $user ] );
$hash = str_replace( '!', '$', $hash );
return ( yourls_phpass_check( $submitted_password, $hash ) );
} else if( yourls_has_md5_password( $user ) ) {
// Stored password is a salted md5 hash: "md5:<$r = rand(10000,99999)>:<md5($r.'thepassword')>"
list( , $salt, ) = explode( ':', $yourls_user_passwords[ $user ] );
return( $yourls_user_passwords[ $user ] == 'md5:'.$salt.':'.md5( $salt . $submitted_password ) );
} else {
// Password stored in clear text
return( $yourls_user_passwords[ $user ] === $submitted_password );
}
}
/**
* Overwrite plaintext passwords in config file with hashed versions.
*
* @since 1.7
* @param string $config_file Full path to file
* @return true|string if overwrite was successful, an error message otherwise
*/
function yourls_hash_passwords_now( $config_file ) {
if( !is_readable( $config_file ) ) {
yourls_debug_log( 'Cannot hash passwords: cannot read file ' . $config_file );
return 'cannot read file'; // not sure that can actually happen...
}
if( !is_writable( $config_file ) ) {
yourls_debug_log( 'Cannot hash passwords: cannot write file ' . $config_file );
return 'cannot write file';
}
$yourls_user_passwords = [];
// Include file to read value of $yourls_user_passwords
// Temporary suppress error reporting to avoid notices about redeclared constants
$errlevel = error_reporting();
error_reporting( 0 );
require $config_file;
error_reporting( $errlevel );
$configdata = file_get_contents( $config_file );
if( $configdata == false ) {
yourls_debug_log('Cannot hash passwords: file_get_contents() false with ' . $config_file);
return 'could not read file';
}
$to_hash = 0; // keep track of number of passwords that need hashing
foreach ( $yourls_user_passwords as $user => $password ) {
// avoid "deprecated" warning when password is null -- see test case in tests/data/auth/preg_replace_problem.php
$password ??= '';
if ( !yourls_has_phpass_password( $user ) && !yourls_has_md5_password( $user ) ) {
$to_hash++;
$hash = yourls_phpass_hash( $password );
// PHP would interpret $ as a variable, so replace it in storage.
$hash = str_replace( '$', '!', $hash );
$quotes = "'" . '"';
$pattern = "/[$quotes]${user}[$quotes]\s*=>\s*[$quotes]" . preg_quote( $password, '/' ) . "[$quotes]/";
$replace = "'$user' => 'phpass:$hash' /* Password encrypted by YOURLS */ ";
$count = 0;
$configdata = preg_replace( $pattern, $replace, $configdata, -1, $count );
// There should be exactly one replacement. Otherwise, fast fail.
if ( $count != 1 ) {
yourls_debug_log( "Problem with preg_replace for password hash of user $user" );
return 'preg_replace problem';
}
}
}
if( $to_hash == 0 ) {
yourls_debug_log('Cannot hash passwords: no password found in ' . $config_file);
return 'no password found';
}
$success = file_put_contents( $config_file, $configdata );
if ( $success === FALSE ) {
yourls_debug_log( 'Failed writing to ' . $config_file );
return 'could not write file';
}
yourls_debug_log('Successfully encrypted passwords in ' . basename($config_file));
return true;
}
/**
* Create a password hash
*
* @since 1.7
* @param string $password password to hash
* @return string hashed password
*/
function yourls_phpass_hash( $password ) {
/**
* Filter for hashing algorithm. See https://www.php.net/manual/en/function.password-hash.php
* Hashing algos are available if PHP was compiled with it.
* PASSWORD_BCRYPT is always available.
*/
$algo = yourls_apply_filter('hash_algo', PASSWORD_BCRYPT);
/**
* Filter for hashing options. See https://www.php.net/manual/en/function.password-hash.php
* A typical option for PASSWORD_BCRYPT would be ['cost' => <int in range 4-31> ]
* We're leaving the options at default values, which means a cost of 10 for PASSWORD_BCRYPT.
*
* If willing to modify this, be warned about the computing time, as there is a 2^n factor.
* See https://gist.github.com/ozh/65a75392b7cb254131cc55afd28de99b for examples.
*/
$options = yourls_apply_filter('hash_options', [] );
return password_hash($password, $algo, $options);
}
/**
* Verify that a password matches a hash
*
* @since 1.7
* @param string $password clear (eg submitted in a form) password
* @param string $hash hash
* @return bool true if the hash matches the password, false otherwise
*/
function yourls_phpass_check( $password, $hash ) {
return password_verify($password, $hash);
}
/**
* Check to see if any passwords are stored as cleartext.
*
* @since 1.7
* @return bool true if any passwords are cleartext
*/
function yourls_has_cleartext_passwords() {
global $yourls_user_passwords;
foreach ( $yourls_user_passwords as $user => $pwdata ) {
if ( !yourls_has_md5_password( $user ) && !yourls_has_phpass_password( $user ) ) {
return true;
}
}
return false;
}
/**
* Check if a user has a md5 hashed password
*
* Check if a user password is 'md5:[38 chars]'.
* TODO: deprecate this when/if we have proper user management with password hashes stored in the DB
*
* @since 1.7
* @param string $user user login
* @return bool true if password hashed, false otherwise
*/
function yourls_has_md5_password( $user ) {
global $yourls_user_passwords;
return( isset( $yourls_user_passwords[ $user ] )
&& substr( $yourls_user_passwords[ $user ], 0, 4 ) == 'md5:'
&& strlen( $yourls_user_passwords[ $user ] ) == 42 // http://www.google.com/search?q=the+answer+to+life+the+universe+and+everything
);
}
/**
* Check if a user's password is hashed with password_hash
*
* Check if a user password is 'phpass:[lots of chars]'.
* (For historical reason we're using 'phpass' as an identifier.)
* TODO: deprecate this when/if we have proper user management with password hashes stored in the DB
*
* @since 1.7
* @param string $user user login
* @return bool true if password hashed with password_hash, otherwise false
*/
function yourls_has_phpass_password( $user ) {
global $yourls_user_passwords;
return( isset( $yourls_user_passwords[ $user ] )
&& substr( $yourls_user_passwords[ $user ], 0, 7 ) == 'phpass:'
);
}
/**
* Check auth against encrypted COOKIE data. Sets user if applicable, returns bool
*
* @return bool true if authenticated, false otherwise
*/
function yourls_check_auth_cookie() {
global $yourls_user_passwords;
foreach( $yourls_user_passwords as $valid_user => $valid_password ) {
if ( yourls_cookie_value( $valid_user ) === $_COOKIE[ yourls_cookie_name() ] ) {
yourls_set_user( $valid_user );
return true;
}
}
return false;
}
/**
* Check auth against signature and timestamp. Sets user if applicable, returns bool
*
* Original usage :
* http://sho.rt/yourls-api.php?timestamp=<timestamp>&signature=<md5 hash>&action=...
* Since 1.7.7 we allow a `hash` parameter and an arbitrary hashed signature, hashed
* with the `hash` function. Examples :
* http://sho.rt/yourls-api.php?timestamp=<timestamp>&signature=<sha512 hash>&hash=sha512&action=...
* http://sho.rt/yourls-api.php?timestamp=<timestamp>&signature=<crc32 hash>&hash=crc32&action=...
*
* @since 1.4.1
* @return bool False if signature or timestamp missing or invalid, true if valid
*/
function yourls_check_signature_timestamp() {
if( !isset( $_REQUEST['signature'] ) OR empty( $_REQUEST['signature'] )
OR !isset( $_REQUEST['timestamp'] ) OR empty( $_REQUEST['timestamp'] )
) {
return false;
}
// Exit if the timestamp argument is outdated or invalid
if( !yourls_check_timestamp( $_REQUEST['timestamp'] )) {
return false;
}
// if there is a hash argument, make sure it's part of the availables algos
$hash_function = isset($_REQUEST['hash']) ? (string)$_REQUEST['hash'] : 'md5';
if( !in_array($hash_function, hash_algos()) ) {
return false;
}
// Check signature & timestamp against all possible users
global $yourls_user_passwords;
foreach( $yourls_user_passwords as $valid_user => $valid_password ) {
if (
hash( $hash_function, $_REQUEST['timestamp'].yourls_auth_signature( $valid_user ) ) === $_REQUEST['signature']
or
hash( $hash_function, yourls_auth_signature( $valid_user ).$_REQUEST['timestamp'] ) === $_REQUEST['signature']
) {
yourls_set_user( $valid_user );
return true;
}
}
// Signature doesn't match known user
return false;
}
/**
* Check auth against signature. Sets user if applicable, returns bool
*
* @since 1.4.1
* @return bool False if signature missing or invalid, true if valid
*/
function yourls_check_signature() {
if( !isset( $_REQUEST['signature'] ) OR empty( $_REQUEST['signature'] ) )
return false;
// Check signature against all possible users
global $yourls_user_passwords;
foreach( $yourls_user_passwords as $valid_user => $valid_password ) {
if ( yourls_auth_signature( $valid_user ) === $_REQUEST['signature'] ) {
yourls_set_user( $valid_user );
return true;
}
}
// Signature doesn't match known user
return false;
}
/**
* Generate secret signature hash
*
* @param false|string $username Username to generate signature for, or false to use current user
* @return string Signature
*/
function yourls_auth_signature( $username = false ) {
if( !$username && defined('YOURLS_USER') ) {
$username = YOURLS_USER;
}
return ( $username ? substr( yourls_salt( $username ), 0, 10 ) : 'Cannot generate auth signature: no username' );
}
/**
* Check if timestamp is not too old
*
* @param int $time Timestamp to check
* @return bool True if timestamp is valid
*/
function yourls_check_timestamp( $time ) {
$now = time();
// Allow timestamp to be a little in the future or the past -- see Issue 766
return yourls_apply_filter( 'check_timestamp', abs( $now - (int)$time ) < yourls_get_nonce_life(), $time );
}
/**
* Store new cookie. No $user will delete the cookie.
*
* @param string $user User login, or empty string to delete cookie
* @return void
*/
function yourls_store_cookie( $user = '' ) {
// No user will delete the cookie with a cookie time from the past
if( !$user ) {
$time = time() - 3600;
} else {
$time = time() + yourls_get_cookie_life();
}
$path = yourls_apply_filter( 'setcookie_path', '/' );
$domain = yourls_apply_filter( 'setcookie_domain', parse_url( yourls_get_yourls_site(), PHP_URL_HOST ) );
$secure = yourls_apply_filter( 'setcookie_secure', yourls_is_ssl() );
$httponly = yourls_apply_filter( 'setcookie_httponly', true );
// Some browsers refuse to store localhost cookie
if ( $domain == 'localhost' )
$domain = '';
yourls_do_action( 'pre_setcookie', $user, $time, $path, $domain, $secure, $httponly );
if ( !headers_sent( $filename, $linenum ) ) {
yourls_setcookie( yourls_cookie_name(), yourls_cookie_value( $user ), $time, $path, $domain, $secure, $httponly );
} else {
// For some reason cookies were not stored: action to be able to debug that
yourls_do_action( 'setcookie_failed', $user );
yourls_debug_log( "Could not store cookie: headers already sent in $filename on line $linenum" );
}
}
/**
* Replacement for PHP's setcookie(), with support for SameSite cookie attribute
*
* @see https://github.com/GoogleChromeLabs/samesite-examples/blob/master/php.md
* @see https://stackoverflow.com/a/59654832/36850
* @see https://www.php.net/manual/en/function.setcookie.php
*
* @since 1.7.7
* @param string $name cookie name
* @param string $value cookie value
* @param int $expire time the cookie expires as a Unix timestamp (number of seconds since the epoch)
* @param string $path path on the server in which the cookie will be available on
* @param string $domain (sub)domain that the cookie is available to
* @param bool $secure if cookie should only be transmitted over a secure HTTPS connection
* @param bool $httponly if cookie will be made accessible only through the HTTP protocol
* @return bool setcookie() result : false if output sent before, true otherwise. This does not indicate whether the user accepted the cookie.
*/
function yourls_setcookie($name, $value, $expire, $path, $domain, $secure, $httponly) {
$samesite = yourls_apply_filter('setcookie_samesite', 'Lax' );
return(setcookie($name, $value, array(
'expires' => $expire,
'path' => $path,
'domain' => $domain,
'samesite' => $samesite,
'secure' => $secure,
'httponly' => $httponly,
)));
}
/**
* Set user name
*
* @param string $user Username
* @return void
*/
function yourls_set_user( $user ) {
if( !defined( 'YOURLS_USER' ) )
define( 'YOURLS_USER', $user );
}
/**
* Get YOURLS_COOKIE_LIFE value (ie the life span of an auth cookie in seconds)
*
* Use this function instead of directly using the constant. This way, its value can be modified by plugins
* on a per case basis
*
* @since 1.7.7
* @see includes/Config/Config.php
* @return integer cookie life span, in seconds
*/
function yourls_get_cookie_life() {
return yourls_apply_filter( 'get_cookie_life', YOURLS_COOKIE_LIFE );
}
/**
* Get YOURLS_NONCE_LIFE value (ie life span of a nonce in seconds)
*
* Use this function instead of directly using the constant. This way, its value can be modified by plugins
* on a per case basis
*
* @since 1.7.7
* @see includes/Config/Config.php
* @see https://en.wikipedia.org/wiki/Cryptographic_nonce
* @return integer nonce life span, in seconds
*/
function yourls_get_nonce_life() {
return yourls_apply_filter( 'get_nonce_life', YOURLS_NONCE_LIFE );
}
/**
* Get YOURLS cookie name
*
* The name is unique for each install, to prevent mismatch between sho.rt and very.sho.rt -- see #1673
*
* TODO: when multi user is implemented, the whole cookie stuff should be reworked to allow storing multiple users
*
* @since 1.7.1
* @return string unique cookie name for a given YOURLS site
*/
function yourls_cookie_name() {
return yourls_apply_filter( 'cookie_name', 'yourls_' . yourls_salt( yourls_get_yourls_site() ) );
}
/**
* Get auth cookie value
*
* @since 1.7.7
* @param string $user user name
* @return string cookie value
*/
function yourls_cookie_value( $user ) {
return yourls_apply_filter( 'set_cookie_value', yourls_salt( $user ?? '' ), $user );
}
/**
* Return a time-dependent string for nonce creation
*
* Actually, this returns a float: ceil rounds up a value but is of type float, see https://www.php.net/ceil
*
* @return float
*/
function yourls_tick() {
return ceil( time() / yourls_get_nonce_life() );
}
/**
* Return hashed string
*
* This function is badly named, it's not a salt or a salted string : it's a cryptographic hash.
*
* @since 1.4.1
* @param string $string string to salt
* @return string hashed string
*/
function yourls_salt( $string ) {
$salt = defined('YOURLS_COOKIEKEY') ? YOURLS_COOKIEKEY : md5(__FILE__) ;
return yourls_apply_filter( 'yourls_salt', hash_hmac( yourls_hmac_algo(), $string, $salt), $string );
}
/**
* Return an available hash_hmac() algorithm
*
* @since 1.8.3
* @return string hash_hmac() algorithm
*/
function yourls_hmac_algo() {
$algo = yourls_apply_filter( 'hmac_algo', 'sha256' );
if( !in_array( $algo, hash_hmac_algos() ) ) {
$algo = 'sha256';
}
return $algo;
}
/**
* Create a time limited, action limited and user limited token
*
* @param string $action Action to create nonce for
* @param false|string $user Optional user string, false for current user
* @return string Nonce token
*/
function yourls_create_nonce($action, $user = false ) {
if( false === $user ) {
$user = defined('YOURLS_USER') ? YOURLS_USER : '-1';
}
$tick = yourls_tick();
$nonce = substr( yourls_salt($tick . $action . $user), 0, 10 );
// Allow plugins to alter the nonce
return yourls_apply_filter( 'create_nonce', $nonce, $action, $user );
}
/**
* Echoes or returns a nonce field for inclusion into a form
*
* @param string $action Action to create nonce for
* @param string $name Optional name of nonce field -- defaults to 'nonce'
* @param false|string $user Optional user string, false if unspecified
* @param bool $echo True to echo, false to return nonce field
* @return string Nonce field
*/
function yourls_nonce_field($action, $name = 'nonce', $user = false, $echo = true ) {
$field = '<input type="hidden" id="'.$name.'" name="'.$name.'" value="'.yourls_create_nonce( $action, $user ).'" />';
if( $echo )
echo $field."\n";
return $field;
}
/**
* Add a nonce to a URL. If URL omitted, adds nonce to current URL
*
* @param string $action Action to create nonce for
* @param string $url Optional URL to add nonce to -- defaults to current URL
* @param string $name Optional name of nonce field -- defaults to 'nonce'
* @param false|string $user Optional user string, false if unspecified
* @return string URL with nonce added
*/
function yourls_nonce_url($action, $url = false, $name = 'nonce', $user = false ) {
$nonce = yourls_create_nonce( $action, $user );
return yourls_add_query_arg( $name, $nonce, $url );
}
/**
* Check validity of a nonce (ie time span, user and action match).
*
* Returns true if valid, dies otherwise (yourls_die() or die($return) if defined).
* If $nonce is false or unspecified, it will use $_REQUEST['nonce']
*
* @param string $action
* @param false|string $nonce Optional, string: nonce value, or false to use $_REQUEST['nonce']
* @param false|string $user Optional, string user, false for current user
* @param string $return Optional, string: message to die with if nonce is invalid
* @return bool|void True if valid, dies otherwise
*/
function yourls_verify_nonce($action, $nonce = false, $user = false, $return = '' ) {
// Get user
if( false === $user ) {
$user = defined('YOURLS_USER') ? YOURLS_USER : '-1';
}
// Get nonce value from $_REQUEST if not specified
if( false === $nonce && isset( $_REQUEST['nonce'] ) ) {
$nonce = $_REQUEST['nonce'];
}
// Allow plugins to short-circuit the rest of the function
if (yourls_apply_filter( 'verify_nonce', false, $action, $nonce, $user, $return ) === true) {
return true;
}
// What nonce should be
$valid = yourls_create_nonce( $action, $user );
if( $nonce === $valid ) {
return true;
} else {
if( $return )
die( $return );
yourls_die( yourls__( 'Unauthorized action or expired link' ), yourls__( 'Error' ), 403 );
}
}
/**
* Check if YOURLS_USER comes from environment variables
*
* @since 1.8.2
* @return bool true if YOURLS_USER and YOURLS_PASSWORD are defined as environment variables
*/
function yourls_is_user_from_env() {
return yourls_apply_filter('is_user_from_env', getenv('YOURLS_USER') && getenv('YOURLS_PASSWORD'));
}
/**
* Check if we should hash passwords in the config file
*
* By default, passwords are hashed. They are not if
* - there is no password in clear text in the config file (ie everything is already hashed)
* - the user defined constant YOURLS_NO_HASH_PASSWORD is true, see https://docs.yourls.org/guide/essentials/credentials.html#i-don-t-want-to-encrypt-my-password
* - YOURLS_USER and YOURLS_PASSWORD are provided by the environment, not the config file
*
* @since 1.8.2
* @return bool
*/
function yourls_maybe_hash_passwords() {
$hash = true;
if ( !yourls_has_cleartext_passwords()
OR (yourls_skip_password_hashing())
OR (yourls_is_user_from_env())
) {
$hash = false;
}
return yourls_apply_filter('maybe_hash_password', $hash );
}
/**
* Check if user setting for skipping password hashing is set
*
* @since 1.8.2
* @return bool
*/
function yourls_skip_password_hashing() {
return yourls_apply_filter('skip_password_hashing', defined('YOURLS_NO_HASH_PASSWORD') && YOURLS_NO_HASH_PASSWORD);
}

110
functions-compat.php Normal file
View File

@ -0,0 +1,110 @@
<?php
/*
* YOURLS
* Compatibility functions when either missing from older PHP versions or not included by default
*/
// @codeCoverageIgnoreStart
/**
* json_encode for PHP, should someone run a distro without php-json -- see http://askubuntu.com/questions/361424/
*
*/
if( !function_exists( 'json_encode' ) ) {
function json_encode( $array ) {
return yourls_array_to_json( $array );
}
}
/**
* Converts an associative array of arbitrary depth and dimension into JSON representation. Used for compatibility with older PHP builds.
*
* @param array $array the array to convert.
* @return mixed The resulting JSON string, or false if the argument was not an array.
* @author Andy Rusterholz
* @link http://php.net/json_encode (see comments)
*/
function yourls_array_to_json( $array ){
if( !is_array( $array ) ){
return false;
}
$associative = count( array_diff( array_keys($array), array_keys( array_keys( $array )) ));
if( $associative ){
$construct = array();
foreach( $array as $key => $value ){
// We first copy each key/value pair into a staging array,
// formatting each key and value properly as we go.
// Format the key:
if( is_numeric( $key ) ){
$key = "key_$key";
}
$key = '"'.addslashes( $key ).'"';
// Format the value:
if( is_array( $value )){
$value = yourls_array_to_json( $value );
} else if( !is_numeric( $value ) || is_string( $value ) ){
$value = '"'.addslashes( $value ).'"';
}
// Add to staging array:
$construct[] = "$key: $value";
}
// Then we collapse the staging array into the JSON form:
$result = "{ " . implode( ", ", $construct ) . " }";
} else { // If the array is a vector (not associative):
$construct = array();
foreach( $array as $value ){
// Format the value:
if( is_array( $value )){
$value = yourls_array_to_json( $value );
} else if( !is_numeric( $value ) || is_string( $value ) ){
$value = '"'.addslashes($value).'"';
}
// Add to staging array:
$construct[] = $value;
}
// Then we collapse the staging array into the JSON form:
$result = "[ " . implode( ", ", $construct ) . " ]";
}
return $result;
}
/**
* BC Math functions (assuming if one doesn't exist, none does)
*
*/
if ( !function_exists( 'bcdiv' ) ) {
function bcdiv( $dividend, $divisor ) {
$quotient = floor( $dividend/$divisor );
return $quotient;
}
function bcmod( $dividend, $modulo ) {
$remainder = $dividend%$modulo;
return $remainder;
}
function bcmul( $left, $right ) {
return $left * $right;
}
function bcadd( $left, $right ) {
return $left + $right;
}
function bcpow( $base, $power ) {
return pow( $base, $power );
}
}
// @codeCoverageIgnoreEnd

67
functions-debug.php Normal file
View File

@ -0,0 +1,67 @@
<?php
/*
* Functions relative to debugging
*/
/**
* Add a message to the debug log
*
* When in debug mode ( YOURLS_DEBUG == true ) the debug log is echoed in yourls_html_footer()
* Log messages are appended to $ydb->debug_log array, which is instanciated within class ezSQLcore_YOURLS
*
* @since 1.7
* @param string $msg Message to add to the debug log
* @return string The message itself
*/
function yourls_debug_log( $msg ) {
yourls_do_action( 'debug_log', $msg );
// Get the DB object ($ydb), get its profiler (\Aura\Sql\Profiler\Profiler), its logger (\Aura\Sql\Profiler\MemoryLogger) and
// pass it a unused argument (loglevel) and the message
yourls_get_db()->getProfiler()->getLogger()->log( 'debug', $msg);
return $msg;
}
/**
* Get the debug log
*
* @since 1.7.3
* @return array
*/
function yourls_get_debug_log() {
return yourls_get_db()->getProfiler()->getLogger()->getMessages();
}
/**
* Get number of SQL queries performed
*
* @return int
*/
function yourls_get_num_queries() {
return yourls_apply_filter( 'get_num_queries', yourls_get_db()->get_num_queries() );
}
/**
* Debug mode set
*
* @since 1.7.3
* @param bool $bool Debug on or off
* @return void
*/
function yourls_debug_mode( $bool ) {
// log queries if true
yourls_get_db()->getProfiler()->setActive( (bool)$bool );
// report notices if true
$level = $bool ? -1 : ( E_ERROR | E_PARSE );
error_reporting( $level );
}
/**
* Return YOURLS debug mode
*
* @since 1.7.7
* @return bool
*/
function yourls_get_debug_mode() {
return defined( 'YOURLS_DEBUG' ) && YOURLS_DEBUG;
}

337
functions-deprecated.php Normal file
View File

@ -0,0 +1,337 @@
<?php
/**
* Deprecated functions from past YOURLS versions. Don't use them, as they may be
* removed in a later version. Use the newer alternatives instead.
*
* Note to devs: when deprecating a function, move it here. Then check all the places
* in core that might be using it, including core plugins.
*
* Usage : yourls_deprecated_function( 'function_name', 'version', 'replacement' );
* Output: "{function_name} is deprecated since version {version}! Use {replacement} instead."
*
* Usage : yourls_deprecated_function( 'function_name', 'version' );
* Output: "{function_name} is deprecated since version {version} with no alternative available."
*
* @see yourls_deprecated_function()
*/
// @codeCoverageIgnoreStart
/**
* Return current admin page, or null if not an admin page. Was not used anywhere.
*
* @return mixed string if admin page, null if not an admin page
* @since 1.6
* @deprecated 1.9.1
*/
function yourls_current_admin_page() {
yourls_deprecated_function( __FUNCTION__, '1.9.1' );
if( yourls_is_admin() ) {
$current = substr( yourls_get_request(), 6 );
if( $current === false )
$current = 'index.php'; // if current page is http://sho.rt/admin/ instead of http://sho.rt/admin/index.php
return $current;
}
return null;
}
/**
* PHP emulation of JS's encodeURI
*
* @link https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/encodeURI
* @deprecated 1.9.1
* @param string $url
* @return string
*/
function yourls_encodeURI($url) {
yourls_deprecated_function( __FUNCTION__, '1.9.1', '' );
// Decode URL all the way
$result = yourls_rawurldecode_while_encoded( $url );
// Encode once
$result = strtr( rawurlencode( $result ), array (
'%3B' => ';', '%2C' => ',', '%2F' => '/', '%3F' => '?', '%3A' => ':', '%40' => '@',
'%26' => '&', '%3D' => '=', '%2B' => '+', '%24' => '$', '%21' => '!', '%2A' => '*',
'%27' => '\'', '%28' => '(', '%29' => ')', '%23' => '#',
) );
// @TODO:
// Known limit: this will most likely break IDN URLs such as http://www.académie-française.fr/
// To fully support IDN URLs, advocate use of a plugin.
return yourls_apply_filter( 'encodeURI', $result, $url );
}
/**
* Check if a file is a plugin file
*
* @deprecated 1.8.3
*/
function yourls_validate_plugin_file( $file ) {
yourls_deprecated_function( __FUNCTION__, '1.8.3', 'yourls_is_a_plugin_file' );
return yourls_is_a_plugin_file($file);
}
/**
* Return a unique(ish) hash for a string to be used as a valid HTML id
*
* @deprecated 1.8.3
*/
function yourls_string2htmlid( $string ) {
yourls_deprecated_function( __FUNCTION__, '1.8.3', 'yourls_unique_element_id' );
return yourls_apply_filter( 'string2htmlid', 'y'.abs( crc32( $string ) ) );
}
/**
* Get search text from query string variables search_protocol, search_slashes and search
*
* Some servers don't like query strings containing "(ht|f)tp(s)://". A javascript bit
* explodes the search text into protocol, slashes and the rest (see JS function
* split_search_text_before_search()) and this function glues pieces back together
* See issue https://github.com/YOURLS/YOURLS/issues/1576
*
* @since 1.7
* @deprecated 1.8.2
* @return string Search string
*/
function yourls_get_search_text() {
yourls_deprecated_function( __FUNCTION__, '1.8.2', 'YOURLS\Views\AdminParams::get_search' );
$view_params = new YOURLS\Views\AdminParams();
return $view_params->get_search();
}
/**
* Retrieve the current time based on specified type. Stolen from WP.
*
* The 'mysql' type will return the time in the format for MySQL DATETIME field.
* The 'timestamp' type will return the current timestamp.
*
* If $gmt is set to either '1' or 'true', then both types will use GMT time.
* if $gmt is false, the output is adjusted with the GMT offset in the WordPress option.
*
* @since 1.6
* @deprecated 1.7.10
*
* @param string $type Either 'mysql' or 'timestamp'.
* @param int|bool $gmt Optional. Whether to use GMT timezone. Default is false.
* @return int|string String if $type is 'gmt', int if $type is 'timestamp'.
*/
function yourls_current_time( $type, $gmt = 0 ) {
yourls_deprecated_function( __FUNCTION__, '1.7.10', 'yourls_get_timestamp' );
switch ( $type ) {
case 'mysql':
return ( $gmt ) ? gmdate( 'Y-m-d H:i:s' ) : gmdate( 'Y-m-d H:i:s', yourls_get_timestamp( time() ));
case 'timestamp':
return ( $gmt ) ? time() : yourls_get_timestamp( time() );
}
}
/**
* Lowercase scheme and domain of an URI - see issues 591, 1630, 1889
*
* Renamed to yourls_normalize_uri() in 1.7.10 because the function now does more than just
* lowercasing the scheme and domain.
*
* @deprecated 1.7.10
*
*/
function yourls_lowercase_scheme_domain( $url ) {
yourls_deprecated_function( __FUNCTION__, '1.7.10', 'yourls_normalize_uri' );
return yourls_normalize_uri( $url );
}
/**
* The original string sanitize function
*
* @deprecated 1.7.10
*
*/
function yourls_sanitize_string( $string, $restrict_to_shorturl_charset = false ) {
yourls_deprecated_function( __FUNCTION__, '1.7.10', 'yourls_sanitize_keyword' );
return yourls_sanitize_keyword( $string, $restrict_to_shorturl_charset );
}
/**
* Return favicon URL (either default or custom)
*
* @deprecated 1.7.10
*
*/
function yourls_favicon( $echo = true ) {
yourls_deprecated_function( __FUNCTION__, '1.7.10', 'yourls_get_yourls_favicon_url' );
return yourls_get_yourls_favicon_url( $echo );
}
/**
* Return array of stats for a given keyword
*
* @deprecated 1.7.10
*
*/
function yourls_get_link_stats( $url ) {
yourls_deprecated_function( __FUNCTION__, '1.7.10', 'yourls_get_keyword_stats' );
return yourls_get_keyword_stats( $url );
}
/**
* Check if a long URL already exists in the DB. Return NULL (doesn't exist) or an object with URL informations.
*
* @since 1.5.1
* @deprecated 1.7.10
*
*/
function yourls_url_exists( $url ) {
yourls_deprecated_function( __FUNCTION__, '1.7.10', 'yourls_long_url_exists' );
return yourls_long_url_exists( $url );
}
/**
* Return word or words if more than one
*
*/
function yourls_plural( $word, $count=1 ) {
yourls_deprecated_function( __FUNCTION__, '1.6', 'yourls_n' );
return $word . ($count > 1 ? 's' : '');
}
/**
* Return list of all shorturls associated to the same long URL. Returns NULL or array of keywords.
*
*/
function yourls_get_duplicate_keywords( $longurl ) {
yourls_deprecated_function( __FUNCTION__, '1.7', 'yourls_get_longurl_keywords' );
if( !yourls_allow_duplicate_longurls() )
return NULL;
return yourls_apply_filter( 'get_duplicate_keywords', yourls_get_longurl_keywords ( $longurl ), $longurl );
}
/**
* Make sure a integer is safe
*
* Note: this function is dumb and dumbly named since it does not intval(). DO NOT USE.
*
*/
function yourls_intval( $int ) {
yourls_deprecated_function( __FUNCTION__, '1.7', 'yourls_sanitize_int' );
return yourls_escape( $int );
}
/**
* Get remote content via a GET request using best transport available
*
*/
function yourls_get_remote_content( $url, $maxlen = 4096, $timeout = 5 ) {
yourls_deprecated_function( __FUNCTION__, '1.7', 'yourls_http_get_body' );
return yourls_http_get_body( $url );
}
/**
* Alias for yourls_apply_filter because I never remember if it's _filter or _filters
*
* At first I thought it made semantically more sense but thinking about it, I was wrong. It's one filter.
* There may be several function hooked into it, but it still the same one filter.
*
* @since 1.6
* @deprecated 1.7.1
*
* @param string $hook the name of the YOURLS element or action
* @param mixed $value the value of the element before filtering
* @return mixed
*/
function yourls_apply_filters( $hook, $value = '' ) {
yourls_deprecated_function( __FUNCTION__, '1.7.1', 'yourls_apply_filter' );
return yourls_apply_filter( $hook, $value );
}
/**
* Check if we'll need interface display function (ie not API or redirection)
*
*/
function yourls_has_interface() {
yourls_deprecated_function( __FUNCTION__, '1.7.1' );
if( yourls_is_API() or yourls_is_GO() )
return false;
return true;
}
/**
* Check if a proxy is defined for HTTP requests
*
* @uses YOURLS_PROXY
* @since 1.7
* @deprecated 1.7.1
* @return bool true if a proxy is defined, false otherwise
*/
function yourls_http_proxy_is_defined() {
yourls_deprecated_function( __FUNCTION__, '1.7.1', 'yourls_http_get_proxy' );
return yourls_apply_filter( 'http_proxy_is_defined', defined( 'YOURLS_PROXY' ) );
}
/**
* Displays translated string with gettext context
*
* This function has been renamed yourls_xe() for consistency with other *e() functions
*
* @see yourls_x()
* @since 1.6
* @deprecated 1.7.1
*
* @param string $text Text to translate
* @param string $context Context information for the translators
* @param string $domain Optional. Domain to retrieve the translated text
* @return string Translated context string without pipe
*/
function yourls_ex( $text, $context, $domain = 'default' ) {
yourls_deprecated_function( __FUNCTION__, '1.7.1', 'yourls_xe' );
echo yourls_xe( $text, $context, $domain );
}
/**
* Escape a string or an array of strings before DB usage. ALWAYS escape before using in a SQL query. Thanks.
*
* Deprecated in 1.7.3 because we moved onto using PDO and using built-in escaping functions, instead of
* rolling our own.
*
* @deprecated 1.7.3
* @param string|array $data string or array of strings to be escaped
* @return string|array escaped data
*/
function yourls_escape( $data ) {
yourls_deprecated_function( __FUNCTION__, '1.7.3', 'PDO' );
if( is_array( $data ) ) {
foreach( $data as $k => $v ) {
if( is_array( $v ) ) {
$data[ $k ] = yourls_escape( $v );
} else {
$data[ $k ] = yourls_escape_real( $v );
}
}
} else {
$data = yourls_escape_real( $data );
}
return $data;
}
/**
* "Real" escape. This function should NOT be called directly. Use yourls_escape() instead.
*
* This function uses a "real" escape if possible, using PDO, MySQL or MySQLi functions,
* with a fallback to a "simple" addslashes
* If you're implementing a custom DB engine or a custom cache system, you can define an
* escape function using filter 'custom_escape_real'
*
* @since 1.7
* @deprecated 1.7.3
* @param string $a string to be escaped
* @return string escaped string
*/
function yourls_escape_real( $string ) {
yourls_deprecated_function( __FUNCTION__, '1.7.3', 'PDO' );
global $ydb;
if( isset( $ydb ) && ( $ydb instanceof \YOURLS\Database\YDB ) )
return $ydb->escape( $string );
// YOURLS DB classes have been bypassed by a custom DB engine or a custom cache layer
return yourls_apply_filter( 'custom_escape_real', addslashes( $string ), $string );
}
// @codeCoverageIgnoreEnd

798
functions-formatting.php Normal file
View File

@ -0,0 +1,798 @@
<?php
/*
* YOURLS
* Function library for anything related to formatting / validating / sanitizing
*/
/**
* Convert an integer (1337) to a string (3jk).
*
* @param int $num Number to convert
* @param string $chars Characters to use for conversion
* @return string Converted number
*/
function yourls_int2string($num, $chars = null) {
if( $chars == null )
$chars = yourls_get_shorturl_charset();
$string = '';
$len = strlen( $chars );
while( $num >= $len ) {
$mod = bcmod( (string)$num, (string)$len );
$num = bcdiv( (string)$num, (string)$len );
$string = $chars[ $mod ] . $string;
}
$string = $chars[ intval( $num ) ] . $string;
return yourls_apply_filter( 'int2string', $string, $num, $chars );
}
/**
* Convert a string (3jk) to an integer (1337)
*
* @param string $string String to convert
* @param string $chars Characters to use for conversion
* @return string Number (as a string)
*/
function yourls_string2int($string, $chars = null) {
if( $chars == null )
$chars = yourls_get_shorturl_charset();
$integer = 0;
$string = strrev( $string );
$baselen = strlen( $chars );
$inputlen = strlen( $string );
for ($i = 0; $i < $inputlen; $i++) {
$index = strpos( $chars, $string[$i] );
$integer = bcadd( (string)$integer, bcmul( (string)$index, bcpow( (string)$baselen, (string)$i ) ) );
}
return yourls_apply_filter( 'string2int', $integer, $string, $chars );
}
/**
* Return a unique string to be used as a valid HTML id
*
* @since 1.8.3
* @param string $prefix Optional prefix
* @return string The unique string
*/
function yourls_unique_element_id($prefix = 'yid') {
static $id_counter = 0;
return yourls_apply_filter( 'unique_element_id', $prefix . (string)++$id_counter );
}
/**
* Make sure a link keyword (ie "1fv" as in "http://sho.rt/1fv") is acceptable
*
* If we are ADDING or EDITING a short URL, the keyword must comply to the short URL charset: every
* character that doesn't belong to it will be removed.
* But otherwise we must have a more conservative approach: we could be checking for a keyword that
* was once valid but now the short URL charset has changed. In such a case, we are treating the keyword for what
* it is: just a part of a URL, hence sanitize it as a URL.
*
* @param string $keyword short URL keyword
* @param bool $restrict_to_shorturl_charset Optional, default false. True if we want the keyword to comply to short URL charset
* @return string The sanitized keyword
*/
function yourls_sanitize_keyword( $keyword, $restrict_to_shorturl_charset = false ) {
if( $restrict_to_shorturl_charset === true ) {
// make a regexp pattern with the shorturl charset, and remove everything but this
$pattern = yourls_make_regexp_pattern( yourls_get_shorturl_charset() );
$valid = (string) substr( preg_replace( '![^'.$pattern.']!', '', $keyword ), 0, 199 );
} else {
$valid = yourls_sanitize_url( $keyword );
}
return yourls_apply_filter( 'sanitize_string', $valid, $keyword, $restrict_to_shorturl_charset );
}
/**
* Sanitize a page title. No HTML per W3C http://www.w3.org/TR/html401/struct/global.html#h-7.4.2
*
*
* @since 1.5
* @param string $unsafe_title Title, potentially unsafe
* @param string $fallback Optional fallback if after sanitization nothing remains
* @return string Safe title
*/
function yourls_sanitize_title( $unsafe_title, $fallback = '' ) {
$title = $unsafe_title;
$title = strip_tags( $title );
$title = preg_replace( "/\s+/", ' ', trim( $title ) );
if ( '' === $title || false === $title ) {
$title = $fallback;
}
return yourls_apply_filter( 'sanitize_title', $title, $unsafe_title, $fallback );
}
/**
* A few sanity checks on the URL. Used for redirection or DB.
* For redirection when you don't trust the URL ($_SERVER variable, query string), see yourls_sanitize_url_safe()
* For display purpose, see yourls_esc_url()
*
* @param string $unsafe_url unsafe URL
* @param array $protocols Optional allowed protocols, default to global $yourls_allowedprotocols
* @return string Safe URL
*/
function yourls_sanitize_url( $unsafe_url, $protocols = array() ) {
$url = yourls_esc_url( $unsafe_url, 'redirection', $protocols );
return yourls_apply_filter( 'sanitize_url', $url, $unsafe_url );
}
/**
* A few sanity checks on the URL, including CRLF. Used for redirection when URL to be sanitized is critical and cannot be trusted.
*
* Use when critical URL comes from user input or environment variable. In such a case, this function will sanitize
* it like yourls_sanitize_url() but will also remove %0A and %0D to prevent CRLF injection.
* Still, some legit URLs contain %0A or %0D (see issue 2056, and for extra fun 1694, 1707, 2030, and maybe others)
* so we're not using this function unless it's used for internal redirection when the target location isn't
* hardcoded, to avoid XSS via CRLF
*
* @since 1.7.2
* @param string $unsafe_url unsafe URL
* @param array $protocols Optional allowed protocols, default to global $yourls_allowedprotocols
* @return string Safe URL
*/
function yourls_sanitize_url_safe( $unsafe_url, $protocols = array() ) {
$url = yourls_esc_url( $unsafe_url, 'safe', $protocols );
return yourls_apply_filter( 'sanitize_url_safe', $url, $unsafe_url );
}
/**
* Perform a replacement while a string is found, eg $subject = '%0%0%0DDD', $search ='%0D' -> $result =''
*
* Stolen from WP's _deep_replace
*
* @param string|array $search Needle, or array of needles.
* @param string $subject Haystack.
* @return string The string with the replaced values.
*/
function yourls_deep_replace($search, $subject ){
$found = true;
while($found) {
$found = false;
foreach( (array) $search as $val ) {
while( strpos( $subject, $val ) !== false ) {
$found = true;
$subject = str_replace( $val, '', $subject );
}
}
}
return $subject;
}
/**
* Make sure an integer is a valid integer (PHP's intval() limits to too small numbers)
*
* @param int $int Integer to check
* @return string Integer as a string
*/
function yourls_sanitize_int($int ) {
return ( substr( preg_replace( '/[^0-9]/', '', strval( $int ) ), 0, 20 ) );
}
/**
* Sanitize an IP address
* No check on validity, just return a sanitized string
*
* @param string $ip IP address
* @return string IP address
*/
function yourls_sanitize_ip($ip ) {
return preg_replace( '/[^0-9a-fA-F:., ]/', '', $ip );
}
/**
* Make sure a date is m(m)/d(d)/yyyy, return false otherwise
*
* @param string $date Date to check
* @return false|mixed Date in format m(m)/d(d)/yyyy or false if invalid
*/
function yourls_sanitize_date($date ) {
if( !preg_match( '!^\d{1,2}/\d{1,2}/\d{4}$!' , $date ) ) {
return false;
}
return $date;
}
/**
* Sanitize a date for SQL search. Return false if malformed input.
*
* @param string $date Date
* @return false|string String in Y-m-d format for SQL search or false if malformed input
*/
function yourls_sanitize_date_for_sql($date) {
if( !yourls_sanitize_date( $date ) )
return false;
return date( 'Y-m-d', strtotime( $date ) );
}
/**
* Return trimmed string, optionally append '[...]' if string is too long
*
* @param string $string String to trim
* @param int $length Maximum length of string
* @param string $append String to append if trimmed
* @return string Trimmed string
*/
function yourls_trim_long_string($string, $length = 60, $append = '[...]') {
$newstring = $string;
if ( mb_strlen( $newstring ) > $length ) {
$newstring = mb_substr( $newstring, 0, $length - mb_strlen( $append ), 'UTF-8' ) . $append;
}
return yourls_apply_filter( 'trim_long_string', $newstring, $string, $length, $append );
}
/**
* Sanitize a version number (1.4.1-whatever-RC1 -> 1.4.1)
*
* The regexp searches for the first digits, then a period, then more digits and periods, and discards
* all the rest.
* For instance, 'mysql-5.5-beta' and '5.5-RC1' return '5.5'
*
* @since 1.4.1
* @param string $version Version number
* @return string Sanitized version number
*/
function yourls_sanitize_version( $version ) {
preg_match( '/([0-9]+\.[0-9.]+).*$/', $version, $matches );
$version = isset($matches[1]) ? trim($matches[1], '.') : '';
return $version;
}
/**
* Sanitize a filename (no Win32 stuff)
*
* @param string $file File name
* @return string|null Sanitized file name (or null if it's just backslashes, ok...)
*/
function yourls_sanitize_filename($file) {
$file = str_replace( '\\', '/', $file ); // sanitize for Win32 installs
$file = preg_replace( '|/+|' ,'/', $file ); // remove any duplicate slash
return $file;
}
/**
* Check if a string seems to be UTF-8. Stolen from WP.
*
* @param string $str String to check
* @return bool Whether string seems valid UTF-8
*/
function yourls_seems_utf8($str) {
$length = strlen( $str );
for ( $i=0; $i < $length; $i++ ) {
$c = ord( $str[ $i ] );
if ( $c < 0x80 ) $n = 0; # 0bbbbbbb
elseif (($c & 0xE0) == 0xC0) $n=1; # 110bbbbb
elseif (($c & 0xF0) == 0xE0) $n=2; # 1110bbbb
elseif (($c & 0xF8) == 0xF0) $n=3; # 11110bbb
elseif (($c & 0xFC) == 0xF8) $n=4; # 111110bb
elseif (($c & 0xFE) == 0xFC) $n=5; # 1111110b
else return false; # Does not match any model
for ($j=0; $j<$n; $j++) { # n bytes matching 10bbbbbb follow ?
if ((++$i == $length) || ((ord($str[$i]) & 0xC0) != 0x80))
return false;
}
}
return true;
}
/**
* Check for PCRE /u modifier support. Stolen from WP.
*
* Just in case "PCRE is not compiled with PCRE_UTF8" which seems to happen
* on some distros
*
* @since 1.7.1
*
* @return bool whether there's /u support or not
*/
function yourls_supports_pcre_u() {
static $utf8_pcre;
if( !isset( $utf8_pcre ) ) {
$utf8_pcre = (bool) @preg_match( '/^./u', 'a' );
}
return $utf8_pcre;
}
/**
* Checks for invalid UTF8 in a string. Stolen from WP
*
* @since 1.6
*
* @param string $string The text which is to be checked.
* @param boolean $strip Optional. Whether to attempt to strip out invalid UTF8. Default is false.
* @return string The checked text.
*/
function yourls_check_invalid_utf8( $string, $strip = false ) {
$string = (string) $string;
if ( 0 === strlen( $string ) ) {
return '';
}
// We can't demand utf8 in the PCRE installation, so just return the string in those cases
if ( ! yourls_supports_pcre_u() ) {
return $string;
}
// preg_match fails when it encounters invalid UTF8 in $string
if ( 1 === @preg_match( '/^./us', $string ) ) {
return $string;
}
// Attempt to strip the bad chars if requested (not recommended)
if ( $strip && function_exists( 'iconv' ) ) {
return iconv( 'utf-8', 'utf-8', $string );
}
return '';
}
/**
* Converts a number of special characters into their HTML entities. Stolen from WP.
*
* Specifically deals with: &, <, >, ", and '.
*
* $quote_style can be set to ENT_COMPAT to encode " to
* &quot;, or ENT_QUOTES to do both. Default is ENT_NOQUOTES where no quotes are encoded.
*
* @since 1.6
*
* @param string $string The text which is to be encoded.
* @param mixed $quote_style Optional. Converts double quotes if set to ENT_COMPAT, both single and double if set to ENT_QUOTES or none if set to ENT_NOQUOTES. Also compatible with old values; converting single quotes if set to 'single', double if set to 'double' or both if otherwise set. Default is ENT_NOQUOTES.
* @param boolean $double_encode Optional. Whether to encode existing html entities. Default is false.
* @return string The encoded text with HTML entities.
*/
function yourls_specialchars( $string, $quote_style = ENT_NOQUOTES, $double_encode = false ) {
$string = (string) $string;
if ( 0 === strlen( $string ) )
return '';
// Don't bother if there are no specialchars - saves some processing
if ( ! preg_match( '/[&<>"\']/', $string ) )
return $string;
// Account for the previous behaviour of the function when the $quote_style is not an accepted value
if ( empty( $quote_style ) )
$quote_style = ENT_NOQUOTES;
elseif ( ! in_array( $quote_style, array( 0, 2, 3, 'single', 'double' ), true ) )
$quote_style = ENT_QUOTES;
$charset = 'UTF-8';
$_quote_style = $quote_style;
if ( $quote_style === 'double' ) {
$quote_style = ENT_COMPAT;
$_quote_style = ENT_COMPAT;
} elseif ( $quote_style === 'single' ) {
$quote_style = ENT_NOQUOTES;
}
// Handle double encoding ourselves
if ( $double_encode ) {
$string = @htmlspecialchars( $string, $quote_style, $charset );
} else {
// Decode &amp; into &
$string = yourls_specialchars_decode( $string, $_quote_style );
// Guarantee every &entity; is valid or re-encode the &
$string = yourls_kses_normalize_entities( $string );
// Now re-encode everything except &entity;
$string = preg_split( '/(&#?x?[0-9a-z]+;)/i', $string, -1, PREG_SPLIT_DELIM_CAPTURE );
for ( $i = 0; $i < count( $string ); $i += 2 )
$string[$i] = @htmlspecialchars( $string[$i], $quote_style, $charset );
$string = implode( '', $string );
}
// Backwards compatibility
if ( 'single' === $_quote_style )
$string = str_replace( "'", '&#039;', $string );
return $string;
}
/**
* Converts a number of HTML entities into their special characters. Stolen from WP.
*
* Specifically deals with: &, <, >, ", and '.
*
* $quote_style can be set to ENT_COMPAT to decode " entities,
* or ENT_QUOTES to do both " and '. Default is ENT_NOQUOTES where no quotes are decoded.
*
* @since 1.6
*
* @param string $string The text which is to be decoded.
* @param mixed $quote_style Optional. Converts double quotes if set to ENT_COMPAT, both single and double if set to ENT_QUOTES or none if set to ENT_NOQUOTES. Also compatible with old _wp_specialchars() values; converting single quotes if set to 'single', double if set to 'double' or both if otherwise set. Default is ENT_NOQUOTES.
* @return string The decoded text without HTML entities.
*/
function yourls_specialchars_decode( $string, $quote_style = ENT_NOQUOTES ) {
$string = (string) $string;
if ( 0 === strlen( $string ) ) {
return '';
}
// Don't bother if there are no entities - saves a lot of processing
if ( strpos( $string, '&' ) === false ) {
return $string;
}
// Match the previous behaviour of _wp_specialchars() when the $quote_style is not an accepted value
if ( empty( $quote_style ) ) {
$quote_style = ENT_NOQUOTES;
} elseif ( !in_array( $quote_style, array( 0, 2, 3, 'single', 'double' ), true ) ) {
$quote_style = ENT_QUOTES;
}
// More complete than get_html_translation_table( HTML_SPECIALCHARS )
$single = array( '&#039;' => '\'', '&#x27;' => '\'' );
$single_preg = array( '/&#0*39;/' => '&#039;', '/&#x0*27;/i' => '&#x27;' );
$double = array( '&quot;' => '"', '&#034;' => '"', '&#x22;' => '"' );
$double_preg = array( '/&#0*34;/' => '&#034;', '/&#x0*22;/i' => '&#x22;' );
$others = array( '&lt;' => '<', '&#060;' => '<', '&gt;' => '>', '&#062;' => '>', '&amp;' => '&', '&#038;' => '&', '&#x26;' => '&' );
$others_preg = array( '/&#0*60;/' => '&#060;', '/&#0*62;/' => '&#062;', '/&#0*38;/' => '&#038;', '/&#x0*26;/i' => '&#x26;' );
$translation = $translation_preg = [];
if ( $quote_style === ENT_QUOTES ) {
$translation = array_merge( $single, $double, $others );
$translation_preg = array_merge( $single_preg, $double_preg, $others_preg );
} elseif ( $quote_style === ENT_COMPAT || $quote_style === 'double' ) {
$translation = array_merge( $double, $others );
$translation_preg = array_merge( $double_preg, $others_preg );
} elseif ( $quote_style === 'single' ) {
$translation = array_merge( $single, $others );
$translation_preg = array_merge( $single_preg, $others_preg );
} elseif ( $quote_style === ENT_NOQUOTES ) {
$translation = $others;
$translation_preg = $others_preg;
}
// Remove zero padding on numeric entities
$string = preg_replace( array_keys( $translation_preg ), array_values( $translation_preg ), $string );
// Replace characters according to translation table
return strtr( $string, $translation );
}
/**
* Escaping for HTML blocks. Stolen from WP
*
* @since 1.6
*
* @param string $text
* @return string
*/
function yourls_esc_html( $text ) {
$safe_text = yourls_check_invalid_utf8( $text );
$safe_text = yourls_specialchars( $safe_text, ENT_QUOTES );
return yourls_apply_filter( 'esc_html', $safe_text, $text );
}
/**
* Escaping for HTML attributes. Stolen from WP
*
* @since 1.6
*
* @param string $text
* @return string
*/
function yourls_esc_attr( $text ) {
$safe_text = yourls_check_invalid_utf8( $text );
$safe_text = yourls_specialchars( $safe_text, ENT_QUOTES );
return yourls_apply_filter( 'esc_attr', $safe_text, $text );
}
/**
* Checks and cleans a URL before printing it. Stolen from WP.
*
* A number of characters are removed from the URL. If the URL is for displaying
* (the default behaviour) ampersands are also replaced.
*
* This function by default "escapes" URL for display purpose (param $context = 'display') but can
* take extra steps in URL sanitization. See yourls_sanitize_url() and yourls_sanitize_url_safe()
*
* @since 1.6
*
* @param string $url The URL to be cleaned.
* @param string $context 'display' or something else. Use yourls_sanitize_url() for database or redirection usage.
* @param array $protocols Optional. Array of allowed protocols, defaults to global $yourls_allowedprotocols
* @return string The cleaned $url
*/
function yourls_esc_url( $url, $context = 'display', $protocols = array() ) {
// trim first -- see #1931
$url = trim( $url );
// make sure there's only one 'http://' at the beginning (prevents pasting a URL right after the default 'http://')
$url = str_replace(
array( 'http://http://', 'http://https://' ),
array( 'http://', 'https://' ),
$url
);
if ( '' == $url )
return $url;
$original_url = $url;
// force scheme and domain to lowercase - see issues 591 and 1630
$url = yourls_normalize_uri( $url );
$url = preg_replace( '|[^a-z0-9-~+_.?#=!&;,/:%@$\|*\'()\[\]\\x80-\\xff]|i', '', $url );
// Previous regexp in YOURLS was '|[^a-z0-9-~+_.?\[\]\^#=!&;,/:%@$\|*`\'<>"()\\x80-\\xff\{\}]|i'
// TODO: check if that was it too destructive
// If $context is 'safe', an extra step is taken to make sure no CRLF injection is possible.
// To be used when $url can be forged by evil user (eg it's from a $_SERVER variable, a query string, etc..)
if ( 'safe' == $context ) {
$strip = array( '%0d', '%0a', '%0D', '%0A' );
$url = yourls_deep_replace( $strip, $url );
}
// Replace ampersands and single quotes only when displaying.
if ( 'display' == $context ) {
$url = yourls_kses_normalize_entities( $url );
$url = str_replace( '&amp;', '&#038;', $url );
$url = str_replace( "'", '&#039;', $url );
}
// If there's a protocol, make sure it's OK
if( yourls_get_protocol($url) !== '' ) {
if ( ! is_array( $protocols ) or ! $protocols ) {
global $yourls_allowedprotocols;
$protocols = yourls_apply_filter( 'esc_url_protocols', $yourls_allowedprotocols );
// Note: $yourls_allowedprotocols is also globally filterable in functions-kses.php/yourls_kses_init()
}
if ( !yourls_is_allowed_protocol( $url, $protocols ) )
return '';
// I didn't use KSES function kses_bad_protocol() because it doesn't work the way I liked (returns //blah from illegal://blah)
}
return yourls_apply_filter( 'esc_url', $url, $original_url, $context );
}
/**
* Normalize a URI : lowercase scheme and domain, convert IDN to UTF8
*
* All in one example: 'HTTP://XN--mgbuq0c.Com/AbCd' -> 'http://طارق.com/AbCd'
* See issues 591, 1630, 1889, 2691
*
* This function is trickier than what seems to be needed at first
*
* First, we need to handle several URI types: http://example.com, mailto:ozh@ozh.ozh, facetime:user@example.com, and so on, see
* yourls_kses_allowed_protocols() in functions-kses.php
* The general rule is that the scheme ("stuff://" or "stuff:") is case insensitive and should be lowercase. But then, depending on the
* scheme, parts of what follows the scheme may or may not be case sensitive.
*
* Second, simply using parse_url() and its opposite http_build_url() is a pretty unsafe process:
* - parse_url() can easily trip up on malformed or weird URLs
* - exploding a URL with parse_url(), lowercasing some stuff, and glueing things back with http_build_url() does not handle well
* "stuff:"-like URI [1] and can result in URLs ending modified [2][3]. We don't want to *validate* URI, we just want to lowercase
* what is supposed to be lowercased.
*
* So, to be conservative, this function:
* - lowercases the scheme
* - does not lowercase anything else on "stuff:" URI
* - tries to lowercase only scheme and domain of "stuff://" URI
*
* [1] http_build_url(parse_url("mailto:ozh")) == "mailto:///ozh"
* [2] http_build_url(parse_url("http://blah#omg")) == "http://blah/#omg"
* [3] http_build_url(parse_url("http://blah?#")) == "http://blah/"
*
* @since 1.7.1
* @param string $url URL
* @return string URL with lowercase scheme and protocol
*/
function yourls_normalize_uri( $url ) {
$scheme = yourls_get_protocol( $url );
if ('' == $scheme) {
// Scheme not found, malformed URL? Something else? Not sure.
return $url;
}
/**
* Case 1 : scheme like "stuff:", as opposed to "stuff://"
* Examples: "mailto:joe@joe.com" or "bitcoin:15p1o8vnWqNkJBJGgwafNgR1GCCd6EGtQR?amount=1&label=Ozh"
* In this case, we only lowercase the scheme, because depending on it, things after should or should not be lowercased
*/
if (substr($scheme, -2, 2) != '//') {
$url = str_replace( $scheme, strtolower( $scheme ), $url );
return $url;
}
/**
* Case 2 : scheme like "stuff://" (eg "http://example.com/" or "ssh://joe@joe.com")
* Here we lowercase the scheme and domain parts
*/
$parts = parse_url($url);
// Most likely malformed stuff, could not parse : we'll just lowercase the scheme and leave the rest untouched
if (false == $parts) {
$url = str_replace( $scheme, strtolower( $scheme ), $url );
return $url;
}
// URL seems parsable, let's do the best we can
$lower = array();
$lower['scheme'] = strtolower( $parts['scheme'] );
if( isset( $parts['host'] ) ) {
// Convert domain to lowercase, with mb_ to preserve UTF8
$lower['host'] = mb_strtolower($parts['host']);
/**
* Convert IDN domains to their UTF8 form so that طارق.net and xn--mgbuq0c.net
* are considered the same. Explicitely mention option and variant to avoid notice
* on PHP 7.2 and 7.3
*/
$lower['host'] = idn_to_utf8($lower['host'], IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46);
}
$url = http_build_url($url, $lower);
return $url;
}
/**
* Escape single quotes, htmlspecialchar " < > &, and fix line endings. Stolen from WP.
*
* Escapes text strings for echoing in JS. It is intended to be used for inline JS
* (in a tag attribute, for example onclick="..."). Note that the strings have to
* be in single quotes. The filter 'js_escape' is also applied here.
*
* @since 1.6
*
* @param string $text The text to be escaped.
* @return string Escaped text.
*/
function yourls_esc_js( $text ) {
$safe_text = yourls_check_invalid_utf8( $text );
$safe_text = yourls_specialchars( $safe_text, ENT_COMPAT );
$safe_text = preg_replace( '/&#(x)?0*(?(1)27|39);?/i', "'", stripslashes( $safe_text ) );
$safe_text = str_replace( "\r", '', $safe_text );
$safe_text = str_replace( "\n", '\\n', addslashes( $safe_text ) );
return yourls_apply_filter( 'esc_js', $safe_text, $text );
}
/**
* Escaping for textarea values. Stolen from WP.
*
* @since 1.6
*
* @param string $text
* @return string
*/
function yourls_esc_textarea( $text ) {
$safe_text = htmlspecialchars( $text, ENT_QUOTES );
return yourls_apply_filter( 'esc_textarea', $safe_text, $text );
}
/**
* Adds backslashes before letters and before a number at the start of a string. Stolen from WP.
*
* @since 1.6
* @param string $string Value to which backslashes will be added.
* @return string String with backslashes inserted.
*/
function yourls_backslashit($string) {
$string = preg_replace('/^([0-9])/', '\\\\\\\\\1', (string)$string);
$string = preg_replace('/([a-z])/i', '\\\\\1', (string)$string);
return $string;
}
/**
* Check if a string seems to be urlencoded
*
* We use rawurlencode instead of urlencode to avoid messing with '+'
*
* @since 1.7
* @param string $string
* @return bool
*/
function yourls_is_rawurlencoded( $string ) {
return rawurldecode( $string ) != $string;
}
/**
* rawurldecode a string till it's not encoded anymore
*
* Deals with multiple encoding (eg "%2521" => "%21" => "!").
* See https://github.com/YOURLS/YOURLS/issues/1303
*
* @since 1.7
* @param string $string
* @return string
*/
function yourls_rawurldecode_while_encoded( $string ) {
$string = rawurldecode( $string );
if( yourls_is_rawurlencoded( $string ) ) {
$string = yourls_rawurldecode_while_encoded( $string );
}
return $string;
}
/**
* Converts readable Javascript code into a valid bookmarklet link
*
* Uses https://github.com/ozh/bookmarkletgen
*
* @since 1.7.1
* @param string $code Javascript code
* @return string Bookmarklet link
*/
function yourls_make_bookmarklet( $code ) {
$book = new \Ozh\Bookmarkletgen\Bookmarkletgen;
return $book->crunch( $code );
}
/**
* Return a timestamp, plus or minus the time offset if defined
*
* @since 1.7.10
* @param string|int $timestamp a timestamp
* @return int a timestamp, plus or minus offset if defined
*/
function yourls_get_timestamp( $timestamp ) {
$offset = yourls_get_time_offset();
$timestamp_offset = (int)$timestamp + ($offset * 3600);
return yourls_apply_filter( 'get_timestamp', $timestamp_offset, $timestamp, $offset );
}
/**
* Get time offset, as defined in config, filtered
*
* @since 1.7.10
* @return int Time offset
*/
function yourls_get_time_offset() {
$offset = defined('YOURLS_HOURS_OFFSET') ? (int)YOURLS_HOURS_OFFSET : 0;
return yourls_apply_filter( 'get_time_offset', $offset );
}
/**
* Return a date() format for a full date + time, filtered
*
* @since 1.7.10
* @param string $format Date format string
* @return string Date format string
*/
function yourls_get_datetime_format( $format ) {
return yourls_apply_filter( 'get_datetime_format', (string)$format );
}
/**
* Return a date() format for date (no time), filtered
*
* @since 1.7.10
* @param string $format Date format string
* @return string Date format string
*/
function yourls_get_date_format( $format ) {
return yourls_apply_filter( 'get_date_format', (string)$format );
}
/**
* Return a date() format for a time (no date), filtered
*
* @since 1.7.10
* @param string $format Date format string
* @return string Date format string
*/
function yourls_get_time_format( $format ) {
return yourls_apply_filter( 'get_time_format', (string)$format );
}

153
functions-geo.php Normal file
View File

@ -0,0 +1,153 @@
<?php
/**
* Function relative to the geolocation functions (ip <-> country <-> flags), currently
* tied to Maxmind's GeoIP but this should evolve to become more pluggable
*/
/**
* Converts an IP to a 2 letter country code, using GeoIP database if available in includes/geo/
*
* @since 1.4
* @param string $ip IP or, if empty string, will be current user IP
* @param string $default Default string to return if IP doesn't resolve to a country (malformed, private IP...)
* @return string 2 letter country code (eg 'US') or $default
*/
function yourls_geo_ip_to_countrycode( $ip = '', $default = '' ) {
// Allow plugins to short-circuit the Geo IP API
$location = yourls_apply_filter( 'shunt_geo_ip_to_countrycode', false, $ip, $default ); // at this point $ip can be '', check if your plugin hooks in here
if ( false !== $location ) {
return $location;
}
if ( yourls_apply_filter( 'geo_use_cloudflare', true )
&& !empty( $_SERVER[ 'HTTP_CF_IPCOUNTRY' ] ) && preg_match( '/^[A-Z]{2}$/', $_SERVER[ 'HTTP_CF_IPCOUNTRY' ] ) ) {
return $_SERVER[ 'HTTP_CF_IPCOUNTRY' ];
}
if ( empty( $ip ) ) {
$ip = yourls_get_IP();
}
// Allow plugins to stick to YOURLS internals but provide another DB
$db = yourls_apply_filter( 'geo_ip_path_to_db', YOURLS_INC.'/geo/GeoLite2-Country.mmdb' );
if ( !is_readable( $db ) ) {
return $default;
}
try {
$reader = new \GeoIp2\Database\Reader( $db );
$record = $reader->country( $ip );
$location = $record->country->isoCode; // eg 'US'
}
catch ( \Exception $e ) {
/*
Unused for now, Exception and $e->getMessage() can be one of :
- Exception: \GeoIp2\Exception\AddressNotFoundException
When: valid IP not found
Error message: "The address 10.0.0.30 is not in the database"
- Exception: \InvalidArgumentException
When: IP is not valid, or DB not readable
Error message: "The value "10.0.0.300" is not a valid IP address", "The file "/path/to/GeoLite2-Country.mmdb" does not exist or is not readable"
- Exception: \MaxMind\Db\Reader\InvalidDatabaseException
When: DB is readable but is corrupt or invalid
Error message: "The MaxMind DB file's search tree is corrupt"
- or obviously \Exception for any other error (?)
*/
}
return yourls_apply_filter( 'geo_ip_to_countrycode', empty( $location ) ? $default : $location, $ip, $default );
}
/**
* Converts a 2 letter country code to long name (ie AU -> Australia)
*
* This associative array is the one used by MaxMind internal functions, it may differ from other lists (eg "A1" does
* not universally stand for "Anon proxy")
*
* @since 1.4
* @param string $code 2 letter country code, eg 'FR'
* @return string Country long name (eg 'France') or an empty string if not found
*/
function yourls_geo_countrycode_to_countryname( $code ) {
// Allow plugins to short-circuit the function
$country = yourls_apply_filter( 'shunt_geo_countrycode_to_countryname', false, $code );
if ( false !== $country ) {
return $country;
}
// Weeeeeeeeeeee
$countries = [
'A1' => 'Anonymous Proxy', 'A2' => 'Satellite Provider', 'AD' => 'Andorra', 'AE' => 'United Arab Emirates', 'AF' => 'Afghanistan',
'AG' => 'Antigua and Barbuda', 'AI' => 'Anguilla', 'AL' => 'Albania', 'AM' => 'Armenia', 'AO' => 'Angola',
'AP' => 'Asia/Pacific Region', 'AQ' => 'Antarctica', 'AR' => 'Argentina', 'AS' => 'American Samoa', 'AT' => 'Austria',
'AU' => 'Australia', 'AW' => 'Aruba', 'AX' => 'Aland Islands', 'AZ' => 'Azerbaijan', 'BA' => 'Bosnia and Herzegovina',
'BB' => 'Barbados', 'BD' => 'Bangladesh', 'BE' => 'Belgium', 'BF' => 'Burkina Faso', 'BG' => 'Bulgaria',
'BH' => 'Bahrain', 'BI' => 'Burundi', 'BJ' => 'Benin', 'BL' => 'Saint Barthelemy', 'BM' => 'Bermuda',
'BN' => 'Brunei Darussalam', 'BO' => 'Bolivia', 'BQ' => 'Bonaire, Saint Eustatius and Saba', 'BR' => 'Brazil', 'BS' => 'Bahamas',
'BT' => 'Bhutan', 'BV' => 'Bouvet Island', 'BW' => 'Botswana', 'BY' => 'Belarus', 'BZ' => 'Belize',
'CA' => 'Canada', 'CC' => 'Cocos (Keeling) Islands', 'CD' => 'Congo, The Democratic Republic of the', 'CF' => 'Central African Republic', 'CG' => 'Congo',
'CH' => 'Switzerland', 'CI' => 'Cote D\'Ivoire', 'CK' => 'Cook Islands', 'CL' => 'Chile', 'CM' => 'Cameroon',
'CN' => 'China', 'CO' => 'Colombia', 'CR' => 'Costa Rica', 'CU' => 'Cuba', 'CV' => 'Cape Verde',
'CW' => 'Curacao', 'CX' => 'Christmas Island', 'CY' => 'Cyprus', 'CZ' => 'Czech Republic', 'DE' => 'Germany',
'DJ' => 'Djibouti', 'DK' => 'Denmark', 'DM' => 'Dominica', 'DO' => 'Dominican Republic', 'DZ' => 'Algeria',
'EC' => 'Ecuador', 'EE' => 'Estonia', 'EG' => 'Egypt', 'EH' => 'Western Sahara', 'ER' => 'Eritrea',
'ES' => 'Spain', 'ET' => 'Ethiopia', 'EU' => 'Europe', 'FI' => 'Finland', 'FJ' => 'Fiji',
'FK' => 'Falkland Islands (Malvinas)', 'FM' => 'Micronesia, Federated States of', 'FO' => 'Faroe Islands', 'FR' => 'France', 'GA' => 'Gabon',
'GB' => 'United Kingdom', 'GD' => 'Grenada', 'GE' => 'Georgia', 'GF' => 'French Guiana', 'GG' => 'Guernsey',
'GH' => 'Ghana', 'GI' => 'Gibraltar', 'GL' => 'Greenland', 'GM' => 'Gambia', 'GN' => 'Guinea',
'GP' => 'Guadeloupe', 'GQ' => 'Equatorial Guinea', 'GR' => 'Greece', 'GS' => 'South Georgia and the South Sandwich Islands', 'GT' => 'Guatemala',
'GU' => 'Guam', 'GW' => 'Guinea-Bissau', 'GY' => 'Guyana', 'HK' => 'Hong Kong', 'HM' => 'Heard Island and McDonald Islands',
'HN' => 'Honduras', 'HR' => 'Croatia', 'HT' => 'Haiti', 'HU' => 'Hungary', 'ID' => 'Indonesia',
'IE' => 'Ireland', 'IL' => 'Israel', 'IM' => 'Isle of Man', 'IN' => 'India', 'IO' => 'British Indian Ocean Territory',
'IQ' => 'Iraq', 'IR' => 'Iran, Islamic Republic of', 'IS' => 'Iceland', 'IT' => 'Italy', 'JE' => 'Jersey',
'JM' => 'Jamaica', 'JO' => 'Jordan', 'JP' => 'Japan', 'KE' => 'Kenya', 'KG' => 'Kyrgyzstan',
'KH' => 'Cambodia', 'KI' => 'Kiribati', 'KM' => 'Comoros', 'KN' => 'Saint Kitts and Nevis', 'KP' => 'Korea, Democratic People\'s Republic of',
'KR' => 'Korea, Republic of', 'KW' => 'Kuwait', 'KY' => 'Cayman Islands', 'KZ' => 'Kazakhstan', 'LA' => 'Lao People\'s Democratic Republic',
'LB' => 'Lebanon', 'LC' => 'Saint Lucia', 'LI' => 'Liechtenstein', 'LK' => 'Sri Lanka', 'LR' => 'Liberia',
'LS' => 'Lesotho', 'LT' => 'Lithuania', 'LU' => 'Luxembourg', 'LV' => 'Latvia', 'LY' => 'Libya',
'MA' => 'Morocco', 'MC' => 'Monaco', 'MD' => 'Moldova, Republic of', 'ME' => 'Montenegro', 'MF' => 'Saint Martin',
'MG' => 'Madagascar', 'MH' => 'Marshall Islands', 'MK' => 'Macedonia', 'ML' => 'Mali', 'MM' => 'Myanmar',
'MN' => 'Mongolia', 'MO' => 'Macau', 'MP' => 'Northern Mariana Islands', 'MQ' => 'Martinique', 'MR' => 'Mauritania',
'MS' => 'Montserrat', 'MT' => 'Malta', 'MU' => 'Mauritius', 'MV' => 'Maldives', 'MW' => 'Malawi',
'MX' => 'Mexico', 'MY' => 'Malaysia', 'MZ' => 'Mozambique', 'NA' => 'Namibia', 'NC' => 'New Caledonia',
'NE' => 'Niger', 'NF' => 'Norfolk Island', 'NG' => 'Nigeria', 'NI' => 'Nicaragua', 'NL' => 'Netherlands',
'NO' => 'Norway', 'NP' => 'Nepal', 'NR' => 'Nauru', 'NU' => 'Niue', 'NZ' => 'New Zealand',
'O1' => 'Other', 'OM' => 'Oman', 'PA' => 'Panama', 'PE' => 'Peru', 'PF' => 'French Polynesia',
'PG' => 'Papua New Guinea', 'PH' => 'Philippines', 'PK' => 'Pakistan', 'PL' => 'Poland', 'PM' => 'Saint Pierre and Miquelon',
'PN' => 'Pitcairn Islands', 'PR' => 'Puerto Rico', 'PS' => 'Palestinian Territory', 'PT' => 'Portugal', 'PW' => 'Palau',
'PY' => 'Paraguay', 'QA' => 'Qatar', 'RE' => 'Reunion', 'RO' => 'Romania', 'RS' => 'Serbia',
'RU' => 'Russian Federation', 'RW' => 'Rwanda', 'SA' => 'Saudi Arabia', 'SB' => 'Solomon Islands', 'SC' => 'Seychelles',
'SD' => 'Sudan', 'SE' => 'Sweden', 'SG' => 'Singapore', 'SH' => 'Saint Helena', 'SI' => 'Slovenia',
'SJ' => 'Svalbard and Jan Mayen', 'SK' => 'Slovakia', 'SL' => 'Sierra Leone', 'SM' => 'San Marino', 'SN' => 'Senegal',
'SO' => 'Somalia', 'SR' => 'Suriname', 'SS' => 'South Sudan', 'ST' => 'Sao Tome and Principe', 'SV' => 'El Salvador',
'SX' => 'Sint Maarten (Dutch part)', 'SY' => 'Syrian Arab Republic', 'SZ' => 'Swaziland', 'TC' => 'Turks and Caicos Islands', 'TD' => 'Chad',
'TF' => 'French Southern Territories', 'TG' => 'Togo', 'TH' => 'Thailand', 'TJ' => 'Tajikistan', 'TK' => 'Tokelau',
'TL' => 'Timor-Leste', 'TM' => 'Turkmenistan', 'TN' => 'Tunisia', 'TO' => 'Tonga', 'TR' => 'Turkey',
'TT' => 'Trinidad and Tobago', 'TV' => 'Tuvalu', 'TW' => 'Taiwan', 'TZ' => 'Tanzania, United Republic of', 'UA' => 'Ukraine',
'UG' => 'Uganda', 'UM' => 'United States Minor Outlying Islands', 'US' => 'United States', 'UY' => 'Uruguay', 'UZ' => 'Uzbekistan',
'VA' => 'Holy See (Vatican City State)', 'VC' => 'Saint Vincent and the Grenadines', 'VE' => 'Venezuela', 'VG' => 'Virgin Islands, British', 'VI' => 'Virgin Islands, U.S.',
'VN' => 'Vietnam', 'VU' => 'Vanuatu', 'WF' => 'Wallis and Futuna', 'WS' => 'Samoa', 'YE' => 'Yemen',
'YT' => 'Mayotte', 'ZA' => 'South Africa', 'ZM' => 'Zambia', 'ZW' => 'Zimbabwe',
];
$code = strtoupper( $code );
return yourls_apply_filter( 'geo_countrycode_to_countryname', isset( $countries[ $code ] ) ? $countries[ $code ] : '' );
}
/**
* Return flag URL from 2 letter country code
* @param string $code
* @return string
*/
function yourls_geo_get_flag( $code ) {
if ( !file_exists( YOURLS_INC.'/geo/flags/flag_'.strtolower( $code ).'.gif' ) ) {
$code = '';
}
$img = yourls_match_current_protocol( yourls_get_yourls_site().'/includes/geo/flags/flag_'.strtolower( $code ).'.gif' );
return (string)yourls_apply_filter( 'geo_get_flag', $img, $code );
}

1037
functions-html.php Normal file

File diff suppressed because it is too large Load Diff

528
functions-http.php Normal file
View File

@ -0,0 +1,528 @@
<?php
/**
* Functions that relate to HTTP requests
*
* On functions using the 3rd party library Requests:
* Their goal here is to provide convenient wrapper functions to the Requests library. There are
* 2 types of functions for each METHOD, where METHOD is 'get' or 'post' (implement more as needed)
* - yourls_http_METHOD() :
* Return a complete Response object (with ->body, ->headers, ->status_code, etc...) or
* a simple string (error message)
* - yourls_http_METHOD_body() :
* Return a string (response body) or null if there was an error
*
* @since 1.7
*/
use WpOrg\Requests\Requests;
/**
* Perform a GET request, return response object or error string message
*
* Notable object properties: body, headers, status_code
*
* @since 1.7
* @see yourls_http_request
* @param string $url URL to request
* @param array $headers HTTP headers to send
* @param array $data GET data
* @param array $options Options to pass to Requests
* @return mixed Response object, or error string
*/
function yourls_http_get( $url, $headers = array(), $data = array(), $options = array() ) {
return yourls_http_request( 'GET', $url, $headers, $data, $options );
}
/**
* Perform a GET request, return body or null if there was an error
*
* @since 1.7
* @see yourls_http_request
* @param string $url URL to request
* @param array $headers HTTP headers to send
* @param array $data GET data
* @param array $options Options to pass to Requests
* @return mixed String (page body) or null if error
*/
function yourls_http_get_body( $url, $headers = array(), $data = array(), $options = array() ) {
$return = yourls_http_get( $url, $headers, $data, $options );
return isset( $return->body ) ? $return->body : null;
}
/**
* Perform a POST request, return response object
*
* Notable object properties: body, headers, status_code
*
* @since 1.7
* @see yourls_http_request
* @param string $url URL to request
* @param array $headers HTTP headers to send
* @param array $data POST data
* @param array $options Options to pass to Requests
* @return mixed Response object, or error string
*/
function yourls_http_post( $url, $headers = array(), $data = array(), $options = array() ) {
return yourls_http_request( 'POST', $url, $headers, $data, $options );
}
/**
* Perform a POST request, return body
*
* Wrapper for yourls_http_request()
*
* @since 1.7
* @see yourls_http_request
* @param string $url URL to request
* @param array $headers HTTP headers to send
* @param array $data POST data
* @param array $options Options to pass to Requests
* @return mixed String (page body) or null if error
*/
function yourls_http_post_body( $url, $headers = array(), $data = array(), $options = array() ) {
$return = yourls_http_post( $url, $headers, $data, $options );
return isset( $return->body ) ? $return->body : null;
}
/**
* Get proxy information
*
* @uses YOURLS_PROXY YOURLS_PROXY_USERNAME YOURLS_PROXY_PASSWORD
* @since 1.7.1
* @return mixed false if no proxy is defined, or string like '10.0.0.201:3128' or array like ('10.0.0.201:3128', 'username', 'password')
*/
function yourls_http_get_proxy() {
$proxy = false;
if( defined( 'YOURLS_PROXY' ) ) {
$proxy = YOURLS_PROXY;
if( defined( 'YOURLS_PROXY_USERNAME' ) && defined( 'YOURLS_PROXY_PASSWORD' ) ) {
$proxy = array( YOURLS_PROXY, YOURLS_PROXY_USERNAME, YOURLS_PROXY_PASSWORD );
}
}
return yourls_apply_filter( 'http_get_proxy', $proxy );
}
/**
* Get list of hosts that should bypass the proxy
*
* @uses YOURLS_PROXY_BYPASS_HOSTS
* @since 1.7.1
* @return mixed false if no host defined, or string like "example.com, *.mycorp.com"
*/
function yourls_http_get_proxy_bypass_host() {
$hosts = defined( 'YOURLS_PROXY_BYPASS_HOSTS' ) ? YOURLS_PROXY_BYPASS_HOSTS : false;
return yourls_apply_filter( 'http_get_proxy_bypass_host', $hosts );
}
/**
* Default HTTP requests options for YOURLS
*
* For a list of all available options, see function request() in /includes/Requests/Requests.php
*
* @since 1.7
* @return array Options
*/
function yourls_http_default_options() {
$options = array(
'timeout' => yourls_apply_filter( 'http_default_options_timeout', 3 ),
'useragent' => yourls_http_user_agent(),
'follow_redirects' => true,
'redirects' => 3,
);
if( yourls_http_get_proxy() ) {
$options['proxy'] = yourls_http_get_proxy();
}
return yourls_apply_filter( 'http_default_options', $options );
}
/**
* Whether URL should be sent through the proxy server.
*
* Concept stolen from WordPress. The idea is to allow some URLs, including localhost and the YOURLS install itself,
* to be requested directly and bypassing any defined proxy.
*
* @uses YOURLS_PROXY
* @uses YOURLS_PROXY_BYPASS_HOSTS
* @since 1.7
* @param string $url URL to check
* @return bool true to request through proxy, false to request directly
*/
function yourls_send_through_proxy( $url ) {
// Allow plugins to short-circuit the whole function
$pre = yourls_apply_filter( 'shunt_send_through_proxy', null, $url );
if ( null !== $pre )
return $pre;
$check = @parse_url( $url );
if( !isset( $check['host'] ) ) {
return false;
}
// Malformed URL, can not process, but this could mean ssl, so let through anyway.
if ( $check === false )
return true;
// Self and loopback URLs are considered local (':' is parse_url() host on '::1')
$home = parse_url( yourls_get_yourls_site() );
$local = array( 'localhost', '127.0.0.1', '127.1', '[::1]', ':', $home['host'] );
if( in_array( $check['host'], $local ) )
return false;
$bypass = yourls_http_get_proxy_bypass_host();
if( $bypass === false OR $bypass === '' ) {
return true;
}
// Build array of hosts to bypass
static $bypass_hosts;
static $wildcard_regex = false;
if ( null == $bypass_hosts ) {
$bypass_hosts = preg_split( '|\s*,\s*|', $bypass );
if ( false !== strpos( $bypass, '*' ) ) {
$wildcard_regex = array();
foreach ( $bypass_hosts as $host ) {
$wildcard_regex[] = str_replace( '\*', '.+', preg_quote( $host, '/' ) );
if ( false !== strpos( $host, '*' ) ) {
$wildcard_regex[] = str_replace( '\*\.', '', preg_quote( $host, '/' ) );
}
}
$wildcard_regex = '/^(' . implode( '|', $wildcard_regex ) . ')$/i';
}
}
if ( !empty( $wildcard_regex ) )
return !preg_match( $wildcard_regex, $check['host'] );
else
return !in_array( $check['host'], $bypass_hosts );
}
/**
* Perform a HTTP request, return response object
*
* @since 1.7
* @param string $type HTTP request type (GET, POST)
* @param string $url URL to request
* @param array $headers Extra headers to send with the request
* @param array $data Data to send either as a query string for GET requests, or in the body for POST requests
* @param array $options Options for the request (see /includes/Requests/Requests.php:request())
* @return object WpOrg\Requests\Response object
*/
function yourls_http_request( $type, $url, $headers, $data, $options ) {
// Allow plugins to short-circuit the whole function
$pre = yourls_apply_filter( 'shunt_yourls_http_request', null, $type, $url, $headers, $data, $options );
if ( null !== $pre )
return $pre;
$options = array_merge( yourls_http_default_options(), $options );
if( yourls_http_get_proxy() && !yourls_send_through_proxy( $url ) ) {
unset( $options['proxy'] );
}
// filter everything
$type = yourls_apply_filter('http_request_type', $type);
$url = yourls_apply_filter('http_request_url', $url);
$headers = yourls_apply_filter('http_request_headers', $headers);
$data = yourls_apply_filter('http_request_data', $data);
$options = yourls_apply_filter('http_request_options', $options);
try {
$result = Requests::request( $url, $headers, $data, $type, $options );
} catch( \WpOrg\Requests\Exception $e ) {
$result = yourls_debug_log( $e->getMessage() . ' (' . $type . ' on ' . $url . ')' );
};
return $result;
}
/**
* Return funky user agent string
*
* @since 1.5
* @return string UA string
*/
function yourls_http_user_agent() {
return yourls_apply_filter( 'http_user_agent', 'YOURLS v'.YOURLS_VERSION.' +http://yourls.org/ (running on '.yourls_get_yourls_site().')' );
}
/**
* Check api.yourls.org if there's a newer version of YOURLS
*
* This function collects various stats to help us improve YOURLS. See the blog post about it:
* http://blog.yourls.org/2014/01/on-yourls-1-7-and-api-yourls-org/
* Results of requests sent to api.yourls.org are stored in option 'core_version_checks' and is an object
* with the following properties:
* - failed_attempts : number of consecutive failed attempts
* - last_attempt : time() of last attempt
* - last_result : content retrieved from api.yourls.org during previous check
* - version_checked : installed YOURLS version that was last checked
*
* @since 1.7
* @return mixed JSON data if api.yourls.org successfully requested, false otherwise
*/
function yourls_check_core_version() {
global $yourls_user_passwords;
$checks = yourls_get_option( 'core_version_checks' );
// Invalidate check data when YOURLS version changes
if ( is_object( $checks ) && YOURLS_VERSION != $checks->version_checked ) {
$checks = false;
}
if( !is_object( $checks ) ) {
$checks = new stdClass;
$checks->failed_attempts = 0;
$checks->last_attempt = 0;
$checks->last_result = '';
$checks->version_checked = YOURLS_VERSION;
}
// Total number of links and clicks
list( $total_urls, $total_clicks ) = array_values(yourls_get_db_stats());
// The collection of stuff to report
$stuff = array(
// Globally uniquish site identifier
// This uses const YOURLS_SITE and not yourls_get_yourls_site() to prevent creating another id for an already known install
'md5' => md5( YOURLS_SITE . YOURLS_ABSPATH ),
// Install information
'failed_attempts' => $checks->failed_attempts,
'yourls_site' => defined( 'YOURLS_SITE' ) ? yourls_get_yourls_site() : 'unknown',
'yourls_version' => defined( 'YOURLS_VERSION' ) ? YOURLS_VERSION : 'unknown',
'php_version' => PHP_VERSION,
'mysql_version' => yourls_get_db()->mysql_version(),
'locale' => yourls_get_locale(),
// custom DB driver if any, and useful common PHP extensions
'db_driver' => defined( 'YOURLS_DB_DRIVER' ) ? YOURLS_DB_DRIVER : 'unset',
'db_ext_pdo' => extension_loaded( 'PDO' ) ? 1 : 0,
'db_ext_mysql' => extension_loaded( 'mysql' ) ? 1 : 0,
'db_ext_mysqli' => extension_loaded( 'mysqli' ) ? 1 : 0,
'ext_curl' => extension_loaded( 'curl' ) ? 1 : 0,
// Config information
'yourls_private' => defined( 'YOURLS_PRIVATE' ) && YOURLS_PRIVATE ? 1 : 0,
'yourls_unique' => defined( 'YOURLS_UNIQUE_URLS' ) && YOURLS_UNIQUE_URLS ? 1 : 0,
'yourls_url_convert' => defined( 'YOURLS_URL_CONVERT' ) ? YOURLS_URL_CONVERT : 'unknown',
// Usage information
'num_users' => count( $yourls_user_passwords ),
'num_active_plugins' => yourls_has_active_plugins(),
'num_pages' => defined( 'YOURLS_PAGEDIR' ) ? count( (array) glob( YOURLS_PAGEDIR .'/*.php') ) : 0,
'num_links' => $total_urls,
'num_clicks' => $total_clicks,
);
$stuff = yourls_apply_filter( 'version_check_stuff', $stuff );
// Send it in
$url = 'http://api.yourls.org/core/version/1.1/';
if( yourls_can_http_over_ssl() ) {
$url = yourls_set_url_scheme($url, 'https');
}
$req = yourls_http_post( $url, array(), $stuff );
$checks->last_attempt = time();
$checks->version_checked = YOURLS_VERSION;
// Unexpected results ?
if( is_string( $req ) or !$req->success ) {
$checks->failed_attempts = $checks->failed_attempts + 1;
yourls_update_option( 'core_version_checks', $checks );
if( is_string($req) ) {
yourls_debug_log('Version check failed: ' . $req);
}
return false;
}
// Parse response
$json = json_decode( trim( $req->body ) );
if( yourls_validate_core_version_response($json) ) {
// All went OK - mark this down
$checks->failed_attempts = 0;
$checks->last_result = $json;
yourls_update_option( 'core_version_checks', $checks );
return $json;
}
// Request returned actual result, but not what we expected
return false;
}
/**
* Make sure response from api.yourls.org is valid
*
* 1) we should get a json object with two following properties:
* 'latest' => a string representing a YOURLS version number, eg '1.2.3'
* 'zipurl' => a string for a zip package URL, from github, eg 'https://api.github.com/repos/YOURLS/YOURLS/zipball/1.2.3'
* 2) 'latest' and version extracted from 'zipurl' should match
* 3) the object should not contain any other key
*
* @since 1.7.7
* @param object $json JSON object to check
* @return bool true if seems legit, false otherwise
*/
function yourls_validate_core_version_response($json) {
return (
yourls_validate_core_version_response_keys($json)
&& $json->latest === yourls_sanitize_version($json->latest)
&& $json->zipurl === yourls_sanitize_url($json->zipurl)
&& $json->latest === yourls_get_version_from_zipball_url($json->zipurl)
&& yourls_is_valid_github_repo_url($json->zipurl)
);
}
/**
* Get version number from Github zipball URL (last part of URL, really)
*
* @since 1.8.3
* @param string $zipurl eg 'https://api.github.com/repos/YOURLS/YOURLS/zipball/1.2.3'
* @return string
*/
function yourls_get_version_from_zipball_url($zipurl) {
$version = '';
$parts = explode('/', parse_url(yourls_sanitize_url($zipurl), PHP_URL_PATH) ?? '');
// expect at least 1 slash in path, return last part
if( count($parts) > 1 ) {
$version = end($parts);
}
return $version;
}
/**
* Check if URL is from YOURLS/YOURLS repo on github
*
* @since 1.8.3
* @param string $url URL to check
* @return bool
*/
function yourls_is_valid_github_repo_url($url) {
$url = yourls_sanitize_url($url);
return (
join('.',array_slice(explode('.', parse_url($url, PHP_URL_HOST) ?? ''), -2, 2)) === 'github.com'
// explodes on '.' (['api','github','com']) and keeps the last two elements
// to make sure domain is either github.com or one of its subdomain (api.github.com for instance)
// TODO: keep an eye on Github API to make sure it doesn't change some day to another domain (githubapi.com, ...)
&& substr( parse_url($url, PHP_URL_PATH), 0, 21 ) === '/repos/YOURLS/YOURLS/'
// make sure path starts with '/repos/YOURLS/YOURLS/'
);
}
/**
* Check if object has only expected keys 'latest' and 'zipurl' containing strings
*
* @since 1.8.3
* @param object $json
* @return bool
*/
function yourls_validate_core_version_response_keys($json) {
$keys = array('latest', 'zipurl');
return (
count(array_diff(array_keys((array)$json), $keys)) === 0
&& isset($json->latest)
&& isset($json->zipurl)
&& is_string($json->latest)
&& is_string($json->zipurl)
);
}
/**
* Determine if we want to check for a newer YOURLS version (and check if applicable)
*
* Currently checks are performed every 24h and only when someone is visiting an admin page.
* In the future (1.8?) maybe check with cronjob emulation instead.
*
* @since 1.7
* @return bool true if a check was needed and successfully performed, false otherwise
*/
function yourls_maybe_check_core_version() {
// Allow plugins to short-circuit the whole function
$pre = yourls_apply_filter('shunt_maybe_check_core_version', null);
if (null !== $pre) {
return $pre;
}
if (yourls_skip_version_check()) {
return false;
}
if (!yourls_is_admin()) {
return false;
}
$checks = yourls_get_option( 'core_version_checks' );
/* We don't want to check if :
- last_result is set (a previous check was performed)
- and it was less than 24h ago (or less than 2h ago if it wasn't successful)
- and version checked matched version running
Otherwise, we want to check.
*/
if( !empty( $checks->last_result )
AND
(
( $checks->failed_attempts == 0 && ( ( time() - $checks->last_attempt ) < 24 * 3600 ) )
OR
( $checks->failed_attempts > 0 && ( ( time() - $checks->last_attempt ) < 2 * 3600 ) )
)
AND ( $checks->version_checked == YOURLS_VERSION )
)
return false;
// We want to check if there's a new version
$new_check = yourls_check_core_version();
// Could not check for a new version, and we don't have ancient data
if( false == $new_check && !isset( $checks->last_result->latest ) )
return false;
return true;
}
/**
* Check if user setting for skipping version check is set
*
* @since 1.8.2
* @return bool
*/
function yourls_skip_version_check() {
return yourls_apply_filter('skip_version_check', defined('YOURLS_NO_VERSION_CHECK') && YOURLS_NO_VERSION_CHECK);
}
/**
* Check if server can perform HTTPS requests, return bool
*
* @since 1.7.1
* @return bool whether the server can perform HTTP requests over SSL
*/
function yourls_can_http_over_ssl() {
$ssl_curl = $ssl_socket = false;
if( function_exists( 'curl_exec' ) ) {
$curl_version = curl_version();
$ssl_curl = ( $curl_version['features'] & CURL_VERSION_SSL );
}
if( function_exists( 'stream_socket_client' ) ) {
$ssl_socket = extension_loaded( 'openssl' ) && function_exists( 'openssl_x509_parse' );
}
return ( $ssl_curl OR $ssl_socket );
}

384
functions-infos.php Normal file
View File

@ -0,0 +1,384 @@
<?php
/**
* Echoes an image tag of Google Charts map from sorted array of 'country_code' => 'number of visits' (sort by DESC)
*
* @param array $countries Array of 'country_code' => 'number of visits'
* @param string $id Optional HTML element ID
* @return void
*/
function yourls_stats_countries_map($countries, $id = null) {
yourls_do_action( 'pre_stats_countries_map' );
// if $id is null then assign a random string
if( $id === null )
$id = uniqid ( 'yourls_stats_map_' );
$data = array_merge( array( 'Country' => 'Hits' ), $countries );
$data = yourls_google_array_to_data_table( $data );
$options = array(
'backgroundColor' => "white",
'colorAxis' => "{colors:['A8D0ED','99C4E4','8AB8DB','7BACD2','6BA1C9','5C95C0','4D89B7','3E7DAE','2E72A5','1F669C']}",
'width' => "550",
'height' => "340",
'theme' => 'maximized'
);
$options = yourls_apply_filter( 'stats_countries_map_options', $options );
$map = yourls_google_viz_code( 'GeoChart', $data, $options, $id );
echo yourls_apply_filter( 'stats_countries_map', $map, $countries, $options, $id );
}
/**
* Echoes an image tag of Google Charts pie from sorted array of 'data' => 'value' (sort by DESC). Optional $limit = (integer) limit list of X first countries, sorted by most visits
*
* @param array $data Array of 'data' => 'value'
* @param int $limit Optional limit list of X first countries
* @param $size Optional size of the image
* @param $id Optional HTML element ID
* @return void
*/
function yourls_stats_pie($data, $limit = 10, $size = '340x220', $id = null) {
yourls_do_action( 'pre_stats_pie' );
// if $id is null then assign a random string
if( $id === null )
$id = uniqid ( 'yourls_stats_pie_' );
// Trim array: $limit first item + the sum of all others
if ( count( $data ) > $limit ) {
$i= 0;
$trim_data = array( 'Others' => 0 );
foreach( $data as $item=>$value ) {
$i++;
if( $i <= $limit ) {
$trim_data[$item] = $value;
} else {
$trim_data['Others'] += $value;
}
}
$data = $trim_data;
}
// Scale items
$_data = yourls_scale_data( $data );
list($width, $height) = explode( 'x', $size );
$options = array(
'theme' => 'maximized',
'width' => $width,
'height' => $height,
'colors' => "['A8D0ED','99C4E4','8AB8DB','7BACD2','6BA1C9','5C95C0','4D89B7','3E7DAE','2E72A5','1F669C']",
'legend' => 'none',
'chartArea' => '{top: "5%", height: "90%"}',
'pieSliceText' => 'label',
);
$options = yourls_apply_filter( 'stats_pie_options', $options );
$script_data = array_merge( array( 'Country' => 'Value' ), $_data );
$script_data = yourls_google_array_to_data_table( $script_data );
$pie = yourls_google_viz_code( 'PieChart', $script_data, $options, $id );
echo yourls_apply_filter( 'stats_pie', $pie, $data, $limit, $size, $options, $id );
}
/**
* Build a list of all daily values between d1/m1/y1 to d2/m2/y2.
*
* @param array $dates
* @return array[] Array of arrays of days, months, years
*/
function yourls_build_list_of_days($dates) {
/* Say we have an array like:
$dates = array (
2009 => array (
'08' => array (
29 => 15,
30 => 5,
),
'09' => array (
'02' => 3,
'03' => 5,
'04' => 2,
'05' => 99,
),
),
)
*/
if( !$dates )
return array();
// Get first & last years from our range. In our example: 2009 & 2009
$first_year = key( $dates );
$_keys = array_keys( $dates );
$last_year = end( $_keys );
reset( $dates );
// Get first & last months from our range. In our example: 08 & 09
$first_month = key( $dates[ $first_year ] );
$_keys = array_keys( $dates[ $last_year ] );
$last_month = end( $_keys );
reset( $dates );
// Get first & last days from our range. In our example: 29 & 05
$first_day = key( $dates[ $first_year ][ $first_month ] );
$_keys = array_keys( $dates[ $last_year ][ $last_month ] );
$last_day = end( $_keys );
unset( $_keys );
// Now build a list of all years (2009), month (08 & 09) and days (all from 2009-08-29 to 2009-09-05)
$list_of_years = array();
$list_of_months = array();
$list_of_days = array();
for ( $year = $first_year; $year <= $last_year; $year++ ) {
$_year = sprintf( '%04d', $year );
$list_of_years[ $_year ] = $_year;
$current_first_month = ( $year == $first_year ? $first_month : '01' );
$current_last_month = ( $year == $last_year ? $last_month : '12' );
for ( $month = $current_first_month; $month <= $current_last_month; $month++ ) {
$_month = sprintf( '%02d', $month );
$list_of_months[ $_month ] = $_month;
$current_first_day = ( $year == $first_year && $month == $first_month ? $first_day : '01' );
$current_last_day = ( $year == $last_year && $month == $last_month ? $last_day : yourls_days_in_month( $month, $year) );
for ( $day = $current_first_day; $day <= $current_last_day; $day++ ) {
$day = sprintf( '%02d', $day );
$key = date( 'M d, Y', mktime( 0, 0, 0, $_month, $day, $_year ) );
$list_of_days[ $key ] = isset( $dates[$_year][$_month][$day] ) ? $dates[$_year][$_month][$day] : 0;
}
}
}
return array(
'list_of_days' => $list_of_days,
'list_of_months' => $list_of_months,
'list_of_years' => $list_of_years,
);
}
/**
* Echoes an image tag of Google Charts line graph from array of values (eg 'number of clicks').
*
* $legend1_list & legend2_list are values used for the 2 x-axis labels. $id is an HTML/JS id
*
* @param array $values Array of values (eg 'number of clicks')
* @param string $id HTML element id
* @return void
*/
function yourls_stats_line($values, $id = null) {
yourls_do_action( 'pre_stats_line' );
// if $id is null then assign a random string
if( $id === null )
$id = uniqid ( 'yourls_stats_line_' );
// If we have only 1 day of data, prepend a fake day with 0 hits for a prettier graph
if ( count( $values ) == 1 )
array_unshift( $values, 0 );
// Keep only a subset of values to keep graph smooth
$values = yourls_array_granularity( $values, 30 );
$data = array_merge( array( 'Time' => 'Hits' ), $values );
$data = yourls_google_array_to_data_table( $data );
$options = array(
"legend" => "none",
"pointSize" => "3",
"theme" => "maximized",
"curveType" => "function",
"width" => 430,
"height" => 220,
"hAxis" => "{minTextSpacing: 80, maxTextLines: 1, maxAlternation: 1}",
"vAxis" => "{minValue: 0, format: '#'}",
"colors" => "['#2a85b3']",
);
$options = yourls_apply_filter( 'stats_line_options', $options );
$lineChart = yourls_google_viz_code( 'LineChart', $data, $options, $id );
echo yourls_apply_filter( 'stats_line', $lineChart, $values, $options, $id );
}
/**
* Return the number of days in a month. From php.net.
*
* @param int $month
* @param int $year
* @return int
*/
function yourls_days_in_month($month, $year) {
// calculate number of days in a month
return $month == 2 ? ( $year % 4 ? 28 : ( $year % 100 ? 29 : ( $year % 400 ? 28 : 29 ) ) ) : ( ( $month - 1 ) % 7 % 2 ? 30 : 31 );
}
/**
* Get max value from date array of 'Aug 12, 2012' = '1337'
*
* @param array $list_of_days
* @return array
*/
function yourls_stats_get_best_day($list_of_days) {
$max = max( $list_of_days );
foreach( $list_of_days as $k=>$v ) {
if ( $v == $max )
return array( 'day' => $k, 'max' => $max );
}
}
/**
* Return domain of a URL
*
* @param string $url
* @param bool $include_scheme
* @return string
*/
function yourls_get_domain($url, $include_scheme = false) {
$parse = @parse_url( $url ); // Hiding ugly stuff coming from malformed referrer URLs
// Get host & scheme. Fall back to path if not found.
$host = isset( $parse['host'] ) ? $parse['host'] : '';
$scheme = isset( $parse['scheme'] ) ? $parse['scheme'] : '';
$path = isset( $parse['path'] ) ? $parse['path'] : '';
if( !$host )
$host = $path;
if ( $include_scheme && $scheme )
$host = $scheme.'://'.$host;
return $host;
}
/**
* Return favicon URL
*
* @param string $url
* @return string
*/
function yourls_get_favicon_url($url) {
return yourls_match_current_protocol( '//www.google.com/s2/favicons?domain=' . yourls_get_domain( $url, false ) );
}
/**
* Scale array of data from 0 to 100 max
*
* @param array $data
* @return array
*/
function yourls_scale_data($data ) {
$max = max( $data );
if( $max > 100 ) {
foreach( $data as $k=>$v ) {
$data[$k] = intval( $v / $max * 100 );
}
}
return $data;
}
/**
* Tweak granularity of array $array: keep only $grain values.
*
* This make less accurate but less messy graphs when too much values.
* See https://developers.google.com/chart/image/docs/gallery/line_charts?hl=en#data-granularity
*
* @param array $array
* @param int $grain
* @param bool $preserve_max
* @return array
*/
function yourls_array_granularity($array, $grain = 100, $preserve_max = true) {
if ( count( $array ) > $grain ) {
$max = max( $array );
$step = intval( count( $array ) / $grain );
$i = 0;
// Loop through each item and unset except every $step (optional preserve the max value)
foreach( $array as $k=>$v ) {
$i++;
if ( $i % $step != 0 ) {
if ( $preserve_max == false ) {
unset( $array[$k] );
} else {
if ( $v < $max )
unset( $array[$k] );
}
}
}
}
return $array;
}
/**
* Transform data array to data table for Google API
*
* @param array $data
* @return string
*/
function yourls_google_array_to_data_table($data){
$str = "var data = google.visualization.arrayToDataTable([\n";
foreach( $data as $label => $values ){
if( !is_array( $values ) ) {
$values = array( $values );
}
$str .= "\t['$label',";
foreach( $values as $value ){
if( !is_numeric( $value ) && strpos( $value, '[' ) !== 0 && strpos( $value, '{' ) !== 0 ) {
$value = "'$value'";
}
$str .= "$value";
}
$str .= "],\n";
}
$str = substr( $str, 0, -2 ) . "\n"; // remove the trailing comma/return, reappend the return
$str .= "]);\n"; // wrap it up
return $str;
}
/**
* Return javascript code that will display the Google Chart
*
* @param string $graph_type
* @param string $data
* @param var $options
* @param string $id
* @return string
*/
function yourls_google_viz_code($graph_type, $data, $options, $id ) {
$function_name = 'yourls_graph' . $id;
$code = "\n<script id=\"$function_name\" type=\"text/javascript\">\n";
$code .= "function $function_name() { \n";
$code .= "$data\n";
$code .= "var options = {\n";
foreach( $options as $field => $value ) {
if( !is_numeric( $value ) && strpos( $value, '[' ) !== 0 && strpos( $value, '{' ) !== 0 ) {
$value = "\"$value\"";
}
$code .= "\t'$field': $value,\n";
}
$code = substr( $code, 0, -2 ) . "\n"; // remove the trailing comma/return, reappend the return
$code .= "\t}\n";
$code .= "new google.visualization.$graph_type( document.getElementById('visualization_$id') ).draw( data, options );";
$code .= "}\n";
$code .= "google.setOnLoadCallback( $function_name );\n";
$code .= "</script>\n";
$code .= "<div id=\"visualization_$id\"></div>\n";
return $code;
}

351
functions-install.php Normal file
View File

@ -0,0 +1,351 @@
<?php
/**
* Check if we have PDO installed, returns bool
*
* @since 1.7.3
* @return bool
*/
function yourls_check_PDO() {
return extension_loaded('pdo');
}
/**
* Check if server has MySQL 5.0+
*
* @return bool
*/
function yourls_check_database_version() {
return ( version_compare( '5.0', yourls_get_database_version() ) <= 0 );
}
/**
* Get DB version
*
* @since 1.7
* @return string sanitized DB version
*/
function yourls_get_database_version() {
// Allow plugins to short-circuit the whole function
$pre = yourls_apply_filter( 'shunt_get_database_version', false );
if ( false !== $pre ) {
return $pre;
}
return yourls_sanitize_version(yourls_get_db()->mysql_version());
}
/**
* Check if PHP > 7.2
*
* As of 1.8 we advertise YOURLS as being 7.4+ but it should work on 7.2 (although untested)
* so we don't want to strictly enforce a limitation that may not be necessary.
*
* @return bool
*/
function yourls_check_php_version() {
return version_compare( PHP_VERSION, '7.2.0', '>=' );
}
/**
* Check if server is an Apache
*
* @return bool
*/
function yourls_is_apache() {
if( !array_key_exists( 'SERVER_SOFTWARE', $_SERVER ) )
return false;
return (
strpos( $_SERVER['SERVER_SOFTWARE'], 'Apache' ) !== false
|| strpos( $_SERVER['SERVER_SOFTWARE'], 'LiteSpeed' ) !== false
);
}
/**
* Check if server is running IIS
*
* @return bool
*/
function yourls_is_iis() {
return ( array_key_exists( 'SERVER_SOFTWARE', $_SERVER ) ? ( strpos( $_SERVER['SERVER_SOFTWARE'], 'IIS' ) !== false ) : false );
}
/**
* Create .htaccess or web.config. Returns boolean
*
* @return bool
*/
function yourls_create_htaccess() {
$host = parse_url( yourls_get_yourls_site() );
$path = ( isset( $host['path'] ) ? $host['path'] : '' );
if ( yourls_is_iis() ) {
// Prepare content for a web.config file
$content = array(
'<?'.'xml version="1.0" encoding="UTF-8"?>',
'<configuration>',
' <system.webServer>',
' <security>',
' <requestFiltering allowDoubleEscaping="true" />',
' </security>',
' <rewrite>',
' <rules>',
' <rule name="YOURLS" stopProcessing="true">',
' <match url="^(.*)$" ignoreCase="false" />',
' <conditions>',
' <add input="{REQUEST_FILENAME}" matchType="IsFile" ignoreCase="false" negate="true" />',
' <add input="{REQUEST_FILENAME}" matchType="IsDirectory" ignoreCase="false" negate="true" />',
' </conditions>',
' <action type="Rewrite" url="'.$path.'/yourls-loader.php" appendQueryString="true" />',
' </rule>',
' </rules>',
' </rewrite>',
' </system.webServer>',
'</configuration>',
);
$filename = YOURLS_ABSPATH.'/web.config';
$marker = 'none';
} else {
// Prepare content for a .htaccess file
$content = array(
'<IfModule mod_rewrite.c>',
'RewriteEngine On',
'RewriteBase '.$path.'/',
'RewriteCond %{REQUEST_FILENAME} !-f',
'RewriteCond %{REQUEST_FILENAME} !-d',
'RewriteRule ^.*$ '.$path.'/yourls-loader.php [L]',
'</IfModule>',
);
$filename = YOURLS_ABSPATH.'/.htaccess';
$marker = 'YOURLS';
}
return ( yourls_insert_with_markers( $filename, $marker, $content ) );
}
/**
* Insert text into a file between BEGIN/END markers, return bool. Stolen from WP
*
* Inserts an array of strings into a file (eg .htaccess ), placing it between
* BEGIN and END markers. Replaces existing marked info. Retains surrounding
* data. Creates file if none exists.
*
* @since 1.3
*
* @param string $filename
* @param string $marker
* @param array $insertion
* @return bool True on write success, false on failure.
*/
function yourls_insert_with_markers( $filename, $marker, $insertion ) {
if ( !file_exists( $filename ) || is_writeable( $filename ) ) {
if ( !file_exists( $filename ) ) {
$markerdata = '';
} else {
$markerdata = explode( "\n", implode( '', file( $filename ) ) );
}
if ( !$f = @fopen( $filename, 'w' ) )
return false;
$foundit = false;
if ( $markerdata ) {
$state = true;
foreach ( $markerdata as $n => $markerline ) {
if ( strpos( $markerline, '# BEGIN ' . $marker ) !== false )
$state = false;
if ( $state ) {
if ( $n + 1 < count( $markerdata ) )
fwrite( $f, "{$markerline}\n" );
else
fwrite( $f, "{$markerline}" );
}
if ( strpos( $markerline, '# END ' . $marker ) !== false ) {
if ( $marker != 'none' )
fwrite( $f, "# BEGIN {$marker}\n" );
if ( is_array( $insertion ) )
foreach ( $insertion as $insertline )
fwrite( $f, "{$insertline}\n" );
if ( $marker != 'none' )
fwrite( $f, "# END {$marker}\n" );
$state = true;
$foundit = true;
}
}
}
if ( !$foundit ) {
if ( $marker != 'none' )
fwrite( $f, "\n\n# BEGIN {$marker}\n" );
foreach ( $insertion as $insertline )
fwrite( $f, "{$insertline}\n" );
if ( $marker != 'none' )
fwrite( $f, "# END {$marker}\n\n" );
}
fclose( $f );
return true;
} else {
return false;
}
}
/**
* Create MySQL tables. Return array( 'success' => array of success strings, 'errors' => array of error strings )
*
* @since 1.3
* @return array An array like array( 'success' => array of success strings, 'errors' => array of error strings )
*/
function yourls_create_sql_tables() {
// Allow plugins (most likely a custom db.php layer in user dir) to short-circuit the whole function
$pre = yourls_apply_filter( 'shunt_yourls_create_sql_tables', null );
// your filter function should return an array of ( 'success' => $success_msg, 'error' => $error_msg ), see below
if ( null !== $pre ) {
return $pre;
}
$ydb = yourls_get_db();
$error_msg = array();
$success_msg = array();
// Create Table Query
$create_tables = array();
$create_tables[YOURLS_DB_TABLE_URL] =
'CREATE TABLE IF NOT EXISTS `'.YOURLS_DB_TABLE_URL.'` ('.
'`keyword` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT "",'.
'`url` text CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,'.
'`title` text COLLATE utf8mb4_unicode_ci DEFAULT NULL,'.
'`timestamp` timestamp NOT NULL DEFAULT current_timestamp(),'.
'`ip` varchar(41) COLLATE utf8mb4_unicode_ci NOT NULL,'.
'`clicks` int(10) unsigned NOT NULL,'.
'PRIMARY KEY (`keyword`),'.
'KEY `ip` (`ip`),'.
'KEY `timestamp` (`timestamp`)'.
') DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;';
$create_tables[YOURLS_DB_TABLE_OPTIONS] =
'CREATE TABLE IF NOT EXISTS `'.YOURLS_DB_TABLE_OPTIONS.'` ('.
'`option_id` bigint(20) unsigned NOT NULL auto_increment,'.
'`option_name` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL default "",'.
'`option_value` longtext COLLATE utf8mb4_unicode_ci NOT NULL,'.
'PRIMARY KEY (`option_id`,`option_name`),'.
'KEY `option_name` (`option_name`)'.
') AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;';
$create_tables[YOURLS_DB_TABLE_LOG] =
'CREATE TABLE IF NOT EXISTS `'.YOURLS_DB_TABLE_LOG.'` ('.
'`click_id` int(11) NOT NULL auto_increment,'.
'`click_time` datetime NOT NULL,'.
'`shorturl` varchar(100) BINARY NOT NULL,'.
'`referrer` varchar(200) NOT NULL,'.
'`user_agent` varchar(255) NOT NULL,'.
'`ip_address` varchar(41) NOT NULL,'.
'`country_code` char(2) NOT NULL,'.
'PRIMARY KEY (`click_id`),'.
'KEY `shorturl` (`shorturl`)'.
') AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;';
$create_table_count = 0;
yourls_debug_mode(true);
// Create tables
foreach ( $create_tables as $table_name => $table_query ) {
$ydb->perform( $table_query );
$create_success = $ydb->fetchAffected( "SHOW TABLES LIKE '$table_name'" );
if( $create_success ) {
$create_table_count++;
$success_msg[] = yourls_s( "Table '%s' created.", $table_name );
} else {
$error_msg[] = yourls_s( "Error creating table '%s'.", $table_name );
}
}
// Initializes the option table
if( !yourls_initialize_options() )
$error_msg[] = yourls__( 'Could not initialize options' );
// Insert sample links
if( !yourls_insert_sample_links() )
$error_msg[] = yourls__( 'Could not insert sample short URLs' );
// Check results of operations
if ( sizeof( $create_tables ) == $create_table_count ) {
$success_msg[] = yourls__( 'YOURLS tables successfully created.' );
} else {
$error_msg[] = yourls__( 'Error creating YOURLS tables.' );
}
return array( 'success' => $success_msg, 'error' => $error_msg );
}
/**
* Initializes the option table
*
* Each yourls_update_option() returns either true on success (option updated) or false on failure (new value == old value, or
* for some reason it could not save to DB).
* Since true & true & true = 1, we cast it to boolean type to return true (or false)
*
* @since 1.7
* @return bool
*/
function yourls_initialize_options() {
return ( bool ) (
yourls_update_option( 'version', YOURLS_VERSION )
& yourls_update_option( 'db_version', YOURLS_DB_VERSION )
& yourls_update_option( 'next_id', 1 )
& yourls_update_option( 'active_plugins', array() )
);
}
/**
* Populates the URL table with a few sample links
*
* @since 1.7
* @return bool
*/
function yourls_insert_sample_links() {
$link1 = yourls_add_new_link( 'https://blog.yourls.org/', 'yourlsblog', 'YOURLS\' Blog' );
$link2 = yourls_add_new_link( 'https://yourls.org/', 'yourls', 'YOURLS: Your Own URL Shortener' );
$link3 = yourls_add_new_link( 'https://ozh.org/', 'ozh', 'ozh.org' );
return ( bool ) (
$link1['status'] == 'success'
& $link2['status'] == 'success'
& $link3['status'] == 'success'
);
}
/**
* Toggle maintenance mode. Inspired from WP. Returns true for success, false otherwise
*
* @param bool $maintenance True to enable, false to disable
* @return bool True on success, false on failure
*/
function yourls_maintenance_mode( $maintenance = true ) {
$file = YOURLS_ABSPATH . '/.maintenance' ;
// Turn maintenance mode on : create .maintenance file
if ( (bool)$maintenance ) {
if ( ! ( $fp = @fopen( $file, 'w' ) ) )
return false;
$maintenance_string = '<?php $maintenance_start = ' . time() . '; ?>';
@fwrite( $fp, $maintenance_string );
@fclose( $fp );
@chmod( $file, 0644 ); // Read and write for owner, read for everybody else
// Not sure why the fwrite would fail if the fopen worked... Just in case
return( is_readable( $file ) );
// Turn maintenance mode off : delete the .maintenance file
} else {
return @unlink($file);
}
}

778
functions-kses.php Normal file
View File

@ -0,0 +1,778 @@
<?php
/**
* YOURLS modification of a small subset from WordPress' KSES implementation.
* Straight from the Let's Not Reinvent The Wheel department.
*/
/**
* kses 0.2.2 - HTML/XHTML filter that only allows some elements and attributes
* Copyright (C) 2002, 2003, 2005 Ulf Harnhammar
*
* This program is free software and open source software; you can redistribute
* it and/or modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 2 of the License,
* or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
* http://www.gnu.org/licenses/gpl.html
*
* [kses strips evil scripts!]
*
* @version 0.2.2
* @copyright (C) 2002, 2003, 2005
* @author Ulf Harnhammar <http://advogato.org/person/metaur/>
*
* @package External
* @subpackage KSES
*
*/
/* NOTE ABOUT GLOBALS
* Two globals are defined: $yourls_allowedentitynames and $yourls_allowedprotocols
* - $yourls_allowedentitynames is used internally in KSES functions to sanitize HTML entities
* - $yourls_allowedprotocols is used in various parts of YOURLS, not just in KSES, albeit being defined here
* Two globals are not defined and unused at this moment: $yourls_allowedtags_all and $yourls_allowedtags
* The code for these vars is here and ready for any future use
*/
// Populate after plugins have loaded to allow user defined values
yourls_add_action( 'plugins_loaded', 'yourls_kses_init' );
/**
* Init KSES globals if not already defined (by a plugin)
*
* @since 1.6
* @return void
*/
function yourls_kses_init() {
global $yourls_allowedentitynames, $yourls_allowedprotocols;
if( ! $yourls_allowedentitynames ) {
$yourls_allowedentitynames = yourls_apply_filter( 'kses_allowed_entities', yourls_kses_allowed_entities() );
}
if( ! $yourls_allowedprotocols ) {
$yourls_allowedprotocols = yourls_apply_filter( 'kses_allowed_protocols', yourls_kses_allowed_protocols() );
}
/** See NOTE ABOUT GLOBALS **
if( ! $yourls_allowedtags_all ) {
$yourls_allowedtags_all = yourls_kses_allowed_tags_all();
$yourls_allowedtags_all = array_map( '_yourls_add_global_attributes', $yourls_allowedtags_all );
$yourls_allowedtags_all = yourls_apply_filter( 'kses_allowed_tags_all', $yourls_allowedtags_all );
} else {
// User defined: let's sanitize
$yourls_allowedtags_all = yourls_kses_array_lc( $yourls_allowedtags_all );
}
if( ! $yourls_allowedtags ) {
$yourls_allowedtags = yourls_kses_allowed_tags();
$yourls_allowedtags = array_map( '_yourls_add_global_attributes', $yourls_allowedtags );
$yourls_allowedtags = yourls_apply_filter( 'kses_allowed_tags', $yourls_allowedtags );
} else {
// User defined: let's sanitize
$yourls_allowedtags = yourls_kses_array_lc( $yourls_allowedtags );
}
/**/
}
/**
* Kses global for all allowable HTML tags.
*
* Complete (?) list of HTML tags. Keep this function available for any plugin or
* future feature that will want to display lots of HTML.
*
* @since 1.6
*
* @return array All tags
*/
function yourls_kses_allowed_tags_all() {
return array(
'address' => array(),
'a' => array(
'href' => true,
'rel' => true,
'rev' => true,
'name' => true,
'target' => true,
),
'abbr' => array(),
'acronym' => array(),
'area' => array(
'alt' => true,
'coords' => true,
'href' => true,
'nohref' => true,
'shape' => true,
'target' => true,
),
'article' => array(
'align' => true,
'dir' => true,
'lang' => true,
'xml:lang' => true,
),
'aside' => array(
'align' => true,
'dir' => true,
'lang' => true,
'xml:lang' => true,
),
'b' => array(),
'big' => array(),
'blockquote' => array(
'cite' => true,
'lang' => true,
'xml:lang' => true,
),
'br' => array(),
'button' => array(
'disabled' => true,
'name' => true,
'type' => true,
'value' => true,
),
'caption' => array(
'align' => true,
),
'cite' => array(
'dir' => true,
'lang' => true,
),
'code' => array(),
'col' => array(
'align' => true,
'char' => true,
'charoff' => true,
'span' => true,
'dir' => true,
'valign' => true,
'width' => true,
),
'del' => array(
'datetime' => true,
),
'dd' => array(),
'details' => array(
'align' => true,
'dir' => true,
'lang' => true,
'open' => true,
'xml:lang' => true,
),
'div' => array(
'align' => true,
'dir' => true,
'lang' => true,
'xml:lang' => true,
),
'dl' => array(),
'dt' => array(),
'em' => array(),
'fieldset' => array(),
'figure' => array(
'align' => true,
'dir' => true,
'lang' => true,
'xml:lang' => true,
),
'figcaption' => array(
'align' => true,
'dir' => true,
'lang' => true,
'xml:lang' => true,
),
'font' => array(
'color' => true,
'face' => true,
'size' => true,
),
'footer' => array(
'align' => true,
'dir' => true,
'lang' => true,
'xml:lang' => true,
),
'form' => array(
'action' => true,
'accept' => true,
'accept-charset' => true,
'enctype' => true,
'method' => true,
'name' => true,
'target' => true,
),
'h1' => array(
'align' => true,
),
'h2' => array(
'align' => true,
),
'h3' => array(
'align' => true,
),
'h4' => array(
'align' => true,
),
'h5' => array(
'align' => true,
),
'h6' => array(
'align' => true,
),
'header' => array(
'align' => true,
'dir' => true,
'lang' => true,
'xml:lang' => true,
),
'hgroup' => array(
'align' => true,
'dir' => true,
'lang' => true,
'xml:lang' => true,
),
'hr' => array(
'align' => true,
'noshade' => true,
'size' => true,
'width' => true,
),
'i' => array(),
'img' => array(
'alt' => true,
'align' => true,
'border' => true,
'height' => true,
'hspace' => true,
'longdesc' => true,
'vspace' => true,
'src' => true,
'usemap' => true,
'width' => true,
),
'ins' => array(
'datetime' => true,
'cite' => true,
),
'kbd' => array(),
'label' => array(
'for' => true,
),
'legend' => array(
'align' => true,
),
'li' => array(
'align' => true,
),
'map' => array(
'name' => true,
),
'menu' => array(
'type' => true,
),
'nav' => array(
'align' => true,
'dir' => true,
'lang' => true,
'xml:lang' => true,
),
'p' => array(
'align' => true,
'dir' => true,
'lang' => true,
'xml:lang' => true,
),
'pre' => array(
'width' => true,
),
'q' => array(
'cite' => true,
),
's' => array(),
'span' => array(
'dir' => true,
'align' => true,
'lang' => true,
'xml:lang' => true,
),
'section' => array(
'align' => true,
'dir' => true,
'lang' => true,
'xml:lang' => true,
),
'small' => array(),
'strike' => array(),
'strong' => array(),
'sub' => array(),
'summary' => array(
'align' => true,
'dir' => true,
'lang' => true,
'xml:lang' => true,
),
'sup' => array(),
'table' => array(
'align' => true,
'bgcolor' => true,
'border' => true,
'cellpadding' => true,
'cellspacing' => true,
'dir' => true,
'rules' => true,
'summary' => true,
'width' => true,
),
'tbody' => array(
'align' => true,
'char' => true,
'charoff' => true,
'valign' => true,
),
'td' => array(
'abbr' => true,
'align' => true,
'axis' => true,
'bgcolor' => true,
'char' => true,
'charoff' => true,
'colspan' => true,
'dir' => true,
'headers' => true,
'height' => true,
'nowrap' => true,
'rowspan' => true,
'scope' => true,
'valign' => true,
'width' => true,
),
'textarea' => array(
'cols' => true,
'rows' => true,
'disabled' => true,
'name' => true,
'readonly' => true,
),
'tfoot' => array(
'align' => true,
'char' => true,
'charoff' => true,
'valign' => true,
),
'th' => array(
'abbr' => true,
'align' => true,
'axis' => true,
'bgcolor' => true,
'char' => true,
'charoff' => true,
'colspan' => true,
'headers' => true,
'height' => true,
'nowrap' => true,
'rowspan' => true,
'scope' => true,
'valign' => true,
'width' => true,
),
'thead' => array(
'align' => true,
'char' => true,
'charoff' => true,
'valign' => true,
),
'title' => array(),
'tr' => array(
'align' => true,
'bgcolor' => true,
'char' => true,
'charoff' => true,
'valign' => true,
),
'tt' => array(),
'u' => array(),
'ul' => array(
'type' => true,
),
'ol' => array(
'start' => true,
'type' => true,
),
'var' => array(),
);
}
/**
* Kses global for default allowable HTML tags. TODO: trim down to necessary only.
*
* Short list of HTML tags used in YOURLS core for display
*
* @since 1.6
*
* @return array Allowed tags
*/
function yourls_kses_allowed_tags() {
return array(
'a' => array(
'href' => true,
'title' => true,
),
'abbr' => array(
'title' => true,
),
'acronym' => array(
'title' => true,
),
'b' => array(),
'blockquote' => array(
'cite' => true,
),
'cite' => array(),
'code' => array(),
'del' => array(
'datetime' => true,
),
'em' => array(),
'i' => array(),
'q' => array(
'cite' => true,
),
'strike' => array(),
'strong' => array(),
);
}
/**
* Kses global for allowable HTML entities.
*
* @since 1.6
*
* @return array Allowed entities
*/
function yourls_kses_allowed_entities() {
return array(
'nbsp', 'iexcl', 'cent', 'pound', 'curren', 'yen',
'brvbar', 'sect', 'uml', 'copy', 'ordf', 'laquo',
'not', 'shy', 'reg', 'macr', 'deg', 'plusmn',
'acute', 'micro', 'para', 'middot', 'cedil', 'ordm',
'raquo', 'iquest', 'Agrave', 'Aacute', 'Acirc', 'Atilde',
'Auml', 'Aring', 'AElig', 'Ccedil', 'Egrave', 'Eacute',
'Ecirc', 'Euml', 'Igrave', 'Iacute', 'Icirc', 'Iuml',
'ETH', 'Ntilde', 'Ograve', 'Oacute', 'Ocirc', 'Otilde',
'Ouml', 'times', 'Oslash', 'Ugrave', 'Uacute', 'Ucirc',
'Uuml', 'Yacute', 'THORN', 'szlig', 'agrave', 'aacute',
'acirc', 'atilde', 'auml', 'aring', 'aelig', 'ccedil',
'egrave', 'eacute', 'ecirc', 'euml', 'igrave', 'iacute',
'icirc', 'iuml', 'eth', 'ntilde', 'ograve', 'oacute',
'ocirc', 'otilde', 'ouml', 'divide', 'oslash', 'ugrave',
'uacute', 'ucirc', 'uuml', 'yacute', 'thorn', 'yuml',
'quot', 'amp', 'lt', 'gt', 'apos', 'OElig',
'oelig', 'Scaron', 'scaron', 'Yuml', 'circ', 'tilde',
'ensp', 'emsp', 'thinsp', 'zwnj', 'zwj', 'lrm',
'rlm', 'ndash', 'mdash', 'lsquo', 'rsquo', 'sbquo',
'ldquo', 'rdquo', 'bdquo', 'dagger', 'Dagger', 'permil',
'lsaquo', 'rsaquo', 'euro', 'fnof', 'Alpha', 'Beta',
'Gamma', 'Delta', 'Epsilon', 'Zeta', 'Eta', 'Theta',
'Iota', 'Kappa', 'Lambda', 'Mu', 'Nu', 'Xi',
'Omicron', 'Pi', 'Rho', 'Sigma', 'Tau', 'Upsilon',
'Phi', 'Chi', 'Psi', 'Omega', 'alpha', 'beta',
'gamma', 'delta', 'epsilon', 'zeta', 'eta', 'theta',
'iota', 'kappa', 'lambda', 'mu', 'nu', 'xi',
'omicron', 'pi', 'rho', 'sigmaf', 'sigma', 'tau',
'upsilon', 'phi', 'chi', 'psi', 'omega', 'thetasym',
'upsih', 'piv', 'bull', 'hellip', 'prime', 'Prime',
'oline', 'frasl', 'weierp', 'image', 'real', 'trade',
'alefsym', 'larr', 'uarr', 'rarr', 'darr', 'harr',
'crarr', 'lArr', 'uArr', 'rArr', 'dArr', 'hArr',
'forall', 'part', 'exist', 'empty', 'nabla', 'isin',
'notin', 'ni', 'prod', 'sum', 'minus', 'lowast',
'radic', 'prop', 'infin', 'ang', 'and', 'or',
'cap', 'cup', 'int', 'sim', 'cong', 'asymp',
'ne', 'equiv', 'le', 'ge', 'sub', 'sup',
'nsub', 'sube', 'supe', 'oplus', 'otimes', 'perp',
'sdot', 'lceil', 'rceil', 'lfloor', 'rfloor', 'lang',
'rang', 'loz', 'spades', 'clubs', 'hearts', 'diams',
);
}
/**
* Kses global for allowable protocols.
*
* @since 1.6
*
* @return array Allowed protocols
*/
function yourls_kses_allowed_protocols() {
// More or less common stuff in links. From http://en.wikipedia.org/wiki/URI_scheme
return array(
// Common
'http://', 'https://', 'ftp://',
'file://', 'smb://',
'sftp://',
'feed:', 'feed://',
'mailto:',
'news:', 'nntp://',
// Old school bearded geek
'gopher://', 'telnet://', 'finger://',
'nntp://', 'worldwind://',
// Dev
'ssh://', 'svn://', 'svn+ssh://', 'git://', 'cvs://',
'apt:',
'market://', // Google Play
'view-source:',
// P2P
'ed2k://', 'magnet:', 'udp://',
// Streaming stuff
'mms://', 'lastfm://', 'spotify:', 'rtsp://',
// Text & voice
'aim:', 'facetime://', 'gtalk:', 'xmpp:',
'irc://', 'ircs://', 'mumble://',
'callto:', 'skype:', 'sip:',
'teamspeak://', 'tel:', 'ventrilo://', 'xfire:',
'ymsgr:', 'tg://', 'whatsapp://',
// Misc
'steam:', 'steam://',
'bitcoin:',
'ldap://', 'ldaps://',
// Purposedly removed for security
/*
'about:', 'chrome://', 'chrome-extension://',
'javascript:',
'data:',
*/
);
}
/**
* Converts and fixes HTML entities.
*
* This function normalizes HTML entities. It will convert "AT&T" to the correct
* "AT&amp;T", "&#00058;" to "&#58;", "&#XYZZY;" to "&amp;#XYZZY;" and so on.
*
* @since 1.6
*
* @param string $string Content to normalize entities
* @return string Content with normalized entities
*/
function yourls_kses_normalize_entities($string) {
# Disarm all entities by converting & to &amp;
$string = str_replace('&', '&amp;', $string);
# Change back the allowed entities in our entity whitelist
$string = preg_replace_callback('/&amp;([A-Za-z]{2,8});/', 'yourls_kses_named_entities', $string);
$string = preg_replace_callback('/&amp;#(0*[0-9]{1,7});/', 'yourls_kses_normalize_entities2', $string);
$string = preg_replace_callback('/&amp;#[Xx](0*[0-9A-Fa-f]{1,6});/', 'yourls_kses_normalize_entities3', $string);
return $string;
}
/**
* Callback for yourls_kses_normalize_entities() regular expression.
*
* This function only accepts valid named entity references, which are finite,
* case-sensitive, and highly scrutinized by HTML and XML validators.
*
* @since 1.6
*
* @param array $matches preg_replace_callback() matches array
* @return string Correctly encoded entity
*/
function yourls_kses_named_entities($matches) {
global $yourls_allowedentitynames;
if ( empty($matches[1]) )
return '';
$i = $matches[1];
return ( ( ! in_array($i, $yourls_allowedentitynames) ) ? "&amp;$i;" : "&$i;" );
}
/**
* Callback for yourls_kses_normalize_entities() regular expression.
*
* This function helps yourls_kses_normalize_entities() to only accept 16-bit values
* and nothing more for &#number; entities.
*
* @access private
* @since 1.6
*
* @param array $matches preg_replace_callback() matches array
* @return string Correctly encoded entity
*/
function yourls_kses_normalize_entities2($matches) {
if ( empty($matches[1]) )
return '';
$i = $matches[1];
if (yourls_valid_unicode($i)) {
$i = str_pad(ltrim($i,'0'), 3, '0', STR_PAD_LEFT);
$i = "&#$i;";
} else {
$i = "&amp;#$i;";
}
return $i;
}
/**
* Callback for yourls_kses_normalize_entities() for regular expression.
*
* This function helps yourls_kses_normalize_entities() to only accept valid Unicode
* numeric entities in hex form.
*
* @access private
* @since 1.6
*
* @param array $matches preg_replace_callback() matches array
* @return string Correctly encoded entity
*/
function yourls_kses_normalize_entities3($matches) {
if ( empty($matches[1]) )
return '';
$hexchars = $matches[1];
return ( ( ! yourls_valid_unicode(hexdec($hexchars)) ) ? "&amp;#x$hexchars;" : '&#x'.ltrim($hexchars,'0').';' );
}
/**
* Helper function to add global attributes to a tag in the allowed html list.
*
* @since 1.6
* @access private
*
* @param array $value An array of attributes.
* @return array The array of attributes with global attributes added.
*/
function _yourls_add_global_attributes( $value ) {
$global_attributes = array(
'class' => true,
'id' => true,
'style' => true,
'title' => true,
);
if ( true === $value )
$value = array();
if ( is_array( $value ) )
return array_merge( $value, $global_attributes );
return $value;
}
/**
* Helper function to determine if a Unicode value is valid.
*
* @since 1.6
*
* @param int $i Unicode value
* @return bool True if the value was a valid Unicode number
*/
function yourls_valid_unicode($i) {
return ( $i == 0x9 || $i == 0xa || $i == 0xd ||
($i >= 0x20 && $i <= 0xd7ff) ||
($i >= 0xe000 && $i <= 0xfffd) ||
($i >= 0x10000 && $i <= 0x10ffff) );
}
/**
* Goes through an array and changes the keys to all lower case.
*
* @since 1.6
*
* @param array $inarray Unfiltered array
* @return array Fixed array with all lowercase keys
*/
function yourls_kses_array_lc($inarray) {
$outarray = array ();
foreach ( (array) $inarray as $inkey => $inval) {
$outkey = strtolower($inkey);
$outarray[$outkey] = array ();
foreach ( (array) $inval as $inkey2 => $inval2) {
$outkey2 = strtolower($inkey2);
$outarray[$outkey][$outkey2] = $inval2;
} # foreach $inval
} # foreach $inarray
return $outarray;
}
/**
* Convert all entities to their character counterparts.
*
* This function decodes numeric HTML entities (&#65; and &#x41;). It doesn't do
* anything with other entities like &auml;, but we don't need them in the URL
* protocol whitelisting system anyway.
*
* @since 1.6
*
* @param string $string Content to change entities
* @return string Content after decoded entities
*/
function yourls_kses_decode_entities($string) {
$string = preg_replace_callback('/&#([0-9]+);/', '_yourls_kses_decode_entities_chr', $string);
$string = preg_replace_callback('/&#[Xx]([0-9A-Fa-f]+);/', '_yourls_kses_decode_entities_chr_hexdec', $string);
return $string;
}
/**
* Regex callback for yourls_kses_decode_entities()
*
* @since 1.6
*
* @param array $match preg match
* @return string
*/
function _yourls_kses_decode_entities_chr( $match ) {
return chr( $match[1] );
}
/**
* Regex callback for yourls_kses_decode_entities()
*
* @since 1.6
*
* @param array $match preg match
* @return string
*/
function _yourls_kses_decode_entities_chr_hexdec( $match ) {
return chr( hexdec( $match[1] ) );
}
/**
* Removes any null characters in $string.
*
* @since 1.6
*
* @param string $string
* @return string
*/
function yourls_kses_no_null($string) {
$string = preg_replace( '/\0+/', '', $string );
$string = preg_replace( '/(\\\\0)+/', '', $string );
return $string;
}

1129
functions-l10n.php Normal file

File diff suppressed because it is too large Load Diff

276
functions-links.php Normal file
View File

@ -0,0 +1,276 @@
<?php
/*
* Functions relative to how YOURLS handle some links
*
*/
/**
* Add a query var to a URL and return URL. Completely stolen from WP.
*
* Works with one of these parameter patterns:
* array( 'var' => 'value' )
* array( 'var' => 'value' ), $url
* 'var', 'value'
* 'var', 'value', $url
* If $url omitted, uses $_SERVER['REQUEST_URI']
*
* The result of this function call is a URL : it should be escaped before being printed as HTML
*
* @since 1.5
* @param string|array $param1 Either newkey or an associative_array.
* @param string $param2 Either newvalue or oldquery or URI.
* @param string $param3 Optional. Old query or URI.
* @return string New URL query string.
*/
function yourls_add_query_arg() {
$ret = '';
if ( is_array( func_get_arg(0) ) ) {
if ( @func_num_args() < 2 || false === @func_get_arg( 1 ) )
$uri = $_SERVER['REQUEST_URI'];
else
$uri = @func_get_arg( 1 );
} else {
if ( @func_num_args() < 3 || false === @func_get_arg( 2 ) )
$uri = $_SERVER['REQUEST_URI'];
else
$uri = @func_get_arg( 2 );
}
$uri = str_replace( '&amp;', '&', $uri );
if ( $frag = strstr( $uri, '#' ) )
$uri = substr( $uri, 0, -strlen( $frag ) );
else
$frag = '';
if ( preg_match( '|^https?://|i', $uri, $matches ) ) {
$protocol = $matches[0];
$uri = substr( $uri, strlen( $protocol ) );
} else {
$protocol = '';
}
if ( strpos( $uri, '?' ) !== false ) {
$parts = explode( '?', $uri, 2 );
if ( 1 == count( $parts ) ) {
$base = '?';
$query = $parts[0];
} else {
$base = $parts[0] . '?';
$query = $parts[1];
}
} elseif ( !empty( $protocol ) || strpos( $uri, '=' ) === false ) {
$base = $uri . '?';
$query = '';
} else {
$base = '';
$query = $uri;
}
parse_str( $query, $qs );
$qs = yourls_urlencode_deep( $qs ); // this re-URL-encodes things that were already in the query string
if ( is_array( func_get_arg( 0 ) ) ) {
$kayvees = func_get_arg( 0 );
$qs = array_merge( $qs, $kayvees );
} else {
$qs[func_get_arg( 0 )] = func_get_arg( 1 );
}
foreach ( (array) $qs as $k => $v ) {
if ( $v === false )
unset( $qs[$k] );
}
$ret = http_build_query( $qs );
$ret = trim( $ret, '?' );
$ret = preg_replace( '#=(&|$)#', '$1', $ret );
$ret = $protocol . $base . $ret . $frag;
$ret = rtrim( $ret, '?' );
return $ret;
}
/**
* Navigates through an array and encodes the values to be used in a URL. Stolen from WP, used in yourls_add_query_arg()
*
* @param array|string $value The array or string to be encoded.
* @return array|string
*/
function yourls_urlencode_deep( $value ) {
$value = is_array( $value ) ? array_map( 'yourls_urlencode_deep', $value ) : urlencode( $value );
return $value;
}
/**
* Remove arg from query. Opposite of yourls_add_query_arg. Stolen from WP.
*
* The result of this function call is a URL : it should be escaped before being printed as HTML
*
* @since 1.5
* @param string|array $key Query key or keys to remove.
* @param bool|string $query Optional. When false uses the $_SERVER value. Default false.
* @return string New URL query string.
*/
function yourls_remove_query_arg( $key, $query = false ) {
if ( is_array( $key ) ) { // removing multiple keys
foreach ( $key as $k )
$query = yourls_add_query_arg( $k, false, $query );
return $query;
}
return yourls_add_query_arg( $key, false, $query );
}
/**
* Converts keyword into short link (prepend with YOURLS base URL) or stat link (sho.rt/abc+)
*
* This function does not check for a valid keyword.
* The resulting link is normalized to allow for IDN translation to UTF8
*
* @param string $keyword Short URL keyword
* @param bool $stats Optional, true to return a stat link (eg sho.rt/abc+)
* @return string Short URL, or keyword stat URL
*/
function yourls_link( $keyword = '', $stats = false ) {
$keyword = yourls_sanitize_keyword($keyword);
if( $stats === true ) {
$keyword = $keyword . '+';
}
$link = yourls_normalize_uri( yourls_get_yourls_site() . '/' . $keyword );
if( yourls_is_ssl() ) {
$link = yourls_set_url_scheme( $link, 'https' );
}
return yourls_apply_filter( 'yourls_link', $link, $keyword );
}
/**
* Converts keyword into stat link (prepend with YOURLS base URL, append +)
*
* This function does not make sure the keyword matches an actual short URL
*
* @param string $keyword Short URL keyword
* @return string Short URL stat link
*/
function yourls_statlink( $keyword = '' ) {
$link = yourls_link( $keyword, true );
return yourls_apply_filter( 'yourls_statlink', $link, $keyword );
}
/**
* Return admin link, with SSL preference if applicable.
*
* @param string $page Page name, eg "index.php"
* @return string
*/
function yourls_admin_url( $page = '' ) {
$admin = yourls_get_yourls_site() . '/admin/' . $page;
if( yourls_is_ssl() or yourls_needs_ssl() ) {
$admin = yourls_set_url_scheme( $admin, 'https' );
}
return yourls_apply_filter( 'admin_url', $admin, $page );
}
/**
* Return YOURLS_SITE or URL under YOURLS setup, with SSL preference
*
* @param bool $echo Echo if true, or return if false
* @param string $url
* @return string
*/
function yourls_site_url($echo = true, $url = '' ) {
$url = yourls_get_relative_url( $url );
$url = trim( yourls_get_yourls_site() . '/' . $url, '/' );
// Do not enforce (checking yourls_need_ssl() ) but check current usage so it won't force SSL on non-admin pages
if( yourls_is_ssl() ) {
$url = yourls_set_url_scheme( $url, 'https' );
}
$url = yourls_apply_filter( 'site_url', $url );
if( $echo ) {
echo $url;
}
return $url;
}
/**
* Get YOURLS_SITE value, trimmed and filtered
*
* In addition of being filtered for plugins to hack this, this function is mostly here
* to help people entering "sho.rt/" instead of "sho.rt" in their config
*
* @since 1.7.7
* @return string YOURLS_SITE, trimmed and filtered
*/
function yourls_get_yourls_site() {
return yourls_apply_filter('get_yourls_site', trim(YOURLS_SITE, '/'));
}
/**
* Change protocol of a URL to HTTPS if we are currently on HTTPS
*
* This function is used to avoid insert 'http://' images or scripts in a page when it's served through HTTPS,
* to avoid "mixed content" errors.
* So:
* - if you are on http://sho.rt/, 'http://something' and 'https://something' are left untouched.
* - if you are on https:/sho.rt/, 'http://something' is changed to 'https://something'
*
* So, arguably, this function is poorly named. It should be something like yourls_match_current_protocol_if_we_re_on_https
*
* @since 1.5.1
* @param string $url a URL
* @param string $normal Optional, the standard scheme (defaults to 'http://')
* @param string $ssl Optional, the SSL scheme (defaults to 'https://')
* @return string the modified URL, if applicable
*/
function yourls_match_current_protocol( $url, $normal = 'http://', $ssl = 'https://' ) {
// we're only doing something if we're currently serving through SSL and the input URL begins with 'http://' or 'https://'
if( yourls_is_ssl() && in_array( yourls_get_protocol($url), array('http://', 'https://') ) ) {
$url = str_replace( $normal, $ssl, $url );
}
return yourls_apply_filter( 'match_current_protocol', $url );
}
/**
* Auto detect custom favicon in /user directory, fallback to YOURLS favicon, and echo/return its URL
*
* This function supersedes function yourls_favicon(), deprecated in 1.7.10, with a better naming.
*
* @since 1.7.10
* @param bool $echo true to echo, false to silently return
* @return string favicon URL
*
*/
function yourls_get_yourls_favicon_url( $echo = true ) {
static $favicon = null;
if( $favicon !== null ) {
if( $echo ) {
echo $favicon;
}
return $favicon;
}
$custom = null;
// search for favicon.(gif|ico|png|jpg|svg)
foreach( array( 'gif', 'ico', 'png', 'jpg', 'svg' ) as $ext ) {
if( file_exists( YOURLS_USERDIR. '/favicon.' . $ext ) ) {
$custom = 'favicon.' . $ext;
break;
}
}
if( $custom ) {
$favicon = yourls_site_url( false, YOURLS_USERURL . '/' . $custom );
} else {
$favicon = yourls_site_url( false ) . '/images/favicon.svg';
}
$favicon = yourls_apply_filter('get_favicon_url', $favicon);
if( $echo ) {
echo $favicon;
}
return $favicon;
}

195
functions-options.php Normal file
View File

@ -0,0 +1,195 @@
<?php
/*
* Functions to deal with the option API
*
*/
/**
* Read an option from DB (or from cache if available). Return value or $default if not found
*
* Pretty much stolen from WordPress
*
* @since 1.4
* @param string $option_name Option name. Expected to not be SQL-escaped.
* @param mixed $default Optional value to return if option doesn't exist. Default false.
* @return mixed Value set for the option.
*/
function yourls_get_option( $option_name, $default = false ) {
// Allow plugins to short-circuit options
$pre = yourls_apply_filter( 'shunt_option_'.$option_name, false );
if ( false !== $pre ) {
return $pre;
}
$option = new \YOURLS\Database\Options(yourls_get_db());
$value = $option->get($option_name, $default);
return yourls_apply_filter( 'get_option_'.$option_name, $value );
}
/**
* Read all options from DB at once
*
* The goal is to read all options at once and then populate array $ydb->option, to prevent further
* SQL queries if we need to read an option value later.
* It's also a simple check whether YOURLS is installed or not (no option = assuming not installed) after
* a check for DB server reachability has been performed
*
* @since 1.4
* @return void
*/
function yourls_get_all_options() {
// Allow plugins to short-circuit all options. (Note: regular plugins are loaded after all options)
$pre = yourls_apply_filter( 'shunt_all_options', false );
if ( false !== $pre ) {
return $pre;
}
$options = new \YOURLS\Database\Options(yourls_get_db());
if ($options->get_all_options() === false) {
// Zero option found but no unexpected error so far: YOURLS isn't installed
yourls_set_installed(false);
return;
}
yourls_set_installed(true);
}
/**
* Update (add if doesn't exist) an option to DB
*
* Pretty much stolen from WordPress
*
* @since 1.4
* @param string $option_name Option name. Expected to not be SQL-escaped.
* @param mixed $newvalue Option value. Must be serializable if non-scalar. Expected to not be SQL-escaped.
* @return bool False if value was not updated, true otherwise.
*/
function yourls_update_option( $option_name, $newvalue ) {
$option = new \YOURLS\Database\Options(yourls_get_db());
$update = $option->update($option_name, $newvalue);
return $update;
}
/**
* Add an option to the DB
*
* Pretty much stolen from WordPress
*
* @since 1.4
* @param string $name Name of option to add. Expected to not be SQL-escaped.
* @param mixed $value Optional option value. Must be serializable if non-scalar. Expected to not be SQL-escaped.
* @return bool False if option was not added and true otherwise.
*/
function yourls_add_option( $name, $value = '' ) {
$option = new \YOURLS\Database\Options(yourls_get_db());
$add = $option->add($name, $value);
return $add;
}
/**
* Delete an option from the DB
*
* Pretty much stolen from WordPress
*
* @since 1.4
* @param string $name Option name to delete. Expected to not be SQL-escaped.
* @return bool True, if option is successfully deleted. False on failure.
*/
function yourls_delete_option( $name ) {
$option = new \YOURLS\Database\Options(yourls_get_db());
$delete = $option->delete($name);
return $delete;
}
/**
* Serialize data if needed. Stolen from WordPress
*
* @since 1.4
* @param mixed $data Data that might be serialized.
* @return mixed A scalar data
*/
function yourls_maybe_serialize( $data ) {
if ( is_array( $data ) || is_object( $data ) )
return serialize( $data );
if ( yourls_is_serialized( $data, false ) )
return serialize( $data );
return $data;
}
/**
* Unserialize value only if it was serialized. Stolen from WP
*
* @since 1.4
* @param string $original Maybe unserialized original, if is needed.
* @return mixed Unserialized data can be any type.
*/
function yourls_maybe_unserialize( $original ) {
if ( yourls_is_serialized( $original ) ) // don't attempt to unserialize data that wasn't serialized going in
return @unserialize( $original );
return $original;
}
/**
* Check value to find if it was serialized. Stolen from WordPress
*
* @since 1.4
* @param mixed $data Value to check to see if was serialized.
* @param bool $strict Optional. Whether to be strict about the end of the string. Defaults true.
* @return bool False if not serialized and true if it was.
*/
function yourls_is_serialized( $data, $strict = true ) {
// if it isn't a string, it isn't serialized
if ( ! is_string( $data ) )
return false;
$data = trim( $data );
if ( 'N;' == $data )
return true;
$length = strlen( $data );
if ( $length < 4 )
return false;
if ( ':' !== $data[1] )
return false;
if ( $strict ) {
$lastc = $data[ $length - 1 ];
if ( ';' !== $lastc && '}' !== $lastc )
return false;
} else {
$semicolon = strpos( $data, ';' );
$brace = strpos( $data, '}' );
// Either ; or } must exist.
if ( false === $semicolon && false === $brace )
return false;
// But neither must be in the first X characters.
if ( false !== $semicolon && $semicolon < 3 )
return false;
if ( false !== $brace && $brace < 4 )
return false;
}
$token = $data[0];
switch ( $token ) {
case 's' :
if ( $strict ) {
if ( '"' !== $data[ $length - 2 ] )
return false;
} elseif ( false === strpos( $data, '"' ) ) {
return false;
}
// or else fall through
case 'a' :
case 'O' :
return (bool) preg_match( "/^{$token}:[0-9]+:/s", $data );
case 'b' :
case 'i' :
case 'd' :
$end = $strict ? '$' : '';
return (bool) preg_match( "/^{$token}:[0-9.E-]+;$end/", $data );
}
return false;
}

941
functions-plugins.php Normal file
View File

@ -0,0 +1,941 @@
<?php
/**
* The filter/plugin API is located in this file, which allows for creating filters
* and hooking functions, and methods. The functions or methods will be run when
* the filter is called.
*
* Any of the syntaxes explained in the PHP documentation for the
* {@link https://www.php.net/manual/en/language.types.callable.php 'callback'}
* type are valid.
*
* This API is heavily inspired by the one I implemented in Zenphoto 1.3, which was heavily inspired by the one used in WordPress.
*
* @author Ozh
* @since 1.5
*/
/**
* This global var will collect filters with the following structure:
* $yourls_filters['hook']['array of priorities']['serialized function names']['array of ['array (functions, accepted_args, filter or action)]']
*
* Real life example :
* print_r($yourls_filters) :
* Array
* (
* [plugins_loaded] => Array
* (
* [10] => Array
* (
* [yourls_kses_init] => Array
* (
* [function] => yourls_kses_init
* [accepted_args] => 1
* [type] => action
* )
* [yourls_tzp_config] => Array
* (
* [function] => yourls_tzp_config
* [accepted_args] => 1
* [type] => action
* )
* )
* )
* [admin_menu] => Array
* (
* [10] => Array
* (
* [ozh_show_db] => Array
* (
* [function] => ozh_show_db
* [accepted_args] =>
* [type] => filter
* )
* )
* )
* )
*
* @var array $yourls_filters
*/
if ( !isset( $yourls_filters ) ) {
$yourls_filters = [];
}
/**
* This global var will collect 'done' actions with the following structure:
* $yourls_actions['hook'] => number of time this action was done
*
* @var array $yourls_actions
*/
if ( !isset( $yourls_actions ) ) {
$yourls_actions = [];
}
/**
* Registers a filtering function
*
* Typical use:
* yourls_add_filter('some_hook', 'function_handler_for_hook');
*
* @link https://docs.yourls.org/development/plugins.html
* @param string $hook the name of the YOURLS element to be filtered or YOURLS action to be triggered
* @param callable $function_name the name of the function that is to be called.
* @param int $priority optional. Used to specify the order in which the functions associated with a
* particular action are executed (default=10, lower=earlier execution, and functions
* with the same priority are executed in the order in which they were added to the
* filter)
* @param int $accepted_args optional. The number of arguments the function accept (default is the number
* provided).
* @param string $type
* @global array $yourls_filters Storage for all of the filters
* @return void
*/
function yourls_add_filter( $hook, $function_name, $priority = 10, $accepted_args = NULL, $type = 'filter' ) {
global $yourls_filters;
// At this point, we cannot check if the function exists, as it may well be defined later (which is OK)
$id = yourls_filter_unique_id($function_name);
$yourls_filters[ $hook ][ $priority ][ $id ] = [
'function' => $function_name,
'accepted_args' => $accepted_args,
'type' => $type,
];
}
/**
* Hooks a function on to a specific action.
*
* Actions are the hooks that YOURLS launches at specific points
* during execution, or when specific events occur. Plugins can specify that
* one or more of its PHP functions are executed at these points, using the
* Action API.
*
* Typical use:
* yourls_add_action('some_hook', 'function_handler_for_hook');
*
* @link https://docs.yourls.org/development/plugins.html
* @param string $hook The name of the action to which the $function_to_add is hooked.
* @param callable $function_name The name of the function you wish to be called.
* @param int $priority Optional. Used to specify the order in which the functions associated with a particular action
* are executed (default: 10). Lower numbers correspond with earlier execution, and functions
* with the same priority are executed in the order in which they were added to the action.
* @param int $accepted_args Optional. The number of arguments the function accept (default 1).
* @return void
*/
function yourls_add_action( $hook, $function_name, $priority = 10, $accepted_args = 1 ) {
yourls_add_filter( $hook, $function_name, $priority, $accepted_args, 'action' );
}
/**
* Build Unique ID for storage and retrieval.
*
* Simply using a function name is not enough, as several functions can have the same name when they are enclosed in classes.
* Possible ways to attach a function to a hook (filter or action):
* - strings:
* yourls_add_filter('my_hook_test', 'my_callback_function');
* yourls_add_filter('my_hook_test', 'My_Class::my_callback_function');
*
* - arrays:
* yourls_add_filter('my_hook_test', array('My_Class','my_callback_function'));
* yourls_add_filter('my_hook_test', array($class_instance, 'my_callback_function'));
*
* - objects:
* yourls_add_filter('my_hook_test', $class_instance_with_invoke_method);
* yourls_add_filter('my_hook_test', $my_callback_function);
*
* @link https://docs.yourls.org/development/hooks.html
* @param string|array|object $function The callable used in a filter or action.
* @return string unique ID for usage as array key
*/
function yourls_filter_unique_id($function) {
// If given a string (function name)
if ( is_string( $function ) ) {
return $function;
}
if ( is_object( $function ) ) {
// Closures are implemented as objects
$function = [ $function, '' ];
}
else {
$function = (array)$function;
}
// Object Class Calling
if ( is_object( $function[0] ) ) {
return spl_object_hash( $function[0] ).$function[1];
}
// Last case, static Calling : $function[0] is a string (Class Name) and $function[1] is a string (Method Name)
return $function[0].'::'.$function[1];
}
/**
* Performs a filtering operation on a value or an event.
*
* Typical use:
*
* 1) Modify a variable if a function is attached to hook 'yourls_hook'
* $yourls_var = "default value";
* $yourls_var = yourls_apply_filter( 'yourls_hook', $yourls_var );
*
* 2) Trigger functions is attached to event 'yourls_event'
* yourls_apply_filter( 'yourls_event' );
* (see yourls_do_action() )
*
* Returns a value which may have been modified by a filter.
*
* @global array $yourls_filters storage for all of the filters
* @param string $hook the name of the YOURLS element or action
* @param mixed $value the value of the element before filtering
* @param true|mixed $is_action true if the function is called by yourls_do_action() - otherwise may be the second parameter of an arbitrary number of parameters
* @return mixed
*/
function yourls_apply_filter( $hook, $value = '', $is_action = false ) {
global $yourls_filters;
$args = func_get_args();
// Do 'all' filters first. We check if $is_action to avoid calling `yourls_call_all_hooks()` twice
if ( !$is_action && isset($yourls_filters['all']) ) {
yourls_call_all_hooks('filter', $hook, $args);
}
// If we have no hook attached to that filter, just return unmodified $value
if ( !isset( $yourls_filters[ $hook ] ) ) {
return $value;
}
// Sort filters by priority
ksort( $yourls_filters[ $hook ] );
// Loops through each filter
reset( $yourls_filters[ $hook ] );
do {
foreach ( (array)current( $yourls_filters[ $hook ] ) as $the_ ) {
$_value = '';
if ( !is_null($the_[ 'function' ]) ) {
$args[ 1 ] = $value;
$count = $the_[ 'accepted_args' ];
if ( is_null( $count ) ) {
$_value = call_user_func_array( $the_[ 'function' ], array_slice( $args, 1 ) );
}
else {
$_value = call_user_func_array( $the_[ 'function' ], array_slice( $args, 1, (int)$count ) );
}
}
if ( $the_[ 'type' ] == 'filter' ) {
$value = $_value;
}
}
} while ( next( $yourls_filters[ $hook ] ) !== false );
// Return the value - this will be actually used only for filters, not for actions (see `yourls_do_action()`)
return $value;
}
/**
* Performs an action triggered by a YOURLS event.
*
* @param string $hook the name of the YOURLS action
* @param mixed $arg action arguments
* @return void
*/
function yourls_do_action( $hook, $arg = '' ) {
global $yourls_actions, $yourls_filters;
// Keep track of actions that are "done"
if ( !isset( $yourls_actions ) ) {
$yourls_actions = [];
}
if ( !isset( $yourls_actions[ $hook ] ) ) {
$yourls_actions[ $hook ] = 1;
}
else {
++$yourls_actions[ $hook ];
}
$args = [];
if ( is_array( $arg ) && 1 == count( $arg ) && isset( $arg[ 0 ] ) && is_object( $arg[ 0 ] ) ) { // array(&$this)
$args[] =& $arg[ 0 ];
}
else {
$args[] = $arg;
}
for ( $a = 2 ; $a < func_num_args() ; $a++ ) {
$args[] = func_get_arg( $a );
}
// Do 'all' actions first
if ( isset($yourls_filters['all']) ) {
yourls_call_all_hooks('action', $hook, $args);
}
yourls_apply_filter( $hook, $args, true );
}
/**
* Retrieve the number times an action is fired.
*
* @param string $hook Name of the action hook.
* @return int The number of times action hook <tt>$hook</tt> is fired
*/
function yourls_did_action( $hook ) {
global $yourls_actions;
return empty( $yourls_actions[ $hook ] ) ? 0 : $yourls_actions[ $hook ];
}
/**
* Execute the 'all' hook, with all of the arguments or parameters that were used for the hook
*
* Internal function used by yourls_do_action() and yourls_apply_filter() - not meant to be used from
* outside these functions.
* This is mostly a debugging function to understand the flow of events.
* See https://docs.yourls.org/development/debugging.html to learn how to use the 'all' hook
*
* @link https://docs.yourls.org/development/debugging.html
* @since 1.8.1
* @param string $type Either 'action' or 'filter'
* @param string $hook The hook name, eg 'plugins_loaded'
* @param mixed $args Variable-length argument lists that were passed to the action or filter
* @return void
*/
function yourls_call_all_hooks($type, $hook, ...$args) {
global $yourls_filters;
// Loops through each filter or action hooked with the 'all' hook
reset( $yourls_filters['all'] );
do {
foreach ( (array) current($yourls_filters['all']) as $the_ )
// Call the hooked function only if it's hooked to the current type of hook (eg 'filter' or 'action')
if ( $the_['type'] == $type && !is_null($the_['function']) ) {
call_user_func_array( $the_['function'], array($type, $hook, $args) );
/**
* Note that we don't return a value here, regardless of $type being an action (obviously) but also
* a filter. Indeed it would not make sense to actually "filter" and return values when we're
* feeding the same function every single hook in YOURLS, no matter their parameters.
*/
}
} while ( next($yourls_filters['all']) !== false );
}
/**
* Removes a function from a specified filter hook.
*
* This function removes a function attached to a specified filter hook. This
* method can be used to remove default functions attached to a specific filter
* hook and possibly replace them with a substitute.
*
* To remove a hook, the $function_to_remove and $priority arguments must match
* when the hook was added.
*
* @global array $yourls_filters storage for all of the filters
* @param string $hook The filter hook to which the function to be removed is hooked.
* @param callable $function_to_remove The name of the function which should be removed.
* @param int $priority optional. The priority of the function (default: 10).
* @return bool Whether the function was registered as a filter before it was removed.
*/
function yourls_remove_filter( $hook, $function_to_remove, $priority = 10 ) {
global $yourls_filters;
$function_to_remove = yourls_filter_unique_id($function_to_remove);
$remove = isset( $yourls_filters[ $hook ][ $priority ][ $function_to_remove ] );
if ( $remove === true ) {
unset ( $yourls_filters[ $hook ][ $priority ][ $function_to_remove ] );
if ( empty( $yourls_filters[ $hook ][ $priority ] ) ) {
unset( $yourls_filters[ $hook ] );
}
}
return $remove;
}
/**
* Removes a function from a specified action hook.
*
* @see yourls_remove_filter()
* @since 1.7.1
* @param string $hook The action hook to which the function to be removed is hooked.
* @param callable $function_to_remove The name of the function which should be removed.
* @param int $priority optional. The priority of the function (default: 10).
* @return bool Whether the function was registered as an action before it was removed.
*/
function yourls_remove_action( $hook, $function_to_remove, $priority = 10 ) {
return yourls_remove_filter( $hook, $function_to_remove, $priority );
}
/**
* Removes all functions from a specified action hook.
*
* @see yourls_remove_all_filters()
* @since 1.7.1
* @param string $hook The action to remove hooks from
* @param int|false $priority optional. The priority of the functions to remove
* @return bool true when it's finished
*/
function yourls_remove_all_actions( $hook, $priority = false ) {
return yourls_remove_all_filters( $hook, $priority );
}
/**
* Removes all functions from a specified filter hook.
*
* @since 1.7.1
* @param string $hook The filter to remove hooks from
* @param int|false $priority optional. The priority of the functions to remove
* @return bool true when it's finished
*/
function yourls_remove_all_filters( $hook, $priority = false ) {
global $yourls_filters;
if ( isset( $yourls_filters[ $hook ] ) ) {
if ( $priority === false ) {
unset( $yourls_filters[ $hook ] );
}
elseif ( isset( $yourls_filters[ $hook ][ $priority ] ) ) {
unset( $yourls_filters[ $hook ][ $priority ] );
}
}
return true;
}
/**
* Return filters for a specific hook.
*
* If hook has filters (or actions, see yourls_has_action()), this will return an array priorities => callbacks.
* See the structure of yourls_filters on top of this file for details.
*
* @since 1.8.3
* @param string $hook The hook to retrieve filters for
* @return array
*/
function yourls_get_filters($hook) {
global $yourls_filters;
return $yourls_filters[$hook] ?? array();
}
/**
* Return actions for a specific hook.
*
* @since 1.8.3
* @param string $hook The hook to retrieve actions for
* @return array
*/
function yourls_get_actions($hook) {
return yourls_get_filters($hook);
}
/**
* Check if any filter has been registered for a hook.
*
* @since 1.5
* @global array $yourls_filters storage for all of the filters
* @param string $hook The name of the filter hook.
* @param callable|false $function_to_check optional. If specified, return the priority of that function on this hook or false if not attached.
* @return int|bool Optionally returns the priority on that hook for the specified function.
*/
function yourls_has_filter( $hook, $function_to_check = false ) {
global $yourls_filters;
$has = !empty( $yourls_filters[ $hook ] );
if ( false === $function_to_check || false === $has ) {
return $has;
}
if ( !$idx = yourls_filter_unique_id($function_to_check) ) {
return false;
}
foreach ( array_keys( $yourls_filters[ $hook ] ) as $priority ) {
if ( isset( $yourls_filters[ $hook ][ $priority ][ $idx ] ) ) {
return $priority;
}
}
return false;
}
/**
* Check if any action has been registered for a hook.
*
* @since 1.5
* @param string $hook
* @param callable|false $function_to_check
* @return bool|int
*/
function yourls_has_action( $hook, $function_to_check = false ) {
return yourls_has_filter( $hook, $function_to_check );
}
/**
* Return number of active plugins
*
* @return int Number of activated plugins
*/
function yourls_has_active_plugins() {
return count( yourls_get_db()->get_plugins() );
}
/**
* List plugins in /user/plugins
*
* @return array Array of [/plugindir/plugin.php]=>array('Name'=>'Ozh', 'Title'=>'Hello', )
*/
function yourls_get_plugins() {
$plugins = (array)glob( YOURLS_PLUGINDIR.'/*/plugin.php' );
if ( is_array( $plugins ) ) {
foreach ( $plugins as $key => $plugin ) {
$plugins[ yourls_plugin_basename( $plugin ) ] = yourls_get_plugin_data( $plugin );
unset( $plugins[ $key ] );
}
}
return empty( $plugins ) ? [] : $plugins;
}
/**
* Check if a plugin is active
*
* @param string $plugin Physical path to plugin file
* @return bool
*/
function yourls_is_active_plugin( $plugin ) {
return yourls_has_active_plugins() > 0 ?
in_array( yourls_plugin_basename( $plugin ), yourls_get_db()->get_plugins() )
: false;
}
/**
* Parse a plugin header
*
* The plugin header has the following form:
* /*
* Plugin Name: <plugin name>
* Plugin URI: <plugin home page>
* Description: <plugin description>
* Version: <plugin version number>
* Author: <author name>
* Author URI: <author home page>
* * /
*
* Or in the form of a phpdoc block
* /**
* * Plugin Name: <plugin name>
* * Plugin URI: <plugin home page>
* * Description: <plugin description>
* * Version: <plugin version number>
* * Author: <author name>
* * Author URI: <author home page>
* * /
*
* @since 1.5
* @param string $file Physical path to plugin file
* @return array Array of 'Field'=>'Value' from plugin comment header lines of the form "Field: Value"
*/
function yourls_get_plugin_data( $file ) {
$fp = fopen( $file, 'r' ); // assuming $file is readable, since yourls_load_plugins() filters this
$data = fread( $fp, 8192 ); // get first 8kb
fclose( $fp );
// Capture all the header within first comment block
if ( !preg_match( '!.*?/\*(.*?)\*/!ms', $data, $matches ) ) {
return [];
}
// Capture each line with "Something: some text"
unset( $data );
$lines = preg_split( "[\n|\r]", $matches[ 1 ] );
unset( $matches );
$plugin_data = [];
foreach ( $lines as $line ) {
if ( !preg_match( '!(\s*)?\*?(\s*)?(.*?):\s+(.*)!', $line, $matches ) ) {
continue;
}
$plugin_data[ trim($matches[3]) ] = yourls_esc_html(trim($matches[4]));
}
return $plugin_data;
}
/**
* Include active plugins
*
* This function includes every 'YOURLS_PLUGINDIR/plugin_name/plugin.php' found in option 'active_plugins'
* It will return a diagnosis array with the following keys:
* (bool)'loaded' : true if plugin(s) loaded, false otherwise
* (string)'info' : extra information
*
* @since 1.5
* @return array Array('loaded' => bool, 'info' => string)
*/
function yourls_load_plugins() {
// Don't load plugins when installing or updating
if ( yourls_is_installing() OR yourls_is_upgrading() OR !yourls_is_installed() ) {
return [
'loaded' => false,
'info' => 'install/upgrade'
];
}
$active_plugins = (array)yourls_get_option( 'active_plugins' );
if ( empty( $active_plugins ) ) {
return [
'loaded' => false,
'info' => 'no active plugin'
];
}
$plugins = [];
foreach ( $active_plugins as $key => $plugin ) {
$file = YOURLS_PLUGINDIR . '/' . $plugin;
if ( yourls_is_a_plugin_file($file) && yourls_activate_plugin_sandbox( $file ) === true ) {
$plugins[] = $plugin;
unset( $active_plugins[ $key ] );
}
}
// Replace active plugin list with list of plugins we just activated
yourls_get_db()->set_plugins( $plugins );
$info = count( $plugins ).' activated';
// $active_plugins should be empty now, if not, a plugin could not be found, or is erroneous : remove it
$missing_count = count( $active_plugins );
if ( $missing_count > 0 ) {
yourls_update_option( 'active_plugins', $plugins );
$message = yourls_n( 'Could not find and deactivate plugin :', 'Could not find and deactivate plugins :', $missing_count );
$missing = '<strong>'.implode( '</strong>, <strong>', $active_plugins ).'</strong>';
yourls_add_notice( $message.' '.$missing );
$info .= ', '.$missing_count.' removed';
}
return [
'loaded' => true,
'info' => $info
];
}
/**
* Check if a file is a plugin file
*
* This doesn't check if the file is a valid PHP file, only that it's correctly named.
*
* @since 1.5
* @param string $file Full pathname to a file
* @return bool
*/
function yourls_is_a_plugin_file($file) {
return false === strpos( $file, '..' )
&& false === strpos( $file, './' )
&& 'plugin.php' === substr( $file, -10 )
&& is_readable( $file );
}
/**
* Activate a plugin
*
* @since 1.5
* @param string $plugin Plugin filename (full or relative to plugins directory)
* @return string|true string if error or true if success
*/
function yourls_activate_plugin( $plugin ) {
// validate file
$plugin = yourls_plugin_basename( $plugin );
$plugindir = yourls_sanitize_filename( YOURLS_PLUGINDIR );
if ( !yourls_is_a_plugin_file($plugindir . '/' . $plugin ) ) {
return yourls__( 'Not a valid plugin file' );
}
// check not activated already
$ydb = yourls_get_db();
if ( yourls_is_active_plugin( $plugin ) ) {
return yourls__( 'Plugin already activated' );
}
// attempt activation.
$attempt = yourls_activate_plugin_sandbox( $plugindir.'/'.$plugin );
if( $attempt !== true ) {
return yourls_s( 'Plugin generated unexpected output. Error was: <br/><pre>%s</pre>', $attempt );
}
// so far, so good: update active plugin list
$ydb->add_plugin( $plugin );
yourls_update_option( 'active_plugins', $ydb->get_plugins() );
yourls_do_action( 'activated_plugin', $plugin );
yourls_do_action( 'activated_'.$plugin );
return true;
}
/**
* Plugin activation sandbox
*
* @since 1.8.3
* @param string $pluginfile Plugin filename (full path)
* @return string|true string if error or true if success
*/
function yourls_activate_plugin_sandbox( $pluginfile ) {
try {
include_once $pluginfile;
return true;
} catch ( \Throwable $e ) {
return $e->getMessage();
}
}
/**
* Deactivate a plugin
*
* @since 1.5
* @param string $plugin Plugin filename (full relative to plugins directory)
* @return string|true string if error or true if success
*/
function yourls_deactivate_plugin( $plugin ) {
$plugin = yourls_plugin_basename( $plugin );
// Check plugin is active
if ( !yourls_is_active_plugin( $plugin ) ) {
return yourls__( 'Plugin not active' );
}
// Check if we have an uninstall file - load if so
$uninst_file = YOURLS_PLUGINDIR . '/' . dirname($plugin) . '/uninstall.php';
if ( file_exists($uninst_file) ) {
define('YOURLS_UNINSTALL_PLUGIN', true);
$attempt = yourls_activate_plugin_sandbox( $uninst_file );
if( $attempt !== true ) {
return yourls_s( 'Plugin generated unexpected output. Error was: <br/><pre>%s</pre>', $attempt );
}
}
// Deactivate the plugin
$ydb = yourls_get_db();
$plugins = $ydb->get_plugins();
$key = array_search( $plugin, $plugins );
if ( $key !== false ) {
array_splice( $plugins, $key, 1 );
}
$ydb->set_plugins( $plugins );
yourls_update_option( 'active_plugins', $plugins );
yourls_do_action( 'deactivated_plugin', $plugin );
yourls_do_action( 'deactivated_'.$plugin );
return true;
}
/**
* Return the path of a plugin file, relative to the plugins directory
*
* @since 1.5
* @param string $file
* @return string
*/
function yourls_plugin_basename( $file ) {
return trim( str_replace( yourls_sanitize_filename( YOURLS_PLUGINDIR ), '', yourls_sanitize_filename( $file ) ), '/' );
}
/**
* Return the URL of the directory a plugin
*
* @since 1.5
* @param string $file
* @return string
*/
function yourls_plugin_url( $file ) {
$url = YOURLS_PLUGINURL.'/'.yourls_plugin_basename( $file );
if ( yourls_is_ssl() or yourls_needs_ssl() ) {
$url = str_replace( 'http://', 'https://', $url );
}
return (string)yourls_apply_filter( 'plugin_url', $url, $file );
}
/**
* Build list of links to plugin admin pages, if any
*
* @since 1.5
* @return array Array of arrays of URL and anchor of plugin admin pages, or empty array if no plugin page
*/
function yourls_list_plugin_admin_pages() {
$plugin_links = [];
foreach ( yourls_get_db()->get_plugin_pages() as $plugin => $page ) {
$plugin_links[ $plugin ] = [
'url' => yourls_admin_url( 'plugins.php?page='.$page[ 'slug' ] ),
'anchor' => $page[ 'title' ],
];
}
return $plugin_links;
}
/**
* Register a plugin administration page
*
* @since 1.5
* @param string $slug
* @param string $title
* @param callable $function
* @return void
*/
function yourls_register_plugin_page( $slug, $title, $function ) {
yourls_get_db()->add_plugin_page( $slug, $title, $function );
}
/**
* Handle plugin administration page
*
* @since 1.5
* @param string $plugin_page
* @return void
*/
function yourls_plugin_admin_page( $plugin_page ) {
// Check the plugin page is actually registered
$pages = yourls_get_db()->get_plugin_pages();
if ( !isset( $pages[ $plugin_page ] ) ) {
yourls_die( yourls__( 'This page does not exist. Maybe a plugin you thought was activated is inactive?' ), yourls__( 'Invalid link' ) );
}
// Check the plugin page function is actually callable
$page_function = $pages[ $plugin_page ][ 'function' ];
if (!is_callable($page_function)) {
yourls_die( yourls__( 'This page cannot be displayed because the displaying function is not callable.' ), yourls__( 'Invalid code' ) );
}
// Draw the page itself
yourls_do_action( 'load-'.$plugin_page );
yourls_html_head( 'plugin_page_'.$plugin_page, $pages[ $plugin_page ][ 'title' ] );
yourls_html_logo();
yourls_html_menu();
$page_function( );
yourls_html_footer();
}
/**
* Callback function: Sort plugins
*
* @link http://php.net/uasort
* @codeCoverageIgnore
*
* @since 1.5
* @param array $plugin_a
* @param array $plugin_b
* @return int 0, 1 or -1, see uasort()
*/
function yourls_plugins_sort_callback( $plugin_a, $plugin_b ) {
$orderby = yourls_apply_filter( 'plugins_sort_callback', 'Plugin Name' );
$order = yourls_apply_filter( 'plugins_sort_callback', 'ASC' );
$a = isset( $plugin_a[ $orderby ] ) ? $plugin_a[ $orderby ] : '';
$b = isset( $plugin_b[ $orderby ] ) ? $plugin_b[ $orderby ] : '';
if ( $a == $b ) {
return 0;
}
if ( 'DESC' == $order ) {
return ( $a < $b ) ? 1 : -1;
}
else {
return ( $a < $b ) ? -1 : 1;
}
}
/**
* Shutdown function, runs just before PHP shuts down execution. Stolen from WP
*
* This function is automatically tied to the script execution end at startup time, see
* var $actions->register_shutdown in includes/Config/Init.php
*
* You can use this function to fire one or several actions when the PHP execution ends.
* Example of use:
* yourls_add_action('shutdown', 'my_plugin_action_this');
* yourls_add_action('shutdown', 'my_plugin_action_that');
* // functions my_plugin_action_this() and my_plugin_action_that() will be triggered
* // after YOURLS is completely executed
*
* @codeCoverageIgnore
* @since 1.5.1
* @return void
*/
function yourls_shutdown() {
yourls_do_action( 'shutdown' );
}
/**
* Returns true.
*
* Useful for returning true to filters easily.
*
* @since 1.7.1
* @return bool True.
*/
function yourls_return_true() {
return true;
}
/**
* Returns false.
*
* Useful for returning false to filters easily.
*
* @since 1.7.1
* @return bool False.
*/
function yourls_return_false() {
return false;
}
/**
* Returns 0.
*
* Useful for returning 0 to filters easily.
*
* @since 1.7.1
* @return int 0.
*/
function yourls_return_zero() {
return 0;
}
/**
* Returns an empty array.
*
* Useful for returning an empty array to filters easily.
*
* @since 1.7.1
* @return array Empty array.
*/
function yourls_return_empty_array() {
return [];
}
/**
* Returns null.
*
* Useful for returning null to filters easily.
*
* @since 1.7.1
* @return null Null value.
*/
function yourls_return_null() {
return null;
}
/**
* Returns an empty string.
*
* Useful for returning an empty string to filters easily.
*
* @since 1.7.1
* @return string Empty string.
*/
function yourls_return_empty_string() {
return '';
}

636
functions-shorturls.php Normal file
View File

@ -0,0 +1,636 @@
<?php
/*
* Functions relative to short URLs: adding, editing, etc
* (either proper short URLs ("http://sho.rt/abc") or "keywords" (the "abc" part)
*/
/**
* Add a new link in the DB, either with custom keyword, or find one
*
* The return array will contain at least the following keys:
* status: string, 'success' or 'fail'
* message: string, a descriptive localized message of what happened in any case
* code: string, a short descriptivish and untranslated message describing what happened
* errorCode: string, a HTTP status code
* statusCode: string, a HTTP status code
* Depending on the operation, it will contain any of the following keys:
* url: array, the short URL creation information, with keys: 'keyword', 'url', 'title', 'date', 'ip', 'clicks'
* title: string, the URL title
* shorturl: string, the proper short URL in full (eg 'http://sho.rt/abc')
* html: string, the HTML part used by the ajax to update the page display if any
*
* For compatibility with early consumers and third parties, when people asked for various data and data formats
* before the internal API was really structured, the return array now collects several redundant information.
*
* @param string $url URL to shorten
* @param string $keyword optional "keyword"
* @param string $title option title
* @return array array with error/success state and short URL information
*/
function yourls_add_new_link( $url, $keyword = '', $title = '' ) {
// Allow plugins to short-circuit the whole function
$pre = yourls_apply_filter( 'shunt_add_new_link', false, $url, $keyword, $title );
if ( false !== $pre ) {
return $pre;
}
/**
* The result array.
*/
$return = [
// Always present :
'status' => '',
'code' => '',
'message' => '',
'errorCode' => '',
'statusCode' => '',
];
// Sanitize URL
$url = yourls_sanitize_url( $url );
if ( !$url || $url == 'http://' || $url == 'https://' ) {
$return['status'] = 'fail';
$return['code'] = 'error:nourl';
$return['message'] = yourls__( 'Missing or malformed URL' );
$return['errorCode'] = $return['statusCode'] = '400'; // 400 Bad Request
return yourls_apply_filter( 'add_new_link_fail_nourl', $return, $url, $keyword, $title );
}
// Prevent DB flood
$ip = yourls_get_IP();
yourls_check_IP_flood( $ip );
// Prevent internal redirection loops: cannot shorten a shortened URL
if (yourls_is_shorturl($url)) {
$return['status'] = 'fail';
$return['code'] = 'error:noloop';
$return['message'] = yourls__( 'URL is a short URL' );
$return['errorCode'] = $return['statusCode'] = '400'; // 400 Bad Request
return yourls_apply_filter( 'add_new_link_fail_noloop', $return, $url, $keyword, $title );
}
yourls_do_action( 'pre_add_new_link', $url, $keyword, $title );
// Check if URL was already stored and we don't accept duplicates
if ( !yourls_allow_duplicate_longurls() && ($url_exists = yourls_long_url_exists( $url )) ) {
yourls_do_action( 'add_new_link_already_stored', $url, $keyword, $title );
$return['status'] = 'fail';
$return['code'] = 'error:url';
$return['url'] = array( 'keyword' => $url_exists->keyword, 'url' => $url, 'title' => $url_exists->title, 'date' => $url_exists->timestamp, 'ip' => $url_exists->ip, 'clicks' => $url_exists->clicks );
$return['message'] = /* //translators: eg "http://someurl/ already exists (short URL: sho.rt/abc)" */ yourls_s('%s already exists in database (short URL: %s)',
yourls_trim_long_string($url), preg_replace('!https?://!', '', yourls_get_yourls_site()) . '/'. $url_exists->keyword );
$return['title'] = $url_exists->title;
$return['shorturl'] = yourls_link($url_exists->keyword);
$return['errorCode'] = $return['statusCode'] = '400'; // 400 Bad Request
return yourls_apply_filter( 'add_new_link_already_stored_filter', $return, $url, $keyword, $title );
}
// Sanitize provided title, or fetch one
if( isset( $title ) && !empty( $title ) ) {
$title = yourls_sanitize_title( $title );
} else {
$title = yourls_get_remote_title( $url );
}
$title = yourls_apply_filter( 'add_new_title', $title, $url, $keyword );
// Custom keyword provided : sanitize and make sure it's free
if ($keyword) {
yourls_do_action( 'add_new_link_custom_keyword', $url, $keyword, $title );
$keyword = yourls_sanitize_keyword( $keyword, true );
$keyword = yourls_apply_filter( 'custom_keyword', $keyword, $url, $title );
if ( !yourls_keyword_is_free( $keyword ) ) {
// This shorturl either reserved or taken already
$return['status'] = 'fail';
$return['code'] = 'error:keyword';
$return['message'] = yourls_s( 'Short URL %s already exists in database or is reserved', $keyword );
$return['errorCode'] = $return['statusCode'] = '400'; // 400 Bad Request
return yourls_apply_filter( 'add_new_link_keyword_exists', $return, $url, $keyword, $title );
}
// Create random keyword
} else {
yourls_do_action( 'add_new_link_create_keyword', $url, $keyword, $title );
$id = yourls_get_next_decimal();
do {
$keyword = yourls_int2string( $id );
$keyword = yourls_apply_filter( 'random_keyword', $keyword, $url, $title );
$id++;
} while ( !yourls_keyword_is_free($keyword) );
yourls_update_next_decimal($id);
}
// We should be all set now. Store the short URL !
$timestamp = date( 'Y-m-d H:i:s' );
try {
if (yourls_insert_link_in_db( $url, $keyword, $title )){
// everything ok, populate needed vars
$return['url'] = array('keyword' => $keyword, 'url' => $url, 'title' => $title, 'date' => $timestamp, 'ip' => $ip );
$return['status'] = 'success';
$return['message'] = /* //translators: eg "http://someurl/ added to DB" */ yourls_s( '%s added to database', yourls_trim_long_string( $url ) );
$return['title'] = $title;
$return['html'] = yourls_table_add_row( $keyword, $url, $title, $ip, 0, time() );
$return['shorturl'] = yourls_link($keyword);
$return['statusCode'] = 200; // 200 OK
} else {
// unknown database error, couldn't store result
$return['status'] = 'fail';
$return['code'] = 'error:db';
$return['message'] = yourls_s( 'Error saving url to database' );
$return['errorCode'] = $return['statusCode'] = '500'; // 500 Internal Server Error
}
} catch (Exception $e) {
// Keyword supposed to be free but the INSERT caused an exception: most likely we're facing a
// concurrency problem. See Issue 2538.
$return['status'] = 'fail';
$return['code'] = 'error:concurrency';
$return['message'] = $e->getMessage();
$return['errorCode'] = $return['statusCode'] = '503'; // 503 Service Unavailable
}
yourls_do_action( 'post_add_new_link', $url, $keyword, $title, $return );
return yourls_apply_filter( 'add_new_link', $return, $url, $keyword, $title );
}
/**
* Determine the allowed character set in short URLs
*
* @return string Acceptable charset for short URLS keywords
*/
function yourls_get_shorturl_charset() {
if ( defined( 'YOURLS_URL_CONVERT' ) && in_array( YOURLS_URL_CONVERT, [ 62, 64 ] ) ) {
$charset = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
}
else {
// defined to 36, or wrongly defined
$charset = '0123456789abcdefghijklmnopqrstuvwxyz';
}
return yourls_apply_filter( 'get_shorturl_charset', $charset );
}
/**
* Is a URL a short URL? Accept either 'http://sho.rt/abc' or 'abc'
*
* @param string $shorturl short URL
* @return bool true if registered short URL, false otherwise
*/
function yourls_is_shorturl( $shorturl ) {
// TODO: make sure this function evolves with the feature set.
$is_short = false;
// Is $shorturl a URL (http://sho.rt/abc) or a keyword (abc) ?
if( yourls_get_protocol( $shorturl ) ) {
$keyword = yourls_get_relative_url( $shorturl );
} else {
$keyword = $shorturl;
}
// Check if it's a valid && used keyword
if( $keyword && $keyword == yourls_sanitize_keyword( $keyword ) && yourls_keyword_is_taken( $keyword ) ) {
$is_short = true;
}
return yourls_apply_filter( 'is_shorturl', $is_short, $shorturl );
}
/**
* Check to see if a given keyword is reserved (ie reserved URL or an existing page). Returns bool
*
* @param string $keyword Short URL keyword
* @return bool True if keyword reserved, false if free to be used
*/
function yourls_keyword_is_reserved( $keyword ) {
global $yourls_reserved_URL;
$keyword = yourls_sanitize_keyword( $keyword );
$reserved = false;
if ( in_array( $keyword, $yourls_reserved_URL)
or yourls_is_page($keyword)
or is_dir( YOURLS_ABSPATH ."/$keyword" )
)
$reserved = true;
return yourls_apply_filter( 'keyword_is_reserved', $reserved, $keyword );
}
/**
* Delete a link in the DB
*
* @param string $keyword Short URL keyword
* @return int Number of links deleted
*/
function yourls_delete_link_by_keyword( $keyword ) {
// Allow plugins to short-circuit the whole function
$pre = yourls_apply_filter( 'shunt_delete_link_by_keyword', null, $keyword );
if ( null !== $pre ) {
return $pre;
}
$table = YOURLS_DB_TABLE_URL;
$keyword = yourls_sanitize_keyword($keyword);
$delete = yourls_get_db()->fetchAffected("DELETE FROM `$table` WHERE `keyword` = :keyword", array('keyword' => $keyword));
yourls_do_action( 'delete_link', $keyword, $delete );
return $delete;
}
/**
* SQL query to insert a new link in the DB. Returns boolean for success or failure of the inserting
*
* @param string $url
* @param string $keyword
* @param string $title
* @return bool true if insert succeeded, false if failed
*/
function yourls_insert_link_in_db($url, $keyword, $title = '' ) {
$url = yourls_sanitize_url($url);
$keyword = yourls_sanitize_keyword($keyword);
$title = yourls_sanitize_title($title);
$timestamp = date('Y-m-d H:i:s');
$ip = yourls_get_IP();
$table = YOURLS_DB_TABLE_URL;
$binds = array(
'keyword' => $keyword,
'url' => $url,
'title' => $title,
'timestamp' => $timestamp,
'ip' => $ip,
);
$insert = yourls_get_db()->fetchAffected("INSERT INTO `$table` (`keyword`, `url`, `title`, `timestamp`, `ip`, `clicks`) VALUES(:keyword, :url, :title, :timestamp, :ip, 0);", $binds);
yourls_do_action( 'insert_link', (bool)$insert, $url, $keyword, $title, $timestamp, $ip );
return (bool)$insert;
}
/**
* Check if a long URL already exists in the DB. Return NULL (doesn't exist) or an object with URL informations.
*
* This function supersedes function yourls_url_exists(), deprecated in 1.7.10, with a better naming.
*
* @since 1.7.10
* @param string $url URL to check if already shortened
* @return mixed NULL if does not already exist in DB, or object with URL information as properties (eg keyword, url, title, ...)
*/
function yourls_long_url_exists( $url ) {
// Allow plugins to short-circuit the whole function
$pre = yourls_apply_filter( 'shunt_url_exists', false, $url );
if ( false !== $pre ) {
return $pre;
}
$table = YOURLS_DB_TABLE_URL;
$url = yourls_sanitize_url($url);
$url_exists = yourls_get_db()->fetchObject("SELECT * FROM `$table` WHERE `url` = :url", array('url'=>$url));
if ($url_exists === false) {
$url_exists = NULL;
}
return yourls_apply_filter( 'url_exists', $url_exists, $url );
}
/**
* Edit a link
*
* @param string $url
* @param string $keyword
* @param string $newkeyword
* @param string $title
* @return array Result of the edit and link information if successful
*/
function yourls_edit_link($url, $keyword, $newkeyword='', $title='' ) {
// Allow plugins to short-circuit the whole function
$pre = yourls_apply_filter( 'shunt_edit_link', null, $keyword, $url, $keyword, $newkeyword, $title );
if ( null !== $pre )
return $pre;
$ydb = yourls_get_db();
$table = YOURLS_DB_TABLE_URL;
$url = yourls_sanitize_url($url);
$keyword = yourls_sanitize_keyword($keyword);
$title = yourls_sanitize_title($title);
$newkeyword = yourls_sanitize_keyword($newkeyword, true);
if(!$url OR !$newkeyword) {
$return['status'] = 'fail';
$return['message'] = yourls__( 'Long URL or Short URL cannot be blank' );
return yourls_apply_filter( 'edit_link', $return, $url, $keyword, $newkeyword, $title );
}
$old_url = $ydb->fetchValue("SELECT `url` FROM `$table` WHERE `keyword` = :keyword", array('keyword' => $keyword));
// Check if new URL is not here already
if ( $old_url != $url && !yourls_allow_duplicate_longurls() ) {
$new_url_already_there = intval($ydb->fetchValue("SELECT COUNT(keyword) FROM `$table` WHERE `url` = :url;", array('url' => $url)));
} else {
$new_url_already_there = false;
}
// Check if the new keyword is not here already
if ( $newkeyword != $keyword ) {
$keyword_is_ok = yourls_keyword_is_free( $newkeyword );
} else {
$keyword_is_ok = true;
}
yourls_do_action( 'pre_edit_link', $url, $keyword, $newkeyword, $new_url_already_there, $keyword_is_ok );
// All clear, update
if ( ( !$new_url_already_there || yourls_allow_duplicate_longurls() ) && $keyword_is_ok ) {
$sql = "UPDATE `$table` SET `url` = :url, `keyword` = :newkeyword, `title` = :title WHERE `keyword` = :keyword";
$binds = array('url' => $url, 'newkeyword' => $newkeyword, 'title' => $title, 'keyword' => $keyword);
$update_url = $ydb->fetchAffected($sql, $binds);
if( $update_url ) {
$return['url'] = array( 'keyword' => $newkeyword,
'shorturl' => yourls_link($newkeyword),
'url' => yourls_esc_url($url),
'display_url' => yourls_esc_html(yourls_trim_long_string($url)),
'title' => yourls_esc_attr($title),
'display_title' => yourls_esc_html(yourls_trim_long_string( $title ))
);
$return['status'] = 'success';
$return['message'] = yourls__( 'Link updated in database' );
} else {
$return['status'] = 'fail';
$return['message'] = /* //translators: "Error updating http://someurl/ (Shorturl: http://sho.rt/blah)" */ yourls_s( 'Error updating %s (Short URL: %s)', yourls_esc_html(yourls_trim_long_string($url)), $keyword ) ;
}
// Nope
} else {
$return['status'] = 'fail';
$return['message'] = yourls__( 'URL or keyword already exists in database' );
}
return yourls_apply_filter( 'edit_link', $return, $url, $keyword, $newkeyword, $title, $new_url_already_there, $keyword_is_ok );
}
/**
* Update a title link (no checks for duplicates etc..)
*
* @param string $keyword
* @param string $title
* @return int number of rows updated
*/
function yourls_edit_link_title( $keyword, $title ) {
// Allow plugins to short-circuit the whole function
$pre = yourls_apply_filter( 'shunt_edit_link_title', null, $keyword, $title );
if ( null !== $pre ) {
return $pre;
}
$keyword = yourls_sanitize_keyword( $keyword );
$title = yourls_sanitize_title( $title );
$table = YOURLS_DB_TABLE_URL;
$update = yourls_get_db()->fetchAffected("UPDATE `$table` SET `title` = :title WHERE `keyword` = :keyword;", array('title' => $title, 'keyword' => $keyword));
return $update;
}
/**
* Check if keyword id is free (ie not already taken, and not reserved). Return bool.
*
* @param string $keyword short URL keyword
* @return bool true if keyword is taken (ie there is a short URL for it), false otherwise
*/
function yourls_keyword_is_free( $keyword ) {
$free = true;
if ( yourls_keyword_is_reserved( $keyword ) or yourls_keyword_is_taken( $keyword, false ) ) {
$free = false;
}
return yourls_apply_filter( 'keyword_is_free', $free, $keyword );
}
/**
* Check if a keyword matches a "page"
*
* @see https://docs.yourls.org/guide/extend/pages.html
* @since 1.7.10
* @param string $keyword Short URL $keyword
* @return bool true if is page, false otherwise
*/
function yourls_is_page($keyword) {
return yourls_apply_filter( 'is_page', file_exists( YOURLS_PAGEDIR . "/$keyword.php" ) );
}
/**
* Check if a keyword is taken (ie there is already a short URL with this id). Return bool.
*
*/
/**
* Check if a keyword is taken (ie there is already a short URL with this id). Return bool.
*
* @param string $keyword short URL keyword
* @param bool $use_cache optional, default true: do we want to use what is cached in memory, if any, or force a new SQL query
* @return bool true if keyword is taken (ie there is a short URL for it), false otherwise
*/
function yourls_keyword_is_taken( $keyword, $use_cache = true ) {
// Allow plugins to short-circuit the whole function
$pre = yourls_apply_filter( 'shunt_keyword_is_taken', false, $keyword );
if ( false !== $pre ) {
return $pre;
}
$taken = false;
// To check if a keyword is already associated with a short URL, we fetch all info matching that keyword. This
// will save a query in case of a redirection in yourls-go.php because info will be cached
if ( yourls_get_keyword_infos($keyword, $use_cache) ) {
$taken = true;
}
return yourls_apply_filter( 'keyword_is_taken', $taken, $keyword );
}
/**
* Return array of all information associated with keyword. Returns false if keyword not found. Set optional $use_cache to false to force fetching from DB
*
* Sincere apologies to native English speakers, we are aware that the plural of 'info' is actually 'info', not 'infos'.
* This function yourls_get_keyword_infos() returns all information, while function yourls_get_keyword_info() (no 's') return only
* one information. Blame YOURLS contributors whose mother tongue is not English :)
*
* @since 1.4
* @param string $keyword Short URL keyword
* @param bool $use_cache Default true, set to false to force fetching from DB
* @return false|object false if not found, object with URL properties if found
*/
function yourls_get_keyword_infos( $keyword, $use_cache = true ) {
$ydb = yourls_get_db();
$keyword = yourls_sanitize_keyword( $keyword );
yourls_do_action( 'pre_get_keyword', $keyword, $use_cache );
if( $ydb->has_infos($keyword) && $use_cache === true ) {
return yourls_apply_filter( 'get_keyword_infos', $ydb->get_infos($keyword), $keyword );
}
yourls_do_action( 'get_keyword_not_cached', $keyword );
$table = YOURLS_DB_TABLE_URL;
$infos = $ydb->fetchObject("SELECT * FROM `$table` WHERE `keyword` = :keyword", array('keyword' => $keyword));
if( $infos ) {
$infos = (array)$infos;
$ydb->set_infos($keyword, $infos);
} else {
// is NULL if not found
$infos = false;
$ydb->set_infos($keyword, false);
}
return yourls_apply_filter( 'get_keyword_infos', $infos, $keyword );
}
/**
* Return information associated with a keyword (eg clicks, URL, title...). Optional $notfound = string default message if nothing found
*
* @param string $keyword Short URL keyword
* @param string $field Field to return (eg 'url', 'title', 'ip', 'clicks', 'timestamp', 'keyword')
* @param false|string $notfound Optional string to return if keyword not found
* @return mixed|string
*/
function yourls_get_keyword_info($keyword, $field, $notfound = false ) {
// Allow plugins to short-circuit the whole function
$pre = yourls_apply_filter( 'shunt_get_keyword_info', false, $keyword, $field, $notfound );
if ( false !== $pre )
return $pre;
$keyword = yourls_sanitize_keyword( $keyword );
$infos = yourls_get_keyword_infos( $keyword );
$return = $notfound;
if ( isset( $infos[ $field ] ) && $infos[ $field ] !== false )
$return = $infos[ $field ];
return yourls_apply_filter( 'get_keyword_info', $return, $keyword, $field, $notfound );
}
/**
* Return title associated with keyword. Optional $notfound = string default message if nothing found
*
* @param string $keyword Short URL keyword
* @param false|string $notfound Optional string to return if keyword not found
* @return mixed|string
*/
function yourls_get_keyword_title( $keyword, $notfound = false ) {
return yourls_get_keyword_info( $keyword, 'title', $notfound );
}
/**
* Return long URL associated with keyword. Optional $notfound = string default message if nothing found
*
* @param string $keyword Short URL keyword
* @param false|string $notfound Optional string to return if keyword not found
* @return mixed|string
*/
function yourls_get_keyword_longurl( $keyword, $notfound = false ) {
return yourls_get_keyword_info( $keyword, 'url', $notfound );
}
/**
* Return number of clicks on a keyword. Optional $notfound = string default message if nothing found
*
* @param string $keyword Short URL keyword
* @param false|string $notfound Optional string to return if keyword not found
* @return mixed|string
*/
function yourls_get_keyword_clicks( $keyword, $notfound = false ) {
return yourls_get_keyword_info( $keyword, 'clicks', $notfound );
}
/**
* Return IP that added a keyword. Optional $notfound = string default message if nothing found
*
* @param string $keyword Short URL keyword
* @param false|string $notfound Optional string to return if keyword not found
* @return mixed|string
*/
function yourls_get_keyword_IP( $keyword, $notfound = false ) {
return yourls_get_keyword_info( $keyword, 'ip', $notfound );
}
/**
* Return timestamp associated with a keyword. Optional $notfound = string default message if nothing found
*
* @param string $keyword Short URL keyword
* @param false|string $notfound Optional string to return if keyword not found
* @return mixed|string
*/
function yourls_get_keyword_timestamp( $keyword, $notfound = false ) {
return yourls_get_keyword_info( $keyword, 'timestamp', $notfound );
}
/**
* Return array of stats for a given keyword
*
* This function supersedes function yourls_get_link_stats(), deprecated in 1.7.10, with a better naming.
*
* @since 1.7.10
* @param string $shorturl short URL keyword
* @return array stats
*/
function yourls_get_keyword_stats( $shorturl ) {
$table_url = YOURLS_DB_TABLE_URL;
$shorturl = yourls_sanitize_keyword( $shorturl );
$res = yourls_get_db()->fetchObject("SELECT * FROM `$table_url` WHERE `keyword` = :keyword", array('keyword' => $shorturl));
if( !$res ) {
// non existent link
$return = array(
'statusCode' => 404,
'message' => 'Error: short URL not found',
);
} else {
$return = array(
'statusCode' => 200,
'message' => 'success',
'link' => array(
'shorturl' => yourls_link($res->keyword),
'url' => $res->url,
'title' => $res->title,
'timestamp'=> $res->timestamp,
'ip' => $res->ip,
'clicks' => $res->clicks,
)
);
}
return yourls_apply_filter( 'get_link_stats', $return, $shorturl );
}
/**
* Return array of keywords that redirect to the submitted long URL
*
* @since 1.7
* @param string $longurl long url
* @param string $order Optional SORT order (can be 'ASC' or 'DESC')
* @return array array of keywords
*/
function yourls_get_longurl_keywords( $longurl, $order = 'ASC' ) {
$longurl = yourls_sanitize_url($longurl);
$table = YOURLS_DB_TABLE_URL;
$sql = "SELECT `keyword` FROM `$table` WHERE `url` = :url";
if (in_array($order, array('ASC','DESC'))) {
$sql .= " ORDER BY `keyword` ".$order;
}
return yourls_apply_filter( 'get_longurl_keywords', yourls_get_db()->fetchCol($sql, array('url'=>$longurl)), $longurl );
}

453
functions-upgrade.php Normal file
View File

@ -0,0 +1,453 @@
<?php
/**
* Upgrade YOURLS and DB schema
*
* Note to devs : prefer update function names using the SQL version, eg yourls_update_to_506(),
* rather than using the YOURLS version number, eg yourls_update_to_18(). This is to allow having
* multiple SQL update during the dev cycle of the same Y0URLS version
*
* @param string|int $step
* @param string $oldver
* @param string $newver
* @param string|int $oldsql
* @param string|int $newsql
* @return void
*/
function yourls_upgrade($step, $oldver, $newver, $oldsql, $newsql ) {
/**
* Sanitize input. Two notes :
* - they should already be sanitized in the caller, eg admin/upgrade.php
* (but hey, let's make sure)
* - some vars may not be used at the moment
* (and this is ok, they are here in case a future upgrade procedure needs them)
*/
$step = intval($step);
$oldsql = intval($oldsql);
$newsql = intval($newsql);
$oldver = yourls_sanitize_version($oldver);
$newver = yourls_sanitize_version($newver);
yourls_maintenance_mode(true);
// special case for 1.3: the upgrade is a multi step procedure
if( $oldsql == 100 ) {
yourls_upgrade_to_14( $step );
}
// other upgrades which are done in a single pass
switch( $step ) {
case 1:
case 2:
if( $oldsql < 210 )
yourls_upgrade_to_141();
if( $oldsql < 220 )
yourls_upgrade_to_143();
if( $oldsql < 250 )
yourls_upgrade_to_15();
if( $oldsql < 482 )
yourls_upgrade_482(); // that was somewhere 1.5 and 1.5.1 ...
if( $oldsql < 506 ) {
/**
* 505 was the botched update with the wrong collation, see #2766
* 506 is the updated collation.
* We want :
* people on 505 to update to 506
* people before 505 to update to the FIXED complete upgrade
*/
if( $oldsql == 505 ) {
yourls_upgrade_505_to_506();
} else {
yourls_upgrade_to_506();
}
}
yourls_redirect_javascript( yourls_admin_url( "upgrade.php?step=3" ) );
break;
case 3:
// Update options to reflect latest version
yourls_update_option( 'version', YOURLS_VERSION );
yourls_update_option( 'db_version', YOURLS_DB_VERSION );
yourls_maintenance_mode(false);
break;
}
}
/************************** 1.6 -> 1.8 **************************/
/**
* Update to 506, just the fix for people who had updated to master on 1.7.10
*
*/
function yourls_upgrade_505_to_506() {
echo "<p>Updating DB. Please wait...</p>";
// Fix collation which was wrongly set at first to utf8mb4_unicode_ci
$query = sprintf('ALTER TABLE `%s` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;', YOURLS_DB_TABLE_URL);
try {
yourls_get_db()->perform($query);
} catch (\Exception $e) {
echo "<p class='error'>Unable to update the DB.</p>";
echo "<p>Could not change collation. You will have to fix things manually :(. The error was
<pre>";
echo $e->getMessage();
echo "/n</pre>";
die();
}
echo "<p class='success'>OK!</p>";
}
/**
* Update to 506
*
*/
function yourls_upgrade_to_506() {
$ydb = yourls_get_db();
$error_msg = [];
echo "<p>Updating DB. Please wait...</p>";
$queries = array(
'database charset' => sprintf('ALTER DATABASE `%s` CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;', YOURLS_DB_NAME),
'options charset' => sprintf('ALTER TABLE `%s` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;', YOURLS_DB_TABLE_OPTIONS),
'short URL varchar' => sprintf("ALTER TABLE `%s` CHANGE `keyword` `keyword` VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '';", YOURLS_DB_TABLE_URL),
'short URL type url' => sprintf("ALTER TABLE `%s` CHANGE `url` `url` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL;", YOURLS_DB_TABLE_URL),
'short URL type title' => sprintf("ALTER TABLE `%s` CHANGE `title` `title` TEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci", YOURLS_DB_TABLE_URL),
'short URL charset' => sprintf('ALTER TABLE `%s` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;', YOURLS_DB_TABLE_URL),
);
foreach($queries as $what => $query) {
try {
$ydb->perform($query);
} catch (\Exception $e) {
$error_msg[] = $e->getMessage();
}
}
if( $error_msg ) {
echo "<p class='error'>Unable to update the DB.</p>";
echo "<p>You will have to manually fix things, sorry for the inconvenience :(</p>";
echo "<p>The errors were:
<pre>";
foreach( $error_msg as $error ) {
echo "$error\n";
}
echo "</pre>";
die();
}
echo "<p class='success'>OK!</p>";
}
/************************** 1.5 -> 1.6 **************************/
/**
* Upgrade r482
*
*/
function yourls_upgrade_482() {
// Change URL title charset to UTF8
$table_url = YOURLS_DB_TABLE_URL;
$sql = "ALTER TABLE `$table_url` CHANGE `title` `title` TEXT CHARACTER SET utf8;";
yourls_get_db()->perform( $sql );
echo "<p>Updating table structure. Please wait...</p>";
}
/************************** 1.4.3 -> 1.5 **************************/
/**
* Main func for upgrade from 1.4.3 to 1.5
*
*/
function yourls_upgrade_to_15( ) {
// Create empty 'active_plugins' entry in the option if needed
if( yourls_get_option( 'active_plugins' ) === false )
yourls_add_option( 'active_plugins', array() );
echo "<p>Enabling the plugin API. Please wait...</p>";
// Alter URL table to store titles
$table_url = YOURLS_DB_TABLE_URL;
$sql = "ALTER TABLE `$table_url` ADD `title` TEXT AFTER `url`;";
yourls_get_db()->perform( $sql );
echo "<p>Updating table structure. Please wait...</p>";
// Update .htaccess
yourls_create_htaccess();
echo "<p>Updating .htaccess file. Please wait...</p>";
}
/************************** 1.4.1 -> 1.4.3 **************************/
/**
* Main func for upgrade from 1.4.1 to 1.4.3
*
*/
function yourls_upgrade_to_143( ) {
// Check if we have 'keyword' (borked install) or 'shorturl' (ok install)
$ydb = yourls_get_db();
$table_log = YOURLS_DB_TABLE_LOG;
$sql = "SHOW COLUMNS FROM `$table_log`";
$cols = $ydb->fetchObjects( $sql );
if ( $cols[2]->Field == 'keyword' ) {
$sql = "ALTER TABLE `$table_log` CHANGE `keyword` `shorturl` VARCHAR( 200 ) BINARY;";
$ydb->query( $sql );
}
echo "<p>Structure of existing tables updated. Please wait...</p>";
}
/************************** 1.4 -> 1.4.1 **************************/
/**
* Main func for upgrade from 1.4 to 1.4.1
*
*/
function yourls_upgrade_to_141( ) {
// Kill old cookies from 1.3 and prior
setcookie('yourls_username', '', time() - 3600 );
setcookie('yourls_password', '', time() - 3600 );
// alter table URL
yourls_alter_url_table_to_141();
// recreate the htaccess file if needed
yourls_create_htaccess();
}
/**
* Alter table URL to 1.4.1
*
*/
function yourls_alter_url_table_to_141() {
$table_url = YOURLS_DB_TABLE_URL;
$alter = "ALTER TABLE `$table_url` CHANGE `keyword` `keyword` VARCHAR( 200 ) BINARY, CHANGE `url` `url` TEXT BINARY ";
yourls_get_db()->perform( $alter );
echo "<p>Structure of existing tables updated. Please wait...</p>";
}
/************************** 1.3 -> 1.4 **************************/
/**
* Main func for upgrade from 1.3-RC1 to 1.4
*
*/
function yourls_upgrade_to_14( $step ) {
switch( $step ) {
case 1:
// create table log & table options
// update table url structure
// update .htaccess
yourls_create_tables_for_14(); // no value returned, assuming it went OK
yourls_alter_url_table_to_14(); // no value returned, assuming it went OK
$clean = yourls_clean_htaccess_for_14(); // returns bool
$create = yourls_create_htaccess(); // returns bool
if ( !$create )
echo "<p class='warning'>Please create your <tt>.htaccess</tt> file (I could not do it for you). Please refer to <a href='http://yourls.org/htaccess'>http://yourls.org/htaccess</a>.";
yourls_redirect_javascript( yourls_admin_url( "upgrade.php?step=2&oldver=1.3&newver=1.4&oldsql=100&newsql=200" ), $create );
break;
case 2:
// convert each link in table url
yourls_update_table_to_14();
break;
case 3:
// update table url structure part 2: recreate indexes
yourls_alter_url_table_to_14_part_two();
// update version & db_version & next_id in the option table
// attempt to drop YOURLS_DB_TABLE_NEXTDEC
yourls_update_options_to_14();
// Now upgrade to 1.4.1
yourls_redirect_javascript( yourls_admin_url( "upgrade.php?step=1&oldver=1.4&newver=1.4.1&oldsql=200&newsql=210" ) );
break;
}
}
/**
* Update options to reflect new version
*
*/
function yourls_update_options_to_14() {
yourls_update_option( 'version', '1.4' );
yourls_update_option( 'db_version', '200' );
if( defined('YOURLS_DB_TABLE_NEXTDEC') ) {
$table = YOURLS_DB_TABLE_NEXTDEC;
$next_id = yourls_get_db()->fetchValue("SELECT `next_id` FROM `$table`");
yourls_update_option( 'next_id', $next_id );
yourls_get_db()->perform( "DROP TABLE `$table`" );
} else {
yourls_update_option( 'next_id', 1 ); // In case someone mistakenly deleted the next_id constant or table too early
}
}
/**
* Create new tables for YOURLS 1.4: options & log
*
*/
function yourls_create_tables_for_14() {
$ydb = yourls_get_db();
$queries = array();
$queries[YOURLS_DB_TABLE_OPTIONS] =
'CREATE TABLE IF NOT EXISTS `'.YOURLS_DB_TABLE_OPTIONS.'` ('.
'`option_id` int(11) unsigned NOT NULL auto_increment,'.
'`option_name` varchar(64) NOT NULL default "",'.
'`option_value` longtext NOT NULL,'.
'PRIMARY KEY (`option_id`,`option_name`),'.
'KEY `option_name` (`option_name`)'.
');';
$queries[YOURLS_DB_TABLE_LOG] =
'CREATE TABLE IF NOT EXISTS `'.YOURLS_DB_TABLE_LOG.'` ('.
'`click_id` int(11) NOT NULL auto_increment,'.
'`click_time` datetime NOT NULL,'.
'`shorturl` varchar(200) NOT NULL,'.
'`referrer` varchar(200) NOT NULL,'.
'`user_agent` varchar(255) NOT NULL,'.
'`ip_address` varchar(41) NOT NULL,'.
'`country_code` char(2) NOT NULL,'.
'PRIMARY KEY (`click_id`),'.
'KEY `shorturl` (`shorturl`)'.
');';
foreach( $queries as $query ) {
$ydb->perform( $query ); // There's no result to be returned to check if table was created (except making another query to check table existence, which we'll avoid)
}
echo "<p>New tables created. Please wait...</p>";
}
/**
* Alter table structure, part 1 (change schema, drop index)
*
*/
function yourls_alter_url_table_to_14() {
$ydb = yourls_get_db();
$table = YOURLS_DB_TABLE_URL;
$alters = array();
$results = array();
$alters[] = "ALTER TABLE `$table` CHANGE `id` `keyword` VARCHAR( 200 ) NOT NULL";
$alters[] = "ALTER TABLE `$table` CHANGE `url` `url` TEXT NOT NULL";
$alters[] = "ALTER TABLE `$table` DROP PRIMARY KEY";
foreach ( $alters as $query ) {
$ydb->perform( $query );
}
echo "<p>Structure of existing tables updated. Please wait...</p>";
}
/**
* Alter table structure, part 2 (recreate indexes after the table is up to date)
*
*/
function yourls_alter_url_table_to_14_part_two() {
$ydb = yourls_get_db();
$table = YOURLS_DB_TABLE_URL;
$alters = array();
$alters[] = "ALTER TABLE `$table` ADD PRIMARY KEY ( `keyword` )";
$alters[] = "ALTER TABLE `$table` ADD INDEX ( `ip` )";
$alters[] = "ALTER TABLE `$table` ADD INDEX ( `timestamp` )";
foreach ( $alters as $query ) {
$ydb->perform( $query );
}
echo "<p>New table index created</p>";
}
/**
* Convert each link from 1.3 (id) to 1.4 (keyword) structure
*
*/
function yourls_update_table_to_14() {
$ydb = yourls_get_db();
$table = YOURLS_DB_TABLE_URL;
// Modify each link to reflect new structure
$chunk = 45;
$from = isset($_GET['from']) ? intval( $_GET['from'] ) : 0 ;
$total = yourls_get_db_stats();
$total = $total['total_links'];
$sql = "SELECT `keyword`,`url` FROM `$table` WHERE 1=1 ORDER BY `url` ASC LIMIT $from, $chunk ;";
$rows = $ydb->fetchObjects($sql);
$count = 0;
$queries = 0;
foreach( $rows as $row ) {
$keyword = $row->keyword;
$url = $row->url;
$newkeyword = yourls_int2string( $keyword );
if( true === $ydb->perform("UPDATE `$table` SET `keyword` = '$newkeyword' WHERE `url` = '$url';") ) {
$queries++;
} else {
echo "<p>Huho... Could not update rown with url='$url', from keyword '$keyword' to keyword '$newkeyword'</p>"; // Find what went wrong :/
}
$count++;
}
// All done for this chunk of queries, did it all go as expected?
$success = true;
if( $count != $queries ) {
$success = false;
$num = $count - $queries;
echo "<p>$num error(s) occured while updating the URL table :(</p>";
}
if ( $count == $chunk ) {
// there are probably other rows to convert
$from = $from + $chunk;
$remain = $total - $from;
echo "<p>Converted $chunk database rows ($remain remaining). Continuing... Please do not close this window until it's finished!</p>";
yourls_redirect_javascript( yourls_admin_url( "upgrade.php?step=2&oldver=1.3&newver=1.4&oldsql=100&newsql=200&from=$from" ), $success );
} else {
// All done
echo '<p>All rows converted! Please wait...</p>';
yourls_redirect_javascript( yourls_admin_url( "upgrade.php?step=3&oldver=1.3&newver=1.4&oldsql=100&newsql=200" ), $success );
}
}
/**
* Clean .htaccess as it existed before 1.4. Returns boolean
*
*/
function yourls_clean_htaccess_for_14() {
$filename = YOURLS_ABSPATH.'/.htaccess';
$result = false;
if( is_writeable( $filename ) ) {
$contents = implode( '', file( $filename ) );
// remove "ShortURL" block
$contents = preg_replace( '/# BEGIN ShortURL.*# END ShortURL/s', '', $contents );
// comment out deprecated RewriteRule
$find = 'RewriteRule .* - [E=REMOTE_USER:%{HTTP:Authorization},L]';
$replace = "# You can safely remove this 5 lines block -- it's no longer used in YOURLS\n".
"# $find";
$contents = str_replace( $find, $replace, $contents );
// Write cleaned file
$f = fopen( $filename, 'w' );
fwrite( $f, $contents );
fclose( $f );
$result = true;
}
return $result;
}

1273
functions.php Normal file

File diff suppressed because it is too large Load Diff

26
load-yourls.php Normal file
View File

@ -0,0 +1,26 @@
<?php
/* Bootstrap YOURLS
*
* This file initialize everything needed for YOURLS
* If you need to bootstrap YOURLS (ie access its functions and features) simply include this file.
*/
require __DIR__ . '/vendor/autoload.php';
// Set up YOURLS config
$config = new \YOURLS\Config\Config;
/* The following require has to be at global level so the variables inside config.php, including user defined if any,
* are registered in the global scope. If this require is moved in \YOURLS\Config\Config, $yourls_user_passwords for
* instance isn't registered.
*/
if (!defined('YOURLS_CONFIGFILE')) {
define('YOURLS_CONFIGFILE', $config->find_config());
}
require_once YOURLS_CONFIGFILE;
$config->define_core_constants();
// Initialize YOURLS with default behaviors
$init_defaults = new \YOURLS\Config\InitDefaults;
new \YOURLS\Config\Init($init_defaults);

20
version.php Normal file
View File

@ -0,0 +1,20 @@
<?php
/**
* YOURLS version
*
* Must be one of the following :
* MAJOR.MINOR (eg 1.8)
* MAJOR.MINOR.PATCH (1.8.1)
* MAJOR.MINOR-SOMETHING (1.8-dev)
* MAJOR.MINOR.PATCH-SOMETHING (1.8.1-donotuse)
*
*/
define( 'YOURLS_VERSION', '1.9.1' );
/**
* YOURLS DB version. Increments when changes are made to the DB schema, to trigger a DB update
*
* Must be a string of an integer.
*
*/
define( 'YOURLS_DB_VERSION', '506' );