" );
+ yourls_add_action( 'admin_notices', function() use ( $message ) { echo (string) $message; } );
+ */
+}
+
+/**
+ * Wrapper function to display admin notices
+ *
+ * @param string $message Message to display
+ * @param string $style Message style (default: 'notice')
+ * @return void
+ */
+function yourls_add_notice( $message, $style = 'notice' ) {
+ // Escape single quotes in $message to avoid breaking the anonymous function
+ $message = yourls_notice_box( strtr( $message, array( "'" => "\'" ) ), $style );
+ yourls_add_action( 'admin_notices', function() use ( $message ) { echo (string) $message; } );
+}
+
+/**
+ * Return a formatted notice
+ *
+ * @param string $message Message to display
+ * @param string $style CSS class to use for the notice
+ * @return string HTML of the notice
+ */
+function yourls_notice_box( $message, $style = 'notice' ) {
+ return <<
+
$message
+
+HTML;
+}
+
+/**
+ * Display a page
+ *
+ * Includes content of a PHP file from the YOURLS_PAGEDIR directory, as if it
+ * were a standard short URL (ie http://sho.rt/$page)
+ *
+ * @since 1.0
+ * @param string $page PHP file to display
+ * @return void
+ */
+function yourls_page( $page ) {
+ if( !yourls_is_page($page)) {
+ yourls_die( yourls_s('Page "%1$s" not found', $page), yourls__('Not found'), 404 );
+ }
+
+ yourls_do_action( 'pre_page', $page );
+ include_once( YOURLS_PAGEDIR . "/$page.php" );
+ yourls_do_action( 'post_page', $page );
+}
+
+/**
+ * Display the language attributes for the HTML tag.
+ *
+ * Builds up a set of html attributes containing the text direction and language
+ * information for the page. Stolen from WP.
+ *
+ * @since 1.6
+ * @return void
+ */
+function yourls_html_language_attributes() {
+ $attributes = array();
+ $output = '';
+
+ $attributes[] = ( yourls_is_rtl() ? 'dir="rtl"' : 'dir="ltr"' );
+
+ $doctype = yourls_apply_filter( 'html_language_attributes_doctype', 'html' );
+ // Experimental: get HTML lang from locale. Should work. Convert fr_FR -> fr-FR
+ if ( $lang = str_replace( '_', '-', yourls_get_locale() ) ) {
+ if( $doctype == 'xhtml' ) {
+ $attributes[] = "xml:lang=\"$lang\"";
+ } else {
+ $attributes[] = "lang=\"$lang\"";
+ }
+ }
+
+ $output = implode( ' ', $attributes );
+ $output = yourls_apply_filter( 'html_language_attributes', $output );
+ echo $output;
+}
+
+/**
+ * Output translated strings used by the Javascript calendar
+ *
+ * @since 1.6
+ * @return void
+ */
+function yourls_l10n_calendar_strings() {
+ echo "\n\n";
+
+ // Dummy returns, to initialize l10n strings used in the calendar
+ yourls__( 'Today' );
+ yourls__( 'Close' );
+}
+
+
+/**
+ * Display a notice if there is a newer version of YOURLS available
+ *
+ * @since 1.7
+ * @param string $compare_with Optional, YOURLS version to compare to
+ * @return void
+ */
+function yourls_new_core_version_notice($compare_with = null) {
+ $compare_with = $compare_with ?: YOURLS_VERSION;
+
+ $checks = yourls_get_option( 'core_version_checks' );
+ $latest = isset($checks->last_result->latest) ? yourls_sanitize_version($checks->last_result->latest) : false;
+
+ if( $latest AND version_compare( $latest, $compare_with, '>' ) ) {
+ yourls_do_action('new_core_version_notice', $latest);
+ $msg = yourls_s( 'YOURLS version %s is available. Please update!', 'http://yourls.org/download', $latest );
+ yourls_add_notice( $msg );
+ }
+}
+
+/**
+ * Display or return HTML for a bookmarklet link
+ *
+ * @since 1.7.1
+ * @param string $href bookmarklet link (presumably minified code with "javascript:" scheme)
+ * @param string $anchor link anchor
+ * @param bool $echo true to display, false to return the HTML
+ * @return string the HTML for a bookmarklet link
+ */
+function yourls_bookmarklet_link( $href, $anchor, $echo = true ) {
+ $alert = yourls_esc_attr__( 'Drag to your toolbar!' );
+ $link = <<$anchor
+LINK;
+
+ if( $echo )
+ echo $link;
+ return $link;
+}
+
+/**
+ * Set HTML context (stats, index, infos, ...)
+ *
+ * @since 1.7.3
+ * @param string $context
+ * @return void
+ */
+function yourls_set_html_context($context) {
+ yourls_get_db()->set_html_context($context);
+}
+
+/**
+ * Get HTML context (stats, index, infos, ...)
+ *
+ * @since 1.7.3
+ * @return string
+ */
+function yourls_get_html_context() {
+ return yourls_get_db()->get_html_context();
+}
+
+/**
+ * Print HTML link for favicon
+ *
+ * @since 1.7.10
+ * @return mixed|void
+ */
+function yourls_html_favicon() {
+ // Allow plugins to short-circuit the whole function
+ $pre = yourls_apply_filter( 'shunt_html_favicon', false );
+ if ( false !== $pre ) {
+ return $pre;
+ }
+
+ printf( '', yourls_get_yourls_favicon_url(false) );
+}
diff --git a/functions-http.php b/functions-http.php
new file mode 100644
index 0000000..db60abc
--- /dev/null
+++ b/functions-http.php
@@ -0,0 +1,528 @@
+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 );
+}
diff --git a/functions-infos.php b/functions-infos.php
new file mode 100644
index 0000000..ed97203
--- /dev/null
+++ b/functions-infos.php
@@ -0,0 +1,384 @@
+ '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\n";
+ $code .= "\n";
+
+ return $code;
+}
diff --git a/functions-install.php b/functions-install.php
new file mode 100644
index 0000000..2ccd118
--- /dev/null
+++ b/functions-install.php
@@ -0,0 +1,351 @@
+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"?>',
+ '',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ ' ',
+ '',
+ );
+
+ $filename = YOURLS_ABSPATH.'/web.config';
+ $marker = 'none';
+
+ } else {
+ // Prepare content for a .htaccess file
+ $content = array(
+ '',
+ 'RewriteEngine On',
+ 'RewriteBase '.$path.'/',
+ 'RewriteCond %{REQUEST_FILENAME} !-f',
+ 'RewriteCond %{REQUEST_FILENAME} !-d',
+ 'RewriteRule ^.*$ '.$path.'/yourls-loader.php [L]',
+ '',
+ );
+
+ $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 = '';
+ @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);
+ }
+}
diff --git a/functions-kses.php b/functions-kses.php
new file mode 100644
index 0000000..08a20a2
--- /dev/null
+++ b/functions-kses.php
@@ -0,0 +1,778 @@
+
+ *
+ * @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&T", ":" to ":", "YZZY;" to "&#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 &
+
+ $string = str_replace('&', '&', $string);
+
+ # Change back the allowed entities in our entity whitelist
+
+ $string = preg_replace_callback('/&([A-Za-z]{2,8});/', 'yourls_kses_named_entities', $string);
+ $string = preg_replace_callback('/&#(0*[0-9]{1,7});/', 'yourls_kses_normalize_entities2', $string);
+ $string = preg_replace_callback('/&#[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) ) ? "&$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 = "&#$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)) ) ? "&#x$hexchars;" : ''.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 (A and A). It doesn't do
+ * anything with other entities like ä, 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;
+}
diff --git a/functions-l10n.php b/functions-l10n.php
new file mode 100644
index 0000000..d72c786
--- /dev/null
+++ b/functions-l10n.php
@@ -0,0 +1,1129 @@
+translate( $text ), $text, $domain );
+}
+
+/**
+ * Retrieves the translation of $text with a given $context. If there is no translation, or
+ * the domain isn't loaded, the original text is returned.
+ *
+ * Quite a few times, there will be collisions with similar translatable text
+ * found in more than two places but with different translated context.
+ *
+ * By including the context in the pot file translators can translate the two
+ * strings differently.
+ *
+ * @since 1.6
+ * @param string $text Text to translate.
+ * @param string $context Context.
+ * @param string $domain Domain to retrieve the translated text.
+ * @return string Translated text
+ */
+function yourls_translate_with_context( $text, $context, $domain = 'default' ) {
+ $translations = yourls_get_translations_for_domain( $domain );
+ return yourls_apply_filter( 'translate_with_context', $translations->translate( $text, $context ), $text, $context, $domain );
+}
+
+/**
+ * Retrieves the translation of $text. If there is no translation, or
+ * the domain isn't loaded, the original text is returned.
+ *
+ * @see yourls_translate() An alias of yourls_translate()
+ * @since 1.6
+ *
+ * @param string $text Text to translate
+ * @param string $domain Optional. Domain to retrieve the translated text
+ * @return string Translated text
+ */
+function yourls__( $text, $domain = 'default' ) {
+ return yourls_translate( $text, $domain );
+}
+
+/**
+ * Return a translated sprintf() string (mix yourls__() and sprintf() in one func)
+ *
+ * Instead of doing sprintf( yourls__( 'string %s' ), $arg ) you can simply use:
+ * yourls_s( 'string %s', $arg )
+ * This function accepts an arbitrary number of arguments:
+ * - first one will be the string to translate, eg "hello %s my name is %s"
+ * - following ones will be the sprintf arguments, eg "world" and "Ozh"
+ * - if there are more arguments passed than needed, the last one will be used as the translation domain
+ *
+ * @see sprintf()
+ * @since 1.6
+ *
+ * @param mixed ...$pattern Text to translate, then $arg1: optional sprintf tokens, and $arg2: translation domain
+ * @return string Translated text
+ */
+function yourls_s( $pattern ) {
+ // Get pattern and pattern arguments
+ $args = func_get_args();
+ // If yourls_s() called by yourls_se(), all arguments are wrapped in the same array key
+ if( count( $args ) == 1 && is_array( $args[0] ) ) {
+ $args = $args[0];
+ }
+ $pattern = $args[0];
+
+ // get list of sprintf tokens (%s and such)
+ $num_of_tokens = substr_count( $pattern, '%' ) - 2 * substr_count( $pattern, '%%' );
+
+ $domain = 'default';
+ // More arguments passed than needed for the sprintf? The last one will be the domain
+ if( $num_of_tokens < ( count( $args ) - 1 ) ) {
+ $domain = array_pop( $args );
+ }
+
+ // Translate text
+ $args[0] = yourls__( $pattern, $domain );
+
+ return call_user_func_array( 'sprintf', $args );
+}
+
+/**
+ * Echo a translated sprintf() string (mix yourls__() and sprintf() in one func)
+ *
+ * Instead of doing printf( yourls__( 'string %s' ), $arg ) you can simply use:
+ * yourls_se( 'string %s', $arg )
+ * This function accepts an arbitrary number of arguments:
+ * - first one will be the string to translate, eg "hello %s my name is %s"
+ * - following ones will be the sprintf arguments, eg "world" and "Ozh"
+ * - if there are more arguments passed than needed, the last one will be used as the translation domain
+ *
+ * @see yourls_s()
+ * @see sprintf()
+ * @since 1.6
+ *
+ * @param string ...$pattern Text to translate, then optional sprintf tokens, and optional translation domain
+ * @return void Translated text
+ */
+function yourls_se( $pattern ) {
+ echo yourls_s( func_get_args() );
+}
+
+
+/**
+ * Retrieves the translation of $text and escapes it for safe use in an attribute.
+ * If there is no translation, or the domain isn't loaded, the original text is returned.
+ *
+ * @see yourls_translate() An alias of yourls_translate()
+ * @see yourls_esc_attr()
+ * @since 1.6
+ *
+ * @param string $text Text to translate
+ * @param string $domain Optional. Domain to retrieve the translated text
+ * @return string Translated text
+ */
+function yourls_esc_attr__( $text, $domain = 'default' ) {
+ return yourls_esc_attr( yourls_translate( $text, $domain ) );
+}
+
+/**
+ * Retrieves the translation of $text and escapes it for safe use in HTML output.
+ * If there is no translation, or the domain isn't loaded, the original text is returned.
+ *
+ * @see yourls_translate() An alias of yourls_translate()
+ * @see yourls_esc_html()
+ * @since 1.6
+ *
+ * @param string $text Text to translate
+ * @param string $domain Optional. Domain to retrieve the translated text
+ * @return string Translated text
+ */
+function yourls_esc_html__( $text, $domain = 'default' ) {
+ return yourls_esc_html( yourls_translate( $text, $domain ) );
+}
+
+/**
+ * Displays the returned translated text from yourls_translate().
+ *
+ * @see yourls_translate() Echoes returned yourls_translate() string
+ * @since 1.6
+ *
+ * @param string $text Text to translate
+ * @param string $domain Optional. Domain to retrieve the translated text
+ * @return void
+ */
+function yourls_e( $text, $domain = 'default' ) {
+ echo yourls_translate( $text, $domain );
+}
+
+/**
+ * Displays translated text that has been escaped for safe use in an attribute.
+ *
+ * @see yourls_translate() Echoes returned yourls_translate() string
+ * @see yourls_esc_attr()
+ * @since 1.6
+ *
+ * @param string $text Text to translate
+ * @param string $domain Optional. Domain to retrieve the translated text
+ * @return void
+ */
+function yourls_esc_attr_e( $text, $domain = 'default' ) {
+ echo yourls_esc_attr( yourls_translate( $text, $domain ) );
+}
+
+/**
+ * Displays translated text that has been escaped for safe use in HTML output.
+ *
+ * @see yourls_translate() Echoes returned yourls_translate() string
+ * @see yourls_esc_html()
+ * @since 1.6
+ *
+ * @param string $text Text to translate
+ * @param string $domain Optional. Domain to retrieve the translated text
+ * @return void
+ */
+function yourls_esc_html_e( $text, $domain = 'default' ) {
+ echo yourls_esc_html( yourls_translate( $text, $domain ) );
+}
+
+/**
+ * Retrieve translated string with gettext context
+ *
+ * Quite a few times, there will be collisions with similar translatable text
+ * found in more than two places but with different translated context.
+ *
+ * By including the context in the pot file translators can translate the two
+ * strings differently.
+ *
+ * @since 1.6
+ *
+ * @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
+ */
+function yourls_x( $text, $context, $domain = 'default' ) {
+ return yourls_translate_with_context( $text, $context, $domain );
+}
+
+/**
+ * Displays translated string with gettext context
+ *
+ * @see yourls_x()
+ * @since 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 void Echoes translated context string
+ */
+function yourls_xe( $text, $context, $domain = 'default' ) {
+ echo yourls_x( $text, $context, $domain );
+}
+
+
+/**
+ * Return translated text, with context, that has been escaped for safe use in an attribute
+ *
+ * @see yourls_translate() Return returned yourls_translate() string
+ * @see yourls_esc_attr()
+ * @see yourls_x()
+ * @since 1.6
+ *
+ * @param string $single
+ * @param string $context
+ * @param string $domain Optional. Domain to retrieve the translated text
+ * @internal param string $text Text to translate
+ * @return string
+ */
+function yourls_esc_attr_x( $single, $context, $domain = 'default' ) {
+ return yourls_esc_attr( yourls_translate_with_context( $single, $context, $domain ) );
+}
+
+/**
+ * Return translated text, with context, that has been escaped for safe use in HTML output
+ *
+ * @see yourls_translate() Return returned yourls_translate() string
+ * @see yourls_esc_attr()
+ * @see yourls_x()
+ * @since 1.6
+ *
+ * @param string $single
+ * @param string $context
+ * @param string $domain Optional. Domain to retrieve the translated text
+ * @internal param string $text Text to translate
+ * @return string
+ */
+function yourls_esc_html_x( $single, $context, $domain = 'default' ) {
+ return yourls_esc_html( yourls_translate_with_context( $single, $context, $domain ) );
+}
+
+/**
+ * Retrieve the plural or single form based on the amount.
+ *
+ * If the domain is not set in the $yourls_l10n list, then a comparison will be made
+ * and either $plural or $single parameters returned.
+ *
+ * If the domain does exist, then the parameters $single, $plural, and $number
+ * will first be passed to the domain's ngettext method. Then it will be passed
+ * to the 'translate_n' filter hook along with the same parameters. The expected
+ * type will be a string.
+ *
+ * @since 1.6
+ * @uses $yourls_l10n Gets list of domain translated string (gettext_reader) objects
+ * @uses yourls_apply_filter() Calls 'translate_n' hook on domains text returned,
+ * along with $single, $plural, and $number parameters. Expected to return string.
+ *
+ * @param string $single The text that will be used if $number is 1
+ * @param string $plural The text that will be used if $number is not 1
+ * @param int $number The number to compare against to use either $single or $plural
+ * @param string $domain Optional. The domain identifier the text should be retrieved in
+ * @return string Either $single or $plural translated text
+ */
+function yourls_n( $single, $plural, $number, $domain = 'default' ) {
+ $translations = yourls_get_translations_for_domain( $domain );
+ $translation = $translations->translate_plural( $single, $plural, $number );
+ return yourls_apply_filter( 'translate_n', $translation, $single, $plural, $number, $domain );
+}
+
+/**
+ * A hybrid of yourls_n() and yourls_x(). It supports contexts and plurals.
+ *
+ * @since 1.6
+ * @see yourls_n()
+ * @see yourls_x()
+ *
+ * @param string $single The text that will be used if $number is 1
+ * @param string $plural The text that will be used if $number is not 1
+ * @param int $number The number to compare against to use either $single or $plural
+ * @param string $context Context information for the translators
+ * @param string $domain Optional. The domain identifier the text should be retrieved in
+ * @return string Either $single or $plural translated text
+ */
+function yourls_nx($single, $plural, $number, $context, $domain = 'default') {
+ $translations = yourls_get_translations_for_domain( $domain );
+ $translation = $translations->translate_plural( $single, $plural, $number, $context );
+ return yourls_apply_filter( 'translate_nx', $translation, $single, $plural, $number, $context, $domain );
+}
+
+/**
+ * Register plural strings in POT file, but don't translate them.
+ *
+ * Used when you want to keep structures with translatable plural strings and
+ * use them later.
+ *
+ * Example:
+ * $messages = array(
+ * 'post' => yourls_n_noop('%s post', '%s posts'),
+ * 'page' => yourls_n_noop('%s pages', '%s pages')
+ * );
+ * ...
+ * $message = $messages[$type];
+ * $usable_text = sprintf( yourls_translate_nooped_plural( $message, $count ), $count );
+ *
+ * @since 1.6
+ * @param string $singular Single form to be i18ned
+ * @param string $plural Plural form to be i18ned
+ * @param string $domain Optional. The domain identifier the text will be retrieved in
+ * @return array array($singular, $plural)
+ */
+function yourls_n_noop( $singular, $plural, $domain = null ) {
+ return array(
+ 0 => $singular,
+ 1 => $plural,
+ 'singular' => $singular,
+ 'plural' => $plural,
+ 'context' => null,
+ 'domain' => $domain
+ );
+}
+
+/**
+ * Register plural strings with context in POT file, but don't translate them.
+ *
+ * @since 1.6
+ * @see yourls_n_noop()
+ *
+ * @param string $singular Single form to be i18ned
+ * @param string $plural Plural form to be i18ned
+ * @param string $context Context information for the translators
+ * @param string $domain Optional. The domain identifier the text will be retrieved in
+ * @return array array($singular, $plural)
+ */
+function yourls_nx_noop( $singular, $plural, $context, $domain = null ) {
+ return array(
+ 0 => $singular,
+ 1 => $plural,
+ 2 => $context,
+ 'singular' => $singular,
+ 'plural' => $plural,
+ 'context' => $context,
+ 'domain' => $domain
+ );
+}
+
+/**
+ * Translate the result of yourls_n_noop() or yourls_nx_noop()
+ *
+ * @since 1.6
+ * @param array $nooped_plural Array with singular, plural and context keys, usually the result of yourls_n_noop() or yourls_nx_noop()
+ * @param int $count Number of objects
+ * @param string $domain Optional. The domain identifier the text should be retrieved in. If $nooped_plural contains
+ * a domain passed to yourls_n_noop() or yourls_nx_noop(), it will override this value.
+ * @return string
+ */
+function yourls_translate_nooped_plural( $nooped_plural, $count, $domain = 'default' ) {
+ if ( $nooped_plural['domain'] )
+ $domain = $nooped_plural['domain'];
+
+ if ( $nooped_plural['context'] )
+ return yourls_nx( $nooped_plural['singular'], $nooped_plural['plural'], $count, $nooped_plural['context'], $domain );
+ else
+ return yourls_n( $nooped_plural['singular'], $nooped_plural['plural'], $count, $domain );
+}
+
+/**
+ * Loads a MO file into the domain $domain.
+ *
+ * If the domain already exists, the translations will be merged. If both
+ * sets have the same string, the translation from the original value will be taken.
+ *
+ * On success, the .mo file will be placed in the $yourls_l10n global by $domain
+ * and will be a MO object.
+ *
+ * @since 1.6
+ * @uses $yourls_l10n Gets list of domain translated string objects
+ *
+ * @param string $domain Unique identifier for retrieving translated strings
+ * @param string $mofile Path to the .mo file
+ * @return bool True on success, false on failure
+ */
+function yourls_load_textdomain( $domain, $mofile ) {
+ global $yourls_l10n;
+
+ $plugin_override = yourls_apply_filter( 'override_load_textdomain', false, $domain, $mofile );
+
+ if ( true == $plugin_override ) {
+ return true;
+ }
+
+ yourls_do_action( 'load_textdomain', $domain, $mofile );
+
+ $mofile = yourls_apply_filter( 'load_textdomain_mofile', $mofile, $domain );
+
+ if ( !is_readable( $mofile ) ) {
+ trigger_error( 'Cannot read file ' . str_replace( YOURLS_ABSPATH.'/', '', $mofile ) . '.'
+ . ' Make sure there is a language file installed. More info: http://yourls.org/translations' );
+ return false;
+ }
+
+ $mo = new MO();
+ if ( !$mo->import_from_file( $mofile ) )
+ return false;
+
+ if ( isset( $yourls_l10n[$domain] ) )
+ $mo->merge_with( $yourls_l10n[$domain] );
+
+ $yourls_l10n[$domain] = &$mo;
+
+ return true;
+}
+
+/**
+ * Unloads translations for a domain
+ *
+ * @since 1.6
+ * @param string $domain Textdomain to be unloaded
+ * @return bool Whether textdomain was unloaded
+ */
+function yourls_unload_textdomain( $domain ) {
+ global $yourls_l10n;
+
+ $plugin_override = yourls_apply_filter( 'override_unload_textdomain', false, $domain );
+
+ if ( $plugin_override )
+ return true;
+
+ yourls_do_action( 'unload_textdomain', $domain );
+
+ if ( isset( $yourls_l10n[$domain] ) ) {
+ unset( $yourls_l10n[$domain] );
+ return true;
+ }
+
+ return false;
+}
+
+/**
+ * Loads default translated strings based on locale.
+ *
+ * Loads the .mo file in YOURLS_LANG_DIR constant path from YOURLS root. The
+ * translated (.mo) file is named based on the locale.
+ *
+ * @since 1.6
+ * @return bool True on success, false on failure
+ */
+function yourls_load_default_textdomain() {
+ $yourls_locale = yourls_get_locale();
+
+ if( !empty( $yourls_locale ) )
+ return yourls_load_textdomain( 'default', YOURLS_LANG_DIR . "/$yourls_locale.mo" );
+
+ return false;
+}
+
+/**
+ * Returns the Translations instance for a domain. If there isn't one,
+ * returns empty Translations instance.
+ *
+ * @param string $domain
+ * @return NOOPTranslations An NOOPTranslations translation instance
+ */
+function yourls_get_translations_for_domain( $domain ) {
+ global $yourls_l10n;
+ if ( !isset( $yourls_l10n[$domain] ) ) {
+ $yourls_l10n[$domain] = new NOOPTranslations;
+ }
+ return $yourls_l10n[$domain];
+}
+
+/**
+ * Whether there are translations for the domain
+ *
+ * @since 1.6
+ * @param string $domain
+ * @return bool Whether there are translations
+ */
+function yourls_is_textdomain_loaded( $domain ) {
+ global $yourls_l10n;
+ return isset( $yourls_l10n[$domain] );
+}
+
+/**
+ * Translates role name. Unused.
+ *
+ * Unused function for the moment, we'll see when there are roles.
+ * From the WP source: Since the role names are in the database and
+ * not in the source there are dummy gettext calls to get them into the POT
+ * file and this function properly translates them back.
+ *
+ * @since 1.6
+ * @param string $name The role name
+ * @return string Translated role name
+ */
+function yourls_translate_user_role( $name ) {
+ return yourls_translate_with_context( $name, 'User role' );
+}
+
+/**
+ * Get all available languages (*.mo files) in a given directory. The default directory is YOURLS_LANG_DIR.
+ *
+ * @since 1.6
+ *
+ * @param string $dir A directory in which to search for language files. The default directory is YOURLS_LANG_DIR.
+ * @return array Array of language codes or an empty array if no languages are present. Language codes are formed by stripping the .mo extension from the language file names.
+ */
+function yourls_get_available_languages( $dir = null ) {
+ $languages = array();
+
+ $dir = is_null( $dir) ? YOURLS_LANG_DIR : $dir;
+
+ foreach( (array) glob( $dir . '/*.mo' ) as $lang_file ) {
+ $languages[] = basename( $lang_file, '.mo' );
+ }
+
+ return yourls_apply_filter( 'get_available_languages', $languages );
+}
+
+/**
+ * Return integer number to format based on the locale.
+ *
+ * @since 1.6
+ *
+ * @param int $number The number to convert based on locale.
+ * @param int $decimals Precision of the number of decimal places.
+ * @return string Converted number in string format.
+ */
+function yourls_number_format_i18n( $number, $decimals = 0 ) {
+ global $yourls_locale_formats;
+ if( !isset( $yourls_locale_formats ) )
+ $yourls_locale_formats = new YOURLS_Locale_Formats();
+
+ $formatted = number_format( $number, abs( intval( $decimals ) ), $yourls_locale_formats->number_format['decimal_point'], $yourls_locale_formats->number_format['thousands_sep'] );
+ return yourls_apply_filter( 'number_format_i18n', $formatted );
+}
+
+/**
+ * Return the date in localized format, based on timestamp.
+ *
+ * If the locale specifies the locale month and weekday, then the locale will
+ * take over the format for the date. If it isn't, then the date format string
+ * will be used instead.
+ *
+ * @since 1.6
+ *
+ * @param string $dateformatstring Format to display the date.
+ * @param bool|int $timestamp Optional, Unix timestamp, default to current timestamp (with offset if applicable)
+ * @return string The date, translated if locale specifies it.
+ */
+function yourls_date_i18n( $dateformatstring, $timestamp = false ) {
+ /**
+ * @var YOURLS_Locale_Formats $yourls_locale_formats
+ */
+ global $yourls_locale_formats;
+ if( !isset( $yourls_locale_formats ) )
+ $yourls_locale_formats = new YOURLS_Locale_Formats();
+
+ if ( false === $timestamp ) {
+ $timestamp = yourls_get_timestamp( time() );
+ }
+
+ // store original value for language with untypical grammars
+ $req_format = $dateformatstring;
+
+ /**
+ * Replace the date format characters with their translatation, if found
+ * Example:
+ * 'l d F Y' gets replaced with '\L\u\n\d\i d \M\a\i Y' in French
+ * We deliberately don't deal with 'I', 'O', 'P', 'T', 'Z' and 'e' in date format (timezones)
+ */
+ if ( ( !empty( $yourls_locale_formats->month ) ) && ( !empty( $yourls_locale_formats->weekday ) ) ) {
+ $datemonth = $yourls_locale_formats->get_month( date( 'm', $timestamp ) );
+ $datemonth_abbrev = $yourls_locale_formats->get_month_abbrev( $datemonth );
+ $dateweekday = $yourls_locale_formats->get_weekday( date( 'w', $timestamp ) );
+ $dateweekday_abbrev = $yourls_locale_formats->get_weekday_abbrev( $dateweekday );
+ $datemeridiem = $yourls_locale_formats->get_meridiem( date( 'a', $timestamp ) );
+ $datemeridiem_capital = $yourls_locale_formats->get_meridiem( date( 'A', $timestamp ) );
+
+ $dateformatstring = ' '.$dateformatstring;
+ $dateformatstring = preg_replace( "/([^\\\])D/", "\\1" . yourls_backslashit( $dateweekday_abbrev ), $dateformatstring );
+ $dateformatstring = preg_replace( "/([^\\\])F/", "\\1" . yourls_backslashit( $datemonth ), $dateformatstring );
+ $dateformatstring = preg_replace( "/([^\\\])l/", "\\1" . yourls_backslashit( $dateweekday ), $dateformatstring );
+ $dateformatstring = preg_replace( "/([^\\\])M/", "\\1" . yourls_backslashit( $datemonth_abbrev ), $dateformatstring );
+ $dateformatstring = preg_replace( "/([^\\\])a/", "\\1" . yourls_backslashit( $datemeridiem ), $dateformatstring );
+ $dateformatstring = preg_replace( "/([^\\\])A/", "\\1" . yourls_backslashit( $datemeridiem_capital ), $dateformatstring );
+
+ $dateformatstring = substr( $dateformatstring, 1, strlen( $dateformatstring ) -1 );
+ }
+
+ $date = date( $dateformatstring, $timestamp );
+
+ // Allow plugins to redo this entirely for languages with untypical grammars
+ return yourls_apply_filter('date_i18n', $date, $req_format, $timestamp);
+}
+
+/**
+ * Class that loads the calendar locale.
+ *
+ * @since 1.6
+ */
+class YOURLS_Locale_Formats {
+ /**
+ * Stores the translated strings for the full weekday names.
+ *
+ * @since 1.6
+ * @var array
+ * @access private
+ */
+ var $weekday;
+
+ /**
+ * Stores the translated strings for the one character weekday names.
+ *
+ * There is a hack to make sure that Tuesday and Thursday, as well
+ * as Sunday and Saturday, don't conflict. See init() method for more.
+ *
+ * @see YOURLS_Locale_Formats::init() for how to handle the hack.
+ *
+ * @since 1.6
+ * @var array
+ * @access private
+ */
+ var $weekday_initial;
+
+ /**
+ * Stores the translated strings for the abbreviated weekday names.
+ *
+ * @since 1.6
+ * @var array
+ * @access private
+ */
+ var $weekday_abbrev;
+
+ /**
+ * Stores the translated strings for the full month names.
+ *
+ * @since 1.6
+ * @var array
+ * @access private
+ */
+ var $month;
+
+ /**
+ * Stores the translated strings for the abbreviated month names.
+ *
+ * @since 1.6
+ * @var array
+ * @access private
+ */
+ var $month_abbrev;
+
+ /**
+ * Stores the translated strings for 'am' and 'pm'.
+ *
+ * Also the capitalized versions.
+ *
+ * @since 1.6
+ * @var array
+ * @access private
+ */
+ var $meridiem;
+
+ /**
+ * Stores the translated number format
+ *
+ * @since 1.6
+ * @var array
+ * @access private
+ */
+ var $number_format;
+
+ /**
+ * The text direction of the locale language.
+ *
+ * Default is left to right 'ltr'.
+ *
+ * @since 1.6
+ * @var string
+ * @access private
+ */
+ var $text_direction = 'ltr';
+
+ /**
+ * Sets up the translated strings and object properties.
+ *
+ * The method creates the translatable strings for various
+ * calendar elements. Which allows for specifying locale
+ * specific calendar names and text direction.
+ *
+ * @since 1.6
+ * @access private
+ * @return void
+ */
+ function init() {
+ // The Weekdays
+ $this->weekday[0] = /* //translators: weekday */ yourls__( 'Sunday' );
+ $this->weekday[1] = /* //translators: weekday */ yourls__( 'Monday' );
+ $this->weekday[2] = /* //translators: weekday */ yourls__( 'Tuesday' );
+ $this->weekday[3] = /* //translators: weekday */ yourls__( 'Wednesday' );
+ $this->weekday[4] = /* //translators: weekday */ yourls__( 'Thursday' );
+ $this->weekday[5] = /* //translators: weekday */ yourls__( 'Friday' );
+ $this->weekday[6] = /* //translators: weekday */ yourls__( 'Saturday' );
+
+ // The first letter of each day. The _%day%_initial suffix is a hack to make
+ // sure the day initials are unique.
+ $this->weekday_initial[yourls__( 'Sunday' )] = /* //translators: one-letter abbreviation of the weekday */ yourls__( 'S_Sunday_initial' );
+ $this->weekday_initial[yourls__( 'Monday' )] = /* //translators: one-letter abbreviation of the weekday */ yourls__( 'M_Monday_initial' );
+ $this->weekday_initial[yourls__( 'Tuesday' )] = /* //translators: one-letter abbreviation of the weekday */ yourls__( 'T_Tuesday_initial' );
+ $this->weekday_initial[yourls__( 'Wednesday' )] = /* //translators: one-letter abbreviation of the weekday */ yourls__( 'W_Wednesday_initial' );
+ $this->weekday_initial[yourls__( 'Thursday' )] = /* //translators: one-letter abbreviation of the weekday */ yourls__( 'T_Thursday_initial' );
+ $this->weekday_initial[yourls__( 'Friday' )] = /* //translators: one-letter abbreviation of the weekday */ yourls__( 'F_Friday_initial' );
+ $this->weekday_initial[yourls__( 'Saturday' )] = /* //translators: one-letter abbreviation of the weekday */ yourls__( 'S_Saturday_initial' );
+
+ foreach ($this->weekday_initial as $weekday_ => $weekday_initial_) {
+ $this->weekday_initial[$weekday_] = preg_replace('/_.+_initial$/', '', $weekday_initial_);
+ }
+
+ // Abbreviations for each day.
+ $this->weekday_abbrev[ yourls__( 'Sunday' ) ] = /* //translators: three-letter abbreviation of the weekday */ yourls__( 'Sun' );
+ $this->weekday_abbrev[ yourls__( 'Monday' ) ] = /* //translators: three-letter abbreviation of the weekday */ yourls__( 'Mon' );
+ $this->weekday_abbrev[ yourls__( 'Tuesday' ) ] = /* //translators: three-letter abbreviation of the weekday */ yourls__( 'Tue' );
+ $this->weekday_abbrev[ yourls__( 'Wednesday' ) ] = /* //translators: three-letter abbreviation of the weekday */ yourls__( 'Wed' );
+ $this->weekday_abbrev[ yourls__( 'Thursday' ) ] = /* //translators: three-letter abbreviation of the weekday */ yourls__( 'Thu' );
+ $this->weekday_abbrev[ yourls__( 'Friday' ) ] = /* //translators: three-letter abbreviation of the weekday */ yourls__( 'Fri' );
+ $this->weekday_abbrev[ yourls__( 'Saturday' ) ] = /* //translators: three-letter abbreviation of the weekday */ yourls__( 'Sat' );
+
+ // The Months
+ $this->month['01'] = /* //translators: month name */ yourls__( 'January' );
+ $this->month['02'] = /* //translators: month name */ yourls__( 'February' );
+ $this->month['03'] = /* //translators: month name */ yourls__( 'March' );
+ $this->month['04'] = /* //translators: month name */ yourls__( 'April' );
+ $this->month['05'] = /* //translators: month name */ yourls__( 'May' );
+ $this->month['06'] = /* //translators: month name */ yourls__( 'June' );
+ $this->month['07'] = /* //translators: month name */ yourls__( 'July' );
+ $this->month['08'] = /* //translators: month name */ yourls__( 'August' );
+ $this->month['09'] = /* //translators: month name */ yourls__( 'September' );
+ $this->month['10'] = /* //translators: month name */ yourls__( 'October' );
+ $this->month['11'] = /* //translators: month name */ yourls__( 'November' );
+ $this->month['12'] = /* //translators: month name */ yourls__( 'December' );
+
+ // Abbreviations for each month. Uses the same hack as above to get around the
+ // 'May' duplication.
+ $this->month_abbrev[ yourls__( 'January' ) ] = /* //translators: three-letter abbreviation of the month */ yourls__( 'Jan_January_abbreviation' );
+ $this->month_abbrev[ yourls__( 'February' ) ] = /* //translators: three-letter abbreviation of the month */ yourls__( 'Feb_February_abbreviation' );
+ $this->month_abbrev[ yourls__( 'March' ) ] = /* //translators: three-letter abbreviation of the month */ yourls__( 'Mar_March_abbreviation' );
+ $this->month_abbrev[ yourls__( 'April' ) ] = /* //translators: three-letter abbreviation of the month */ yourls__( 'Apr_April_abbreviation' );
+ $this->month_abbrev[ yourls__( 'May' ) ] = /* //translators: three-letter abbreviation of the month */ yourls__( 'May_May_abbreviation' );
+ $this->month_abbrev[ yourls__( 'June' ) ] = /* //translators: three-letter abbreviation of the month */ yourls__( 'Jun_June_abbreviation' );
+ $this->month_abbrev[ yourls__( 'July' ) ] = /* //translators: three-letter abbreviation of the month */ yourls__( 'Jul_July_abbreviation' );
+ $this->month_abbrev[ yourls__( 'August' ) ] = /* //translators: three-letter abbreviation of the month */ yourls__( 'Aug_August_abbreviation' );
+ $this->month_abbrev[ yourls__( 'September' ) ] = /* //translators: three-letter abbreviation of the month */ yourls__( 'Sep_September_abbreviation' );
+ $this->month_abbrev[ yourls__( 'October' ) ] = /* //translators: three-letter abbreviation of the month */ yourls__( 'Oct_October_abbreviation' );
+ $this->month_abbrev[ yourls__( 'November' ) ] = /* //translators: three-letter abbreviation of the month */ yourls__( 'Nov_November_abbreviation' );
+ $this->month_abbrev[ yourls__( 'December' ) ] = /* //translators: three-letter abbreviation of the month */ yourls__( 'Dec_December_abbreviation' );
+
+ foreach ($this->month_abbrev as $month_ => $month_abbrev_) {
+ $this->month_abbrev[$month_] = preg_replace('/_.+_abbreviation$/', '', $month_abbrev_);
+ }
+
+ // The Meridiems
+ $this->meridiem['am'] = yourls__( 'am' );
+ $this->meridiem['pm'] = yourls__( 'pm' );
+ $this->meridiem['AM'] = yourls__( 'AM' );
+ $this->meridiem['PM'] = yourls__( 'PM' );
+
+ // Numbers formatting
+ // See http://php.net/number_format
+
+ /* //translators: $thousands_sep argument for http://php.net/number_format, default is , */
+ $trans = yourls__( 'number_format_thousands_sep' );
+ $this->number_format['thousands_sep'] = ('number_format_thousands_sep' == $trans) ? ',' : $trans;
+
+ /* //translators: $dec_point argument for http://php.net/number_format, default is . */
+ $trans = yourls__( 'number_format_decimal_point' );
+ $this->number_format['decimal_point'] = ('number_format_decimal_point' == $trans) ? '.' : $trans;
+
+ // Set text direction.
+ if ( isset( $GLOBALS['text_direction'] ) )
+ $this->text_direction = $GLOBALS['text_direction'];
+ /* //translators: 'rtl' or 'ltr'. This sets the text direction for YOURLS. */
+ elseif ( 'rtl' == yourls_x( 'ltr', 'text direction' ) )
+ $this->text_direction = 'rtl';
+ }
+
+ /**
+ * Retrieve the full translated weekday word.
+ *
+ * Week starts on translated Sunday and can be fetched
+ * by using 0 (zero). So the week starts with 0 (zero)
+ * and ends on Saturday with is fetched by using 6 (six).
+ *
+ * @since 1.6
+ * @access public
+ *
+ * @param int|string $weekday_number 0 for Sunday through 6 Saturday
+ * @return string Full translated weekday
+ */
+ function get_weekday( $weekday_number ) {
+ return $this->weekday[ $weekday_number ];
+ }
+
+ /**
+ * Retrieve the translated weekday initial.
+ *
+ * The weekday initial is retrieved by the translated
+ * full weekday word. When translating the weekday initial
+ * pay attention to make sure that the starting letter does
+ * not conflict.
+ *
+ * @since 1.6
+ * @access public
+ *
+ * @param string $weekday_name
+ * @return string
+ */
+ function get_weekday_initial( $weekday_name ) {
+ return $this->weekday_initial[ $weekday_name ];
+ }
+
+ /**
+ * Retrieve the translated weekday abbreviation.
+ *
+ * The weekday abbreviation is retrieved by the translated
+ * full weekday word.
+ *
+ * @since 1.6
+ * @access public
+ *
+ * @param string $weekday_name Full translated weekday word
+ * @return string Translated weekday abbreviation
+ */
+ function get_weekday_abbrev( $weekday_name ) {
+ return $this->weekday_abbrev[ $weekday_name ];
+ }
+
+ /**
+ * Retrieve the full translated month by month number.
+ *
+ * The $month_number parameter has to be a string
+ * because it must have the '0' in front of any number
+ * that is less than 10. Starts from '01' and ends at
+ * '12'.
+ *
+ * You can use an integer instead and it will add the
+ * '0' before the numbers less than 10 for you.
+ *
+ * @since 1.6
+ * @access public
+ *
+ * @param string|int $month_number '01' through '12'
+ * @return string Translated full month name
+ */
+ function get_month( $month_number ) {
+ return $this->month[ sprintf( '%02s', $month_number ) ];
+ }
+
+ /**
+ * Retrieve translated version of month abbreviation string.
+ *
+ * The $month_name parameter is expected to be the translated or
+ * translatable version of the month.
+ *
+ * @since 1.6
+ * @access public
+ *
+ * @param string $month_name Translated month to get abbreviated version
+ * @return string Translated abbreviated month
+ */
+ function get_month_abbrev( $month_name ) {
+ return $this->month_abbrev[ $month_name ];
+ }
+
+ /**
+ * Retrieve translated version of meridiem string.
+ *
+ * The $meridiem parameter is expected to not be translated.
+ *
+ * @since 1.6
+ * @access public
+ *
+ * @param string $meridiem Either 'am', 'pm', 'AM', or 'PM'. Not translated version.
+ * @return string Translated version
+ */
+ function get_meridiem( $meridiem ) {
+ return $this->meridiem[ $meridiem ];
+ }
+
+ /**
+ * Global variables are deprecated. For backwards compatibility only.
+ *
+ * @deprecated For backwards compatibility only.
+ * @access private
+ *
+ * @since 1.6
+ * @return void
+ */
+ function register_globals() {
+ $GLOBALS['weekday'] = $this->weekday;
+ $GLOBALS['weekday_initial'] = $this->weekday_initial;
+ $GLOBALS['weekday_abbrev'] = $this->weekday_abbrev;
+ $GLOBALS['month'] = $this->month;
+ $GLOBALS['month_abbrev'] = $this->month_abbrev;
+ }
+
+ /**
+ * Constructor which calls helper methods to set up object variables
+ *
+ * @uses YOURLS_Locale_Formats::init()
+ * @uses YOURLS_Locale_Formats::register_globals()
+ * @since 1.6
+ *
+ * @return YOURLS_Locale_Formats
+ */
+ function __construct() {
+ $this->init();
+ $this->register_globals();
+ }
+
+ /**
+ * Checks if current locale is RTL.
+ *
+ * @since 1.6
+ * @return bool Whether locale is RTL.
+ */
+ function is_rtl() {
+ return 'rtl' == $this->text_direction;
+ }
+}
+
+/**
+ * Loads a custom translation file (for a plugin, a theme, a public interface...) if locale is defined
+ *
+ * The .mo file should be named based on the domain with a dash, and then the locale exactly,
+ * eg 'myplugin-pt_BR.mo'
+ *
+ * @since 1.6
+ *
+ * @param string $domain Unique identifier (the "domain") for retrieving translated strings
+ * @param string $path Full path to directory containing MO files.
+ * @return mixed Returns nothing if locale undefined, otherwise return bool: true on success, false on failure
+ */
+function yourls_load_custom_textdomain( $domain, $path ) {
+ $locale = yourls_apply_filter( 'load_custom_textdomain', yourls_get_locale(), $domain );
+ if( !empty( $locale ) ) {
+ $mofile = rtrim( $path, '/' ) . '/'. $domain . '-' . $locale . '.mo';
+ return yourls_load_textdomain( $domain, $mofile );
+ }
+}
+
+/**
+ * Checks if current locale is RTL. Stolen from WP.
+ *
+ * @since 1.6
+ * @return bool Whether locale is RTL.
+ */
+function yourls_is_rtl() {
+ global $yourls_locale_formats;
+ if( !isset( $yourls_locale_formats ) )
+ $yourls_locale_formats = new YOURLS_Locale_Formats();
+
+ return $yourls_locale_formats->is_rtl();
+}
+
+/**
+ * Return translated weekday abbreviation (3 letters, eg 'Fri' for 'Friday')
+ *
+ * The $weekday var can be a textual string ('Friday'), a integer (0 to 6) or an empty string
+ * If $weekday is an empty string, the function returns an array of all translated weekday abbrev
+ *
+ * @since 1.6
+ * @param mixed $weekday A full textual weekday, eg "Friday", or an integer (0 = Sunday, 1 = Monday, .. 6 = Saturday)
+ * @return mixed Translated weekday abbreviation, eg "Ven" (abbrev of "Vendredi") for "Friday" or 5, or array of all weekday abbrev
+ */
+function yourls_l10n_weekday_abbrev( $weekday = '' ){
+ global $yourls_locale_formats;
+ if( !isset( $yourls_locale_formats ) )
+ $yourls_locale_formats = new YOURLS_Locale_Formats();
+
+ if( $weekday === '' )
+ return $yourls_locale_formats->weekday_abbrev;
+
+ if( is_int( $weekday ) ) {
+ $day = $yourls_locale_formats->weekday[ $weekday ];
+ return $yourls_locale_formats->weekday_abbrev[ $day ];
+ } else {
+ return $yourls_locale_formats->weekday_abbrev[ yourls__( $weekday ) ];
+ }
+}
+
+/**
+ * Return translated weekday initial (1 letter, eg 'F' for 'Friday')
+ *
+ * The $weekday var can be a textual string ('Friday'), a integer (0 to 6) or an empty string
+ * If $weekday is an empty string, the function returns an array of all translated weekday initials
+ *
+ * @since 1.6
+ * @param mixed $weekday A full textual weekday, eg "Friday", an integer (0 = Sunday, 1 = Monday, .. 6 = Saturday) or empty string
+ * @return mixed Translated weekday initial, eg "V" (initial of "Vendredi") for "Friday" or 5, or array of all weekday initials
+ */
+function yourls_l10n_weekday_initial( $weekday = '' ){
+ global $yourls_locale_formats;
+ if( !isset( $yourls_locale_formats ) )
+ $yourls_locale_formats = new YOURLS_Locale_Formats();
+
+ if( $weekday === '' )
+ return $yourls_locale_formats->weekday_initial;
+
+ if( is_int( $weekday ) ) {
+ $weekday = $yourls_locale_formats->weekday[ $weekday ];
+ return $yourls_locale_formats->weekday_initial[ $weekday ];
+ } else {
+ return $yourls_locale_formats->weekday_initial[ yourls__( $weekday ) ];
+ }
+}
+
+/**
+ * Return translated month abbrevation (3 letters, eg 'Nov' for 'November')
+ *
+ * The $month var can be a textual string ('November'), a integer (1 to 12), a two digits strings ('01' to '12), or an empty string
+ * If $month is an empty string, the function returns an array of all translated abbrev months ('January' => 'Jan', ...)
+ *
+ * @since 1.6
+ * @param mixed $month Empty string, a full textual weekday, eg "November", or an integer (1 = January, .., 12 = December)
+ * @return mixed Translated month abbrev (eg "Nov"), or array of all translated abbrev months
+ */
+function yourls_l10n_month_abbrev( $month = '' ){
+ global $yourls_locale_formats;
+ if( !isset( $yourls_locale_formats ) )
+ $yourls_locale_formats = new YOURLS_Locale_Formats();
+
+ if( $month === '' )
+ return $yourls_locale_formats->month_abbrev;
+
+ if( intval( $month ) > 0 ) {
+ $month = sprintf('%02d', intval( $month ) );
+ $month = $yourls_locale_formats->month[ $month ];
+ return $yourls_locale_formats->month_abbrev[ $month ];
+ } else {
+ return $yourls_locale_formats->month_abbrev[ yourls__( $month ) ];
+ }
+}
+
+/**
+ * Return array of all translated months
+ *
+ * @since 1.6
+ * @return array Array of all translated months
+ */
+function yourls_l10n_months(){
+ global $yourls_locale_formats;
+ if( !isset( $yourls_locale_formats ) )
+ $yourls_locale_formats = new YOURLS_Locale_Formats();
+
+ return $yourls_locale_formats->month;
+}
diff --git a/functions-links.php b/functions-links.php
new file mode 100644
index 0000000..3bb994b
--- /dev/null
+++ b/functions-links.php
@@ -0,0 +1,276 @@
+ '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( '&', '&', $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;
+}
diff --git a/functions-options.php b/functions-options.php
new file mode 100644
index 0000000..f2728f4
--- /dev/null
+++ b/functions-options.php
@@ -0,0 +1,195 @@
+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;
+}
diff --git a/functions-plugins.php b/functions-plugins.php
new file mode 100644
index 0000000..dab4f96
--- /dev/null
+++ b/functions-plugins.php
@@ -0,0 +1,941 @@
+ 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 $hook 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 URI:
+ * Description:
+ * Version:
+ * Author:
+ * Author URI:
+ * * /
+ *
+ * Or in the form of a phpdoc block
+ * /**
+ * * Plugin Name:
+ * * Plugin URI:
+ * * Description:
+ * * Version:
+ * * Author:
+ * * Author URI:
+ * * /
+ *
+ * @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 = ''.implode( ', ', $active_plugins ).'';
+ 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:
%s
', $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:
%s
', $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 '';
+}
diff --git a/functions-shorturls.php b/functions-shorturls.php
new file mode 100644
index 0000000..93dffb7
--- /dev/null
+++ b/functions-shorturls.php
@@ -0,0 +1,636 @@
+ '',
+ '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 );
+}
diff --git a/functions-upgrade.php b/functions-upgrade.php
new file mode 100644
index 0000000..4b48145
--- /dev/null
+++ b/functions-upgrade.php
@@ -0,0 +1,453 @@
+ 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 "
Updating DB. Please wait...
";
+ // 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 "
Unable to update the DB.
";
+ echo "
Could not change collation. You will have to fix things manually :(. The error was
+
Structure of existing tables updated. Please wait...
";
+}
+
+/**
+ * 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 "
New table index created
";
+}
+
+/**
+ * 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 "
Huho... Could not update rown with url='$url', from keyword '$keyword' to keyword '$newkeyword'
"; // 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 "
$num error(s) occured while updating the URL table :(
";
+ }
+
+ if ( $count == $chunk ) {
+ // there are probably other rows to convert
+ $from = $from + $chunk;
+ $remain = $total - $from;
+ echo "
Converted $chunk database rows ($remain remaining). Continuing... Please do not close this window until it's finished!
';
+ }
+ yourls_do_action( 'post_redirect_javascript', $location );
+}
+
+/**
+ * Return an HTTP status code
+ *
+ * @param int $code
+ * @return string
+ */
+function yourls_get_HTTP_status( $code ) {
+ $code = intval( $code );
+ $headers_desc = [
+ 100 => 'Continue',
+ 101 => 'Switching Protocols',
+ 102 => 'Processing',
+
+ 200 => 'OK',
+ 201 => 'Created',
+ 202 => 'Accepted',
+ 203 => 'Non-Authoritative Information',
+ 204 => 'No Content',
+ 205 => 'Reset Content',
+ 206 => 'Partial Content',
+ 207 => 'Multi-Status',
+ 226 => 'IM Used',
+
+ 300 => 'Multiple Choices',
+ 301 => 'Moved Permanently',
+ 302 => 'Found',
+ 303 => 'See Other',
+ 304 => 'Not Modified',
+ 305 => 'Use Proxy',
+ 306 => 'Reserved',
+ 307 => 'Temporary Redirect',
+
+ 400 => 'Bad Request',
+ 401 => 'Unauthorized',
+ 402 => 'Payment Required',
+ 403 => 'Forbidden',
+ 404 => 'Not Found',
+ 405 => 'Method Not Allowed',
+ 406 => 'Not Acceptable',
+ 407 => 'Proxy Authentication Required',
+ 408 => 'Request Timeout',
+ 409 => 'Conflict',
+ 410 => 'Gone',
+ 411 => 'Length Required',
+ 412 => 'Precondition Failed',
+ 413 => 'Request Entity Too Large',
+ 414 => 'Request-URI Too Long',
+ 415 => 'Unsupported Media Type',
+ 416 => 'Requested Range Not Satisfiable',
+ 417 => 'Expectation Failed',
+ 422 => 'Unprocessable Entity',
+ 423 => 'Locked',
+ 424 => 'Failed Dependency',
+ 426 => 'Upgrade Required',
+
+ 500 => 'Internal Server Error',
+ 501 => 'Not Implemented',
+ 502 => 'Bad Gateway',
+ 503 => 'Service Unavailable',
+ 504 => 'Gateway Timeout',
+ 505 => 'HTTP Version Not Supported',
+ 506 => 'Variant Also Negotiates',
+ 507 => 'Insufficient Storage',
+ 510 => 'Not Extended'
+ ];
+
+ return $headers_desc[$code] ?? '';
+}
+
+/**
+ * Log a redirect (for stats)
+ *
+ * This function does not check for the existence of a valid keyword, in order to save a query. Make sure the keyword
+ * exists before calling it.
+ *
+ * @since 1.4
+ * @param string $keyword short URL keyword
+ * @return mixed Result of the INSERT query (1 on success)
+ */
+function yourls_log_redirect( $keyword ) {
+ // Allow plugins to short-circuit the whole function
+ $pre = yourls_apply_filter( 'shunt_log_redirect', false, $keyword );
+ if ( false !== $pre ) {
+ return $pre;
+ }
+
+ if (!yourls_do_log_redirect()) {
+ return true;
+ }
+
+ $table = YOURLS_DB_TABLE_LOG;
+ $ip = yourls_get_IP();
+ $binds = [
+ 'now' => date( 'Y-m-d H:i:s' ),
+ 'keyword' => yourls_sanitize_keyword($keyword),
+ 'referrer' => substr( yourls_get_referrer(), 0, 200 ),
+ 'ua' => substr(yourls_get_user_agent(), 0, 255),
+ 'ip' => $ip,
+ 'location' => yourls_geo_ip_to_countrycode($ip),
+ ];
+
+ // Try and log. An error probably means a concurrency problem : just skip the logging
+ try {
+ $result = yourls_get_db()->fetchAffected("INSERT INTO `$table` (click_time, shorturl, referrer, user_agent, ip_address, country_code) VALUES (:now, :keyword, :referrer, :ua, :ip, :location)", $binds );
+ } catch (Exception $e) {
+ $result = 0;
+ }
+
+ return $result;
+}
+
+/**
+ * Check if we want to not log redirects (for stats)
+ *
+ * @return bool
+ */
+function yourls_do_log_redirect() {
+ return ( !defined( 'YOURLS_NOSTATS' ) || YOURLS_NOSTATS != true );
+}
+
+/**
+ * Check if an upgrade is needed
+ *
+ * @return bool
+ */
+function yourls_upgrade_is_needed() {
+ // check YOURLS_DB_VERSION exist && match values stored in YOURLS_DB_TABLE_OPTIONS
+ list( $currentver, $currentsql ) = yourls_get_current_version_from_sql();
+ if ( $currentsql < YOURLS_DB_VERSION ) {
+ return true;
+ }
+
+ // Check if YOURLS_VERSION exist && match value stored in YOURLS_DB_TABLE_OPTIONS, update DB if required
+ if ( $currentver < YOURLS_VERSION ) {
+ yourls_update_option( 'version', YOURLS_VERSION );
+ }
+
+ return false;
+}
+
+/**
+ * Get current version & db version as stored in the options DB. Prior to 1.4 there's no option table.
+ *
+ * @return array
+ */
+function yourls_get_current_version_from_sql() {
+ $currentver = yourls_get_option( 'version' );
+ $currentsql = yourls_get_option( 'db_version' );
+
+ // Values if version is 1.3
+ if ( !$currentver ) {
+ $currentver = '1.3';
+ }
+ if ( !$currentsql ) {
+ $currentsql = '100';
+ }
+
+ return [ $currentver, $currentsql ];
+}
+
+/**
+ * Determine if the current page is private
+ *
+ * @return bool
+ */
+function yourls_is_private() {
+ $private = defined( 'YOURLS_PRIVATE' ) && YOURLS_PRIVATE;
+
+ if ( $private ) {
+
+ // Allow overruling for particular pages:
+
+ // API
+ if ( yourls_is_API() && defined( 'YOURLS_PRIVATE_API' ) ) {
+ $private = YOURLS_PRIVATE_API;
+ }
+ // Stat pages
+ elseif ( yourls_is_infos() && defined( 'YOURLS_PRIVATE_INFOS' ) ) {
+ $private = YOURLS_PRIVATE_INFOS;
+ }
+ // Others future cases ?
+ }
+
+ return yourls_apply_filter( 'is_private', $private );
+}
+
+/**
+ * Allow several short URLs for the same long URL ?
+ *
+ * @return bool
+ */
+function yourls_allow_duplicate_longurls() {
+ // special treatment if API to check for WordPress plugin requests
+ if ( yourls_is_API() && isset( $_REQUEST[ 'source' ] ) && $_REQUEST[ 'source' ] == 'plugin' ) {
+ return false;
+ }
+
+ return yourls_apply_filter('allow_duplicate_longurls', defined('YOURLS_UNIQUE_URLS') && !YOURLS_UNIQUE_URLS);
+}
+
+/**
+ * Check if an IP shortens URL too fast to prevent DB flood. Return true, or die.
+ *
+ * @param string $ip
+ * @return bool|mixed|string
+ */
+function yourls_check_IP_flood( $ip = '' ) {
+
+ // Allow plugins to short-circuit the whole function
+ $pre = yourls_apply_filter( 'shunt_check_IP_flood', false, $ip );
+ if ( false !== $pre )
+ return $pre;
+
+ yourls_do_action( 'pre_check_ip_flood', $ip ); // at this point $ip can be '', check it if your plugin hooks in here
+
+ // Raise white flag if installing or if no flood delay defined
+ if(
+ ( defined('YOURLS_FLOOD_DELAY_SECONDS') && YOURLS_FLOOD_DELAY_SECONDS === 0 ) ||
+ !defined('YOURLS_FLOOD_DELAY_SECONDS') ||
+ yourls_is_installing()
+ )
+ return true;
+
+ // Don't throttle logged in users
+ if( yourls_is_private() ) {
+ if( yourls_is_valid_user() === true )
+ return true;
+ }
+
+ // Don't throttle whitelist IPs
+ if( defined( 'YOURLS_FLOOD_IP_WHITELIST' ) && YOURLS_FLOOD_IP_WHITELIST ) {
+ $whitelist_ips = explode( ',', YOURLS_FLOOD_IP_WHITELIST );
+ foreach( (array)$whitelist_ips as $whitelist_ip ) {
+ $whitelist_ip = trim( $whitelist_ip );
+ if ( $whitelist_ip == $ip )
+ return true;
+ }
+ }
+
+ $ip = ( $ip ? yourls_sanitize_ip( $ip ) : yourls_get_IP() );
+
+ yourls_do_action( 'check_ip_flood', $ip );
+
+ $table = YOURLS_DB_TABLE_URL;
+ $lasttime = yourls_get_db()->fetchValue( "SELECT `timestamp` FROM $table WHERE `ip` = :ip ORDER BY `timestamp` DESC LIMIT 1", [ 'ip' => $ip ] );
+ if( $lasttime ) {
+ $now = date( 'U' );
+ $then = date( 'U', strtotime( $lasttime ) );
+ if( ( $now - $then ) <= YOURLS_FLOOD_DELAY_SECONDS ) {
+ // Flood!
+ yourls_do_action( 'ip_flood', $ip, $now - $then );
+ yourls_die( yourls__( 'Too many URLs added too fast. Slow down please.' ), yourls__( 'Too Many Requests' ), 429 );
+ }
+ }
+
+ return true;
+}
+
+/**
+ * Check if YOURLS is installing
+ *
+ * @since 1.6
+ * @return bool
+ */
+function yourls_is_installing() {
+ return (bool)yourls_apply_filter( 'is_installing', defined( 'YOURLS_INSTALLING' ) && YOURLS_INSTALLING );
+}
+
+/**
+ * Check if YOURLS is upgrading
+ *
+ * @since 1.6
+ * @return bool
+ */
+function yourls_is_upgrading() {
+ return (bool)yourls_apply_filter( 'is_upgrading', defined( 'YOURLS_UPGRADING' ) && YOURLS_UPGRADING );
+}
+
+/**
+ * Check if YOURLS is installed
+ *
+ * Checks property $ydb->installed that is created by yourls_get_all_options()
+ *
+ * See inline comment for updating from 1.3 or prior.
+ *
+ * @return bool
+ */
+function yourls_is_installed() {
+ return (bool)yourls_apply_filter( 'is_installed', yourls_get_db()->is_installed() );
+}
+
+/**
+ * Set installed state
+ *
+ * @since 1.7.3
+ * @param bool $bool whether YOURLS is installed or not
+ * @return void
+ */
+function yourls_set_installed( $bool ) {
+ yourls_get_db()->set_installed( $bool );
+}
+
+/**
+ * Generate random string of (int)$length length and type $type (see function for details)
+ *
+ * @param int $length
+ * @param int $type
+ * @param string $charlist
+ * @return mixed|string
+ */
+function yourls_rnd_string ( $length = 5, $type = 0, $charlist = '' ) {
+ $length = intval( $length );
+
+ // define possible characters
+ switch ( $type ) {
+
+ // no vowels to make no offending word, no 0/1/o/l to avoid confusion between letters & digits. Perfect for passwords.
+ case '1':
+ $possible = "23456789bcdfghjkmnpqrstvwxyz";
+ break;
+
+ // Same, with lower + upper
+ case '2':
+ $possible = "23456789bcdfghjkmnpqrstvwxyzBCDFGHJKMNPQRSTVWXYZ";
+ break;
+
+ // all letters, lowercase
+ case '3':
+ $possible = "abcdefghijklmnopqrstuvwxyz";
+ break;
+
+ // all letters, lowercase + uppercase
+ case '4':
+ $possible = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
+ break;
+
+ // all digits & letters lowercase
+ case '5':
+ $possible = "0123456789abcdefghijklmnopqrstuvwxyz";
+ break;
+
+ // all digits & letters lowercase + uppercase
+ case '6':
+ $possible = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
+ break;
+
+ // custom char list, or comply to charset as defined in config
+ default:
+ case '0':
+ $possible = $charlist ? $charlist : yourls_get_shorturl_charset();
+ break;
+ }
+
+ $str = substr( str_shuffle( $possible ), 0, $length );
+ return yourls_apply_filter( 'rnd_string', $str, $length, $type, $charlist );
+}
+
+/**
+ * Check if we're in API mode.
+ *
+ * @return bool
+ */
+function yourls_is_API() {
+ return (bool)yourls_apply_filter( 'is_API', defined( 'YOURLS_API' ) && YOURLS_API );
+}
+
+/**
+ * Check if we're in Ajax mode.
+ *
+ * @return bool
+ */
+function yourls_is_Ajax() {
+ return (bool)yourls_apply_filter( 'is_Ajax', defined( 'YOURLS_AJAX' ) && YOURLS_AJAX );
+}
+
+/**
+ * Check if we're in GO mode (yourls-go.php).
+ *
+ * @return bool
+ */
+function yourls_is_GO() {
+ return (bool)yourls_apply_filter( 'is_GO', defined( 'YOURLS_GO' ) && YOURLS_GO );
+}
+
+/**
+ * Check if we're displaying stats infos (yourls-infos.php). Returns bool
+ *
+ * @return bool
+ */
+function yourls_is_infos() {
+ return (bool)yourls_apply_filter( 'is_infos', defined( 'YOURLS_INFOS' ) && YOURLS_INFOS );
+}
+
+/**
+ * Check if we're in the admin area. Returns bool. Does not relate with user rights.
+ *
+ * @return bool
+ */
+function yourls_is_admin() {
+ return (bool)yourls_apply_filter( 'is_admin', defined( 'YOURLS_ADMIN' ) && YOURLS_ADMIN );
+}
+
+/**
+ * Check if the server seems to be running on Windows. Not exactly sure how reliable this is.
+ *
+ * @return bool
+ */
+function yourls_is_windows() {
+ return defined( 'DIRECTORY_SEPARATOR' ) && DIRECTORY_SEPARATOR == '\\';
+}
+
+/**
+ * Check if SSL is required.
+ *
+ * @return bool
+ */
+function yourls_needs_ssl() {
+ return (bool)yourls_apply_filter( 'needs_ssl', defined( 'YOURLS_ADMIN_SSL' ) && YOURLS_ADMIN_SSL );
+}
+
+/**
+ * Check if SSL is used. Stolen from WP.
+ *
+ * @return bool
+ */
+function yourls_is_ssl() {
+ $is_ssl = false;
+ if ( isset( $_SERVER[ 'HTTPS' ] ) ) {
+ if ( 'on' == strtolower( $_SERVER[ 'HTTPS' ] ) ) {
+ $is_ssl = true;
+ }
+ if ( '1' == $_SERVER[ 'HTTPS' ] ) {
+ $is_ssl = true;
+ }
+ }
+ elseif ( isset( $_SERVER[ 'HTTP_X_FORWARDED_PROTO' ] ) ) {
+ if ( 'https' == strtolower( $_SERVER[ 'HTTP_X_FORWARDED_PROTO' ] ) ) {
+ $is_ssl = true;
+ }
+ }
+ elseif ( isset( $_SERVER[ 'SERVER_PORT' ] ) && ( '443' == $_SERVER[ 'SERVER_PORT' ] ) ) {
+ $is_ssl = true;
+ }
+ return (bool)yourls_apply_filter( 'is_ssl', $is_ssl );
+}
+
+/**
+ * Get a remote page title
+ *
+ * This function returns a string: either the page title as defined in HTML, or the URL if not found
+ * The function tries to convert funky characters found in titles to UTF8, from the detected charset.
+ * Charset in use is guessed from HTML meta tag, or if not found, from server's 'content-type' response.
+ *
+ * @param string $url URL
+ * @return string Title (sanitized) or the URL if no title found
+ */
+function yourls_get_remote_title( $url ) {
+ // Allow plugins to short-circuit the whole function
+ $pre = yourls_apply_filter( 'shunt_get_remote_title', false, $url );
+ if ( false !== $pre ) {
+ return $pre;
+ }
+
+ $url = yourls_sanitize_url( $url );
+
+ // Only deal with http(s)://
+ if ( !in_array( yourls_get_protocol( $url ), [ 'http://', 'https://' ] ) ) {
+ return $url;
+ }
+
+ $title = $charset = false;
+
+ $max_bytes = yourls_apply_filter( 'get_remote_title_max_byte', 32768 ); // limit data fetching to 32K in order to find a tag
+
+ $response = yourls_http_get( $url, [], [], [ 'max_bytes' => $max_bytes ] ); // can be a Request object or an error string
+ if ( is_string( $response ) ) {
+ return $url;
+ }
+
+ // Page content. No content? Return the URL
+ $content = $response->body;
+ if ( !$content ) {
+ return $url;
+ }
+
+ // look for . No title found? Return the URL
+ if ( preg_match( '/(.*?)<\/title>/is', $content, $found ) ) {
+ $title = $found[ 1 ];
+ unset( $found );
+ }
+ if ( !$title ) {
+ return $url;
+ }
+
+ // Now we have a title. We'll try to get proper utf8 from it.
+
+ // Get charset as (and if) defined by the HTML meta tag. We should match
+ //
+ // or and all possible variations: see https://gist.github.com/ozh/7951236
+ if ( preg_match( '/]*charset\s*=["\' ]*([a-zA-Z0-9\-_]+)/is', $content, $found ) ) {
+ $charset = $found[ 1 ];
+ unset( $found );
+ }
+ else {
+ // No charset found in HTML. Get charset as (and if) defined by the server response
+ $_charset = current( $response->headers->getValues( 'content-type' ) );
+ if ( preg_match( '/charset=(\S+)/', $_charset, $found ) ) {
+ $charset = trim( $found[ 1 ], ';' );
+ unset( $found );
+ }
+ }
+
+ // Conversion to utf-8 if what we have is not utf8 already
+ if ( strtolower( $charset ) != 'utf-8' && function_exists( 'mb_convert_encoding' ) ) {
+ // We use @ to remove warnings because mb_ functions are easily bitching about illegal chars
+ if ( $charset ) {
+ $title = @mb_convert_encoding( $title, 'UTF-8', $charset );
+ }
+ else {
+ $title = @mb_convert_encoding( $title, 'UTF-8' );
+ }
+ }
+
+ // Remove HTML entities
+ $title = html_entity_decode( $title, ENT_QUOTES, 'UTF-8' );
+
+ // Strip out evil things
+ $title = yourls_sanitize_title( $title, $url );
+
+ return (string)yourls_apply_filter( 'get_remote_title', $title, $url );
+}
+
+/**
+ * Quick UA check for mobile devices.
+ *
+ * @return bool
+ */
+function yourls_is_mobile_device() {
+ // Strings searched
+ $mobiles = [
+ 'android', 'blackberry', 'blazer',
+ 'compal', 'elaine', 'fennec', 'hiptop',
+ 'iemobile', 'iphone', 'ipod', 'ipad',
+ 'iris', 'kindle', 'opera mobi', 'opera mini',
+ 'palm', 'phone', 'pocket', 'psp', 'symbian',
+ 'treo', 'wap', 'windows ce', 'windows phone'
+ ];
+
+ // Current user-agent
+ $current = strtolower( $_SERVER['HTTP_USER_AGENT'] );
+
+ // Check and return
+ $is_mobile = ( str_replace( $mobiles, '', $current ) != $current );
+ return (bool)yourls_apply_filter( 'is_mobile_device', $is_mobile );
+}
+
+/**
+ * Get request in YOURLS base (eg in 'http://sho.rt/yourls/abcd' get 'abdc')
+ *
+ * With no parameter passed, this function will guess current page and consider
+ * it is the requested page.
+ * For testing purposes, parameters can be passed.
+ *
+ * @since 1.5
+ * @param string $yourls_site Optional, YOURLS installation URL (default to constant YOURLS_SITE)
+ * @param string $uri Optional, page requested (default to $_SERVER['REQUEST_URI'] eg '/yourls/abcd' )
+ * @return string request relative to YOURLS base (eg 'abdc')
+ */
+function yourls_get_request($yourls_site = '', $uri = '') {
+ // Allow plugins to short-circuit the whole function
+ $pre = yourls_apply_filter( 'shunt_get_request', false );
+ if ( false !== $pre ) {
+ return $pre;
+ }
+
+ yourls_do_action( 'pre_get_request', $yourls_site, $uri );
+
+ // Default values
+ if ( '' === $yourls_site ) {
+ $yourls_site = yourls_get_yourls_site();
+ }
+ if ( '' === $uri ) {
+ $uri = $_SERVER[ 'REQUEST_URI' ];
+ }
+
+ // Even though the config sample states YOURLS_SITE should be set without trailing slash...
+ $yourls_site = rtrim( $yourls_site, '/' );
+
+ // Now strip the YOURLS_SITE path part out of the requested URI, and get the request relative to YOURLS base
+ // +---------------------------+-------------------------+---------------------+--------------+
+ // | if we request | and YOURLS is hosted on | YOURLS path part is | "request" is |
+ // +---------------------------+-------------------------+---------------------+--------------+
+ // | http://sho.rt/abc | http://sho.rt | / | abc |
+ // | https://SHO.rt/subdir/abc | https://shor.rt/subdir/ | /subdir/ | abc |
+ // +---------------------------+-------------------------+---------------------+--------------+
+ // and so on. You can find various test cases in /tests/tests/utilities/get_request.php
+
+ // Take only the URL_PATH part of YOURLS_SITE (ie "https://sho.rt:1337/path/to/yourls" -> "/path/to/yourls")
+ $yourls_site = parse_url( $yourls_site, PHP_URL_PATH ).'/';
+
+ // Strip path part from request if exists
+ $request = $uri;
+ if ( substr( $uri, 0, strlen( $yourls_site ) ) == $yourls_site ) {
+ $request = ltrim( substr( $uri, strlen( $yourls_site ) ), '/' );
+ }
+
+ // Unless request looks like a full URL (ie request is a simple keyword) strip query string
+ if ( !preg_match( "@^[a-zA-Z]+://.+@", $request ) ) {
+ $request = current( explode( '?', $request ) );
+ }
+
+ $request = yourls_sanitize_url( $request );
+
+ return (string)yourls_apply_filter( 'get_request', $request );
+}
+
+/**
+ * Fix $_SERVER['REQUEST_URI'] variable for various setups. Stolen from WP.
+ *
+ * @return void
+ */
+function yourls_fix_request_uri() {
+
+ $default_server_values = [
+ 'SERVER_SOFTWARE' => '',
+ 'REQUEST_URI' => '',
+ ];
+ $_SERVER = array_merge( $default_server_values, $_SERVER );
+
+ // Fix for IIS when running with PHP ISAPI
+ if ( empty( $_SERVER[ 'REQUEST_URI' ] ) || ( php_sapi_name() != 'cgi-fcgi' && preg_match( '/^Microsoft-IIS\//', $_SERVER[ 'SERVER_SOFTWARE' ] ) ) ) {
+
+ // IIS Mod-Rewrite
+ if ( isset( $_SERVER[ 'HTTP_X_ORIGINAL_URL' ] ) ) {
+ $_SERVER[ 'REQUEST_URI' ] = $_SERVER[ 'HTTP_X_ORIGINAL_URL' ];
+ }
+ // IIS Isapi_Rewrite
+ elseif ( isset( $_SERVER[ 'HTTP_X_REWRITE_URL' ] ) ) {
+ $_SERVER[ 'REQUEST_URI' ] = $_SERVER[ 'HTTP_X_REWRITE_URL' ];
+ }
+ else {
+ // Use ORIG_PATH_INFO if there is no PATH_INFO
+ if ( !isset( $_SERVER[ 'PATH_INFO' ] ) && isset( $_SERVER[ 'ORIG_PATH_INFO' ] ) ) {
+ $_SERVER[ 'PATH_INFO' ] = $_SERVER[ 'ORIG_PATH_INFO' ];
+ }
+
+ // Some IIS + PHP configurations puts the script-name in the path-info (No need to append it twice)
+ if ( isset( $_SERVER[ 'PATH_INFO' ] ) ) {
+ if ( $_SERVER[ 'PATH_INFO' ] == $_SERVER[ 'SCRIPT_NAME' ] ) {
+ $_SERVER[ 'REQUEST_URI' ] = $_SERVER[ 'PATH_INFO' ];
+ }
+ else {
+ $_SERVER[ 'REQUEST_URI' ] = $_SERVER[ 'SCRIPT_NAME' ].$_SERVER[ 'PATH_INFO' ];
+ }
+ }
+
+ // Append the query string if it exists and isn't null
+ if ( !empty( $_SERVER[ 'QUERY_STRING' ] ) ) {
+ $_SERVER[ 'REQUEST_URI' ] .= '?'.$_SERVER[ 'QUERY_STRING' ];
+ }
+ }
+ }
+}
+
+/**
+ * Check for maintenance mode. If yes, die. See yourls_maintenance_mode(). Stolen from WP.
+ *
+ * @return void
+ */
+function yourls_check_maintenance_mode() {
+ $file = YOURLS_ABSPATH . '/.maintenance' ;
+
+ if ( !file_exists( $file ) || yourls_is_upgrading() || yourls_is_installing() ) {
+ return;
+ }
+
+ global $maintenance_start;
+ include_once( $file );
+ // If the $maintenance_start timestamp is older than 10 minutes, don't die.
+ if ( ( time() - $maintenance_start ) >= 600 ) {
+ return;
+ }
+
+ // Use any /user/maintenance.php file
+ if( file_exists( YOURLS_USERDIR.'/maintenance.php' ) ) {
+ include_once( YOURLS_USERDIR.'/maintenance.php' );
+ die();
+ }
+
+ // Or use the default messages
+ $title = yourls__( 'Service temporarily unavailable' );
+ $message = yourls__( 'Our service is currently undergoing scheduled maintenance.' ) . "
\n
" .
+ yourls__( 'Things should not last very long, thank you for your patience and please excuse the inconvenience' );
+ yourls_die( $message, $title , 503 );
+}
+
+/**
+ * Check if a URL protocol is allowed
+ *
+ * Checks a URL against a list of whitelisted protocols. Protocols must be defined with
+ * their complete scheme name, ie 'stuff:' or 'stuff://' (for instance, 'mailto:' is a valid
+ * protocol, 'mailto://' isn't, and 'http:' with no double slashed isn't either
+ *
+ * @since 1.6
+ * @see yourls_get_protocol()
+ *
+ * @param string $url URL to be check
+ * @param array $protocols Optional. Array of protocols, defaults to global $yourls_allowedprotocols
+ * @return bool true if protocol allowed, false otherwise
+ */
+function yourls_is_allowed_protocol( $url, $protocols = [] ) {
+ if ( empty( $protocols ) ) {
+ global $yourls_allowedprotocols;
+ $protocols = $yourls_allowedprotocols;
+ }
+
+ return yourls_apply_filter( 'is_allowed_protocol', in_array( yourls_get_protocol( $url ), $protocols ), $url, $protocols );
+}
+
+/**
+ * Get protocol from a URL (eg mailto:, http:// ...)
+ *
+ * What we liberally call a "protocol" in YOURLS is the scheme name + colon + double slashes if present of a URI. Examples:
+ * "something://blah" -> "something://"
+ * "something:blah" -> "something:"
+ * "something:/blah" -> "something:"
+ *
+ * Unit Tests for this function are located in tests/format/urls.php
+ *
+ * @since 1.6
+ *
+ * @param string $url URL to be check
+ * @return string Protocol, with slash slash if applicable. Empty string if no protocol
+ */
+function yourls_get_protocol( $url ) {
+ /*
+ http://en.wikipedia.org/wiki/URI_scheme#Generic_syntax
+ The scheme name consists of a sequence of characters beginning with a letter and followed by any
+ combination of letters, digits, plus ("+"), period ("."), or hyphen ("-"). Although schemes are
+ case-insensitive, the canonical form is lowercase and documents that specify schemes must do so
+ with lowercase letters. It is followed by a colon (":").
+ */
+ preg_match( '!^[a-zA-Z][a-zA-Z0-9+.-]+:(//)?!', $url, $matches );
+ return (string)yourls_apply_filter( 'get_protocol', isset( $matches[0] ) ? $matches[0] : '', $url );
+}
+
+/**
+ * Get relative URL (eg 'abc' from 'http://sho.rt/abc')
+ *
+ * Treat indifferently http & https. If a URL isn't relative to the YOURLS install, return it as is
+ * or return empty string if $strict is true
+ *
+ * @since 1.6
+ * @param string $url URL to relativize
+ * @param bool $strict if true and if URL isn't relative to YOURLS install, return empty string
+ * @return string URL
+ */
+function yourls_get_relative_url( $url, $strict = true ) {
+ $url = yourls_sanitize_url( $url );
+
+ // Remove protocols to make it easier
+ $noproto_url = str_replace( 'https:', 'http:', $url );
+ $noproto_site = str_replace( 'https:', 'http:', yourls_get_yourls_site() );
+
+ // Trim URL from YOURLS root URL : if no modification made, URL wasn't relative
+ $_url = str_replace( $noproto_site.'/', '', $noproto_url );
+ if ( $_url == $noproto_url ) {
+ $_url = ( $strict ? '' : $url );
+ }
+ return yourls_apply_filter( 'get_relative_url', $_url, $url );
+}
+
+/**
+ * Marks a function as deprecated and informs that it has been used. Stolen from WP.
+ *
+ * There is a hook deprecated_function that will be called that can be used
+ * to get the backtrace up to what file and function called the deprecated
+ * function.
+ *
+ * The current behavior is to trigger a user error if YOURLS_DEBUG is true.
+ *
+ * This function is to be used in every function that is deprecated.
+ *
+ * @since 1.6
+ * @uses yourls_do_action() Calls 'deprecated_function' and passes the function name, what to use instead,
+ * and the version the function was deprecated in.
+ * @uses yourls_apply_filter() Calls 'deprecated_function_trigger_error' and expects boolean value of true to do
+ * trigger or false to not trigger error.
+ *
+ * @param string $function The function that was called
+ * @param string $version The version of WordPress that deprecated the function
+ * @param string $replacement Optional. The function that should have been called
+ * @return void
+ */
+function yourls_deprecated_function( $function, $version, $replacement = null ) {
+
+ yourls_do_action( 'deprecated_function', $function, $replacement, $version );
+
+ // Allow plugin to filter the output error trigger
+ if ( yourls_get_debug_mode() && yourls_apply_filter( 'deprecated_function_trigger_error', true ) ) {
+ if ( ! is_null( $replacement ) )
+ trigger_error( sprintf( yourls__('%1$s is deprecated since version %2$s! Use %3$s instead.'), $function, $version, $replacement ) );
+ else
+ trigger_error( sprintf( yourls__('%1$s is deprecated since version %2$s with no alternative available.'), $function, $version ) );
+ }
+}
+
+/**
+ * Explode a URL in an array of ( 'protocol' , 'slashes if any', 'rest of the URL' )
+ *
+ * Some hosts trip up when a query string contains 'http://' - see http://git.io/j1FlJg
+ * The idea is that instead of passing the whole URL to a bookmarklet, eg index.php?u=http://blah.com,
+ * we pass it by pieces to fool the server, eg index.php?proto=http:&slashes=//&rest=blah.com
+ *
+ * Known limitation: this won't work if the rest of the URL itself contains 'http://', for example
+ * if rest = blah.com/file.php?url=http://foo.com
+ *
+ * Sample returns:
+ *
+ * with 'mailto:jsmith@example.com?subject=hey' :
+ * array( 'protocol' => 'mailto:', 'slashes' => '', 'rest' => 'jsmith@example.com?subject=hey' )
+ *
+ * with 'http://example.com/blah.html' :
+ * array( 'protocol' => 'http:', 'slashes' => '//', 'rest' => 'example.com/blah.html' )
+ *
+ * @since 1.7
+ * @param string $url URL to be parsed
+ * @param array $array Optional, array of key names to be used in returned array
+ * @return array|false false if no protocol found, array of ('protocol' , 'slashes', 'rest') otherwise
+ */
+function yourls_get_protocol_slashes_and_rest( $url, $array = [ 'protocol', 'slashes', 'rest' ] ) {
+ $proto = yourls_get_protocol( $url );
+
+ if ( !$proto or count( $array ) != 3 ) {
+ return false;
+ }
+
+ list( $null, $rest ) = explode( $proto, $url, 2 );
+
+ list( $proto, $slashes ) = explode( ':', $proto );
+
+ return [
+ $array[ 0 ] => $proto.':',
+ $array[ 1 ] => $slashes,
+ $array[ 2 ] => $rest
+ ];
+}
+
+/**
+ * Set URL scheme (HTTP or HTTPS) to a URL
+ *
+ * @since 1.7.1
+ * @param string $url URL
+ * @param string $scheme scheme, either 'http' or 'https'
+ * @return string URL with chosen scheme
+ */
+function yourls_set_url_scheme( $url, $scheme = '' ) {
+ if ( in_array( $scheme, [ 'http', 'https' ] ) ) {
+ $url = preg_replace( '!^[a-zA-Z0-9+.-]+://!', $scheme.'://', $url );
+ }
+ return $url;
+}
+
+/**
+ * Tell if there is a new YOURLS version
+ *
+ * This function checks, if needed, if there's a new version of YOURLS and, if applicable, displays
+ * an update notice.
+ *
+ * @since 1.7.3
+ * @return void
+ */
+function yourls_tell_if_new_version() {
+ yourls_debug_log( 'Check for new version: '.( yourls_maybe_check_core_version() ? 'yes' : 'no' ) );
+ yourls_new_core_version_notice(YOURLS_VERSION);
+}
diff --git a/load-yourls.php b/load-yourls.php
new file mode 100644
index 0000000..ae630b1
--- /dev/null
+++ b/load-yourls.php
@@ -0,0 +1,26 @@
+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);
diff --git a/version.php b/version.php
new file mode 100644
index 0000000..48a95e5
--- /dev/null
+++ b/version.php
@@ -0,0 +1,20 @@
+