<?php

/**
 * Aura SQL wrapper for YOURLS that creates the allmighty YDB object.
 *
 * A fine example of a "class that knows too much" (see https://en.wikipedia.org/wiki/God_object)
 *
 * Note to plugin authors: you most likely SHOULD NOT use directly methods and properties of this class. Use instead
 * function wrappers (eg don't use $ydb->option, or $ydb->set_option(), use yourls_*_options() functions instead).
 *
 * @since 1.7.3
 */

namespace YOURLS\Database;

use YOURLS\Admin\Logger;
use Aura\Sql\ExtendedPdo;
use PDO;

class YDB extends ExtendedPdo {

    /**
     * Debug mode, default false
     * @var bool
     */
    protected $debug = false;

    /**
     * Page context (ie "infos", "bookmark", "plugins"...)
     * @var string
     */
    protected $context = '';

    /**
     * Information related to a short URL keyword (eg timestamp, long URL, ...)
     *
     * @var array
     *
     */
    protected $infos = array();

    /**
     * Is YOURLS installed and ready to run?
     * @var bool
     */
    protected $installed = false;

    /**
     * Options
     * @var array
     */
    protected $option = array();

    /**
     * Plugin admin pages informations
     * @var array
     */
    protected $plugin_pages = array();

    /**
     * Plugin informations
     * @var array
     */
    protected $plugins = array();

    /**
     * Are we emulating prepare statements ?
     * @var bool
     */
    protected $is_emulate_prepare;

    /**
     * @since 1.7.3
     * @param string $dsn         The data source name
     * @param string $user        The username
     * @param string $pass        The password
     * @param array  $options     Driver-specific options
     * @param array  $attributes  Attributes to set after a connection
     */
    public function __construct($dsn, $user, $pass, $options, $attributes) {
        parent::__construct($dsn, $user, $pass, $options, $attributes);
    }

    /**
     * Init everything needed
     *
     * Everything we need to set up is done here in init(), not in the constructor, so even
     * when the connection fails (eg config error or DB dead), the constructor has worked
     * and we have a $ydb object properly instantiated (and for instance yourls_die() can
     * correctly die, even if using $ydb methods)
     *
     * @since  1.7.3
     * @return void
     */
    public function init() {
        $this->connect_to_DB();

        $this->set_emulate_state();

        $this->start_profiler();
    }

    /**
     * Check if we emulate prepare statements, and set bool flag accordingly
     *
     * Check if current driver can PDO::getAttribute(PDO::ATTR_EMULATE_PREPARES)
     * Some combinations of PHP/MySQL don't support this function. See
     * https://travis-ci.org/YOURLS/YOURLS/jobs/271423782#L481
     *
     * @since  1.7.3
     * @return void
     */
    public function set_emulate_state() {
        try {
            $this->is_emulate_prepare = $this->getAttribute(PDO::ATTR_EMULATE_PREPARES);
        } catch (\PDOException $e) {
            $this->is_emulate_prepare = false;
        }
    }

    /**
     * Get emulate status
     *
     * @since  1.7.3
     * @return bool
     */
    public function get_emulate_state() {
        return $this->is_emulate_prepare;
    }

    /**
     * Initiate real connection to DB server
     *
     * This is to check that the server is running and/or the config is OK
     *
     * @since  1.7.3
     * @return void
     * @throws \PDOException
     */
    public function connect_to_DB() {
        try {
            $this->connect();
        } catch ( \Exception $e ) {
            $this->dead_or_error($e);
        }
    }

    /**
     * Die with an error message
     *
     * @since  1.7.3
     *
     * @param \Exception $exception
     *
     * @return void
     */
    public function dead_or_error(\Exception $exception) {
        // Use any /user/db_error.php file
        if( file_exists( YOURLS_USERDIR . '/db_error.php' ) ) {
            include_once( YOURLS_USERDIR . '/db_error.php' );
            die();
        }

        $message  = yourls__( 'Incorrect DB config, or could not connect to DB' );
        $message .= '<br/>' . get_class($exception) .': ' . $exception->getMessage();

        yourls_die( yourls__( $message ), yourls__( 'Fatal error' ), 503 );
        die();
    }

    /**
     * Start a Message Logger
     *
     * @since  1.7.3
     * @see    \YOURLS\Admin\Logger
     * @see    \Aura\Sql\Profiler
     * @return void
     */
    public function start_profiler() {
        $this->profiler = new Logger($this);
    }

    /**
     * @param string $context
     */
    public function set_html_context($context) {
        $this->context = $context;
    }

    /**
     * @return string
     */
    public function get_html_context() {
        return $this->context;
    }

    // Options low level functions, see \YOURLS\Database\Options

    /**
     * @param string $name
     * @param mixed  $value
     */
    public function set_option($name, $value) {
        $this->option[$name] = $value;
    }

    /**
     * @param  string $name
     * @return bool
     */
    public function has_option($name) {
        return array_key_exists($name, $this->option);
    }

    /**
     * @param  string $name
     * @return string
     */
    public function get_option($name) {
        return $this->option[$name];
    }

    /**
     * @param string $name
     */
    public function delete_option($name) {
        unset($this->option[$name]);
    }

    // Infos (related to keyword) low level functions

    /**
     * @param string $keyword
     * @param mixed  $infos
     */
    public function set_infos($keyword, $infos) {
        $this->infos[$keyword] = $infos;
    }

    /**
     * @param  string $keyword
     * @return bool
     */
    public function has_infos($keyword) {
        return array_key_exists($keyword, $this->infos);
    }

    /**
     * @param  string $keyword
     * @return array
     */
    public function get_infos($keyword) {
        return $this->infos[$keyword];
    }

    /**
     * @param string $keyword
     */
    public function delete_infos($keyword) {
        unset($this->infos[$keyword]);
    }

    /**
     * @todo: infos & options are working the same way here. Abstract this.
     */

    // Plugin low level functions, see functions-plugins.php

    /**
     * @return array
     */
    public function get_plugins() {
        return $this->plugins;
    }

    /**
     * @param array $plugins
     */
    public function set_plugins(array $plugins) {
        $this->plugins = $plugins;
    }

    /**
     * @param string $plugin  plugin filename
     */
    public function add_plugin($plugin) {
        $this->plugins[] = $plugin;
    }

    /**
     * @param string $plugin  plugin filename
     */
    public function remove_plugin($plugin) {
        unset($this->plugins[$plugin]);
    }

    // Plugin Pages low level functions, see functions-plugins.php

    /**
     * @return array
     */
    public function get_plugin_pages() {
        return $this->plugin_pages;
    }

    /**
     * @param array $pages
     */
    public function set_plugin_pages(array $pages) {
        $this->plugin_pages = $pages;
    }

    /**
     * @param string   $slug
     * @param string   $title
     * @param callable $function
     */
    public function add_plugin_page($slug, $title, $function) {
        $this->plugin_pages[$slug] = array(
            'slug'     => $slug,
            'title'    => $title,
            'function' => $function,
        );
    }

    /**
     * @param string $slug
     */
    public function remove_plugin_page($slug) {
        unset($this->plugin_pages[$slug]);
    }

    /**
     * Return count of SQL queries performed
     *
     * @since  1.7.3
     * @return int
     */
    public function get_num_queries() {
        return count( (array) $this->get_queries() );
    }

    /**
     * Return SQL queries performed
     *
     * Aura\Sql\Profiler logs every PDO command issued. But depending on PDO::ATTR_EMULATE_PREPARES, some are
     * actually sent to the mysql server or not :
     *  - if PDO::ATTR_EMULATE_PREPARES is true, prepare() statements are not sent to the server and are performed
     *    internally, so they are removed from the logger
     *  - if PDO::ATTR_EMULATE_PREPARES is false, prepare() statements are actually performed by the mysql server,
     *    and count as an actual query
     *
     * Resulting array is something like:
     *   array (
     *      0 => array (
     *           'duration' => 1.0010569095611572265625,
     *           'function' => 'connect',
     *           'statement' => NULL,
     *           'bind_values' => array (),
     *           'trace' => ...back trace...,
     *       ),
     *       // key index might not be sequential if 'prepare' function are filtered out
     *       2 => array (
     *           'duration' => 0.000999927520751953125,
     *           'function' => 'perform',
     *           'statement' => 'SELECT option_value FROM yourls_options WHERE option_name = :option_name LIMIT 1',
     *           'bind_values' => array ( 'option_name' => 'test_option' ),
     *           'trace' => ...back trace...,
     *       ),
     *   );
     *
     * @since  1.7.3
     * @return array
     */
    public function get_queries() {
        $queries = $this->getProfiler()->getProfiles();

        if ($this->get_emulate_state()) {
            // keep queries if $query['function'] != 'prepare'
            $queries = array_filter($queries, function($query) {return $query['function'] !== 'prepare';});
        }

        return $queries;
    }

    /**
     * Set YOURLS installed state
     *
     * @since  1.7.3
     * @param  bool $bool
     * @return void
     */
    public function set_installed($bool) {
        $this->installed = $bool;
    }

    /**
     * Get YOURLS installed state
     *
     * @since  1.7.3
     * @return bool
     */
    public function is_installed() {
        return $this->installed;
    }

    /**
     * Return standardized DB version
     *
     * The regex removes everything that's not a number at the start of the string, or remove anything that's not a number and what
     * follows after that.
     *   'omgmysql-5.5-ubuntu-4.20' => '5.5'
     *   'mysql5.5-ubuntu-4.20'     => '5.5'
     *   '5.5-ubuntu-4.20'          => '5.5'
     *   '5.5-beta2'                => '5.5'
     *   '5.5'                      => '5.5'
     *
     * @since  1.7.3
     * @return string
     */
    public function mysql_version() {
        $version = $this->pdo->getAttribute(PDO::ATTR_SERVER_VERSION);
        return $version;
    }

    /**
     * Deprecated properties since 1.7.3, unused in 3rd party plugins as far as I know
     *
     * $ydb->DB_driver
     * $ydb->captured_errors
     * $ydb->dbh
     * $ydb->result
     * $ydb->rows_affected
     * $ydb->show_errors
     */

    /**
     * Deprecated functions since 1.7.3
     */

    // @codeCoverageIgnoreStart

    public function escape($string) {
        yourls_deprecated_function( '$ydb->'.__FUNCTION__, '1.7.3', 'PDO' );
        // This will escape using PDO->quote(), but then remove the enclosing quotes
        return substr($this->quote($string), 1, -1);
    }

    public function get_col($query) {
        yourls_deprecated_function( '$ydb->'.__FUNCTION__, '1.7.3', 'PDO' );
        yourls_debug_log('LEGACY SQL: '.$query);
        return $this->fetchCol($query);
    }

    public function get_results($query) {
        yourls_deprecated_function( '$ydb->'.__FUNCTION__, '1.7.3', 'PDO' );
        yourls_debug_log('LEGACY SQL: '.$query);
        $stm = parent::query($query);
        return($stm->fetchAll(PDO::FETCH_OBJ));
    }

    public function get_row($query) {
        yourls_deprecated_function( '$ydb->'.__FUNCTION__, '1.7.3', 'PDO' );
        yourls_debug_log('LEGACY SQL: '.$query);
        $row = $this->fetchObjects($query);
        return isset($row[0]) ? $row[0] : null;
    }

    public function get_var($query) {
        yourls_deprecated_function( '$ydb->'.__FUNCTION__, '1.7.3', 'PDO' );
        yourls_debug_log('LEGACY SQL: '.$query);
        return $this->fetchValue($query);
    }

    public function query($query) {
        yourls_deprecated_function( '$ydb->'.__FUNCTION__, '1.7.3', 'PDO' );
        yourls_debug_log('LEGACY SQL: '.$query);
        return $this->fetchAffected($query);
    }
    // @codeCoverageIgnoreEnd
}


<?php

/**
 * YOURLS simple logger
 *
 * @since 1.7.3
 */

namespace YOURLS\Admin;

use YOURLS\Database\YDB;

class Logger extends \Aura\Sql\Profiler {

    /**
     * the YDB instance
     * @var YDB
     */
    protected $ydb;

    /**
     * Debug log where messages are stored
     * @var array
     */
    protected $debug_log = array();

    public function __construct(YDB $ydb) {
        $this->ydb = $ydb;
        /**
         * @todo Extend this to allow calling an external logger
         */
    }

    /**
     * @param string $message
     */
    public function log($message) {
        yourls_do_action('debug_log', $message);
        $this->debug_log[] = $message;
    }

    public function get_log() {
        return $this->debug_log;
    }

    /**
     * Extends \Aura\Sql\Profiler::addProfile() to log queries in our YOURLS logger
     *
     * @since  1.7.3
     * @param  float  $duration     The query duration.
     * @param  string $function     The PDO method that made the entry.
     * @param  string $statement    The SQL query statement.
     * @param  array  $bind_values  The values bound to the statement.
     * @return void
     */
    public function addProfile($duration, $function, $statement, array $bind_values = array() ) {
        parent::addProfile($duration, $function, $statement, $bind_values);

        if($function == 'connect') {
            $this->log( sprintf('SQL: CONNECT (%s s)', number_format($duration, 5)) );
            return;
        }

        // If we are emulating prepare, don't log 'prepare' statement, as they are not actual queries sent to the server
        // See YDB::get_queries() for details
        if ($this->ydb->get_emulate_state() && $function !== 'prepare') {
            $this->log( sprintf('SQL: %s (%s s)', $this->pretty_format($statement, $bind_values), number_format($duration, 5) ) );
        }
    }

    /**
     * Format PDO statement with bind/values replacement
     *
     * This replaces PDO binds such as 'key_name = :name' with corresponding array values, eg array('name'=>'some value')
     * This is merely a cosmetic replacement to allow for readability: the result WILL NOT be valid SQL! (eg no proper quotes)
     *
     * @since  1.7.3
     * @param  string $statement  SQL query with PDO style named placeholders
     * @param  array  $values     Optional array of values corresponding to placeholders
     * @return string             Readable SQL query with placeholders replaced
     */
    public function pretty_format($statement, array $values = array() ) {
        if (!$values) {
            return $statement;
        }

        return preg_replace_callback(
            '/:([^\s;)]*)/',

            /**
             * @param string $matches
             */
            function ($matches) use ($values) {
                $replacement = isset( $values[$matches[1]] ) ? $values[$matches[1]] : '';
                if(is_array($replacement)) {
                    $replacement = implode(",", $replacement);
                }
                return "'$replacement'";
            },
            $statement
        );
    }

}


<?php
/*************************************************************************/
/* This class stores associative arrays in an xml formatted string.       */
/* There's also a function thar retrieves them. If you try to use        */
/* xml2array with a general xml, it can fail, since there can be some    */
/* repeated indexes....                                                  */
/* Source: http://www.phpclasses.org/browse/package/2286/                */
/*************************************************************************/

// @codeCoverageIgnoreStart

class yourls_array2xml {
	var $text;
	var $arrays, $keys, $node_flag, $depth, $xml_parser;
	/*Converts an array to an xml string*/
	function array2xml($array) {
	//global $text;
	$this->text="<?xml version=\"1.0\" encoding=\"iso-8859-1\"?><result>";
	$this->text.= $this->array_transform($array);
	$this->text .="</result>";
	return $this->text;
	}

	function array_transform($array){
	//global $array_text;
	foreach($array as $key => $value){
	if(!is_array($value)){
	       //BEGIN code mod by Doug Vanderweide, 13 Jan 2011
	       //does $value contain html entities?
	       if(strlen($value) != strlen(htmlentities($value))) {
	               //if so, encode as CDATA
	               $value = "<![CDATA[" . htmlentities($value) . "]]>";
	       }
	       $this->text .= "<$key>$value</$key>";
	       //END code mod
	 } else {
	 $this->text.="<$key>";
	 $this->array_transform($value);
	 $this->text.="</$key>";
	 }
	}
	//return $array_text;

	}
	/*Transform an XML string to associative array "XML Parser Functions"*/
	function xml2array($xml){
	$this->depth=-1;
	$this->xml_parser = xml_parser_create();
	xml_set_object($this->xml_parser, $this);
	xml_parser_set_option ($this->xml_parser,XML_OPTION_CASE_FOLDING,0);//Don't put tags uppercase
	xml_set_element_handler($this->xml_parser, "startElement", "endElement");
	xml_set_character_data_handler($this->xml_parser,"characterData");
	xml_parse($this->xml_parser,$xml,true);
	xml_parser_free($this->xml_parser);
	return $this->arrays[0];

	}
	function startElement($parser, $name, $attrs)
	 {
	   $this->keys[]=$name; //We add a key
	   $this->node_flag=1;
	   $this->depth++;
	 }
	function characterData($parser,$data)
	 {
	   $key=end($this->keys);
	   $this->arrays[$this->depth][$key]=$data;
	   $this->node_flag=0; //So that we don't add as an array, but as an element
	 }
	function endElement($parser, $name)
	 {
	   $key=array_pop($this->keys);
	   //If $node_flag==1 we add as an array, if not, as an element
	   if($this->node_flag==1){
	     $this->arrays[$this->depth][$key]=$this->arrays[$this->depth+1];
	     unset($this->arrays[$this->depth+1]);
	   }
	   $this->node_flag=1;
	   $this->depth--;
	 }

}//End of the class

// @codeCoverageIgnoreEnd


<?php
/**
 * YOURLS Translation API
 *
 * YOURLS modification of a small subset from WordPress' Translation API implementation.
 * GPL License
 *
 * @package POMO
 * @subpackage i18n
 */

/**
 * Load POMO files required to run library
 */
use \POMO\MO;
use POMO\Translations\NOOPTranslations;

/**
 * Gets the current locale.
 *
 * If the locale is set, then it will filter the locale in the 'get_locale' filter
 * hook and return the value.
 *
 * If the locale is not set already, then the YOURLS_LANG constant is used if it is
 * defined. Then it is filtered through the 'get_locale' filter hook and the value
 * for the locale global set and the locale is returned.
 *
 * The process to get the locale should only be done once, but the locale will
 * always be filtered using the 'get_locale' hook.
 *
 * @since 1.6
 * @uses yourls_apply_filter() Calls 'get_locale' hook on locale value.
 * @uses $yourls_locale Gets the locale stored in the global.
 *
 * @return string The locale of the blog or from the 'get_locale' hook.
 */
function yourls_get_locale() {
	global $yourls_locale;

	if ( !isset( $yourls_locale ) ) {
		// YOURLS_LANG is defined in config.
		if ( defined( 'YOURLS_LANG' ) )
			$yourls_locale = YOURLS_LANG;
	}

    if ( !$yourls_locale )
        $yourls_locale = '';

	return yourls_apply_filter( 'get_locale', $yourls_locale );
}

/**
 * Retrieves the translation of $text. If there is no translation, or
 * the domain isn't loaded, the original text is returned.
 *
 * @see yourls__() Don't use yourls_translate() directly, use yourls__()
 * @since 1.6
 * @uses yourls_apply_filter() Calls 'translate' on domain translated text
 *		with the untranslated text as second parameter.
 *
 * @param string $text Text to translate.
 * @param string $domain Domain to retrieve the translated text.
 * @return string Translated text
 */
function yourls_translate( $text, $domain = 'default' ) {
	$translations = yourls_get_translations_for_domain( $domain );
	return yourls_apply_filter( 'translate', $translations->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 string $pattern Text to translate
 * @param string $arg1, $arg2... Optional: sprintf tokens, and 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
 * @param string $arg1, $arg2... Optional: sprintf tokens, and translation domain
 * @return string 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
 */
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
 */
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
 */
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 string 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()
 *
 */
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()
 */
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" );
}

/**
 * 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
 */
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 $unixtimestamp    Optional. Unix timestamp.
 * @param bool     $gmt              Optional, default is false. Whether to convert to GMT for time.
 * @return string The date, translated if locale specifies it.
 */
function yourls_date_i18n( $dateformatstring, $unixtimestamp = false, $gmt = false ) {
	global $yourls_locale_formats;
	if( !isset( $yourls_locale_formats ) )
		$yourls_locale_formats = new YOURLS_Locale_Formats();

	$i = $unixtimestamp;

	if ( false === $i ) {
		if ( ! $gmt )
			$i = yourls_current_time( 'timestamp' );
		else
			$i = time();
		// we should not let date() interfere with our
		// specially computed timestamp
		$gmt = true;
	}

	// store original value for language with untypical grammars
	// see http://core.trac.wordpress.org/ticket/9396
	$req_format = $dateformatstring;

	$datefunc = $gmt? 'gmdate' : 'date';

	if ( ( !empty( $yourls_locale_formats->month ) ) && ( !empty( $yourls_locale_formats->weekday ) ) ) {
		$datemonth            = $yourls_locale_formats->get_month( $datefunc( 'm', $i ) );
		$datemonth_abbrev     = $yourls_locale_formats->get_month_abbrev( $datemonth );
		$dateweekday          = $yourls_locale_formats->get_weekday( $datefunc( 'w', $i ) );
		$dateweekday_abbrev   = $yourls_locale_formats->get_weekday_abbrev( $dateweekday );
		$datemeridiem         = $yourls_locale_formats->get_meridiem( $datefunc( 'a', $i ) );
		$datemeridiem_capital = $yourls_locale_formats->get_meridiem( $datefunc( 'A', $i ) );

		$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 );
	}
	$timezone_formats = array( 'P', 'I', 'O', 'T', 'Z', 'e' );
	$timezone_formats_re = implode( '|', $timezone_formats );
	if ( preg_match( "/$timezone_formats_re/", $dateformatstring ) ) {

		// TODO: implement a timezone option
		$timezone_string = yourls_get_option( 'timezone_string' );
		if ( $timezone_string ) {
			$timezone_object = timezone_open( $timezone_string );
			$date_object = date_create( null, $timezone_object );
			foreach( $timezone_formats as $timezone_format ) {
				if ( false !== strpos( $dateformatstring, $timezone_format ) ) {
					$formatted = date_format( $date_object, $timezone_format );
					$dateformatstring = ' '.$dateformatstring;
					$dateformatstring = preg_replace( "/([^\\\])$timezone_format/", "\\1" . yourls_backslashit( $formatted ), $dateformatstring );
					$dateformatstring = substr( $dateformatstring, 1, strlen( $dateformatstring ) -1 );
				}
			}
		}
	}
	$j = @$datefunc( $dateformatstring, $i );
	// allow plugins to redo this entirely for languages with untypical grammars
	$j = yourls_apply_filter('date_i18n', $j, $req_format, $i, $gmt);
	return $j;
}

/**
 * Retrieve the current time based on specified type. Stolen from WP.
 *
 * The 'mysql' type will return the time in the format for MySQL DATETIME field.
 * The 'timestamp' type will return the current timestamp.
 *
 * If $gmt is set to either '1' or 'true', then both types will use GMT time.
 * if $gmt is false, the output is adjusted with the GMT offset in the WordPress option.
 *
 * @since 1.6
 *
 * @param string $type Either 'mysql' or 'timestamp'.
 * @param int|bool $gmt Optional. Whether to use GMT timezone. Default is false.
 * @return int|string String if $type is 'gmt', int if $type is 'timestamp'.
 */
function yourls_current_time( $type, $gmt = 0 ) {
	switch ( $type ) {
		case 'mysql':
			return ( $gmt ) ? gmdate( 'Y-m-d H:i:s' ) : gmdate( 'Y-m-d H:i:s', time() + YOURLS_HOURS_OFFSET * 3600 );
			break;
		case 'timestamp':
			return ( $gmt ) ? time() : time() + YOURLS_HOURS_OFFSET * 3600;
			break;
	}
}

/**
 * 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
	 */
	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 $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
	 */
	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;
}


<?php

/**
 * YOURLS Options
 *
 * Note to plugin authors: you most likely SHOULD NOT use directly methods and properties of this class. Use instead
 * function wrappers (eg don't use $ydb->option, or $ydb->get(), use yourls_*_options() functions instead).
 *
 * Note to devs: this class internally uses function wrappers eg yourls_*_options() instead of direct methods, to
 * comply to any filter set in the function wrappers (eg $this->update() uses yourls_get_option()).
 * Maybe in the future this will look as a dumb idea?
 * The alternative would be to move return filters from function wrappers to here, but I think this will make things
 * less readable for users.
 *
 * @since 1.7.3
 */

namespace YOURLS\Database;

use YOURLS\Database\YDB;
use PDOException;

class Options {

    /**
     * Hold a copy of the all mighty $ydb global
     *
     * @var \YOURLS\Database\YDB
     */
    protected $ydb;

    public function __construct(YDB $ydb) {
        $this->ydb = $ydb;
    }

    /**
     * Read all options from DB at once, return bool
     *
     * @since  1.7.3
     * @return bool    True on success, false on failure (eg table missing or empty)
     */
    public function get_all_options() {
        // Get option values from DB
        $table = YOURLS_DB_TABLE_OPTIONS;
        $sql = "SELECT option_name, option_value FROM $table WHERE 1=1";

        try {
            $options = (array) $this->ydb->fetchPairs($sql);

        } catch ( PDOException $e ) {

            // We could not fetch value from the table. Let's check if the option table exists
            try {
                $check = $this->ydb->fetchAffected(sprintf("SHOW TABLES LIKE '%s'", $table));
                // Table doesn't exist
                if ($check ==0) {
                    return false;
                }

            // Error at this point means the database isn't readable
            } catch ( PDOException $e ) {
                $this->ydb->dead_or_error($e);
            }

        }

        // Unlikely scenario, but who knows: table exists, but is empty
        if (empty($options)) {
            return false;
        }

        foreach ($options as $name => $value) {
            $this->ydb->set_option($name, yourls_maybe_unserialize($value));
        }

        yourls_apply_filter('get_all_options', 'deprecated');

        return true;
    }

    /**
     * Get option value from DB (or from cache if available). Return value or $default if not found
     *
     * @since  1.7.3
     * @param  string $name     Option name
     * @param  string $default  Optional value to return if option doesn't exist
     * @return mixed            Value set for the option
     */
    public function get($name, $default) {
        $name = trim((string)$name);
        if (empty($name)) {
            return $default;
        }

        // Check if option value is cached already
        if($this->ydb->has_option($name)) {
            return $this->ydb->get_option($name);
        }

        // Get option value from DB
        $table = YOURLS_DB_TABLE_OPTIONS;
        $sql = "SELECT option_value FROM $table WHERE option_name = :option_name LIMIT 1";
        $bind = array('option_name' => $name);

        // Use fechOne() to get array('option_value'=>$value), or false if not found.
        // This way, we can effectively store false as an option value, and not confuse with false as the default return value
        $value = $this->ydb->fetchOne($sql, $bind);
        if($value !== false) {
            $value = yourls_maybe_unserialize( $value['option_value'] );
            // Cache option value to save a DB query if needed later
            $this->ydb->set_option($name, $value);
        } else {
            $value = $default;
        }

        /**
         * We don't cache value if option is not set, to make a difference between "not found: returning false"
         * and "found, and value is false".
         * This way, we can:
         * $check = yourls_get_option('doesnt_exist'); // false
         * yourls_add_option('doesnt_exist', 'value'); // will work, because check on has_option() will return false
         */

        return $value;
    }

    /**
     * Update (add if doesn't exist) an option to DB
     *
     * @since  1.7.3
     * @param  string $name      Option name. Expected to not be SQL-escaped.
     * @param  mixed  $newvalue  Option value.
     * @return bool              False if value was not updated, true otherwise.
     */
    public function update($name, $newvalue) {
        $name = trim((string)$name);
        if (empty($name)) {
            return false;
        }

        // Use clone to break object refs -- see commit 09b989d375bac65e692277f61a84fede2fb04ae3
        if (is_object($newvalue)) {
            $newvalue = clone $newvalue;
        }

        $oldvalue = yourls_get_option($name);

        // If the new and old values are the same, no need to update.
        if ($newvalue === $oldvalue) {
            return false;
        }

        // If this is a new option, just add it
        if (false === $oldvalue) {
            return $this->add($name, $newvalue);
        }

        $_newvalue = yourls_maybe_serialize($newvalue);
        $table = YOURLS_DB_TABLE_OPTIONS;
        $sql  = "UPDATE $table SET option_value = :value WHERE option_name = :name";
        $bind = array('name' => $name, 'value' => $_newvalue);
        $do   = $this->ydb->fetchAffected($sql, $bind);

        if($do !== 1) {
            // Something went wrong :(
            return false;
        }

        // Cache option value to save a DB query if needed later
        $this->ydb->set_option($name, $newvalue);
    	yourls_do_action( 'update_option', $name, $oldvalue, $newvalue );
        return true;
    }

    /**
     * Add an option to the DB
     *
     * @since  1.7.3
     * @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 (eg already exists), true otherwise.
     */
    public function add($name, $value) {
        $name = trim((string)$name);
        if (empty($name)) {
            return false;
        }

        // Use clone to break object refs -- see commit 09b989d375bac65e692277f61a84fede2fb04ae3
        if (is_object($value)) {
            $value = clone $value;
        }

        // Make sure the option doesn't already exist
        if ($this->ydb->has_option($name)) {
            return false;
        }
        // if (false !== yourls_get_option($name)) {
            // return false;
        // }

        $table = YOURLS_DB_TABLE_OPTIONS;
        $_value = yourls_maybe_serialize($value);
        $sql  = "INSERT INTO $table (option_name, option_value) VALUES (:name, :value)";
        $bind = array('name' => $name, 'value' => $_value);
        $do   = $this->ydb->fetchAffected($sql, $bind);

        if($do !== 1) {
            // Something went wrong :(
            return false;
        }

        // Cache option value to save a DB query if needed later
        $this->ydb->set_option($name, $value);
        yourls_do_action('add_option', $name, $_value);

        return true;
    }

    /**
     * Delete option from DB
     *
     * @since  1.7.3
     * @param  string $name  Option name to delete. Expected to not be SQL-escaped.
     * @return bool          False if option was not deleted (eg not found), true otherwise.
     */
    public function delete($name) {
        $name = trim((string)$name);
        if (empty($name)) {
            return false;
        }

        $table = YOURLS_DB_TABLE_OPTIONS;
        $sql = "DELETE FROM $table WHERE option_name = :name";
        $bind = array('name' => $name);
        $do   = $this->ydb->fetchAffected($sql, $bind);

        if($do !== 1) {
            // Something went wrong :(
            return false;
        }

        yourls_do_action('delete_option', $name);
        $this->ydb->delete_option($name);
        return true;
    }

}