Attempt to fix Bug 30216 - Improve language fallback loop detection.
[lhc/web/wiklou.git] / languages / Language.php
1 <?php
2 /**
3 * Internationalisation code
4 *
5 * @file
6 * @ingroup Language
7 */
8
9 /**
10 * @defgroup Language Language
11 */
12
13 if ( !defined( 'MEDIAWIKI' ) ) {
14 echo "This file is part of MediaWiki, it is not a valid entry point.\n";
15 exit( 1 );
16 }
17
18 # Read language names
19 global $wgLanguageNames;
20 require_once( dirname( __FILE__ ) . '/Names.php' );
21
22 if ( function_exists( 'mb_strtoupper' ) ) {
23 mb_internal_encoding( 'UTF-8' );
24 }
25
26 /**
27 * a fake language converter
28 *
29 * @ingroup Language
30 */
31 class FakeConverter {
32 var $mLang;
33 function __construct( $langobj ) { $this->mLang = $langobj; }
34 function autoConvertToAllVariants( $text ) { return array( $this->mLang->getCode() => $text ); }
35 function convert( $t ) { return $t; }
36 function convertTitle( $t ) { return $t->getPrefixedText(); }
37 function getVariants() { return array( $this->mLang->getCode() ); }
38 function getPreferredVariant() { return $this->mLang->getCode(); }
39 function getDefaultVariant() { return $this->mLang->getCode(); }
40 function getURLVariant() { return ''; }
41 function getConvRuleTitle() { return false; }
42 function findVariantLink( &$l, &$n, $ignoreOtherCond = false ) { }
43 function getExtraHashOptions() { return ''; }
44 function getParsedTitle() { return ''; }
45 function markNoConversion( $text, $noParse = false ) { return $text; }
46 function convertCategoryKey( $key ) { return $key; }
47 function convertLinkToAllVariants( $text ) { return $this->autoConvertToAllVariants( $text ); }
48 function armourMath( $text ) { return $text; }
49 }
50
51 /**
52 * Internationalisation code
53 * @ingroup Language
54 */
55 class Language {
56 var $mConverter, $mVariants, $mCode, $mLoaded = false;
57 var $mMagicExtensions = array(), $mMagicHookDone = false;
58
59 var $mNamespaceIds, $namespaceNames, $namespaceAliases;
60 var $dateFormatStrings = array();
61 var $mExtendedSpecialPageAliases;
62
63 /**
64 * ReplacementArray object caches
65 */
66 var $transformData = array();
67
68 /**
69 * @var LocalisationCache
70 */
71 static public $dataCache;
72
73 static public $mLangObjCache = array();
74
75 static public $mWeekdayMsgs = array(
76 'sunday', 'monday', 'tuesday', 'wednesday', 'thursday',
77 'friday', 'saturday'
78 );
79
80 static public $mWeekdayAbbrevMsgs = array(
81 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'
82 );
83
84 static public $mMonthMsgs = array(
85 'january', 'february', 'march', 'april', 'may_long', 'june',
86 'july', 'august', 'september', 'october', 'november',
87 'december'
88 );
89 static public $mMonthGenMsgs = array(
90 'january-gen', 'february-gen', 'march-gen', 'april-gen', 'may-gen', 'june-gen',
91 'july-gen', 'august-gen', 'september-gen', 'october-gen', 'november-gen',
92 'december-gen'
93 );
94 static public $mMonthAbbrevMsgs = array(
95 'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug',
96 'sep', 'oct', 'nov', 'dec'
97 );
98
99 static public $mIranianCalendarMonthMsgs = array(
100 'iranian-calendar-m1', 'iranian-calendar-m2', 'iranian-calendar-m3',
101 'iranian-calendar-m4', 'iranian-calendar-m5', 'iranian-calendar-m6',
102 'iranian-calendar-m7', 'iranian-calendar-m8', 'iranian-calendar-m9',
103 'iranian-calendar-m10', 'iranian-calendar-m11', 'iranian-calendar-m12'
104 );
105
106 static public $mHebrewCalendarMonthMsgs = array(
107 'hebrew-calendar-m1', 'hebrew-calendar-m2', 'hebrew-calendar-m3',
108 'hebrew-calendar-m4', 'hebrew-calendar-m5', 'hebrew-calendar-m6',
109 'hebrew-calendar-m7', 'hebrew-calendar-m8', 'hebrew-calendar-m9',
110 'hebrew-calendar-m10', 'hebrew-calendar-m11', 'hebrew-calendar-m12',
111 'hebrew-calendar-m6a', 'hebrew-calendar-m6b'
112 );
113
114 static public $mHebrewCalendarMonthGenMsgs = array(
115 'hebrew-calendar-m1-gen', 'hebrew-calendar-m2-gen', 'hebrew-calendar-m3-gen',
116 'hebrew-calendar-m4-gen', 'hebrew-calendar-m5-gen', 'hebrew-calendar-m6-gen',
117 'hebrew-calendar-m7-gen', 'hebrew-calendar-m8-gen', 'hebrew-calendar-m9-gen',
118 'hebrew-calendar-m10-gen', 'hebrew-calendar-m11-gen', 'hebrew-calendar-m12-gen',
119 'hebrew-calendar-m6a-gen', 'hebrew-calendar-m6b-gen'
120 );
121
122 static public $mHijriCalendarMonthMsgs = array(
123 'hijri-calendar-m1', 'hijri-calendar-m2', 'hijri-calendar-m3',
124 'hijri-calendar-m4', 'hijri-calendar-m5', 'hijri-calendar-m6',
125 'hijri-calendar-m7', 'hijri-calendar-m8', 'hijri-calendar-m9',
126 'hijri-calendar-m10', 'hijri-calendar-m11', 'hijri-calendar-m12'
127 );
128
129 /**
130 * Get a cached language object for a given language code
131 * @param $code String
132 * @return Language
133 */
134 static function factory( $code ) {
135 if ( !isset( self::$mLangObjCache[$code] ) ) {
136 if ( count( self::$mLangObjCache ) > 10 ) {
137 // Don't keep a billion objects around, that's stupid.
138 self::$mLangObjCache = array();
139 }
140 self::$mLangObjCache[$code] = self::newFromCode( $code );
141 }
142 return self::$mLangObjCache[$code];
143 }
144
145 /**
146 * Create a language object for a given language code
147 * @param $code String
148 * @return Language
149 */
150 protected static function newFromCode( $code ) {
151 // Protect against path traversal below
152 if ( !Language::isValidCode( $code )
153 || strcspn( $code, ":/\\\000" ) !== strlen( $code ) )
154 {
155 throw new MWException( "Invalid language code \"$code\"" );
156 }
157
158 if ( !Language::isValidBuiltInCode( $code ) ) {
159 // It's not possible to customise this code with class files, so
160 // just return a Language object. This is to support uselang= hacks.
161 $lang = new Language;
162 $lang->setCode( $code );
163 return $lang;
164 }
165
166 // Check if there is a language class for the code
167 $class = self::classFromCode( $code );
168 self::preloadLanguageClass( $class );
169 if ( MWInit::classExists( $class ) ) {
170 $lang = new $class;
171 return $lang;
172 }
173
174 // Keep trying the fallback list until we find an existing class
175 $fallbacks = Language::getFallbacksFor( $code );
176 foreach ( $fallbacks as $fallbackCode ) {
177 if ( !Language::isValidBuiltInCode( $fallbackCode ) ) {
178 throw new MWException( "Invalid fallback '$fallbackCode' in fallback sequence for '$code'" );
179 }
180
181 $class = self::classFromCode( $fallbackCode );
182 self::preloadLanguageClass( $class );
183 if ( MWInit::classExists( $class ) ) {
184 $lang = Language::newFromCode( $fallbackCode );
185 $lang->setCode( $code );
186 return $lang;
187 }
188 }
189
190 throw new MWException( "Invalid fallback sequence for language '$code'" );
191 }
192
193 /**
194 * Returns true if a language code string is of a valid form, whether or
195 * not it exists. This includes codes which are used solely for
196 * customisation via the MediaWiki namespace.
197 *
198 * @param $code string
199 *
200 * @return bool
201 */
202 public static function isValidCode( $code ) {
203 return
204 strcspn( $code, ":/\\\000" ) === strlen( $code )
205 && !preg_match( Title::getTitleInvalidRegex(), $code );
206 }
207
208 /**
209 * Returns true if a language code is of a valid form for the purposes of
210 * internal customisation of MediaWiki, via Messages*.php.
211 *
212 * @param $code string
213 *
214 * @since 1.18
215 * @return bool
216 */
217 public static function isValidBuiltInCode( $code ) {
218 return preg_match( '/^[a-z0-9-]*$/i', $code );
219 }
220
221 public static function classFromCode( $code ) {
222 if ( $code == 'en' ) {
223 return 'Language';
224 } else {
225 return 'Language' . str_replace( '-', '_', ucfirst( $code ) );
226 }
227 }
228
229 public static function preloadLanguageClass( $class ) {
230 global $IP;
231
232 if ( $class === 'Language' ) {
233 return;
234 }
235
236 if ( !defined( 'MW_COMPILED' ) ) {
237 // Preload base classes to work around APC/PHP5 bug
238 if ( file_exists( "$IP/languages/classes/$class.deps.php" ) ) {
239 include_once( "$IP/languages/classes/$class.deps.php" );
240 }
241 if ( file_exists( "$IP/languages/classes/$class.php" ) ) {
242 include_once( "$IP/languages/classes/$class.php" );
243 }
244 }
245 }
246
247 /**
248 * Get the LocalisationCache instance
249 *
250 * @return LocalisationCache
251 */
252 public static function getLocalisationCache() {
253 if ( is_null( self::$dataCache ) ) {
254 global $wgLocalisationCacheConf;
255 $class = $wgLocalisationCacheConf['class'];
256 self::$dataCache = new $class( $wgLocalisationCacheConf );
257 }
258 return self::$dataCache;
259 }
260
261 function __construct() {
262 $this->mConverter = new FakeConverter( $this );
263 // Set the code to the name of the descendant
264 if ( get_class( $this ) == 'Language' ) {
265 $this->mCode = 'en';
266 } else {
267 $this->mCode = str_replace( '_', '-', strtolower( substr( get_class( $this ), 8 ) ) );
268 }
269 self::getLocalisationCache();
270 }
271
272 /**
273 * Reduce memory usage
274 */
275 function __destruct() {
276 foreach ( $this as $name => $value ) {
277 unset( $this->$name );
278 }
279 }
280
281 /**
282 * Hook which will be called if this is the content language.
283 * Descendants can use this to register hook functions or modify globals
284 */
285 function initContLang() { }
286
287 /**
288 * Same as getFallbacksFor for current language.
289 * @return array|bool
290 * @deprecated in 1.19
291 */
292 function getFallbackLanguageCode() {
293 wfDeprecated( __METHOD__ );
294 return self::getFallbackFor( $this->mCode );
295 }
296
297 /**
298 * @return array
299 * @since 1.19
300 */
301 function getFallbackLanguages() {
302 return self::getFallbacksFor( $this->mCode );
303 }
304
305 /**
306 * Exports $wgBookstoreListEn
307 * @return array
308 */
309 function getBookstoreList() {
310 return self::$dataCache->getItem( $this->mCode, 'bookstoreList' );
311 }
312
313 /**
314 * @return array
315 */
316 function getNamespaces() {
317 if ( is_null( $this->namespaceNames ) ) {
318 global $wgMetaNamespace, $wgMetaNamespaceTalk, $wgExtraNamespaces;
319
320 $this->namespaceNames = self::$dataCache->getItem( $this->mCode, 'namespaceNames' );
321 $validNamespaces = MWNamespace::getCanonicalNamespaces();
322
323 $this->namespaceNames = $wgExtraNamespaces + $this->namespaceNames + $validNamespaces;
324
325 $this->namespaceNames[NS_PROJECT] = $wgMetaNamespace;
326 if ( $wgMetaNamespaceTalk ) {
327 $this->namespaceNames[NS_PROJECT_TALK] = $wgMetaNamespaceTalk;
328 } else {
329 $talk = $this->namespaceNames[NS_PROJECT_TALK];
330 $this->namespaceNames[NS_PROJECT_TALK] =
331 $this->fixVariableInNamespace( $talk );
332 }
333
334 # Sometimes a language will be localised but not actually exist on this wiki.
335 foreach( $this->namespaceNames as $key => $text ) {
336 if ( !isset( $validNamespaces[$key] ) ) {
337 unset( $this->namespaceNames[$key] );
338 }
339 }
340
341 # The above mixing may leave namespaces out of canonical order.
342 # Re-order by namespace ID number...
343 ksort( $this->namespaceNames );
344 }
345 return $this->namespaceNames;
346 }
347
348 /**
349 * A convenience function that returns the same thing as
350 * getNamespaces() except with the array values changed to ' '
351 * where it found '_', useful for producing output to be displayed
352 * e.g. in <select> forms.
353 *
354 * @return array
355 */
356 function getFormattedNamespaces() {
357 $ns = $this->getNamespaces();
358 foreach ( $ns as $k => $v ) {
359 $ns[$k] = strtr( $v, '_', ' ' );
360 }
361 return $ns;
362 }
363
364 /**
365 * Get a namespace value by key
366 * <code>
367 * $mw_ns = $wgContLang->getNsText( NS_MEDIAWIKI );
368 * echo $mw_ns; // prints 'MediaWiki'
369 * </code>
370 *
371 * @param $index Int: the array key of the namespace to return
372 * @return mixed, string if the namespace value exists, otherwise false
373 */
374 function getNsText( $index ) {
375 $ns = $this->getNamespaces();
376 return isset( $ns[$index] ) ? $ns[$index] : false;
377 }
378
379 /**
380 * A convenience function that returns the same thing as
381 * getNsText() except with '_' changed to ' ', useful for
382 * producing output.
383 *
384 * @param $index string
385 *
386 * @return array
387 */
388 function getFormattedNsText( $index ) {
389 $ns = $this->getNsText( $index );
390 return strtr( $ns, '_', ' ' );
391 }
392
393 /**
394 * Returns gender-dependent namespace alias if available.
395 * @param $index Int: namespace index
396 * @param $gender String: gender key (male, female... )
397 * @return String
398 * @since 1.18
399 */
400 function getGenderNsText( $index, $gender ) {
401 $ns = self::$dataCache->getItem( $this->mCode, 'namespaceGenderAliases' );
402 return isset( $ns[$index][$gender] ) ? $ns[$index][$gender] : $this->getNsText( $index );
403 }
404
405 /**
406 * Whether this language makes distinguishes genders for example in
407 * namespaces.
408 * @return bool
409 * @since 1.18
410 */
411 function needsGenderDistinction() {
412 $aliases = self::$dataCache->getItem( $this->mCode, 'namespaceGenderAliases' );
413 return count( $aliases ) > 0;
414 }
415
416 /**
417 * Get a namespace key by value, case insensitive.
418 * Only matches namespace names for the current language, not the
419 * canonical ones defined in Namespace.php.
420 *
421 * @param $text String
422 * @return mixed An integer if $text is a valid value otherwise false
423 */
424 function getLocalNsIndex( $text ) {
425 $lctext = $this->lc( $text );
426 $ids = $this->getNamespaceIds();
427 return isset( $ids[$lctext] ) ? $ids[$lctext] : false;
428 }
429
430 /**
431 * @return array
432 */
433 function getNamespaceAliases() {
434 if ( is_null( $this->namespaceAliases ) ) {
435 $aliases = self::$dataCache->getItem( $this->mCode, 'namespaceAliases' );
436 if ( !$aliases ) {
437 $aliases = array();
438 } else {
439 foreach ( $aliases as $name => $index ) {
440 if ( $index === NS_PROJECT_TALK ) {
441 unset( $aliases[$name] );
442 $name = $this->fixVariableInNamespace( $name );
443 $aliases[$name] = $index;
444 }
445 }
446 }
447
448 $genders = self::$dataCache->getItem( $this->mCode, 'namespaceGenderAliases' );
449 foreach ( $genders as $index => $forms ) {
450 foreach ( $forms as $alias ) {
451 $aliases[$alias] = $index;
452 }
453 }
454
455 $this->namespaceAliases = $aliases;
456 }
457 return $this->namespaceAliases;
458 }
459
460 /**
461 * @return array
462 */
463 function getNamespaceIds() {
464 if ( is_null( $this->mNamespaceIds ) ) {
465 global $wgNamespaceAliases;
466 # Put namespace names and aliases into a hashtable.
467 # If this is too slow, then we should arrange it so that it is done
468 # before caching. The catch is that at pre-cache time, the above
469 # class-specific fixup hasn't been done.
470 $this->mNamespaceIds = array();
471 foreach ( $this->getNamespaces() as $index => $name ) {
472 $this->mNamespaceIds[$this->lc( $name )] = $index;
473 }
474 foreach ( $this->getNamespaceAliases() as $name => $index ) {
475 $this->mNamespaceIds[$this->lc( $name )] = $index;
476 }
477 if ( $wgNamespaceAliases ) {
478 foreach ( $wgNamespaceAliases as $name => $index ) {
479 $this->mNamespaceIds[$this->lc( $name )] = $index;
480 }
481 }
482 }
483 return $this->mNamespaceIds;
484 }
485
486
487 /**
488 * Get a namespace key by value, case insensitive. Canonical namespace
489 * names override custom ones defined for the current language.
490 *
491 * @param $text String
492 * @return mixed An integer if $text is a valid value otherwise false
493 */
494 function getNsIndex( $text ) {
495 $lctext = $this->lc( $text );
496 if ( ( $ns = MWNamespace::getCanonicalIndex( $lctext ) ) !== null ) {
497 return $ns;
498 }
499 $ids = $this->getNamespaceIds();
500 return isset( $ids[$lctext] ) ? $ids[$lctext] : false;
501 }
502
503 /**
504 * short names for language variants used for language conversion links.
505 *
506 * @param $code String
507 * @return string
508 */
509 function getVariantname( $code ) {
510 return $this->getMessageFromDB( "variantname-$code" );
511 }
512
513 /**
514 * @param $name string
515 * @return string
516 */
517 function specialPage( $name ) {
518 $aliases = $this->getSpecialPageAliases();
519 if ( isset( $aliases[$name][0] ) ) {
520 $name = $aliases[$name][0];
521 }
522 return $this->getNsText( NS_SPECIAL ) . ':' . $name;
523 }
524
525 /**
526 * @return array
527 */
528 function getQuickbarSettings() {
529 return array(
530 $this->getMessage( 'qbsettings-none' ),
531 $this->getMessage( 'qbsettings-fixedleft' ),
532 $this->getMessage( 'qbsettings-fixedright' ),
533 $this->getMessage( 'qbsettings-floatingleft' ),
534 $this->getMessage( 'qbsettings-floatingright' ),
535 $this->getMessage( 'qbsettings-directionality' )
536 );
537 }
538
539 /**
540 * @return array
541 */
542 function getDatePreferences() {
543 return self::$dataCache->getItem( $this->mCode, 'datePreferences' );
544 }
545
546 /**
547 * @return array
548 */
549 function getDateFormats() {
550 return self::$dataCache->getItem( $this->mCode, 'dateFormats' );
551 }
552
553 /**
554 * @return array|string
555 */
556 function getDefaultDateFormat() {
557 $df = self::$dataCache->getItem( $this->mCode, 'defaultDateFormat' );
558 if ( $df === 'dmy or mdy' ) {
559 global $wgAmericanDates;
560 return $wgAmericanDates ? 'mdy' : 'dmy';
561 } else {
562 return $df;
563 }
564 }
565
566 /**
567 * @return array
568 */
569 function getDatePreferenceMigrationMap() {
570 return self::$dataCache->getItem( $this->mCode, 'datePreferenceMigrationMap' );
571 }
572
573 /**
574 * @param $image
575 * @return array|null
576 */
577 function getImageFile( $image ) {
578 return self::$dataCache->getSubitem( $this->mCode, 'imageFiles', $image );
579 }
580
581 /**
582 * @return array
583 */
584 function getExtraUserToggles() {
585 return self::$dataCache->getItem( $this->mCode, 'extraUserToggles' );
586 }
587
588 /**
589 * @param $tog
590 * @return string
591 */
592 function getUserToggle( $tog ) {
593 return $this->getMessageFromDB( "tog-$tog" );
594 }
595
596 /**
597 * Get language names, indexed by code.
598 * If $customisedOnly is true, only returns codes with a messages file
599 *
600 * @param $customisedOnly bool
601 *
602 * @return array
603 */
604 public static function getLanguageNames( $customisedOnly = false ) {
605 global $wgExtraLanguageNames;
606 static $coreLanguageNames;
607
608 if ( $coreLanguageNames === null ) {
609 include( MWInit::compiledPath( 'languages/Names.php' ) );
610 }
611
612 $allNames = $wgExtraLanguageNames + $coreLanguageNames;
613 if ( !$customisedOnly ) {
614 return $allNames;
615 }
616
617 global $IP;
618 $names = array();
619 $dir = opendir( "$IP/languages/messages" );
620 while ( false !== ( $file = readdir( $dir ) ) ) {
621 $code = self::getCodeFromFileName( $file, 'Messages' );
622 if ( $code && isset( $allNames[$code] ) ) {
623 $names[$code] = $allNames[$code];
624 }
625 }
626 closedir( $dir );
627 return $names;
628 }
629
630 /**
631 * Get translated language names. This is done on best effort and
632 * by default this is exactly the same as Language::getLanguageNames.
633 * The CLDR extension provides translated names.
634 * @param $code String Language code.
635 * @return Array language code => language name
636 * @since 1.18.0
637 */
638 public static function getTranslatedLanguageNames( $code ) {
639 $names = array();
640 wfRunHooks( 'LanguageGetTranslatedLanguageNames', array( &$names, $code ) );
641
642 foreach ( self::getLanguageNames() as $code => $name ) {
643 if ( !isset( $names[$code] ) ) $names[$code] = $name;
644 }
645
646 return $names;
647 }
648
649 /**
650 * Get a message from the MediaWiki namespace.
651 *
652 * @param $msg String: message name
653 * @return string
654 */
655 function getMessageFromDB( $msg ) {
656 return wfMsgExt( $msg, array( 'parsemag', 'language' => $this ) );
657 }
658
659 /**
660 * @param $code string
661 * @return string
662 */
663 function getLanguageName( $code ) {
664 $names = self::getLanguageNames();
665 if ( !array_key_exists( $code, $names ) ) {
666 return '';
667 }
668 return $names[$code];
669 }
670
671 /**
672 * @param $key string
673 * @return string
674 */
675 function getMonthName( $key ) {
676 return $this->getMessageFromDB( self::$mMonthMsgs[$key - 1] );
677 }
678
679 /**
680 * @return array
681 */
682 function getMonthNamesArray() {
683 $monthNames = array( '' );
684 for ( $i=1; $i < 13; $i++ ) {
685 $monthNames[] = $this->getMonthName( $i );
686 }
687 return $monthNames;
688 }
689
690 /**
691 * @param $key string
692 * @return string
693 */
694 function getMonthNameGen( $key ) {
695 return $this->getMessageFromDB( self::$mMonthGenMsgs[$key - 1] );
696 }
697
698 /**
699 * @param $key string
700 * @return string
701 */
702 function getMonthAbbreviation( $key ) {
703 return $this->getMessageFromDB( self::$mMonthAbbrevMsgs[$key - 1] );
704 }
705
706 /**
707 * @return array
708 */
709 function getMonthAbbreviationsArray() {
710 $monthNames = array( '' );
711 for ( $i=1; $i < 13; $i++ ) {
712 $monthNames[] = $this->getMonthAbbreviation( $i );
713 }
714 return $monthNames;
715 }
716
717 /**
718 * @param $key string
719 * @return string
720 */
721 function getWeekdayName( $key ) {
722 return $this->getMessageFromDB( self::$mWeekdayMsgs[$key - 1] );
723 }
724
725 /**
726 * @param $key string
727 * @return string
728 */
729 function getWeekdayAbbreviation( $key ) {
730 return $this->getMessageFromDB( self::$mWeekdayAbbrevMsgs[$key - 1] );
731 }
732
733 /**
734 * @param $key string
735 * @return string
736 */
737 function getIranianCalendarMonthName( $key ) {
738 return $this->getMessageFromDB( self::$mIranianCalendarMonthMsgs[$key - 1] );
739 }
740
741 /**
742 * @param $key string
743 * @return string
744 */
745 function getHebrewCalendarMonthName( $key ) {
746 return $this->getMessageFromDB( self::$mHebrewCalendarMonthMsgs[$key - 1] );
747 }
748
749 /**
750 * @param $key string
751 * @return string
752 */
753 function getHebrewCalendarMonthNameGen( $key ) {
754 return $this->getMessageFromDB( self::$mHebrewCalendarMonthGenMsgs[$key - 1] );
755 }
756
757 /**
758 * @param $key string
759 * @return string
760 */
761 function getHijriCalendarMonthName( $key ) {
762 return $this->getMessageFromDB( self::$mHijriCalendarMonthMsgs[$key - 1] );
763 }
764
765 /**
766 * Used by date() and time() to adjust the time output.
767 *
768 * @param $ts Int the time in date('YmdHis') format
769 * @param $tz Mixed: adjust the time by this amount (default false, mean we
770 * get user timecorrection setting)
771 * @return int
772 */
773 function userAdjust( $ts, $tz = false ) {
774 global $wgUser, $wgLocalTZoffset;
775
776 if ( $tz === false ) {
777 $tz = $wgUser->getOption( 'timecorrection' );
778 }
779
780 $data = explode( '|', $tz, 3 );
781
782 if ( $data[0] == 'ZoneInfo' ) {
783 wfSuppressWarnings();
784 $userTZ = timezone_open( $data[2] );
785 wfRestoreWarnings();
786 if ( $userTZ !== false ) {
787 $date = date_create( $ts, timezone_open( 'UTC' ) );
788 date_timezone_set( $date, $userTZ );
789 $date = date_format( $date, 'YmdHis' );
790 return $date;
791 }
792 # Unrecognized timezone, default to 'Offset' with the stored offset.
793 $data[0] = 'Offset';
794 }
795
796 $minDiff = 0;
797 if ( $data[0] == 'System' || $tz == '' ) {
798 #  Global offset in minutes.
799 if ( isset( $wgLocalTZoffset ) ) {
800 $minDiff = $wgLocalTZoffset;
801 }
802 } elseif ( $data[0] == 'Offset' ) {
803 $minDiff = intval( $data[1] );
804 } else {
805 $data = explode( ':', $tz );
806 if ( count( $data ) == 2 ) {
807 $data[0] = intval( $data[0] );
808 $data[1] = intval( $data[1] );
809 $minDiff = abs( $data[0] ) * 60 + $data[1];
810 if ( $data[0] < 0 ) {
811 $minDiff = -$minDiff;
812 }
813 } else {
814 $minDiff = intval( $data[0] ) * 60;
815 }
816 }
817
818 # No difference ? Return time unchanged
819 if ( 0 == $minDiff ) {
820 return $ts;
821 }
822
823 wfSuppressWarnings(); // E_STRICT system time bitching
824 # Generate an adjusted date; take advantage of the fact that mktime
825 # will normalize out-of-range values so we don't have to split $minDiff
826 # into hours and minutes.
827 $t = mktime( (
828 (int)substr( $ts, 8, 2 ) ), # Hours
829 (int)substr( $ts, 10, 2 ) + $minDiff, # Minutes
830 (int)substr( $ts, 12, 2 ), # Seconds
831 (int)substr( $ts, 4, 2 ), # Month
832 (int)substr( $ts, 6, 2 ), # Day
833 (int)substr( $ts, 0, 4 ) ); # Year
834
835 $date = date( 'YmdHis', $t );
836 wfRestoreWarnings();
837
838 return $date;
839 }
840
841 /**
842 * This is a workalike of PHP's date() function, but with better
843 * internationalisation, a reduced set of format characters, and a better
844 * escaping format.
845 *
846 * Supported format characters are dDjlNwzWFmMntLoYyaAgGhHiscrU. See the
847 * PHP manual for definitions. There are a number of extensions, which
848 * start with "x":
849 *
850 * xn Do not translate digits of the next numeric format character
851 * xN Toggle raw digit (xn) flag, stays set until explicitly unset
852 * xr Use roman numerals for the next numeric format character
853 * xh Use hebrew numerals for the next numeric format character
854 * xx Literal x
855 * xg Genitive month name
856 *
857 * xij j (day number) in Iranian calendar
858 * xiF F (month name) in Iranian calendar
859 * xin n (month number) in Iranian calendar
860 * xiY Y (full year) in Iranian calendar
861 *
862 * xjj j (day number) in Hebrew calendar
863 * xjF F (month name) in Hebrew calendar
864 * xjt t (days in month) in Hebrew calendar
865 * xjx xg (genitive month name) in Hebrew calendar
866 * xjn n (month number) in Hebrew calendar
867 * xjY Y (full year) in Hebrew calendar
868 *
869 * xmj j (day number) in Hijri calendar
870 * xmF F (month name) in Hijri calendar
871 * xmn n (month number) in Hijri calendar
872 * xmY Y (full year) in Hijri calendar
873 *
874 * xkY Y (full year) in Thai solar calendar. Months and days are
875 * identical to the Gregorian calendar
876 * xoY Y (full year) in Minguo calendar or Juche year.
877 * Months and days are identical to the
878 * Gregorian calendar
879 * xtY Y (full year) in Japanese nengo. Months and days are
880 * identical to the Gregorian calendar
881 *
882 * Characters enclosed in double quotes will be considered literal (with
883 * the quotes themselves removed). Unmatched quotes will be considered
884 * literal quotes. Example:
885 *
886 * "The month is" F => The month is January
887 * i's" => 20'11"
888 *
889 * Backslash escaping is also supported.
890 *
891 * Input timestamp is assumed to be pre-normalized to the desired local
892 * time zone, if any.
893 *
894 * @param $format String
895 * @param $ts String: 14-character timestamp
896 * YYYYMMDDHHMMSS
897 * 01234567890123
898 * @todo handling of "o" format character for Iranian, Hebrew, Hijri & Thai?
899 *
900 * @return string
901 */
902 function sprintfDate( $format, $ts ) {
903 $s = '';
904 $raw = false;
905 $roman = false;
906 $hebrewNum = false;
907 $unix = false;
908 $rawToggle = false;
909 $iranian = false;
910 $hebrew = false;
911 $hijri = false;
912 $thai = false;
913 $minguo = false;
914 $tenno = false;
915 for ( $p = 0; $p < strlen( $format ); $p++ ) {
916 $num = false;
917 $code = $format[$p];
918 if ( $code == 'x' && $p < strlen( $format ) - 1 ) {
919 $code .= $format[++$p];
920 }
921
922 if ( ( $code === 'xi' || $code == 'xj' || $code == 'xk' || $code == 'xm' || $code == 'xo' || $code == 'xt' ) && $p < strlen( $format ) - 1 ) {
923 $code .= $format[++$p];
924 }
925
926 switch ( $code ) {
927 case 'xx':
928 $s .= 'x';
929 break;
930 case 'xn':
931 $raw = true;
932 break;
933 case 'xN':
934 $rawToggle = !$rawToggle;
935 break;
936 case 'xr':
937 $roman = true;
938 break;
939 case 'xh':
940 $hebrewNum = true;
941 break;
942 case 'xg':
943 $s .= $this->getMonthNameGen( substr( $ts, 4, 2 ) );
944 break;
945 case 'xjx':
946 if ( !$hebrew ) $hebrew = self::tsToHebrew( $ts );
947 $s .= $this->getHebrewCalendarMonthNameGen( $hebrew[1] );
948 break;
949 case 'd':
950 $num = substr( $ts, 6, 2 );
951 break;
952 case 'D':
953 if ( !$unix ) $unix = wfTimestamp( TS_UNIX, $ts );
954 $s .= $this->getWeekdayAbbreviation( gmdate( 'w', $unix ) + 1 );
955 break;
956 case 'j':
957 $num = intval( substr( $ts, 6, 2 ) );
958 break;
959 case 'xij':
960 if ( !$iranian ) {
961 $iranian = self::tsToIranian( $ts );
962 }
963 $num = $iranian[2];
964 break;
965 case 'xmj':
966 if ( !$hijri ) {
967 $hijri = self::tsToHijri( $ts );
968 }
969 $num = $hijri[2];
970 break;
971 case 'xjj':
972 if ( !$hebrew ) {
973 $hebrew = self::tsToHebrew( $ts );
974 }
975 $num = $hebrew[2];
976 break;
977 case 'l':
978 if ( !$unix ) {
979 $unix = wfTimestamp( TS_UNIX, $ts );
980 }
981 $s .= $this->getWeekdayName( gmdate( 'w', $unix ) + 1 );
982 break;
983 case 'N':
984 if ( !$unix ) {
985 $unix = wfTimestamp( TS_UNIX, $ts );
986 }
987 $w = gmdate( 'w', $unix );
988 $num = $w ? $w : 7;
989 break;
990 case 'w':
991 if ( !$unix ) {
992 $unix = wfTimestamp( TS_UNIX, $ts );
993 }
994 $num = gmdate( 'w', $unix );
995 break;
996 case 'z':
997 if ( !$unix ) {
998 $unix = wfTimestamp( TS_UNIX, $ts );
999 }
1000 $num = gmdate( 'z', $unix );
1001 break;
1002 case 'W':
1003 if ( !$unix ) {
1004 $unix = wfTimestamp( TS_UNIX, $ts );
1005 }
1006 $num = gmdate( 'W', $unix );
1007 break;
1008 case 'F':
1009 $s .= $this->getMonthName( substr( $ts, 4, 2 ) );
1010 break;
1011 case 'xiF':
1012 if ( !$iranian ) {
1013 $iranian = self::tsToIranian( $ts );
1014 }
1015 $s .= $this->getIranianCalendarMonthName( $iranian[1] );
1016 break;
1017 case 'xmF':
1018 if ( !$hijri ) {
1019 $hijri = self::tsToHijri( $ts );
1020 }
1021 $s .= $this->getHijriCalendarMonthName( $hijri[1] );
1022 break;
1023 case 'xjF':
1024 if ( !$hebrew ) {
1025 $hebrew = self::tsToHebrew( $ts );
1026 }
1027 $s .= $this->getHebrewCalendarMonthName( $hebrew[1] );
1028 break;
1029 case 'm':
1030 $num = substr( $ts, 4, 2 );
1031 break;
1032 case 'M':
1033 $s .= $this->getMonthAbbreviation( substr( $ts, 4, 2 ) );
1034 break;
1035 case 'n':
1036 $num = intval( substr( $ts, 4, 2 ) );
1037 break;
1038 case 'xin':
1039 if ( !$iranian ) {
1040 $iranian = self::tsToIranian( $ts );
1041 }
1042 $num = $iranian[1];
1043 break;
1044 case 'xmn':
1045 if ( !$hijri ) {
1046 $hijri = self::tsToHijri ( $ts );
1047 }
1048 $num = $hijri[1];
1049 break;
1050 case 'xjn':
1051 if ( !$hebrew ) {
1052 $hebrew = self::tsToHebrew( $ts );
1053 }
1054 $num = $hebrew[1];
1055 break;
1056 case 't':
1057 if ( !$unix ) {
1058 $unix = wfTimestamp( TS_UNIX, $ts );
1059 }
1060 $num = gmdate( 't', $unix );
1061 break;
1062 case 'xjt':
1063 if ( !$hebrew ) {
1064 $hebrew = self::tsToHebrew( $ts );
1065 }
1066 $num = $hebrew[3];
1067 break;
1068 case 'L':
1069 if ( !$unix ) {
1070 $unix = wfTimestamp( TS_UNIX, $ts );
1071 }
1072 $num = gmdate( 'L', $unix );
1073 break;
1074 case 'o':
1075 if ( !$unix ) {
1076 $unix = wfTimestamp( TS_UNIX, $ts );
1077 }
1078 $num = date( 'o', $unix );
1079 break;
1080 case 'Y':
1081 $num = substr( $ts, 0, 4 );
1082 break;
1083 case 'xiY':
1084 if ( !$iranian ) {
1085 $iranian = self::tsToIranian( $ts );
1086 }
1087 $num = $iranian[0];
1088 break;
1089 case 'xmY':
1090 if ( !$hijri ) {
1091 $hijri = self::tsToHijri( $ts );
1092 }
1093 $num = $hijri[0];
1094 break;
1095 case 'xjY':
1096 if ( !$hebrew ) {
1097 $hebrew = self::tsToHebrew( $ts );
1098 }
1099 $num = $hebrew[0];
1100 break;
1101 case 'xkY':
1102 if ( !$thai ) {
1103 $thai = self::tsToYear( $ts, 'thai' );
1104 }
1105 $num = $thai[0];
1106 break;
1107 case 'xoY':
1108 if ( !$minguo ) {
1109 $minguo = self::tsToYear( $ts, 'minguo' );
1110 }
1111 $num = $minguo[0];
1112 break;
1113 case 'xtY':
1114 if ( !$tenno ) {
1115 $tenno = self::tsToYear( $ts, 'tenno' );
1116 }
1117 $num = $tenno[0];
1118 break;
1119 case 'y':
1120 $num = substr( $ts, 2, 2 );
1121 break;
1122 case 'a':
1123 $s .= intval( substr( $ts, 8, 2 ) ) < 12 ? 'am' : 'pm';
1124 break;
1125 case 'A':
1126 $s .= intval( substr( $ts, 8, 2 ) ) < 12 ? 'AM' : 'PM';
1127 break;
1128 case 'g':
1129 $h = substr( $ts, 8, 2 );
1130 $num = $h % 12 ? $h % 12 : 12;
1131 break;
1132 case 'G':
1133 $num = intval( substr( $ts, 8, 2 ) );
1134 break;
1135 case 'h':
1136 $h = substr( $ts, 8, 2 );
1137 $num = sprintf( '%02d', $h % 12 ? $h % 12 : 12 );
1138 break;
1139 case 'H':
1140 $num = substr( $ts, 8, 2 );
1141 break;
1142 case 'i':
1143 $num = substr( $ts, 10, 2 );
1144 break;
1145 case 's':
1146 $num = substr( $ts, 12, 2 );
1147 break;
1148 case 'c':
1149 if ( !$unix ) {
1150 $unix = wfTimestamp( TS_UNIX, $ts );
1151 }
1152 $s .= gmdate( 'c', $unix );
1153 break;
1154 case 'r':
1155 if ( !$unix ) {
1156 $unix = wfTimestamp( TS_UNIX, $ts );
1157 }
1158 $s .= gmdate( 'r', $unix );
1159 break;
1160 case 'U':
1161 if ( !$unix ) {
1162 $unix = wfTimestamp( TS_UNIX, $ts );
1163 }
1164 $num = $unix;
1165 break;
1166 case '\\':
1167 # Backslash escaping
1168 if ( $p < strlen( $format ) - 1 ) {
1169 $s .= $format[++$p];
1170 } else {
1171 $s .= '\\';
1172 }
1173 break;
1174 case '"':
1175 # Quoted literal
1176 if ( $p < strlen( $format ) - 1 ) {
1177 $endQuote = strpos( $format, '"', $p + 1 );
1178 if ( $endQuote === false ) {
1179 # No terminating quote, assume literal "
1180 $s .= '"';
1181 } else {
1182 $s .= substr( $format, $p + 1, $endQuote - $p - 1 );
1183 $p = $endQuote;
1184 }
1185 } else {
1186 # Quote at end of string, assume literal "
1187 $s .= '"';
1188 }
1189 break;
1190 default:
1191 $s .= $format[$p];
1192 }
1193 if ( $num !== false ) {
1194 if ( $rawToggle || $raw ) {
1195 $s .= $num;
1196 $raw = false;
1197 } elseif ( $roman ) {
1198 $s .= self::romanNumeral( $num );
1199 $roman = false;
1200 } elseif ( $hebrewNum ) {
1201 $s .= self::hebrewNumeral( $num );
1202 $hebrewNum = false;
1203 } else {
1204 $s .= $this->formatNum( $num, true );
1205 }
1206 }
1207 }
1208 return $s;
1209 }
1210
1211 private static $GREG_DAYS = array( 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 );
1212 private static $IRANIAN_DAYS = array( 31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 29 );
1213
1214 /**
1215 * Algorithm by Roozbeh Pournader and Mohammad Toossi to convert
1216 * Gregorian dates to Iranian dates. Originally written in C, it
1217 * is released under the terms of GNU Lesser General Public
1218 * License. Conversion to PHP was performed by Niklas Laxström.
1219 *
1220 * Link: http://www.farsiweb.info/jalali/jalali.c
1221 *
1222 * @param $ts string
1223 *
1224 * @return string
1225 */
1226 private static function tsToIranian( $ts ) {
1227 $gy = substr( $ts, 0, 4 ) -1600;
1228 $gm = substr( $ts, 4, 2 ) -1;
1229 $gd = substr( $ts, 6, 2 ) -1;
1230
1231 # Days passed from the beginning (including leap years)
1232 $gDayNo = 365 * $gy
1233 + floor( ( $gy + 3 ) / 4 )
1234 - floor( ( $gy + 99 ) / 100 )
1235 + floor( ( $gy + 399 ) / 400 );
1236
1237
1238 // Add days of the past months of this year
1239 for ( $i = 0; $i < $gm; $i++ ) {
1240 $gDayNo += self::$GREG_DAYS[$i];
1241 }
1242
1243 // Leap years
1244 if ( $gm > 1 && ( ( $gy % 4 === 0 && $gy % 100 !== 0 || ( $gy % 400 == 0 ) ) ) ) {
1245 $gDayNo++;
1246 }
1247
1248 // Days passed in current month
1249 $gDayNo += $gd;
1250
1251 $jDayNo = $gDayNo - 79;
1252
1253 $jNp = floor( $jDayNo / 12053 );
1254 $jDayNo %= 12053;
1255
1256 $jy = 979 + 33 * $jNp + 4 * floor( $jDayNo / 1461 );
1257 $jDayNo %= 1461;
1258
1259 if ( $jDayNo >= 366 ) {
1260 $jy += floor( ( $jDayNo - 1 ) / 365 );
1261 $jDayNo = floor( ( $jDayNo - 1 ) % 365 );
1262 }
1263
1264 for ( $i = 0; $i < 11 && $jDayNo >= self::$IRANIAN_DAYS[$i]; $i++ ) {
1265 $jDayNo -= self::$IRANIAN_DAYS[$i];
1266 }
1267
1268 $jm = $i + 1;
1269 $jd = $jDayNo + 1;
1270
1271 return array( $jy, $jm, $jd );
1272 }
1273
1274 /**
1275 * Converting Gregorian dates to Hijri dates.
1276 *
1277 * Based on a PHP-Nuke block by Sharjeel which is released under GNU/GPL license
1278 *
1279 * @link http://phpnuke.org/modules.php?name=News&file=article&sid=8234&mode=thread&order=0&thold=0
1280 *
1281 * @param $ts string
1282 *
1283 * @return string
1284 */
1285 private static function tsToHijri( $ts ) {
1286 $year = substr( $ts, 0, 4 );
1287 $month = substr( $ts, 4, 2 );
1288 $day = substr( $ts, 6, 2 );
1289
1290 $zyr = $year;
1291 $zd = $day;
1292 $zm = $month;
1293 $zy = $zyr;
1294
1295 if (
1296 ( $zy > 1582 ) || ( ( $zy == 1582 ) && ( $zm > 10 ) ) ||
1297 ( ( $zy == 1582 ) && ( $zm == 10 ) && ( $zd > 14 ) )
1298 )
1299 {
1300 $zjd = (int)( ( 1461 * ( $zy + 4800 + (int)( ( $zm - 14 ) / 12 ) ) ) / 4 ) +
1301 (int)( ( 367 * ( $zm - 2 - 12 * ( (int)( ( $zm - 14 ) / 12 ) ) ) ) / 12 ) -
1302 (int)( ( 3 * (int)( ( ( $zy + 4900 + (int)( ( $zm - 14 ) / 12 ) ) / 100 ) ) ) / 4 ) +
1303 $zd - 32075;
1304 } else {
1305 $zjd = 367 * $zy - (int)( ( 7 * ( $zy + 5001 + (int)( ( $zm - 9 ) / 7 ) ) ) / 4 ) +
1306 (int)( ( 275 * $zm ) / 9 ) + $zd + 1729777;
1307 }
1308
1309 $zl = $zjd -1948440 + 10632;
1310 $zn = (int)( ( $zl - 1 ) / 10631 );
1311 $zl = $zl - 10631 * $zn + 354;
1312 $zj = ( (int)( ( 10985 - $zl ) / 5316 ) ) * ( (int)( ( 50 * $zl ) / 17719 ) ) + ( (int)( $zl / 5670 ) ) * ( (int)( ( 43 * $zl ) / 15238 ) );
1313 $zl = $zl - ( (int)( ( 30 - $zj ) / 15 ) ) * ( (int)( ( 17719 * $zj ) / 50 ) ) - ( (int)( $zj / 16 ) ) * ( (int)( ( 15238 * $zj ) / 43 ) ) + 29;
1314 $zm = (int)( ( 24 * $zl ) / 709 );
1315 $zd = $zl - (int)( ( 709 * $zm ) / 24 );
1316 $zy = 30 * $zn + $zj - 30;
1317
1318 return array( $zy, $zm, $zd );
1319 }
1320
1321 /**
1322 * Converting Gregorian dates to Hebrew dates.
1323 *
1324 * Based on a JavaScript code by Abu Mami and Yisrael Hersch
1325 * (abu-mami@kaluach.net, http://www.kaluach.net), who permitted
1326 * to translate the relevant functions into PHP and release them under
1327 * GNU GPL.
1328 *
1329 * The months are counted from Tishrei = 1. In a leap year, Adar I is 13
1330 * and Adar II is 14. In a non-leap year, Adar is 6.
1331 *
1332 * @param $ts string
1333 *
1334 * @return string
1335 */
1336 private static function tsToHebrew( $ts ) {
1337 # Parse date
1338 $year = substr( $ts, 0, 4 );
1339 $month = substr( $ts, 4, 2 );
1340 $day = substr( $ts, 6, 2 );
1341
1342 # Calculate Hebrew year
1343 $hebrewYear = $year + 3760;
1344
1345 # Month number when September = 1, August = 12
1346 $month += 4;
1347 if ( $month > 12 ) {
1348 # Next year
1349 $month -= 12;
1350 $year++;
1351 $hebrewYear++;
1352 }
1353
1354 # Calculate day of year from 1 September
1355 $dayOfYear = $day;
1356 for ( $i = 1; $i < $month; $i++ ) {
1357 if ( $i == 6 ) {
1358 # February
1359 $dayOfYear += 28;
1360 # Check if the year is leap
1361 if ( $year % 400 == 0 || ( $year % 4 == 0 && $year % 100 > 0 ) ) {
1362 $dayOfYear++;
1363 }
1364 } elseif ( $i == 8 || $i == 10 || $i == 1 || $i == 3 ) {
1365 $dayOfYear += 30;
1366 } else {
1367 $dayOfYear += 31;
1368 }
1369 }
1370
1371 # Calculate the start of the Hebrew year
1372 $start = self::hebrewYearStart( $hebrewYear );
1373
1374 # Calculate next year's start
1375 if ( $dayOfYear <= $start ) {
1376 # Day is before the start of the year - it is the previous year
1377 # Next year's start
1378 $nextStart = $start;
1379 # Previous year
1380 $year--;
1381 $hebrewYear--;
1382 # Add days since previous year's 1 September
1383 $dayOfYear += 365;
1384 if ( ( $year % 400 == 0 ) || ( $year % 100 != 0 && $year % 4 == 0 ) ) {
1385 # Leap year
1386 $dayOfYear++;
1387 }
1388 # Start of the new (previous) year
1389 $start = self::hebrewYearStart( $hebrewYear );
1390 } else {
1391 # Next year's start
1392 $nextStart = self::hebrewYearStart( $hebrewYear + 1 );
1393 }
1394
1395 # Calculate Hebrew day of year
1396 $hebrewDayOfYear = $dayOfYear - $start;
1397
1398 # Difference between year's days
1399 $diff = $nextStart - $start;
1400 # Add 12 (or 13 for leap years) days to ignore the difference between
1401 # Hebrew and Gregorian year (353 at least vs. 365/6) - now the
1402 # difference is only about the year type
1403 if ( ( $year % 400 == 0 ) || ( $year % 100 != 0 && $year % 4 == 0 ) ) {
1404 $diff += 13;
1405 } else {
1406 $diff += 12;
1407 }
1408
1409 # Check the year pattern, and is leap year
1410 # 0 means an incomplete year, 1 means a regular year, 2 means a complete year
1411 # This is mod 30, to work on both leap years (which add 30 days of Adar I)
1412 # and non-leap years
1413 $yearPattern = $diff % 30;
1414 # Check if leap year
1415 $isLeap = $diff >= 30;
1416
1417 # Calculate day in the month from number of day in the Hebrew year
1418 # Don't check Adar - if the day is not in Adar, we will stop before;
1419 # if it is in Adar, we will use it to check if it is Adar I or Adar II
1420 $hebrewDay = $hebrewDayOfYear;
1421 $hebrewMonth = 1;
1422 $days = 0;
1423 while ( $hebrewMonth <= 12 ) {
1424 # Calculate days in this month
1425 if ( $isLeap && $hebrewMonth == 6 ) {
1426 # Adar in a leap year
1427 if ( $isLeap ) {
1428 # Leap year - has Adar I, with 30 days, and Adar II, with 29 days
1429 $days = 30;
1430 if ( $hebrewDay <= $days ) {
1431 # Day in Adar I
1432 $hebrewMonth = 13;
1433 } else {
1434 # Subtract the days of Adar I
1435 $hebrewDay -= $days;
1436 # Try Adar II
1437 $days = 29;
1438 if ( $hebrewDay <= $days ) {
1439 # Day in Adar II
1440 $hebrewMonth = 14;
1441 }
1442 }
1443 }
1444 } elseif ( $hebrewMonth == 2 && $yearPattern == 2 ) {
1445 # Cheshvan in a complete year (otherwise as the rule below)
1446 $days = 30;
1447 } elseif ( $hebrewMonth == 3 && $yearPattern == 0 ) {
1448 # Kislev in an incomplete year (otherwise as the rule below)
1449 $days = 29;
1450 } else {
1451 # Odd months have 30 days, even have 29
1452 $days = 30 - ( $hebrewMonth - 1 ) % 2;
1453 }
1454 if ( $hebrewDay <= $days ) {
1455 # In the current month
1456 break;
1457 } else {
1458 # Subtract the days of the current month
1459 $hebrewDay -= $days;
1460 # Try in the next month
1461 $hebrewMonth++;
1462 }
1463 }
1464
1465 return array( $hebrewYear, $hebrewMonth, $hebrewDay, $days );
1466 }
1467
1468 /**
1469 * This calculates the Hebrew year start, as days since 1 September.
1470 * Based on Carl Friedrich Gauss algorithm for finding Easter date.
1471 * Used for Hebrew date.
1472 *
1473 * @param $year int
1474 *
1475 * @return string
1476 */
1477 private static function hebrewYearStart( $year ) {
1478 $a = intval( ( 12 * ( $year - 1 ) + 17 ) % 19 );
1479 $b = intval( ( $year - 1 ) % 4 );
1480 $m = 32.044093161144 + 1.5542417966212 * $a + $b / 4.0 - 0.0031777940220923 * ( $year - 1 );
1481 if ( $m < 0 ) {
1482 $m--;
1483 }
1484 $Mar = intval( $m );
1485 if ( $m < 0 ) {
1486 $m++;
1487 }
1488 $m -= $Mar;
1489
1490 $c = intval( ( $Mar + 3 * ( $year - 1 ) + 5 * $b + 5 ) % 7 );
1491 if ( $c == 0 && $a > 11 && $m >= 0.89772376543210 ) {
1492 $Mar++;
1493 } elseif ( $c == 1 && $a > 6 && $m >= 0.63287037037037 ) {
1494 $Mar += 2;
1495 } elseif ( $c == 2 || $c == 4 || $c == 6 ) {
1496 $Mar++;
1497 }
1498
1499 $Mar += intval( ( $year - 3761 ) / 100 ) - intval( ( $year - 3761 ) / 400 ) - 24;
1500 return $Mar;
1501 }
1502
1503 /**
1504 * Algorithm to convert Gregorian dates to Thai solar dates,
1505 * Minguo dates or Minguo dates.
1506 *
1507 * Link: http://en.wikipedia.org/wiki/Thai_solar_calendar
1508 * http://en.wikipedia.org/wiki/Minguo_calendar
1509 * http://en.wikipedia.org/wiki/Japanese_era_name
1510 *
1511 * @param $ts String: 14-character timestamp
1512 * @param $cName String: calender name
1513 * @return Array: converted year, month, day
1514 */
1515 private static function tsToYear( $ts, $cName ) {
1516 $gy = substr( $ts, 0, 4 );
1517 $gm = substr( $ts, 4, 2 );
1518 $gd = substr( $ts, 6, 2 );
1519
1520 if ( !strcmp( $cName, 'thai' ) ) {
1521 # Thai solar dates
1522 # Add 543 years to the Gregorian calendar
1523 # Months and days are identical
1524 $gy_offset = $gy + 543;
1525 } elseif ( ( !strcmp( $cName, 'minguo' ) ) || !strcmp( $cName, 'juche' ) ) {
1526 # Minguo dates
1527 # Deduct 1911 years from the Gregorian calendar
1528 # Months and days are identical
1529 $gy_offset = $gy - 1911;
1530 } elseif ( !strcmp( $cName, 'tenno' ) ) {
1531 # Nengō dates up to Meiji period
1532 # Deduct years from the Gregorian calendar
1533 # depending on the nengo periods
1534 # Months and days are identical
1535 if ( ( $gy < 1912 ) || ( ( $gy == 1912 ) && ( $gm < 7 ) ) || ( ( $gy == 1912 ) && ( $gm == 7 ) && ( $gd < 31 ) ) ) {
1536 # Meiji period
1537 $gy_gannen = $gy - 1868 + 1;
1538 $gy_offset = $gy_gannen;
1539 if ( $gy_gannen == 1 ) {
1540 $gy_offset = '元';
1541 }
1542 $gy_offset = '明治' . $gy_offset;
1543 } elseif (
1544 ( ( $gy == 1912 ) && ( $gm == 7 ) && ( $gd == 31 ) ) ||
1545 ( ( $gy == 1912 ) && ( $gm >= 8 ) ) ||
1546 ( ( $gy > 1912 ) && ( $gy < 1926 ) ) ||
1547 ( ( $gy == 1926 ) && ( $gm < 12 ) ) ||
1548 ( ( $gy == 1926 ) && ( $gm == 12 ) && ( $gd < 26 ) )
1549 )
1550 {
1551 # Taishō period
1552 $gy_gannen = $gy - 1912 + 1;
1553 $gy_offset = $gy_gannen;
1554 if ( $gy_gannen == 1 ) {
1555 $gy_offset = '元';
1556 }
1557 $gy_offset = '大正' . $gy_offset;
1558 } elseif (
1559 ( ( $gy == 1926 ) && ( $gm == 12 ) && ( $gd >= 26 ) ) ||
1560 ( ( $gy > 1926 ) && ( $gy < 1989 ) ) ||
1561 ( ( $gy == 1989 ) && ( $gm == 1 ) && ( $gd < 8 ) )
1562 )
1563 {
1564 # Shōwa period
1565 $gy_gannen = $gy - 1926 + 1;
1566 $gy_offset = $gy_gannen;
1567 if ( $gy_gannen == 1 ) {
1568 $gy_offset = '元';
1569 }
1570 $gy_offset = '昭和' . $gy_offset;
1571 } else {
1572 # Heisei period
1573 $gy_gannen = $gy - 1989 + 1;
1574 $gy_offset = $gy_gannen;
1575 if ( $gy_gannen == 1 ) {
1576 $gy_offset = '元';
1577 }
1578 $gy_offset = '平成' . $gy_offset;
1579 }
1580 } else {
1581 $gy_offset = $gy;
1582 }
1583
1584 return array( $gy_offset, $gm, $gd );
1585 }
1586
1587 /**
1588 * Roman number formatting up to 3000
1589 *
1590 * @param $num int
1591 *
1592 * @return string
1593 */
1594 static function romanNumeral( $num ) {
1595 static $table = array(
1596 array( '', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X' ),
1597 array( '', 'X', 'XX', 'XXX', 'XL', 'L', 'LX', 'LXX', 'LXXX', 'XC', 'C' ),
1598 array( '', 'C', 'CC', 'CCC', 'CD', 'D', 'DC', 'DCC', 'DCCC', 'CM', 'M' ),
1599 array( '', 'M', 'MM', 'MMM' )
1600 );
1601
1602 $num = intval( $num );
1603 if ( $num > 3000 || $num <= 0 ) {
1604 return $num;
1605 }
1606
1607 $s = '';
1608 for ( $pow10 = 1000, $i = 3; $i >= 0; $pow10 /= 10, $i-- ) {
1609 if ( $num >= $pow10 ) {
1610 $s .= $table[$i][floor( $num / $pow10 )];
1611 }
1612 $num = $num % $pow10;
1613 }
1614 return $s;
1615 }
1616
1617 /**
1618 * Hebrew Gematria number formatting up to 9999
1619 *
1620 * @param $num int
1621 *
1622 * @return string
1623 */
1624 static function hebrewNumeral( $num ) {
1625 static $table = array(
1626 array( '', 'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י' ),
1627 array( '', 'י', 'כ', 'ל', 'מ', 'נ', 'ס', 'ע', 'פ', 'צ', 'ק' ),
1628 array( '', 'ק', 'ר', 'ש', 'ת', 'תק', 'תר', 'תש', 'תת', 'תתק', 'תתר' ),
1629 array( '', 'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י' )
1630 );
1631
1632 $num = intval( $num );
1633 if ( $num > 9999 || $num <= 0 ) {
1634 return $num;
1635 }
1636
1637 $s = '';
1638 for ( $pow10 = 1000, $i = 3; $i >= 0; $pow10 /= 10, $i-- ) {
1639 if ( $num >= $pow10 ) {
1640 if ( $num == 15 || $num == 16 ) {
1641 $s .= $table[0][9] . $table[0][$num - 9];
1642 $num = 0;
1643 } else {
1644 $s .= $table[$i][intval( ( $num / $pow10 ) )];
1645 if ( $pow10 == 1000 ) {
1646 $s .= "'";
1647 }
1648 }
1649 }
1650 $num = $num % $pow10;
1651 }
1652 if ( strlen( $s ) == 2 ) {
1653 $str = $s . "'";
1654 } else {
1655 $str = substr( $s, 0, strlen( $s ) - 2 ) . '"';
1656 $str .= substr( $s, strlen( $s ) - 2, 2 );
1657 }
1658 $start = substr( $str, 0, strlen( $str ) - 2 );
1659 $end = substr( $str, strlen( $str ) - 2 );
1660 switch( $end ) {
1661 case 'כ':
1662 $str = $start . 'ך';
1663 break;
1664 case 'מ':
1665 $str = $start . 'ם';
1666 break;
1667 case 'נ':
1668 $str = $start . 'ן';
1669 break;
1670 case 'פ':
1671 $str = $start . 'ף';
1672 break;
1673 case 'צ':
1674 $str = $start . 'ץ';
1675 break;
1676 }
1677 return $str;
1678 }
1679
1680 /**
1681 * This is meant to be used by time(), date(), and timeanddate() to get
1682 * the date preference they're supposed to use, it should be used in
1683 * all children.
1684 *
1685 *<code>
1686 * function timeanddate([...], $format = true) {
1687 * $datePreference = $this->dateFormat($format);
1688 * [...]
1689 * }
1690 *</code>
1691 *
1692 * @param $usePrefs Mixed: if true, the user's preference is used
1693 * if false, the site/language default is used
1694 * if int/string, assumed to be a format.
1695 * @return string
1696 */
1697 function dateFormat( $usePrefs = true ) {
1698 global $wgUser;
1699
1700 if ( is_bool( $usePrefs ) ) {
1701 if ( $usePrefs ) {
1702 $datePreference = $wgUser->getDatePreference();
1703 } else {
1704 $datePreference = (string)User::getDefaultOption( 'date' );
1705 }
1706 } else {
1707 $datePreference = (string)$usePrefs;
1708 }
1709
1710 // return int
1711 if ( $datePreference == '' ) {
1712 return 'default';
1713 }
1714
1715 return $datePreference;
1716 }
1717
1718 /**
1719 * Get a format string for a given type and preference
1720 * @param $type string May be date, time or both
1721 * @param $pref string The format name as it appears in Messages*.php
1722 *
1723 * @return string
1724 */
1725 function getDateFormatString( $type, $pref ) {
1726 if ( !isset( $this->dateFormatStrings[$type][$pref] ) ) {
1727 if ( $pref == 'default' ) {
1728 $pref = $this->getDefaultDateFormat();
1729 $df = self::$dataCache->getSubitem( $this->mCode, 'dateFormats', "$pref $type" );
1730 } else {
1731 $df = self::$dataCache->getSubitem( $this->mCode, 'dateFormats', "$pref $type" );
1732 if ( is_null( $df ) ) {
1733 $pref = $this->getDefaultDateFormat();
1734 $df = self::$dataCache->getSubitem( $this->mCode, 'dateFormats', "$pref $type" );
1735 }
1736 }
1737 $this->dateFormatStrings[$type][$pref] = $df;
1738 }
1739 return $this->dateFormatStrings[$type][$pref];
1740 }
1741
1742 /**
1743 * @param $ts Mixed: the time format which needs to be turned into a
1744 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
1745 * @param $adj Bool: whether to adjust the time output according to the
1746 * user configured offset ($timecorrection)
1747 * @param $format Mixed: true to use user's date format preference
1748 * @param $timecorrection String|bool the time offset as returned by
1749 * validateTimeZone() in Special:Preferences
1750 * @return string
1751 */
1752 function date( $ts, $adj = false, $format = true, $timecorrection = false ) {
1753 $ts = wfTimestamp( TS_MW, $ts );
1754 if ( $adj ) {
1755 $ts = $this->userAdjust( $ts, $timecorrection );
1756 }
1757 $df = $this->getDateFormatString( 'date', $this->dateFormat( $format ) );
1758 return $this->sprintfDate( $df, $ts );
1759 }
1760
1761 /**
1762 * @param $ts Mixed: the time format which needs to be turned into a
1763 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
1764 * @param $adj Bool: whether to adjust the time output according to the
1765 * user configured offset ($timecorrection)
1766 * @param $format Mixed: true to use user's date format preference
1767 * @param $timecorrection String|bool the time offset as returned by
1768 * validateTimeZone() in Special:Preferences
1769 * @return string
1770 */
1771 function time( $ts, $adj = false, $format = true, $timecorrection = false ) {
1772 $ts = wfTimestamp( TS_MW, $ts );
1773 if ( $adj ) {
1774 $ts = $this->userAdjust( $ts, $timecorrection );
1775 }
1776 $df = $this->getDateFormatString( 'time', $this->dateFormat( $format ) );
1777 return $this->sprintfDate( $df, $ts );
1778 }
1779
1780 /**
1781 * @param $ts Mixed: the time format which needs to be turned into a
1782 * date('YmdHis') format with wfTimestamp(TS_MW,$ts)
1783 * @param $adj Bool: whether to adjust the time output according to the
1784 * user configured offset ($timecorrection)
1785 * @param $format Mixed: what format to return, if it's false output the
1786 * default one (default true)
1787 * @param $timecorrection String|bool the time offset as returned by
1788 * validateTimeZone() in Special:Preferences
1789 * @return string
1790 */
1791 function timeanddate( $ts, $adj = false, $format = true, $timecorrection = false ) {
1792 $ts = wfTimestamp( TS_MW, $ts );
1793 if ( $adj ) {
1794 $ts = $this->userAdjust( $ts, $timecorrection );
1795 }
1796 $df = $this->getDateFormatString( 'both', $this->dateFormat( $format ) );
1797 return $this->sprintfDate( $df, $ts );
1798 }
1799
1800 /**
1801 * @param $key string
1802 * @return array|null
1803 */
1804 function getMessage( $key ) {
1805 return self::$dataCache->getSubitem( $this->mCode, 'messages', $key );
1806 }
1807
1808 /**
1809 * @return array
1810 */
1811 function getAllMessages() {
1812 return self::$dataCache->getItem( $this->mCode, 'messages' );
1813 }
1814
1815 /**
1816 * @param $in
1817 * @param $out
1818 * @param $string
1819 * @return string
1820 */
1821 function iconv( $in, $out, $string ) {
1822 # This is a wrapper for iconv in all languages except esperanto,
1823 # which does some nasty x-conversions beforehand
1824
1825 # Even with //IGNORE iconv can whine about illegal characters in
1826 # *input* string. We just ignore those too.
1827 # REF: http://bugs.php.net/bug.php?id=37166
1828 # REF: https://bugzilla.wikimedia.org/show_bug.cgi?id=16885
1829 wfSuppressWarnings();
1830 $text = iconv( $in, $out . '//IGNORE', $string );
1831 wfRestoreWarnings();
1832 return $text;
1833 }
1834
1835 // callback functions for uc(), lc(), ucwords(), ucwordbreaks()
1836
1837 /**
1838 * @param $matches array
1839 * @return mixed|string
1840 */
1841 function ucwordbreaksCallbackAscii( $matches ) {
1842 return $this->ucfirst( $matches[1] );
1843 }
1844
1845 /**
1846 * @param $matches array
1847 * @return string
1848 */
1849 function ucwordbreaksCallbackMB( $matches ) {
1850 return mb_strtoupper( $matches[0] );
1851 }
1852
1853 /**
1854 * @param $matches array
1855 * @return string
1856 */
1857 function ucCallback( $matches ) {
1858 list( $wikiUpperChars ) = self::getCaseMaps();
1859 return strtr( $matches[1], $wikiUpperChars );
1860 }
1861
1862 /**
1863 * @param $matches array
1864 * @return string
1865 */
1866 function lcCallback( $matches ) {
1867 list( , $wikiLowerChars ) = self::getCaseMaps();
1868 return strtr( $matches[1], $wikiLowerChars );
1869 }
1870
1871 /**
1872 * @param $matches array
1873 * @return string
1874 */
1875 function ucwordsCallbackMB( $matches ) {
1876 return mb_strtoupper( $matches[0] );
1877 }
1878
1879 /**
1880 * @param $matches array
1881 * @return string
1882 */
1883 function ucwordsCallbackWiki( $matches ) {
1884 list( $wikiUpperChars ) = self::getCaseMaps();
1885 return strtr( $matches[0], $wikiUpperChars );
1886 }
1887
1888 /**
1889 * Make a string's first character uppercase
1890 *
1891 * @param $str string
1892 *
1893 * @return string
1894 */
1895 function ucfirst( $str ) {
1896 $o = ord( $str );
1897 if ( $o < 96 ) { // if already uppercase...
1898 return $str;
1899 } elseif ( $o < 128 ) {
1900 return ucfirst( $str ); // use PHP's ucfirst()
1901 } else {
1902 // fall back to more complex logic in case of multibyte strings
1903 return $this->uc( $str, true );
1904 }
1905 }
1906
1907 /**
1908 * Convert a string to uppercase
1909 *
1910 * @param $str string
1911 * @param $first bool
1912 *
1913 * @return string
1914 */
1915 function uc( $str, $first = false ) {
1916 if ( function_exists( 'mb_strtoupper' ) ) {
1917 if ( $first ) {
1918 if ( $this->isMultibyte( $str ) ) {
1919 return mb_strtoupper( mb_substr( $str, 0, 1 ) ) . mb_substr( $str, 1 );
1920 } else {
1921 return ucfirst( $str );
1922 }
1923 } else {
1924 return $this->isMultibyte( $str ) ? mb_strtoupper( $str ) : strtoupper( $str );
1925 }
1926 } else {
1927 if ( $this->isMultibyte( $str ) ) {
1928 $x = $first ? '^' : '';
1929 return preg_replace_callback(
1930 "/$x([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/",
1931 array( $this, 'ucCallback' ),
1932 $str
1933 );
1934 } else {
1935 return $first ? ucfirst( $str ) : strtoupper( $str );
1936 }
1937 }
1938 }
1939
1940 /**
1941 * @param $str string
1942 * @return mixed|string
1943 */
1944 function lcfirst( $str ) {
1945 $o = ord( $str );
1946 if ( !$o ) {
1947 return strval( $str );
1948 } elseif ( $o >= 128 ) {
1949 return $this->lc( $str, true );
1950 } elseif ( $o > 96 ) {
1951 return $str;
1952 } else {
1953 $str[0] = strtolower( $str[0] );
1954 return $str;
1955 }
1956 }
1957
1958 /**
1959 * @param $str string
1960 * @param $first bool
1961 * @return mixed|string
1962 */
1963 function lc( $str, $first = false ) {
1964 if ( function_exists( 'mb_strtolower' ) ) {
1965 if ( $first ) {
1966 if ( $this->isMultibyte( $str ) ) {
1967 return mb_strtolower( mb_substr( $str, 0, 1 ) ) . mb_substr( $str, 1 );
1968 } else {
1969 return strtolower( substr( $str, 0, 1 ) ) . substr( $str, 1 );
1970 }
1971 } else {
1972 return $this->isMultibyte( $str ) ? mb_strtolower( $str ) : strtolower( $str );
1973 }
1974 } else {
1975 if ( $this->isMultibyte( $str ) ) {
1976 $x = $first ? '^' : '';
1977 return preg_replace_callback(
1978 "/$x([A-Z]|[\\xc0-\\xff][\\x80-\\xbf]*)/",
1979 array( $this, 'lcCallback' ),
1980 $str
1981 );
1982 } else {
1983 return $first ? strtolower( substr( $str, 0, 1 ) ) . substr( $str, 1 ) : strtolower( $str );
1984 }
1985 }
1986 }
1987
1988 /**
1989 * @param $str string
1990 * @return bool
1991 */
1992 function isMultibyte( $str ) {
1993 return (bool)preg_match( '/[\x80-\xff]/', $str );
1994 }
1995
1996 /**
1997 * @param $str string
1998 * @return mixed|string
1999 */
2000 function ucwords( $str ) {
2001 if ( $this->isMultibyte( $str ) ) {
2002 $str = $this->lc( $str );
2003
2004 // regexp to find first letter in each word (i.e. after each space)
2005 $replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)| ([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
2006
2007 // function to use to capitalize a single char
2008 if ( function_exists( 'mb_strtoupper' ) ) {
2009 return preg_replace_callback(
2010 $replaceRegexp,
2011 array( $this, 'ucwordsCallbackMB' ),
2012 $str
2013 );
2014 } else {
2015 return preg_replace_callback(
2016 $replaceRegexp,
2017 array( $this, 'ucwordsCallbackWiki' ),
2018 $str
2019 );
2020 }
2021 } else {
2022 return ucwords( strtolower( $str ) );
2023 }
2024 }
2025
2026 /**
2027 * capitalize words at word breaks
2028 *
2029 * @param $str string
2030 * @return mixed
2031 */
2032 function ucwordbreaks( $str ) {
2033 if ( $this->isMultibyte( $str ) ) {
2034 $str = $this->lc( $str );
2035
2036 // since \b doesn't work for UTF-8, we explicitely define word break chars
2037 $breaks = "[ \-\(\)\}\{\.,\?!]";
2038
2039 // find first letter after word break
2040 $replaceRegexp = "/^([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)|$breaks([a-z]|[\\xc0-\\xff][\\x80-\\xbf]*)/";
2041
2042 if ( function_exists( 'mb_strtoupper' ) ) {
2043 return preg_replace_callback(
2044 $replaceRegexp,
2045 array( $this, 'ucwordbreaksCallbackMB' ),
2046 $str
2047 );
2048 } else {
2049 return preg_replace_callback(
2050 $replaceRegexp,
2051 array( $this, 'ucwordsCallbackWiki' ),
2052 $str
2053 );
2054 }
2055 } else {
2056 return preg_replace_callback(
2057 '/\b([\w\x80-\xff]+)\b/',
2058 array( $this, 'ucwordbreaksCallbackAscii' ),
2059 $str
2060 );
2061 }
2062 }
2063
2064 /**
2065 * Return a case-folded representation of $s
2066 *
2067 * This is a representation such that caseFold($s1)==caseFold($s2) if $s1
2068 * and $s2 are the same except for the case of their characters. It is not
2069 * necessary for the value returned to make sense when displayed.
2070 *
2071 * Do *not* perform any other normalisation in this function. If a caller
2072 * uses this function when it should be using a more general normalisation
2073 * function, then fix the caller.
2074 *
2075 * @param $s string
2076 *
2077 * @return string
2078 */
2079 function caseFold( $s ) {
2080 return $this->uc( $s );
2081 }
2082
2083 /**
2084 * @param $s string
2085 * @return string
2086 */
2087 function checkTitleEncoding( $s ) {
2088 if ( is_array( $s ) ) {
2089 wfDebugDieBacktrace( 'Given array to checkTitleEncoding.' );
2090 }
2091 # Check for non-UTF-8 URLs
2092 $ishigh = preg_match( '/[\x80-\xff]/', $s );
2093 if ( !$ishigh ) {
2094 return $s;
2095 }
2096
2097 $isutf8 = preg_match( '/^([\x00-\x7f]|[\xc0-\xdf][\x80-\xbf]|' .
2098 '[\xe0-\xef][\x80-\xbf]{2}|[\xf0-\xf7][\x80-\xbf]{3})+$/', $s );
2099 if ( $isutf8 ) {
2100 return $s;
2101 }
2102
2103 return $this->iconv( $this->fallback8bitEncoding(), 'utf-8', $s );
2104 }
2105
2106 /**
2107 * @return array
2108 */
2109 function fallback8bitEncoding() {
2110 return self::$dataCache->getItem( $this->mCode, 'fallback8bitEncoding' );
2111 }
2112
2113 /**
2114 * Most writing systems use whitespace to break up words.
2115 * Some languages such as Chinese don't conventionally do this,
2116 * which requires special handling when breaking up words for
2117 * searching etc.
2118 *
2119 * @return bool
2120 */
2121 function hasWordBreaks() {
2122 return true;
2123 }
2124
2125 /**
2126 * Some languages such as Chinese require word segmentation,
2127 * Specify such segmentation when overridden in derived class.
2128 *
2129 * @param $string String
2130 * @return String
2131 */
2132 function segmentByWord( $string ) {
2133 return $string;
2134 }
2135
2136 /**
2137 * Some languages have special punctuation need to be normalized.
2138 * Make such changes here.
2139 *
2140 * @param $string String
2141 * @return String
2142 */
2143 function normalizeForSearch( $string ) {
2144 return self::convertDoubleWidth( $string );
2145 }
2146
2147 /**
2148 * convert double-width roman characters to single-width.
2149 * range: ff00-ff5f ~= 0020-007f
2150 *
2151 * @param $string string
2152 *
2153 * @return string
2154 */
2155 protected static function convertDoubleWidth( $string ) {
2156 static $full = null;
2157 static $half = null;
2158
2159 if ( $full === null ) {
2160 $fullWidth = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
2161 $halfWidth = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
2162 $full = str_split( $fullWidth, 3 );
2163 $half = str_split( $halfWidth );
2164 }
2165
2166 $string = str_replace( $full, $half, $string );
2167 return $string;
2168 }
2169
2170 /**
2171 * @param $string string
2172 * @param $pattern string
2173 * @return string
2174 */
2175 protected static function insertSpace( $string, $pattern ) {
2176 $string = preg_replace( $pattern, " $1 ", $string );
2177 $string = preg_replace( '/ +/', ' ', $string );
2178 return $string;
2179 }
2180
2181 /**
2182 * @param $termsArray array
2183 * @return array
2184 */
2185 function convertForSearchResult( $termsArray ) {
2186 # some languages, e.g. Chinese, need to do a conversion
2187 # in order for search results to be displayed correctly
2188 return $termsArray;
2189 }
2190
2191 /**
2192 * Get the first character of a string.
2193 *
2194 * @param $s string
2195 * @return string
2196 */
2197 function firstChar( $s ) {
2198 $matches = array();
2199 preg_match(
2200 '/^([\x00-\x7f]|[\xc0-\xdf][\x80-\xbf]|' .
2201 '[\xe0-\xef][\x80-\xbf]{2}|[\xf0-\xf7][\x80-\xbf]{3})/',
2202 $s,
2203 $matches
2204 );
2205
2206 if ( isset( $matches[1] ) ) {
2207 if ( strlen( $matches[1] ) != 3 ) {
2208 return $matches[1];
2209 }
2210
2211 // Break down Hangul syllables to grab the first jamo
2212 $code = utf8ToCodepoint( $matches[1] );
2213 if ( $code < 0xac00 || 0xd7a4 <= $code ) {
2214 return $matches[1];
2215 } elseif ( $code < 0xb098 ) {
2216 return "\xe3\x84\xb1";
2217 } elseif ( $code < 0xb2e4 ) {
2218 return "\xe3\x84\xb4";
2219 } elseif ( $code < 0xb77c ) {
2220 return "\xe3\x84\xb7";
2221 } elseif ( $code < 0xb9c8 ) {
2222 return "\xe3\x84\xb9";
2223 } elseif ( $code < 0xbc14 ) {
2224 return "\xe3\x85\x81";
2225 } elseif ( $code < 0xc0ac ) {
2226 return "\xe3\x85\x82";
2227 } elseif ( $code < 0xc544 ) {
2228 return "\xe3\x85\x85";
2229 } elseif ( $code < 0xc790 ) {
2230 return "\xe3\x85\x87";
2231 } elseif ( $code < 0xcc28 ) {
2232 return "\xe3\x85\x88";
2233 } elseif ( $code < 0xce74 ) {
2234 return "\xe3\x85\x8a";
2235 } elseif ( $code < 0xd0c0 ) {
2236 return "\xe3\x85\x8b";
2237 } elseif ( $code < 0xd30c ) {
2238 return "\xe3\x85\x8c";
2239 } elseif ( $code < 0xd558 ) {
2240 return "\xe3\x85\x8d";
2241 } else {
2242 return "\xe3\x85\x8e";
2243 }
2244 } else {
2245 return '';
2246 }
2247 }
2248
2249 function initEncoding() {
2250 # Some languages may have an alternate char encoding option
2251 # (Esperanto X-coding, Japanese furigana conversion, etc)
2252 # If this language is used as the primary content language,
2253 # an override to the defaults can be set here on startup.
2254 }
2255
2256 /**
2257 * @param $s string
2258 * @return string
2259 */
2260 function recodeForEdit( $s ) {
2261 # For some languages we'll want to explicitly specify
2262 # which characters make it into the edit box raw
2263 # or are converted in some way or another.
2264 global $wgEditEncoding;
2265 if ( $wgEditEncoding == '' || $wgEditEncoding == 'UTF-8' ) {
2266 return $s;
2267 } else {
2268 return $this->iconv( 'UTF-8', $wgEditEncoding, $s );
2269 }
2270 }
2271
2272 /**
2273 * @param $s string
2274 * @return string
2275 */
2276 function recodeInput( $s ) {
2277 # Take the previous into account.
2278 global $wgEditEncoding;
2279 if ( $wgEditEncoding != '' ) {
2280 $enc = $wgEditEncoding;
2281 } else {
2282 $enc = 'UTF-8';
2283 }
2284 if ( $enc == 'UTF-8' ) {
2285 return $s;
2286 } else {
2287 return $this->iconv( $enc, 'UTF-8', $s );
2288 }
2289 }
2290
2291 /**
2292 * Convert a UTF-8 string to normal form C. In Malayalam and Arabic, this
2293 * also cleans up certain backwards-compatible sequences, converting them
2294 * to the modern Unicode equivalent.
2295 *
2296 * This is language-specific for performance reasons only.
2297 *
2298 * @param $s string
2299 *
2300 * @return string
2301 */
2302 function normalize( $s ) {
2303 global $wgAllUnicodeFixes;
2304 $s = UtfNormal::cleanUp( $s );
2305 if ( $wgAllUnicodeFixes ) {
2306 $s = $this->transformUsingPairFile( 'normalize-ar.ser', $s );
2307 $s = $this->transformUsingPairFile( 'normalize-ml.ser', $s );
2308 }
2309
2310 return $s;
2311 }
2312
2313 /**
2314 * Transform a string using serialized data stored in the given file (which
2315 * must be in the serialized subdirectory of $IP). The file contains pairs
2316 * mapping source characters to destination characters.
2317 *
2318 * The data is cached in process memory. This will go faster if you have the
2319 * FastStringSearch extension.
2320 *
2321 * @param $file string
2322 * @param $string string
2323 *
2324 * @return string
2325 */
2326 function transformUsingPairFile( $file, $string ) {
2327 if ( !isset( $this->transformData[$file] ) ) {
2328 $data = wfGetPrecompiledData( $file );
2329 if ( $data === false ) {
2330 throw new MWException( __METHOD__ . ": The transformation file $file is missing" );
2331 }
2332 $this->transformData[$file] = new ReplacementArray( $data );
2333 }
2334 return $this->transformData[$file]->replace( $string );
2335 }
2336
2337 /**
2338 * For right-to-left language support
2339 *
2340 * @return bool
2341 */
2342 function isRTL() {
2343 return self::$dataCache->getItem( $this->mCode, 'rtl' );
2344 }
2345
2346 /**
2347 * Return the correct HTML 'dir' attribute value for this language.
2348 * @return String
2349 */
2350 function getDir() {
2351 return $this->isRTL() ? 'rtl' : 'ltr';
2352 }
2353
2354 /**
2355 * Return 'left' or 'right' as appropriate alignment for line-start
2356 * for this language's text direction.
2357 *
2358 * Should be equivalent to CSS3 'start' text-align value....
2359 *
2360 * @return String
2361 */
2362 function alignStart() {
2363 return $this->isRTL() ? 'right' : 'left';
2364 }
2365
2366 /**
2367 * Return 'right' or 'left' as appropriate alignment for line-end
2368 * for this language's text direction.
2369 *
2370 * Should be equivalent to CSS3 'end' text-align value....
2371 *
2372 * @return String
2373 */
2374 function alignEnd() {
2375 return $this->isRTL() ? 'left' : 'right';
2376 }
2377
2378 /**
2379 * A hidden direction mark (LRM or RLM), depending on the language direction
2380 *
2381 * @param $opposite Boolean Get the direction mark opposite to your language
2382 * @return string
2383 */
2384 function getDirMark( $opposite = false ) {
2385 $rtl = "\xE2\x80\x8F";
2386 $ltr = "\xE2\x80\x8E";
2387 if( $opposite ) { return $this->isRTL() ? $ltr : $rtl; }
2388 return $this->isRTL() ? $rtl : $ltr;
2389 }
2390
2391 /**
2392 * @return array
2393 */
2394 function capitalizeAllNouns() {
2395 return self::$dataCache->getItem( $this->mCode, 'capitalizeAllNouns' );
2396 }
2397
2398 /**
2399 * An arrow, depending on the language direction
2400 *
2401 * @return string
2402 */
2403 function getArrow() {
2404 return $this->isRTL() ? '←' : '→';
2405 }
2406
2407 /**
2408 * To allow "foo[[bar]]" to extend the link over the whole word "foobar"
2409 *
2410 * @return bool
2411 */
2412 function linkPrefixExtension() {
2413 return self::$dataCache->getItem( $this->mCode, 'linkPrefixExtension' );
2414 }
2415
2416 /**
2417 * @return array
2418 */
2419 function getMagicWords() {
2420 return self::$dataCache->getItem( $this->mCode, 'magicWords' );
2421 }
2422
2423 protected function doMagicHook() {
2424 if ( $this->mMagicHookDone ) {
2425 return;
2426 }
2427 $this->mMagicHookDone = true;
2428 wfProfileIn( 'LanguageGetMagic' );
2429 wfRunHooks( 'LanguageGetMagic', array( &$this->mMagicExtensions, $this->getCode() ) );
2430 wfProfileOut( 'LanguageGetMagic' );
2431 }
2432
2433 /**
2434 * Fill a MagicWord object with data from here
2435 *
2436 * @param $mw
2437 */
2438 function getMagic( $mw ) {
2439 $this->doMagicHook();
2440
2441 if ( isset( $this->mMagicExtensions[$mw->mId] ) ) {
2442 $rawEntry = $this->mMagicExtensions[$mw->mId];
2443 } else {
2444 $magicWords = $this->getMagicWords();
2445 if ( isset( $magicWords[$mw->mId] ) ) {
2446 $rawEntry = $magicWords[$mw->mId];
2447 } else {
2448 $rawEntry = false;
2449 }
2450 }
2451
2452 if ( !is_array( $rawEntry ) ) {
2453 error_log( "\"$rawEntry\" is not a valid magic word for \"$mw->mId\"" );
2454 } else {
2455 $mw->mCaseSensitive = $rawEntry[0];
2456 $mw->mSynonyms = array_slice( $rawEntry, 1 );
2457 }
2458 }
2459
2460 /**
2461 * Add magic words to the extension array
2462 *
2463 * @param $newWords array
2464 */
2465 function addMagicWordsByLang( $newWords ) {
2466 $fallbackChain = $this->getFallbackLanguages();
2467 $fallbackChain = array_reverse( $fallbackChain );
2468 foreach ( $fallbackChain as $code ) {
2469 if ( isset( $newWords[$code] ) ) {
2470 $this->mMagicExtensions = $newWords[$code] + $this->mMagicExtensions;
2471 }
2472 }
2473 }
2474
2475 /**
2476 * Get special page names, as an associative array
2477 * case folded alias => real name
2478 */
2479 function getSpecialPageAliases() {
2480 // Cache aliases because it may be slow to load them
2481 if ( is_null( $this->mExtendedSpecialPageAliases ) ) {
2482 // Initialise array
2483 $this->mExtendedSpecialPageAliases =
2484 self::$dataCache->getItem( $this->mCode, 'specialPageAliases' );
2485 wfRunHooks( 'LanguageGetSpecialPageAliases',
2486 array( &$this->mExtendedSpecialPageAliases, $this->getCode() ) );
2487 }
2488
2489 return $this->mExtendedSpecialPageAliases;
2490 }
2491
2492 /**
2493 * Italic is unsuitable for some languages
2494 *
2495 * @param $text String: the text to be emphasized.
2496 * @return string
2497 */
2498 function emphasize( $text ) {
2499 return "<em>$text</em>";
2500 }
2501
2502 /**
2503 * Normally we output all numbers in plain en_US style, that is
2504 * 293,291.235 for twohundredninetythreethousand-twohundredninetyone
2505 * point twohundredthirtyfive. However this is not sutable for all
2506 * languages, some such as Pakaran want ੨੯੩,੨੯੫.੨੩੫ and others such as
2507 * Icelandic just want to use commas instead of dots, and dots instead
2508 * of commas like "293.291,235".
2509 *
2510 * An example of this function being called:
2511 * <code>
2512 * wfMsg( 'message', $wgLang->formatNum( $num ) )
2513 * </code>
2514 *
2515 * See LanguageGu.php for the Gujarati implementation and
2516 * $separatorTransformTable on MessageIs.php for
2517 * the , => . and . => , implementation.
2518 *
2519 * @todo check if it's viable to use localeconv() for the decimal
2520 * separator thing.
2521 * @param $number Mixed: the string to be formatted, should be an integer
2522 * or a floating point number.
2523 * @param $nocommafy Bool: set to true for special numbers like dates
2524 * @return string
2525 */
2526 function formatNum( $number, $nocommafy = false ) {
2527 global $wgTranslateNumerals;
2528 if ( !$nocommafy ) {
2529 $number = $this->commafy( $number );
2530 $s = $this->separatorTransformTable();
2531 if ( $s ) {
2532 $number = strtr( $number, $s );
2533 }
2534 }
2535
2536 if ( $wgTranslateNumerals ) {
2537 $s = $this->digitTransformTable();
2538 if ( $s ) {
2539 $number = strtr( $number, $s );
2540 }
2541 }
2542
2543 return $number;
2544 }
2545
2546 /**
2547 * @param $number string
2548 * @return string
2549 */
2550 function parseFormattedNumber( $number ) {
2551 $s = $this->digitTransformTable();
2552 if ( $s ) {
2553 $number = strtr( $number, array_flip( $s ) );
2554 }
2555
2556 $s = $this->separatorTransformTable();
2557 if ( $s ) {
2558 $number = strtr( $number, array_flip( $s ) );
2559 }
2560
2561 $number = strtr( $number, array( ',' => '' ) );
2562 return $number;
2563 }
2564
2565 /**
2566 * Adds commas to a given number
2567 *
2568 * @param $_ mixed
2569 * @return string
2570 */
2571 function commafy( $_ ) {
2572 return strrev( (string)preg_replace( '/(\d{3})(?=\d)(?!\d*\.)/', '$1,', strrev( $_ ) ) );
2573 }
2574
2575 /**
2576 * @return array
2577 */
2578 function digitTransformTable() {
2579 return self::$dataCache->getItem( $this->mCode, 'digitTransformTable' );
2580 }
2581
2582 /**
2583 * @return array
2584 */
2585 function separatorTransformTable() {
2586 return self::$dataCache->getItem( $this->mCode, 'separatorTransformTable' );
2587 }
2588
2589 /**
2590 * Take a list of strings and build a locale-friendly comma-separated
2591 * list, using the local comma-separator message.
2592 * The last two strings are chained with an "and".
2593 *
2594 * @param $l Array
2595 * @return string
2596 */
2597 function listToText( $l ) {
2598 $s = '';
2599 $m = count( $l ) - 1;
2600 if ( $m == 1 ) {
2601 return $l[0] . $this->getMessageFromDB( 'and' ) . $this->getMessageFromDB( 'word-separator' ) . $l[1];
2602 } else {
2603 for ( $i = $m; $i >= 0; $i-- ) {
2604 if ( $i == $m ) {
2605 $s = $l[$i];
2606 } elseif ( $i == $m - 1 ) {
2607 $s = $l[$i] . $this->getMessageFromDB( 'and' ) . $this->getMessageFromDB( 'word-separator' ) . $s;
2608 } else {
2609 $s = $l[$i] . $this->getMessageFromDB( 'comma-separator' ) . $s;
2610 }
2611 }
2612 return $s;
2613 }
2614 }
2615
2616 /**
2617 * Take a list of strings and build a locale-friendly comma-separated
2618 * list, using the local comma-separator message.
2619 * @param $list array of strings to put in a comma list
2620 * @return string
2621 */
2622 function commaList( $list ) {
2623 return implode(
2624 $list,
2625 wfMsgExt(
2626 'comma-separator',
2627 array( 'parsemag', 'escapenoentities', 'language' => $this )
2628 )
2629 );
2630 }
2631
2632 /**
2633 * Take a list of strings and build a locale-friendly semicolon-separated
2634 * list, using the local semicolon-separator message.
2635 * @param $list array of strings to put in a semicolon list
2636 * @return string
2637 */
2638 function semicolonList( $list ) {
2639 return implode(
2640 $list,
2641 wfMsgExt(
2642 'semicolon-separator',
2643 array( 'parsemag', 'escapenoentities', 'language' => $this )
2644 )
2645 );
2646 }
2647
2648 /**
2649 * Same as commaList, but separate it with the pipe instead.
2650 * @param $list array of strings to put in a pipe list
2651 * @return string
2652 */
2653 function pipeList( $list ) {
2654 return implode(
2655 $list,
2656 wfMsgExt(
2657 'pipe-separator',
2658 array( 'escapenoentities', 'language' => $this )
2659 )
2660 );
2661 }
2662
2663 /**
2664 * Truncate a string to a specified length in bytes, appending an optional
2665 * string (e.g. for ellipses)
2666 *
2667 * The database offers limited byte lengths for some columns in the database;
2668 * multi-byte character sets mean we need to ensure that only whole characters
2669 * are included, otherwise broken characters can be passed to the user
2670 *
2671 * If $length is negative, the string will be truncated from the beginning
2672 *
2673 * @param $string String to truncate
2674 * @param $length Int: maximum length (including ellipses)
2675 * @param $ellipsis String to append to the truncated text
2676 * @param $adjustLength Boolean: Subtract length of ellipsis from $length.
2677 * $adjustLength was introduced in 1.18, before that behaved as if false.
2678 * @return string
2679 */
2680 function truncate( $string, $length, $ellipsis = '...', $adjustLength = true ) {
2681 # Use the localized ellipsis character
2682 if ( $ellipsis == '...' ) {
2683 $ellipsis = wfMsgExt( 'ellipsis', array( 'escapenoentities', 'language' => $this ) );
2684 }
2685 # Check if there is no need to truncate
2686 if ( $length == 0 ) {
2687 return $ellipsis; // convention
2688 } elseif ( strlen( $string ) <= abs( $length ) ) {
2689 return $string; // no need to truncate
2690 }
2691 $stringOriginal = $string;
2692 # If ellipsis length is >= $length then we can't apply $adjustLength
2693 if ( $adjustLength && strlen( $ellipsis ) >= abs( $length ) ) {
2694 $string = $ellipsis; // this can be slightly unexpected
2695 # Otherwise, truncate and add ellipsis...
2696 } else {
2697 $eLength = $adjustLength ? strlen( $ellipsis ) : 0;
2698 if ( $length > 0 ) {
2699 $length -= $eLength;
2700 $string = substr( $string, 0, $length ); // xyz...
2701 $string = $this->removeBadCharLast( $string );
2702 $string = $string . $ellipsis;
2703 } else {
2704 $length += $eLength;
2705 $string = substr( $string, $length ); // ...xyz
2706 $string = $this->removeBadCharFirst( $string );
2707 $string = $ellipsis . $string;
2708 }
2709 }
2710 # Do not truncate if the ellipsis makes the string longer/equal (bug 22181).
2711 # This check is *not* redundant if $adjustLength, due to the single case where
2712 # LEN($ellipsis) > ABS($limit arg); $stringOriginal could be shorter than $string.
2713 if ( strlen( $string ) < strlen( $stringOriginal ) ) {
2714 return $string;
2715 } else {
2716 return $stringOriginal;
2717 }
2718 }
2719
2720 /**
2721 * Remove bytes that represent an incomplete Unicode character
2722 * at the end of string (e.g. bytes of the char are missing)
2723 *
2724 * @param $string String
2725 * @return string
2726 */
2727 protected function removeBadCharLast( $string ) {
2728 if ( $string != '' ) {
2729 $char = ord( $string[strlen( $string ) - 1] );
2730 $m = array();
2731 if ( $char >= 0xc0 ) {
2732 # We got the first byte only of a multibyte char; remove it.
2733 $string = substr( $string, 0, -1 );
2734 } elseif ( $char >= 0x80 &&
2735 preg_match( '/^(.*)(?:[\xe0-\xef][\x80-\xbf]|' .
2736 '[\xf0-\xf7][\x80-\xbf]{1,2})$/', $string, $m ) )
2737 {
2738 # We chopped in the middle of a character; remove it
2739 $string = $m[1];
2740 }
2741 }
2742 return $string;
2743 }
2744
2745 /**
2746 * Remove bytes that represent an incomplete Unicode character
2747 * at the start of string (e.g. bytes of the char are missing)
2748 *
2749 * @param $string String
2750 * @return string
2751 */
2752 protected function removeBadCharFirst( $string ) {
2753 if ( $string != '' ) {
2754 $char = ord( $string[0] );
2755 if ( $char >= 0x80 && $char < 0xc0 ) {
2756 # We chopped in the middle of a character; remove the whole thing
2757 $string = preg_replace( '/^[\x80-\xbf]+/', '', $string );
2758 }
2759 }
2760 return $string;
2761 }
2762
2763 /**
2764 * Truncate a string of valid HTML to a specified length in bytes,
2765 * appending an optional string (e.g. for ellipses), and return valid HTML
2766 *
2767 * This is only intended for styled/linked text, such as HTML with
2768 * tags like <span> and <a>, were the tags are self-contained (valid HTML).
2769 * Also, this will not detect things like "display:none" CSS.
2770 *
2771 * Note: since 1.18 you do not need to leave extra room in $length for ellipses.
2772 *
2773 * @param string $text HTML string to truncate
2774 * @param int $length (zero/positive) Maximum length (including ellipses)
2775 * @param string $ellipsis String to append to the truncated text
2776 * @return string
2777 */
2778 function truncateHtml( $text, $length, $ellipsis = '...' ) {
2779 # Use the localized ellipsis character
2780 if ( $ellipsis == '...' ) {
2781 $ellipsis = wfMsgExt( 'ellipsis', array( 'escapenoentities', 'language' => $this ) );
2782 }
2783 # Check if there is clearly no need to truncate
2784 if ( $length <= 0 ) {
2785 return $ellipsis; // no text shown, nothing to format (convention)
2786 } elseif ( strlen( $text ) <= $length ) {
2787 return $text; // string short enough even *with* HTML (short-circuit)
2788 }
2789
2790 $dispLen = 0; // innerHTML legth so far
2791 $testingEllipsis = false; // checking if ellipses will make string longer/equal?
2792 $tagType = 0; // 0-open, 1-close
2793 $bracketState = 0; // 1-tag start, 2-tag name, 0-neither
2794 $entityState = 0; // 0-not entity, 1-entity
2795 $tag = $ret = ''; // accumulated tag name, accumulated result string
2796 $openTags = array(); // open tag stack
2797 $maybeState = null; // possible truncation state
2798
2799 $textLen = strlen( $text );
2800 $neLength = max( 0, $length - strlen( $ellipsis ) ); // non-ellipsis len if truncated
2801 for ( $pos = 0; true; ++$pos ) {
2802 # Consider truncation once the display length has reached the maximim.
2803 # We check if $dispLen > 0 to grab tags for the $neLength = 0 case.
2804 # Check that we're not in the middle of a bracket/entity...
2805 if ( $dispLen && $dispLen >= $neLength && $bracketState == 0 && !$entityState ) {
2806 if ( !$testingEllipsis ) {
2807 $testingEllipsis = true;
2808 # Save where we are; we will truncate here unless there turn out to
2809 # be so few remaining characters that truncation is not necessary.
2810 if ( !$maybeState ) { // already saved? ($neLength = 0 case)
2811 $maybeState = array( $ret, $openTags ); // save state
2812 }
2813 } elseif ( $dispLen > $length && $dispLen > strlen( $ellipsis ) ) {
2814 # String in fact does need truncation, the truncation point was OK.
2815 list( $ret, $openTags ) = $maybeState; // reload state
2816 $ret = $this->removeBadCharLast( $ret ); // multi-byte char fix
2817 $ret .= $ellipsis; // add ellipsis
2818 break;
2819 }
2820 }
2821 if ( $pos >= $textLen ) break; // extra iteration just for above checks
2822
2823 # Read the next char...
2824 $ch = $text[$pos];
2825 $lastCh = $pos ? $text[$pos - 1] : '';
2826 $ret .= $ch; // add to result string
2827 if ( $ch == '<' ) {
2828 $this->truncate_endBracket( $tag, $tagType, $lastCh, $openTags ); // for bad HTML
2829 $entityState = 0; // for bad HTML
2830 $bracketState = 1; // tag started (checking for backslash)
2831 } elseif ( $ch == '>' ) {
2832 $this->truncate_endBracket( $tag, $tagType, $lastCh, $openTags );
2833 $entityState = 0; // for bad HTML
2834 $bracketState = 0; // out of brackets
2835 } elseif ( $bracketState == 1 ) {
2836 if ( $ch == '/' ) {
2837 $tagType = 1; // close tag (e.g. "</span>")
2838 } else {
2839 $tagType = 0; // open tag (e.g. "<span>")
2840 $tag .= $ch;
2841 }
2842 $bracketState = 2; // building tag name
2843 } elseif ( $bracketState == 2 ) {
2844 if ( $ch != ' ' ) {
2845 $tag .= $ch;
2846 } else {
2847 // Name found (e.g. "<a href=..."), add on tag attributes...
2848 $pos += $this->truncate_skip( $ret, $text, "<>", $pos + 1 );
2849 }
2850 } elseif ( $bracketState == 0 ) {
2851 if ( $entityState ) {
2852 if ( $ch == ';' ) {
2853 $entityState = 0;
2854 $dispLen++; // entity is one displayed char
2855 }
2856 } else {
2857 if ( $neLength == 0 && !$maybeState ) {
2858 // Save state without $ch. We want to *hit* the first
2859 // display char (to get tags) but not *use* it if truncating.
2860 $maybeState = array( substr( $ret, 0, -1 ), $openTags );
2861 }
2862 if ( $ch == '&' ) {
2863 $entityState = 1; // entity found, (e.g. "&#160;")
2864 } else {
2865 $dispLen++; // this char is displayed
2866 // Add the next $max display text chars after this in one swoop...
2867 $max = ( $testingEllipsis ? $length : $neLength ) - $dispLen;
2868 $skipped = $this->truncate_skip( $ret, $text, "<>&", $pos + 1, $max );
2869 $dispLen += $skipped;
2870 $pos += $skipped;
2871 }
2872 }
2873 }
2874 }
2875 // Close the last tag if left unclosed by bad HTML
2876 $this->truncate_endBracket( $tag, $text[$textLen - 1], $tagType, $openTags );
2877 while ( count( $openTags ) > 0 ) {
2878 $ret .= '</' . array_pop( $openTags ) . '>'; // close open tags
2879 }
2880 return $ret;
2881 }
2882
2883 /**
2884 * truncateHtml() helper function
2885 * like strcspn() but adds the skipped chars to $ret
2886 *
2887 * @param $ret
2888 * @param $text
2889 * @param $search
2890 * @param $start
2891 * @param $len
2892 * @return int
2893 */
2894 private function truncate_skip( &$ret, $text, $search, $start, $len = null ) {
2895 if ( $len === null ) {
2896 $len = -1; // -1 means "no limit" for strcspn
2897 } elseif ( $len < 0 ) {
2898 $len = 0; // sanity
2899 }
2900 $skipCount = 0;
2901 if ( $start < strlen( $text ) ) {
2902 $skipCount = strcspn( $text, $search, $start, $len );
2903 $ret .= substr( $text, $start, $skipCount );
2904 }
2905 return $skipCount;
2906 }
2907
2908 /**
2909 * truncateHtml() helper function
2910 * (a) push or pop $tag from $openTags as needed
2911 * (b) clear $tag value
2912 * @param String &$tag Current HTML tag name we are looking at
2913 * @param int $tagType (0-open tag, 1-close tag)
2914 * @param char $lastCh Character before the '>' that ended this tag
2915 * @param array &$openTags Open tag stack (not accounting for $tag)
2916 */
2917 private function truncate_endBracket( &$tag, $tagType, $lastCh, &$openTags ) {
2918 $tag = ltrim( $tag );
2919 if ( $tag != '' ) {
2920 if ( $tagType == 0 && $lastCh != '/' ) {
2921 $openTags[] = $tag; // tag opened (didn't close itself)
2922 } elseif ( $tagType == 1 ) {
2923 if ( $openTags && $tag == $openTags[count( $openTags ) - 1] ) {
2924 array_pop( $openTags ); // tag closed
2925 }
2926 }
2927 $tag = '';
2928 }
2929 }
2930
2931 /**
2932 * Grammatical transformations, needed for inflected languages
2933 * Invoked by putting {{grammar:case|word}} in a message
2934 *
2935 * @param $word string
2936 * @param $case string
2937 * @return string
2938 */
2939 function convertGrammar( $word, $case ) {
2940 global $wgGrammarForms;
2941 if ( isset( $wgGrammarForms[$this->getCode()][$case][$word] ) ) {
2942 return $wgGrammarForms[$this->getCode()][$case][$word];
2943 }
2944 return $word;
2945 }
2946
2947 /**
2948 * Provides an alternative text depending on specified gender.
2949 * Usage {{gender:username|masculine|feminine|neutral}}.
2950 * username is optional, in which case the gender of current user is used,
2951 * but only in (some) interface messages; otherwise default gender is used.
2952 * If second or third parameter are not specified, masculine is used.
2953 * These details may be overriden per language.
2954 *
2955 * @param $gender string
2956 * @param $forms array
2957 *
2958 * @return string
2959 */
2960 function gender( $gender, $forms ) {
2961 if ( !count( $forms ) ) {
2962 return '';
2963 }
2964 $forms = $this->preConvertPlural( $forms, 2 );
2965 if ( $gender === 'male' ) {
2966 return $forms[0];
2967 }
2968 if ( $gender === 'female' ) {
2969 return $forms[1];
2970 }
2971 return isset( $forms[2] ) ? $forms[2] : $forms[0];
2972 }
2973
2974 /**
2975 * Plural form transformations, needed for some languages.
2976 * For example, there are 3 form of plural in Russian and Polish,
2977 * depending on "count mod 10". See [[w:Plural]]
2978 * For English it is pretty simple.
2979 *
2980 * Invoked by putting {{plural:count|wordform1|wordform2}}
2981 * or {{plural:count|wordform1|wordform2|wordform3}}
2982 *
2983 * Example: {{plural:{{NUMBEROFARTICLES}}|article|articles}}
2984 *
2985 * @param $count Integer: non-localized number
2986 * @param $forms Array: different plural forms
2987 * @return string Correct form of plural for $count in this language
2988 */
2989 function convertPlural( $count, $forms ) {
2990 if ( !count( $forms ) ) {
2991 return '';
2992 }
2993 $forms = $this->preConvertPlural( $forms, 2 );
2994
2995 return ( $count == 1 ) ? $forms[0] : $forms[1];
2996 }
2997
2998 /**
2999 * Checks that convertPlural was given an array and pads it to requested
3000 * amount of forms by copying the last one.
3001 *
3002 * @param $count Integer: How many forms should there be at least
3003 * @param $forms Array of forms given to convertPlural
3004 * @return array Padded array of forms or an exception if not an array
3005 */
3006 protected function preConvertPlural( /* Array */ $forms, $count ) {
3007 while ( count( $forms ) < $count ) {
3008 $forms[] = $forms[count( $forms ) - 1];
3009 }
3010 return $forms;
3011 }
3012
3013 /**
3014 * This translates the duration ("1 week", "4 days", etc)
3015 * as well as the expiry time (which is an absolute timestamp).
3016 * @param $str String: the validated block duration in English
3017 * @return Somehow translated block duration
3018 * @see LanguageFi.php for example implementation
3019 */
3020 function translateBlockExpiry( $str ) {
3021 $duration = SpecialBlock::getSuggestedDurations( $this );
3022 foreach( $duration as $show => $value ){
3023 if ( strcmp( $str, $value ) == 0 ) {
3024 return htmlspecialchars( trim( $show ) );
3025 }
3026 }
3027
3028 // Since usually only infinite or indefinite is only on list, so try
3029 // equivalents if still here.
3030 $indefs = array( 'infinite', 'infinity', 'indefinite' );
3031 if ( in_array( $str, $indefs ) ) {
3032 foreach( $indefs as $val ) {
3033 $show = array_search( $val, $duration, true );
3034 if ( $show !== false ) {
3035 return htmlspecialchars( trim( $show ) );
3036 }
3037 }
3038 }
3039 // If no duration is given, but a timestamp, display that
3040 return ( strtotime( $str ) ? $this->timeanddate( strtotime( $str ) ) : $str );
3041 }
3042
3043 /**
3044 * languages like Chinese need to be segmented in order for the diff
3045 * to be of any use
3046 *
3047 * @param $text String
3048 * @return String
3049 */
3050 function segmentForDiff( $text ) {
3051 return $text;
3052 }
3053
3054 /**
3055 * and unsegment to show the result
3056 *
3057 * @param $text String
3058 * @return String
3059 */
3060 function unsegmentForDiff( $text ) {
3061 return $text;
3062 }
3063
3064 /**
3065 * convert text to all supported variants
3066 *
3067 * @param $text string
3068 * @return array
3069 */
3070 function autoConvertToAllVariants( $text ) {
3071 return $this->mConverter->autoConvertToAllVariants( $text );
3072 }
3073
3074 /**
3075 * convert text to different variants of a language.
3076 *
3077 * @param $text string
3078 * @return string
3079 */
3080 function convert( $text ) {
3081 return $this->mConverter->convert( $text );
3082 }
3083
3084
3085 /**
3086 * Convert a Title object to a string in the preferred variant
3087 *
3088 * @param $title Title
3089 * @return string
3090 */
3091 function convertTitle( $title ) {
3092 return $this->mConverter->convertTitle( $title );
3093 }
3094
3095 /**
3096 * Check if this is a language with variants
3097 *
3098 * @return bool
3099 */
3100 function hasVariants() {
3101 return sizeof( $this->getVariants() ) > 1;
3102 }
3103
3104 /**
3105 * Put custom tags (e.g. -{ }-) around math to prevent conversion
3106 *
3107 * @param $text string
3108 * @return string
3109 */
3110 function armourMath( $text ) {
3111 return $this->mConverter->armourMath( $text );
3112 }
3113
3114 /**
3115 * Perform output conversion on a string, and encode for safe HTML output.
3116 * @param $text String text to be converted
3117 * @param $isTitle Bool whether this conversion is for the article title
3118 * @return string
3119 * @todo this should get integrated somewhere sane
3120 */
3121 function convertHtml( $text, $isTitle = false ) {
3122 return htmlspecialchars( $this->convert( $text, $isTitle ) );
3123 }
3124
3125 /**
3126 * @param $key string
3127 * @return string
3128 */
3129 function convertCategoryKey( $key ) {
3130 return $this->mConverter->convertCategoryKey( $key );
3131 }
3132
3133 /**
3134 * Get the list of variants supported by this language
3135 * see sample implementation in LanguageZh.php
3136 *
3137 * @return array an array of language codes
3138 */
3139 function getVariants() {
3140 return $this->mConverter->getVariants();
3141 }
3142
3143 /**
3144 * @return string
3145 */
3146 function getPreferredVariant() {
3147 return $this->mConverter->getPreferredVariant();
3148 }
3149
3150 /**
3151 * @return string
3152 */
3153 function getDefaultVariant() {
3154 return $this->mConverter->getDefaultVariant();
3155 }
3156
3157 /**
3158 * @return string
3159 */
3160 function getURLVariant() {
3161 return $this->mConverter->getURLVariant();
3162 }
3163
3164 /**
3165 * If a language supports multiple variants, it is
3166 * possible that non-existing link in one variant
3167 * actually exists in another variant. this function
3168 * tries to find it. See e.g. LanguageZh.php
3169 *
3170 * @param $link String: the name of the link
3171 * @param $nt Mixed: the title object of the link
3172 * @param $ignoreOtherCond Boolean: to disable other conditions when
3173 * we need to transclude a template or update a category's link
3174 * @return null the input parameters may be modified upon return
3175 */
3176 function findVariantLink( &$link, &$nt, $ignoreOtherCond = false ) {
3177 $this->mConverter->findVariantLink( $link, $nt, $ignoreOtherCond );
3178 }
3179
3180 /**
3181 * If a language supports multiple variants, converts text
3182 * into an array of all possible variants of the text:
3183 * 'variant' => text in that variant
3184 *
3185 * @deprecated since 1.17 Use autoConvertToAllVariants()
3186 *
3187 * @param $text string
3188 *
3189 * @return string
3190 */
3191 function convertLinkToAllVariants( $text ) {
3192 return $this->mConverter->convertLinkToAllVariants( $text );
3193 }
3194
3195 /**
3196 * returns language specific options used by User::getPageRenderHash()
3197 * for example, the preferred language variant
3198 *
3199 * @return string
3200 */
3201 function getExtraHashOptions() {
3202 return $this->mConverter->getExtraHashOptions();
3203 }
3204
3205 /**
3206 * For languages that support multiple variants, the title of an
3207 * article may be displayed differently in different variants. this
3208 * function returns the apporiate title defined in the body of the article.
3209 *
3210 * @return string
3211 */
3212 function getParsedTitle() {
3213 return $this->mConverter->getParsedTitle();
3214 }
3215
3216 /**
3217 * Enclose a string with the "no conversion" tag. This is used by
3218 * various functions in the Parser
3219 *
3220 * @param $text String: text to be tagged for no conversion
3221 * @param $noParse bool
3222 * @return string the tagged text
3223 */
3224 function markNoConversion( $text, $noParse = false ) {
3225 return $this->mConverter->markNoConversion( $text, $noParse );
3226 }
3227
3228 /**
3229 * A regular expression to match legal word-trailing characters
3230 * which should be merged onto a link of the form [[foo]]bar.
3231 *
3232 * @return string
3233 */
3234 function linkTrail() {
3235 return self::$dataCache->getItem( $this->mCode, 'linkTrail' );
3236 }
3237
3238 /**
3239 * @return Language
3240 */
3241 function getLangObj() {
3242 return $this;
3243 }
3244
3245 /**
3246 * Get the RFC 3066 code for this language object
3247 *
3248 * @return string
3249 */
3250 function getCode() {
3251 return $this->mCode;
3252 }
3253
3254 /**
3255 * @param $code string
3256 */
3257 function setCode( $code ) {
3258 $this->mCode = $code;
3259 }
3260
3261 /**
3262 * Get the name of a file for a certain language code
3263 * @param $prefix string Prepend this to the filename
3264 * @param $code string Language code
3265 * @param $suffix string Append this to the filename
3266 * @return string $prefix . $mangledCode . $suffix
3267 */
3268 static function getFileName( $prefix = 'Language', $code, $suffix = '.php' ) {
3269 // Protect against path traversal
3270 if ( !Language::isValidCode( $code )
3271 || strcspn( $code, ":/\\\000" ) !== strlen( $code ) )
3272 {
3273 throw new MWException( "Invalid language code \"$code\"" );
3274 }
3275
3276 return $prefix . str_replace( '-', '_', ucfirst( $code ) ) . $suffix;
3277 }
3278
3279 /**
3280 * Get the language code from a file name. Inverse of getFileName()
3281 * @param $filename string $prefix . $languageCode . $suffix
3282 * @param $prefix string Prefix before the language code
3283 * @param $suffix string Suffix after the language code
3284 * @return string Language code, or false if $prefix or $suffix isn't found
3285 */
3286 static function getCodeFromFileName( $filename, $prefix = 'Language', $suffix = '.php' ) {
3287 $m = null;
3288 preg_match( '/' . preg_quote( $prefix, '/' ) . '([A-Z][a-z_]+)' .
3289 preg_quote( $suffix, '/' ) . '/', $filename, $m );
3290 if ( !count( $m ) ) {
3291 return false;
3292 }
3293 return str_replace( '_', '-', strtolower( $m[1] ) );
3294 }
3295
3296 /**
3297 * @param $code string
3298 * @return string
3299 */
3300 static function getMessagesFileName( $code ) {
3301 global $IP;
3302 return self::getFileName( "$IP/languages/messages/Messages", $code, '.php' );
3303 }
3304
3305 /**
3306 * @param $code string
3307 * @return string
3308 */
3309 static function getClassFileName( $code ) {
3310 global $IP;
3311 return self::getFileName( "$IP/languages/classes/Language", $code, '.php' );
3312 }
3313
3314 /**
3315 * Get the first fallback for a given language
3316 *
3317 * @param $code string
3318 *
3319 * @return false|string
3320 */
3321 static function getFallbackFor( $code ) {
3322 if ( $code === 'en' ) {
3323 return false;
3324 } else {
3325 $fallbacks = self::getFallbacksFor( $code );
3326 $first = array_shift( $fallbacks );
3327 return $first;
3328 }
3329 }
3330
3331 /**
3332 * Get the ordered list of fallback languages.
3333 *
3334 * @since 1.19
3335 * @param $code string Language code
3336 * @return array
3337 */
3338 static function getFallbacksFor( $code ) {
3339 if ( $code === 'en' ) {
3340 return array();
3341 } else {
3342 $v = self::getLocalisationCache()->getItem( $code, 'fallback' );
3343 $v = array_map( 'trim', explode( ',', $v ) );
3344 if ( $v[count( $v ) - 1] !== 'en' ) {
3345 $v[] = 'en';
3346 }
3347 return $v;
3348 }
3349 }
3350
3351 /**
3352 * Get all messages for a given language
3353 * WARNING: this may take a long time
3354 *
3355 * @param $code string
3356 *
3357 * @return array
3358 */
3359 static function getMessagesFor( $code ) {
3360 return self::getLocalisationCache()->getItem( $code, 'messages' );
3361 }
3362
3363 /**
3364 * Get a message for a given language
3365 *
3366 * @param $key string
3367 * @param $code string
3368 *
3369 * @return string
3370 */
3371 static function getMessageFor( $key, $code ) {
3372 return self::getLocalisationCache()->getSubitem( $code, 'messages', $key );
3373 }
3374
3375 /**
3376 * @param $talk
3377 * @return mixed
3378 */
3379 function fixVariableInNamespace( $talk ) {
3380 if ( strpos( $talk, '$1' ) === false ) {
3381 return $talk;
3382 }
3383
3384 global $wgMetaNamespace;
3385 $talk = str_replace( '$1', $wgMetaNamespace, $talk );
3386
3387 # Allow grammar transformations
3388 # Allowing full message-style parsing would make simple requests
3389 # such as action=raw much more expensive than they need to be.
3390 # This will hopefully cover most cases.
3391 $talk = preg_replace_callback( '/{{grammar:(.*?)\|(.*?)}}/i',
3392 array( &$this, 'replaceGrammarInNamespace' ), $talk );
3393 return str_replace( ' ', '_', $talk );
3394 }
3395
3396 /**
3397 * @param $m string
3398 * @return string
3399 */
3400 function replaceGrammarInNamespace( $m ) {
3401 return $this->convertGrammar( trim( $m[2] ), trim( $m[1] ) );
3402 }
3403
3404 /**
3405 * @throws MWException
3406 * @return array
3407 */
3408 static function getCaseMaps() {
3409 static $wikiUpperChars, $wikiLowerChars;
3410 if ( isset( $wikiUpperChars ) ) {
3411 return array( $wikiUpperChars, $wikiLowerChars );
3412 }
3413
3414 wfProfileIn( __METHOD__ );
3415 $arr = wfGetPrecompiledData( 'Utf8Case.ser' );
3416 if ( $arr === false ) {
3417 throw new MWException(
3418 "Utf8Case.ser is missing, please run \"make\" in the serialized directory\n" );
3419 }
3420 $wikiUpperChars = $arr['wikiUpperChars'];
3421 $wikiLowerChars = $arr['wikiLowerChars'];
3422 wfProfileOut( __METHOD__ );
3423 return array( $wikiUpperChars, $wikiLowerChars );
3424 }
3425
3426 /**
3427 * Decode an expiry (block, protection, etc) which has come from the DB
3428 *
3429 * @param $expiry String: Database expiry String
3430 * @param $format Bool|Int true to process using language functions, or TS_ constant
3431 * to return the expiry in a given timestamp
3432 * @return String
3433 */
3434 public function formatExpiry( $expiry, $format = true ) {
3435 static $infinity, $infinityMsg;
3436 if( $infinity === null ){
3437 $infinityMsg = wfMessage( 'infiniteblock' );
3438 $infinity = wfGetDB( DB_SLAVE )->getInfinity();
3439 }
3440
3441 if ( $expiry == '' || $expiry == $infinity ) {
3442 return $format === true
3443 ? $infinityMsg
3444 : $infinity;
3445 } else {
3446 return $format === true
3447 ? $this->timeanddate( $expiry )
3448 : wfTimestamp( $format, $expiry );
3449 }
3450 }
3451
3452 /**
3453 * @todo Document
3454 * @param $seconds int|float
3455 * @param $format String Optional, one of ("avoidseconds","avoidminutes"):
3456 * "avoidseconds" - don't mention seconds if $seconds >= 1 hour
3457 * "avoidminutes" - don't mention seconds/minutes if $seconds > 48 hours
3458 * @return string
3459 */
3460 function formatTimePeriod( $seconds, $format = false ) {
3461 if ( round( $seconds * 10 ) < 100 ) {
3462 $s = $this->formatNum( sprintf( "%.1f", round( $seconds * 10 ) / 10 ) );
3463 $s .= $this->getMessageFromDB( 'seconds-abbrev' );
3464 } elseif ( round( $seconds ) < 60 ) {
3465 $s = $this->formatNum( round( $seconds ) );
3466 $s .= $this->getMessageFromDB( 'seconds-abbrev' );
3467 } elseif ( round( $seconds ) < 3600 ) {
3468 $minutes = floor( $seconds / 60 );
3469 $secondsPart = round( fmod( $seconds, 60 ) );
3470 if ( $secondsPart == 60 ) {
3471 $secondsPart = 0;
3472 $minutes++;
3473 }
3474 $s = $this->formatNum( $minutes ) . $this->getMessageFromDB( 'minutes-abbrev' );
3475 $s .= ' ';
3476 $s .= $this->formatNum( $secondsPart ) . $this->getMessageFromDB( 'seconds-abbrev' );
3477 } elseif ( round( $seconds ) <= 2*86400 ) {
3478 $hours = floor( $seconds / 3600 );
3479 $minutes = floor( ( $seconds - $hours * 3600 ) / 60 );
3480 $secondsPart = round( $seconds - $hours * 3600 - $minutes * 60 );
3481 if ( $secondsPart == 60 ) {
3482 $secondsPart = 0;
3483 $minutes++;
3484 }
3485 if ( $minutes == 60 ) {
3486 $minutes = 0;
3487 $hours++;
3488 }
3489 $s = $this->formatNum( $hours ) . $this->getMessageFromDB( 'hours-abbrev' );
3490 $s .= ' ';
3491 $s .= $this->formatNum( $minutes ) . $this->getMessageFromDB( 'minutes-abbrev' );
3492 if ( !in_array( $format, array( 'avoidseconds', 'avoidminutes' ) ) ) {
3493 $s .= ' ' . $this->formatNum( $secondsPart ) .
3494 $this->getMessageFromDB( 'seconds-abbrev' );
3495 }
3496 } else {
3497 $days = floor( $seconds / 86400 );
3498 if ( $format === 'avoidminutes' ) {
3499 $hours = round( ( $seconds - $days * 86400 ) / 3600 );
3500 if ( $hours == 24 ) {
3501 $hours = 0;
3502 $days++;
3503 }
3504 $s = $this->formatNum( $days ) . $this->getMessageFromDB( 'days-abbrev' );
3505 $s .= ' ';
3506 $s .= $this->formatNum( $hours ) . $this->getMessageFromDB( 'hours-abbrev' );
3507 } elseif ( $format === 'avoidseconds' ) {
3508 $hours = floor( ( $seconds - $days * 86400 ) / 3600 );
3509 $minutes = round( ( $seconds - $days * 86400 - $hours * 3600 ) / 60 );
3510 if ( $minutes == 60 ) {
3511 $minutes = 0;
3512 $hours++;
3513 }
3514 if ( $hours == 24 ) {
3515 $hours = 0;
3516 $days++;
3517 }
3518 $s = $this->formatNum( $days ) . $this->getMessageFromDB( 'days-abbrev' );
3519 $s .= ' ';
3520 $s .= $this->formatNum( $hours ) . $this->getMessageFromDB( 'hours-abbrev' );
3521 $s .= ' ';
3522 $s .= $this->formatNum( $minutes ) . $this->getMessageFromDB( 'minutes-abbrev' );
3523 } else {
3524 $s = $this->formatNum( $days ) . $this->getMessageFromDB( 'days-abbrev' );
3525 $s .= ' ';
3526 $s .= $this->formatTimePeriod( $seconds - $days * 86400, $format );
3527 }
3528 }
3529 return $s;
3530 }
3531
3532 /**
3533 * @param $bps int
3534 * @return string
3535 */
3536 function formatBitrate( $bps ) {
3537 $units = array( 'bps', 'kbps', 'Mbps', 'Gbps' );
3538 if ( $bps <= 0 ) {
3539 return $this->formatNum( $bps ) . $units[0];
3540 }
3541 $unitIndex = floor( log10( $bps ) / 3 );
3542 $mantissa = $bps / pow( 1000, $unitIndex );
3543 if ( $mantissa < 10 ) {
3544 $mantissa = round( $mantissa, 1 );
3545 } else {
3546 $mantissa = round( $mantissa );
3547 }
3548 return $this->formatNum( $mantissa ) . $units[$unitIndex];
3549 }
3550
3551 /**
3552 * Format a size in bytes for output, using an appropriate
3553 * unit (B, KB, MB or GB) according to the magnitude in question
3554 *
3555 * @param $size int Size to format
3556 * @return string Plain text (not HTML)
3557 */
3558 function formatSize( $size ) {
3559 // For small sizes no decimal places necessary
3560 $round = 0;
3561 if ( $size > 1024 ) {
3562 $size = $size / 1024;
3563 if ( $size > 1024 ) {
3564 $size = $size / 1024;
3565 // For MB and bigger two decimal places are smarter
3566 $round = 2;
3567 if ( $size > 1024 ) {
3568 $size = $size / 1024;
3569 $msg = 'size-gigabytes';
3570 } else {
3571 $msg = 'size-megabytes';
3572 }
3573 } else {
3574 $msg = 'size-kilobytes';
3575 }
3576 } else {
3577 $msg = 'size-bytes';
3578 }
3579 $size = round( $size, $round );
3580 $text = $this->getMessageFromDB( $msg );
3581 return str_replace( '$1', $this->formatNum( $size ), $text );
3582 }
3583
3584 /**
3585 * Get the conversion rule title, if any.
3586 *
3587 * @return string
3588 */
3589 function getConvRuleTitle() {
3590 return $this->mConverter->getConvRuleTitle();
3591 }
3592 }