Merge "Add TitleQuickPermissions hook to Title::checkQuickPermissions"
[lhc/web/wiklou.git] / languages / Language.php
index df1b156..ea34363 100644 (file)
@@ -32,7 +32,7 @@ if ( !defined( 'MEDIAWIKI' ) ) {
 
 # Read language names
 global $wgLanguageNames;
-require_once( __DIR__ . '/Names.php' );
+require_once __DIR__ . '/Names.php';
 
 if ( function_exists( 'mb_strtoupper' ) ) {
        mb_internal_encoding( 'UTF-8' );
@@ -170,6 +170,14 @@ class Language {
                'seconds' => 1,
        );
 
+       /**
+        * Cache for language fallbacks.
+        * @see Language::getFallbacksIncludingSiteLanguage
+        * @since 1.21
+        * @var array
+        */
+       static private $fallbackLanguageCache = array();
+
        /**
         * Get a cached or new language object for a given language code
         * @param $code String
@@ -326,13 +334,19 @@ class Language {
         * @return bool
         */
        public static function isValidCode( $code ) {
-               return
-                       // People think language codes are html safe, so enforce it.
-                       // Ideally we should only allow a-zA-Z0-9-
-                       // but, .+ and other chars are often used for {{int:}} hacks
-                       // see bugs 37564, 37587, 36938
+               static $cache = array();
+               if ( isset( $cache[$code] ) ) {
+                       return $cache[$code];
+               }
+               // People think language codes are html safe, so enforce it.
+               // Ideally we should only allow a-zA-Z0-9-
+               // but, .+ and other chars are often used for {{int:}} hacks
+               // see bugs 37564, 37587, 36938
+               $cache[$code] =
                        strcspn( $code, ":/\\\000&<>'\"" ) === strlen( $code )
                        && !preg_match( Title::getTitleInvalidRegex(), $code );
+
+               return $cache[$code];
        }
 
        /**
@@ -372,7 +386,7 @@ class Language {
                static $coreLanguageNames;
 
                if ( $coreLanguageNames === null ) {
-                       include( MWInit::compiledPath( 'languages/Names.php' ) );
+                       include MWInit::compiledPath( 'languages/Names.php' );
                }
 
                if ( isset( $coreLanguageNames[$tag] )
@@ -408,10 +422,8 @@ class Language {
                        return;
                }
 
-               if ( !defined( 'MW_COMPILED' ) ) {
-                       if ( file_exists( "$IP/languages/classes/$class.php" ) ) {
-                               include_once( "$IP/languages/classes/$class.php" );
-                       }
+               if ( file_exists( "$IP/languages/classes/$class.php" ) ) {
+                       include_once "$IP/languages/classes/$class.php";
                }
        }
 
@@ -845,7 +857,7 @@ class Language {
                static $coreLanguageNames;
 
                if ( $coreLanguageNames === null ) {
-                       include( MWInit::compiledPath( 'languages/Names.php' ) );
+                       include MWInit::compiledPath( 'languages/Names.php' );
                }
 
                $names = array();
@@ -1078,6 +1090,7 @@ class Language {
         * @param $zone DateTimeZone: Timezone of $ts
         * @todo handling of "o" format character for Iranian, Hebrew, Hijri & Thai?
         *
+        * @throws MWException
         * @return string
         */
        function sprintfDate( $format, $ts, DateTimeZone $zone = null ) {
@@ -1093,6 +1106,15 @@ class Language {
                $thai = false;
                $minguo = false;
                $tenno = false;
+
+               if ( strlen( $ts ) !== 14 ) {
+                       throw new MWException( __METHOD__ . ": The timestamp $ts should have 14 characters" );
+               }
+
+               if ( !ctype_digit( $ts ) ) {
+                       throw new MWException( __METHOD__ . ": The timestamp $ts should be a number" );
+               }
+
                for ( $p = 0; $p < strlen( $format ); $p++ ) {
                        $num = false;
                        $code = $format[$p];
@@ -1822,7 +1844,7 @@ class Language {
                }
                $start = substr( $str, 0, strlen( $str ) - 2 );
                $end = substr( $str, strlen( $str ) - 2 );
-               switch( $end ) {
+               switch ( $end ) {
                        case 'כ':
                                $str = $start . 'ך';
                                break;
@@ -1961,6 +1983,8 @@ class Language {
         * @param $type string May be date, time or both
         * @param $pref string The format name as it appears in Messages*.php
         *
+        * @since 1.22 New type 'pretty' that provides a more readable timestamp format
+        *
         * @return string
         */
        function getDateFormatString( $type, $pref ) {
@@ -1970,7 +1994,12 @@ class Language {
                                $df = self::$dataCache->getSubitem( $this->mCode, 'dateFormats', "$pref $type" );
                        } else {
                                $df = self::$dataCache->getSubitem( $this->mCode, 'dateFormats', "$pref $type" );
-                               if ( is_null( $df ) ) {
+
+                               if ( $type === 'pretty' && $df === null ) {
+                                       $df = $this->getDateFormatString( 'date', $pref );
+                               }
+
+                               if ( $df === null ) {
                                        $pref = $this->getDefaultDateFormat();
                                        $df = self::$dataCache->getSubitem( $this->mCode, 'dateFormats', "$pref $type" );
                                }
@@ -2203,6 +2232,79 @@ class Language {
                return $this->internalUserTimeAndDate( 'both', $ts, $user, $options );
        }
 
+       /**
+        * Convert an MWTimestamp into a pretty human-readable timestamp using
+        * the given user preferences and relative base time.
+        *
+        * DO NOT USE THIS FUNCTION DIRECTLY. Instead, call MWTimestamp::getHumanTimestamp
+        * on your timestamp object, which will then call this function. Calling
+        * this function directly will cause hooks to be skipped over.
+        *
+        * @see MWTimestamp::getHumanTimestamp
+        * @param MWTimestamp $ts Timestamp to prettify
+        * @param MWTimestamp $relativeTo Base timestamp
+        * @param User $user User preferences to use
+        * @return string Human timestamp
+        * @since 1.21
+        */
+       public function getHumanTimestamp( MWTimestamp $ts, MWTimestamp $relativeTo, User $user ) {
+               $diff = $ts->diff( $relativeTo );
+               $diffDay = (bool)( (int)$ts->timestamp->format( 'w' ) - (int)$relativeTo->timestamp->format( 'w' ) );
+               $days = $diff->days ?: (int)$diffDay;
+               if ( $diff->invert || $days > 5 && $ts->timestamp->format( 'Y' ) !== $relativeTo->timestamp->format( 'Y' ) ) {
+                       // Timestamps are in different years: use full timestamp
+                       // Also do full timestamp for future dates
+                       /**
+                        * @FIXME Add better handling of future timestamps.
+                        */
+                       $format = $this->getDateFormatString( 'both', $user->getDatePreference() ?: 'default' );
+                       $ts = $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) );
+               } elseif ( $days > 5 ) {
+                       // Timestamps are in same year,  but more than 5 days ago: show day and month only.
+                       $format = $this->getDateFormatString( 'pretty', $user->getDatePreference() ?: 'default' );
+                       $ts = $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) );
+               } elseif ( $days > 1 ) {
+                       // Timestamp within the past week: show the day of the week and time
+                       $format = $this->getDateFormatString( 'time', $user->getDatePreference() ?: 'default' );
+                       $weekday = self::$mWeekdayMsgs[$ts->timestamp->format( 'w' )];
+                       $ts = wfMessage( "$weekday-at" )
+                               ->inLanguage( $this )
+                               ->params( $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) ) )
+                               ->text();
+               } elseif ( $days == 1 ) {
+                       // Timestamp was yesterday: say 'yesterday' and the time.
+                       $format = $this->getDateFormatString( 'time', $user->getDatePreference() ?: 'default' );
+                       $ts = wfMessage( 'yesterday-at' )
+                               ->inLanguage( $this )
+                               ->params( $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) ) )
+                               ->text();
+               } elseif ( $diff->h > 1 || $diff->h == 1 && $diff->i > 30 ) {
+                       // Timestamp was today, but more than 90 minutes ago: say 'today' and the time.
+                       $format = $this->getDateFormatString( 'time', $user->getDatePreference() ?: 'default' );
+                       $ts = wfMessage( 'today-at' )
+                               ->inLanguage( $this )
+                               ->params( $this->sprintfDate( $format, $ts->getTimestamp( TS_MW ) ) )
+                               ->text();
+
+               // From here on in, the timestamp was soon enough ago so that we can simply say
+               // XX units ago, e.g., "2 hours ago" or "5 minutes ago"
+               } elseif ( $diff->h == 1 ) {
+                       // Less than 90 minutes, but more than an hour ago.
+                       $ts = wfMessage( 'hours-ago' )->inLanguage( $this )->numParams( 1 )->text();
+               } elseif ( $diff->i >= 1 ) {
+                       // A few minutes ago.
+                       $ts = wfMessage( 'minutes-ago' )->inLanguage( $this )->numParams( $diff->i )->text();
+               } elseif ( $diff->s >= 30 ) {
+                       // Less than a minute, but more than 30 sec ago.
+                       $ts = wfMessage( 'seconds-ago' )->inLanguage( $this )->numParams( $diff->s )->text();
+               } else {
+                       // Less than 30 seconds ago.
+                       $ts = wfMessage( 'just-now' )->text();
+               }
+
+               return $ts;
+       }
+
        /**
         * @param $key string
         * @return array|null
@@ -3503,10 +3605,6 @@ class Language {
         * @return string Correct form of plural for $count in this language
         */
        function convertPlural( $count, $forms ) {
-               if ( !count( $forms ) ) {
-                       return '';
-               }
-
                // Handle explicit n=pluralform cases
                foreach ( $forms as $index => $form ) {
                        if ( preg_match( '/\d+=/i', $form ) ) {
@@ -3517,7 +3615,11 @@ class Language {
                                unset( $forms[$index] );
                        }
                }
+
                $forms = array_values( $forms );
+               if ( !count( $forms ) ) {
+                       return '';
+               }
 
                $pluralForm = $this->getPluralRuleIndexNumber( $count );
                $pluralForm = min( $pluralForm, count( $forms ) - 1 );
@@ -3959,6 +4061,36 @@ class Language {
                }
        }
 
+       /**
+        * Get the ordered list of fallback languages, ending with the fallback
+        * language chain for the site language.
+        *
+        * @since 1.22
+        * @param string $code Language code
+        * @return array array( fallbacks, site fallbacks )
+        */
+       public static function getFallbacksIncludingSiteLanguage( $code ) {
+               global $wgLanguageCode;
+
+               // Usually, we will only store a tiny number of fallback chains, so we
+               // keep them in static memory.
+               $cacheKey = "{$code}-{$wgLanguageCode}";
+
+               if ( !array_key_exists( $cacheKey, self::$fallbackLanguageCache ) ) {
+                       $fallbacks = self::getFallbacksFor( $code );
+
+                       // Append the site's fallback chain, including the site language itself
+                       $siteFallbacks = self::getFallbacksFor( $wgLanguageCode );
+                       array_unshift( $siteFallbacks, $wgLanguageCode );
+
+                       // Eliminate any languages already included in the chain
+                       $siteFallbacks = array_diff( $siteFallbacks, $fallbacks );
+
+                       self::$fallbackLanguageCache[$cacheKey] = array( $fallbacks, $siteFallbacks );
+               }
+               return self::$fallbackLanguageCache[$cacheKey];
+       }
+
        /**
         * Get all messages for a given language
         * WARNING: this may take a long time. If you just need all message *keys*
@@ -4059,15 +4191,14 @@ class Language {
         * @since 1.18
         */
        public function formatExpiry( $expiry, $format = true ) {
-               static $infinity, $infinityMsg;
+               static $infinity;
                if ( $infinity === null ) {
-                       $infinityMsg = wfMessage( 'infiniteblock' );
                        $infinity = wfGetDB( DB_SLAVE )->getInfinity();
                }
 
                if ( $expiry == '' || $expiry == $infinity ) {
                        return $format === true
-                               ? $infinityMsg
+                               ? $this->getMessageFromDB( 'infiniteblock' )
                                : $infinity;
                } else {
                        return $format === true