diff options
author | Pierre Schmitz <pierre@archlinux.de> | 2015-12-17 09:15:42 +0100 |
---|---|---|
committer | Pierre Schmitz <pierre@archlinux.de> | 2015-12-17 09:44:51 +0100 |
commit | a1789ddde42033f1b05cc4929491214ee6e79383 (patch) | |
tree | 63615735c4ddffaaabf2428946bb26f90899f7bf /includes | |
parent | 9e06a62f265e3a2aaabecc598d4bc617e06fa32d (diff) |
Update to MediaWiki 1.26.0
Diffstat (limited to 'includes')
732 files changed, 32314 insertions, 11064 deletions
diff --git a/includes/AjaxDispatcher.php b/includes/AjaxDispatcher.php index b14114d7..96892d71 100644 --- a/includes/AjaxDispatcher.php +++ b/includes/AjaxDispatcher.php @@ -124,9 +124,9 @@ class AjaxDispatcher { $result = call_user_func_array( $this->func_name, $this->args ); if ( $result === false || $result === null ) { - wfDebug( __METHOD__ . ' ERROR while dispatching ' - . $this->func_name . "(" . var_export( $this->args, true ) . "): " - . "no data returned\n" ); + wfDebug( __METHOD__ . ' ERROR while dispatching ' . + $this->func_name . "(" . var_export( $this->args, true ) . "): " . + "no data returned\n" ); wfHttpError( 500, 'Internal Error', "{$this->func_name} returned no data" ); @@ -141,9 +141,9 @@ class AjaxDispatcher { wfDebug( __METHOD__ . ' dispatch complete for ' . $this->func_name . "\n" ); } } catch ( Exception $e ) { - wfDebug( __METHOD__ . ' ERROR while dispatching ' - . $this->func_name . "(" . var_export( $this->args, true ) . "): " - . get_class( $e ) . ": " . $e->getMessage() . "\n" ); + wfDebug( __METHOD__ . ' ERROR while dispatching ' . + $this->func_name . "(" . var_export( $this->args, true ) . "): " . + get_class( $e ) . ": " . $e->getMessage() . "\n" ); if ( !headers_sent() ) { wfHttpError( 500, 'Internal Error', diff --git a/includes/AjaxResponse.php b/includes/AjaxResponse.php index 8e9f490f..6c2efc29 100644 --- a/includes/AjaxResponse.php +++ b/includes/AjaxResponse.php @@ -86,7 +86,7 @@ class AjaxResponse { $this->mDisabled = false; $this->mText = ''; - $this->mResponseCode = '200 OK'; + $this->mResponseCode = 200; $this->mLastModified = false; $this->mContentType = 'application/x-wiki'; @@ -158,16 +158,20 @@ class AjaxResponse { */ function sendHeaders() { if ( $this->mResponseCode ) { - $n = preg_replace( '/^ *(\d+)/', '\1', $this->mResponseCode ); - header( "Status: " . $this->mResponseCode, true, (int)$n ); + // For back-compat, it is supported that mResponseCode be a string like " 200 OK" + // (with leading space and the status message after). Cast response code to an integer + // to take advantage of PHP's conversion rules which will turn " 200 OK" into 200. + // http://php.net/string#language.types.string.conversion + $n = intval( trim( $this->mResponseCode ) ); + HttpStatus::header( $n ); } - header ( "Content-Type: " . $this->mContentType ); + header( "Content-Type: " . $this->mContentType ); if ( $this->mLastModified ) { - header ( "Last-Modified: " . $this->mLastModified ); + header( "Last-Modified: " . $this->mLastModified ); } else { - header ( "Last-Modified: " . gmdate( "D, d M Y H:i:s" ) . " GMT" ); + header( "Last-Modified: " . gmdate( "D, d M Y H:i:s" ) . " GMT" ); } if ( $this->mCacheDuration ) { @@ -189,20 +193,20 @@ class AjaxResponse { } else { # Let the client do the caching. Cache is not purged. - header ( "Expires: " . gmdate( "D, d M Y H:i:s", time() + $this->mCacheDuration ) . " GMT" ); - header ( "Cache-Control: s-maxage={$this->mCacheDuration}," . + header( "Expires: " . gmdate( "D, d M Y H:i:s", time() + $this->mCacheDuration ) . " GMT" ); + header( "Cache-Control: s-maxage={$this->mCacheDuration}," . "public,max-age={$this->mCacheDuration}" ); } } else { # always expired, always modified - header ( "Expires: Mon, 26 Jul 1997 05:00:00 GMT" ); // Date in the past - header ( "Cache-Control: no-cache, must-revalidate" ); // HTTP/1.1 - header ( "Pragma: no-cache" ); // HTTP/1.0 + header( "Expires: Mon, 26 Jul 1997 05:00:00 GMT" ); // Date in the past + header( "Cache-Control: no-cache, must-revalidate" ); // HTTP/1.1 + header( "Pragma: no-cache" ); // HTTP/1.0 } if ( $this->mVary ) { - header ( "Vary: " . $this->mVary ); + header( "Vary: " . $this->mVary ); } } @@ -246,7 +250,7 @@ class AjaxResponse { $ismodsince >= $wgCacheEpoch ) { ini_set( 'zlib.output_compression', 0 ); - $this->setResponseCode( "304 Not Modified" ); + $this->setResponseCode( 304 ); $this->disable(); $this->mLastModified = $lastmod; diff --git a/includes/AuthPlugin.php b/includes/AuthPlugin.php index 45ad4d1b..badf47c3 100644 --- a/includes/AuthPlugin.php +++ b/includes/AuthPlugin.php @@ -120,6 +120,8 @@ class AuthPlugin { * The User object is passed by reference so it can be modified; don't * forget the & on your function declaration. * + * @deprecated since 1.26, use the UserLoggedIn hook instead. And assigning + * a different User object to $user is no longer supported. * @param User $user * @return bool */ @@ -204,6 +206,7 @@ class AuthPlugin { * Update user information in the external authentication database. * Return true if successful. * + * @deprecated since 1.26, use the UserSaveSettings hook instead. * @param User $user * @return bool */ @@ -215,6 +218,7 @@ class AuthPlugin { * Update user groups in the external authentication database. * Return true if successful. * + * @deprecated since 1.26, use the UserGroupsChanged hook instead. * @param User $user * @param array $addgroups Groups to add. * @param array $delgroups Groups to remove. @@ -278,6 +282,8 @@ class AuthPlugin { * The User object is passed by reference so it can be modified; don't * forget the & on your function declaration. * + * @deprecated since 1.26, use the UserLoggedIn hook instead. And assigning + * a different User object to $user is no longer supported. * @param User $user * @param bool $autocreate True if user is being autocreated on login */ @@ -326,11 +332,21 @@ class AuthPluginUser { return -1; } + /** + * Indicate whether the user is locked + * @deprecated since 1.26, use the UserIsLocked hook instead. + * @return bool + */ public function isLocked() { # Override this! return false; } + /** + * Indicate whether the user is hidden + * @deprecated since 1.26, use the UserIsHidden hook instead. + * @return bool + */ public function isHidden() { # Override this! return false; diff --git a/includes/Block.php b/includes/Block.php index 873a26d8..c5a16fce 100644 --- a/includes/Block.php +++ b/includes/Block.php @@ -23,15 +23,16 @@ class Block { /** @var string */ public $mReason; - /** @var bool|string */ + /** @var string */ public $mTimestamp; - /** @var int */ + /** @var bool */ public $mAuto; - /** @var bool|string */ + /** @var string */ public $mExpiry; + /** @var bool */ public $mHideName; /** @var int */ @@ -65,10 +66,10 @@ class Block { protected $blocker; /** @var bool */ - protected $isHardblock = true; + protected $isHardblock; /** @var bool */ - protected $isAutoblocking = true; + protected $isAutoblocking; # TYPE constants const TYPE_USER = 1; @@ -78,59 +79,84 @@ class Block { const TYPE_ID = 5; /** - * @todo FIXME: Don't know what the best format to have for this constructor - * is, but fourteen optional parameters certainly isn't it. - * @param string $address - * @param int $user - * @param int $by - * @param string $reason - * @param mixed $timestamp - * @param int $auto - * @param string $expiry - * @param int $anonOnly - * @param int $createAccount - * @param int $enableAutoblock - * @param int $hideName - * @param int $blockEmail - * @param int $allowUsertalk - * @param string $byText + * Create a new block with specified parameters on a user, IP or IP range. + * + * @param array $options Parameters of the block: + * address string|User Target user name, User object, IP address or IP range + * user int Override target user ID (for foreign users) + * by int User ID of the blocker + * reason string Reason of the block + * timestamp string The time at which the block comes into effect + * auto bool Is this an automatic block? + * expiry string Timestamp of expiration of the block or 'infinity' + * anonOnly bool Only disallow anonymous actions + * createAccount bool Disallow creation of new accounts + * enableAutoblock bool Enable automatic blocking + * hideName bool Hide the target user name + * blockEmail bool Disallow sending emails + * allowUsertalk bool Allow the target to edit its own talk page + * byText string Username of the blocker (for foreign users) + * + * @since 1.26 accepts $options array instead of individual parameters; order + * of parameters above reflects the original order */ - function __construct( $address = '', $user = 0, $by = 0, $reason = '', - $timestamp = 0, $auto = 0, $expiry = '', $anonOnly = 0, $createAccount = 0, $enableAutoblock = 0, - $hideName = 0, $blockEmail = 0, $allowUsertalk = 0, $byText = '' - ) { - if ( $timestamp === 0 ) { - $timestamp = wfTimestampNow(); - } + function __construct( $options = array() ) { + $defaults = array( + 'address' => '', + 'user' => null, + 'by' => null, + 'reason' => '', + 'timestamp' => '', + 'auto' => false, + 'expiry' => '', + 'anonOnly' => false, + 'createAccount' => false, + 'enableAutoblock' => false, + 'hideName' => false, + 'blockEmail' => false, + 'allowUsertalk' => false, + 'byText' => '', + ); - if ( count( func_get_args() ) > 0 ) { - # Soon... :D - # wfDeprecated( __METHOD__ . " with arguments" ); + if ( func_num_args() > 1 || !is_array( $options ) ) { + $options = array_combine( + array_slice( array_keys( $defaults ), 0, func_num_args() ), + func_get_args() + ); + wfDeprecated( __METHOD__ . ' with multiple arguments', '1.26' ); } - $this->setTarget( $address ); - if ( $this->target instanceof User && $user ) { - $this->forcedTargetID = $user; // needed for foreign users - } - if ( $by ) { // local user - $this->setBlocker( User::newFromId( $by ) ); - } else { // foreign user - $this->setBlocker( $byText ); + $options += $defaults; + + $this->setTarget( $options['address'] ); + + if ( $this->target instanceof User && $options['user'] ) { + # Needed for foreign users + $this->forcedTargetID = $options['user']; } - $this->mReason = $reason; - $this->mTimestamp = wfTimestamp( TS_MW, $timestamp ); - $this->mAuto = $auto; - $this->isHardblock( !$anonOnly ); - $this->prevents( 'createaccount', $createAccount ); - if ( $expiry == 'infinity' || $expiry == wfGetDB( DB_SLAVE )->getInfinity() ) { - $this->mExpiry = 'infinity'; + + if ( $options['by'] ) { + # Local user + $this->setBlocker( User::newFromID( $options['by'] ) ); } else { - $this->mExpiry = wfTimestamp( TS_MW, $expiry ); + # Foreign user + $this->setBlocker( $options['byText'] ); } - $this->isAutoblocking( $enableAutoblock ); - $this->mHideName = $hideName; - $this->prevents( 'sendemail', $blockEmail ); - $this->prevents( 'editownusertalk', !$allowUsertalk ); + + $this->mReason = $options['reason']; + $this->mTimestamp = wfTimestamp( TS_MW, $options['timestamp'] ); + $this->mExpiry = wfGetDB( DB_SLAVE )->decodeExpiry( $options['expiry'] ); + + # Boolean settings + $this->mAuto = (bool)$options['auto']; + $this->mHideName = (bool)$options['hideName']; + $this->isHardblock( !$options['anonOnly'] ); + $this->isAutoblocking( (bool)$options['enableAutoblock'] ); + + # Prevention measures + $this->prevents( 'sendemail', (bool)$options['blockEmail'] ); + $this->prevents( 'editownusertalk', !$options['allowUsertalk'] ); + $this->prevents( 'createaccount', (bool)$options['createAccount'] ); $this->mFromMaster = false; } @@ -375,16 +401,11 @@ class Block { $this->mTimestamp = wfTimestamp( TS_MW, $row->ipb_timestamp ); $this->mAuto = $row->ipb_auto; $this->mHideName = $row->ipb_deleted; - $this->mId = $row->ipb_id; + $this->mId = (int)$row->ipb_id; $this->mParentBlockId = $row->ipb_parent_block_id; // I wish I didn't have to do this - $db = wfGetDB( DB_SLAVE ); - if ( $row->ipb_expiry == $db->getInfinity() ) { - $this->mExpiry = 'infinity'; - } else { - $this->mExpiry = wfTimestamp( TS_MW, $row->ipb_expiry ); - } + $this->mExpiry = wfGetDB( DB_SLAVE )->decodeExpiry( $row->ipb_expiry ); $this->isHardblock( !$row->ipb_anon_only ); $this->isAutoblocking( $row->ipb_enable_autoblock ); @@ -452,11 +473,15 @@ class Block { $dbw->insert( 'ipblocks', $row, __METHOD__, array( 'IGNORE' ) ); $affected = $dbw->affectedRows(); + $this->mId = $dbw->insertId(); # Don't collide with expired blocks. - # Do this after trying to insert to avoid pointless gap locks. + # Do this after trying to insert to avoid locking. if ( !$affected ) { - $dbw->delete( 'ipblocks', + # T96428: The ipb_address index uses a prefix on a field, so + # use a standard SELECT + DELETE to avoid annoying gap locks. + $ids = $dbw->selectFieldValues( 'ipblocks', + 'ipb_id', array( 'ipb_address' => $row['ipb_address'], 'ipb_user' => $row['ipb_user'], @@ -464,13 +489,14 @@ class Block { ), __METHOD__ ); - - $dbw->insert( 'ipblocks', $row, __METHOD__, array( 'IGNORE' ) ); - $affected = $dbw->affectedRows(); + if ( $ids ) { + $dbw->delete( 'ipblocks', array( 'ipb_id' => $ids ), __METHOD__ ); + $dbw->insert( 'ipblocks', $row, __METHOD__, array( 'IGNORE' ) ); + $affected = $dbw->affectedRows(); + $this->mId = $dbw->insertId(); + } } - $this->mId = $dbw->insertId(); - if ( $affected ) { $auto_ipd_ids = $this->doRetroactiveAutoblock(); return array( 'id' => $this->mId, 'autoIds' => $auto_ipd_ids ); @@ -1113,7 +1139,7 @@ class Block { $blocks = array(); foreach ( $rows as $row ) { $block = self::newFromRow( $row ); - if ( !$block->deleteIfExpired() ) { + if ( !$block->deleteIfExpired() ) { $blocks[] = $block; } } diff --git a/includes/CategoryFinder.php b/includes/CategoryFinder.php index 33de7404..77c43bf0 100644 --- a/includes/CategoryFinder.php +++ b/includes/CategoryFinder.php @@ -27,7 +27,7 @@ * articles are in one or all of a given subset of categories. * * Example use : - * <code> + * @code * # Determines whether the article with the page_id 12345 is in both * # "Category 1" and "Category 2" or their subcategories, respectively * @@ -39,7 +39,7 @@ * ); * $a = $cf->run(); * print implode( ',' , $a ); - * </code> + * @endcode * */ class CategoryFinder { diff --git a/includes/CategoryViewer.php b/includes/CategoryViewer.php index 66079c01..e2c31a66 100644 --- a/includes/CategoryViewer.php +++ b/includes/CategoryViewer.php @@ -329,7 +329,7 @@ class CategoryViewer extends ContextSource { 'category' => array( 'LEFT JOIN', array( 'cat_title = page_title', 'page_namespace' => NS_CATEGORY - )) + ) ) ) ); diff --git a/includes/Collation.php b/includes/Collation.php index 481d8e70..c1f0b388 100644 --- a/includes/Collation.php +++ b/includes/Collation.php @@ -320,16 +320,16 @@ class IcuCollation extends Collation { // intl extension produces non null-terminated // strings. Appending '' fixes it so that it doesn't generate // a warning on each access in debug php. - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $key = $this->mainCollator->getSortKey( $string ) . ''; - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); return $key; } function getPrimarySortKey( $string ) { - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $key = $this->primaryCollator->getSortKey( $string ) . ''; - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); return $key; } diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index c13aa5f4..268a8d19 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -75,7 +75,7 @@ $wgConfigRegistry = array( * MediaWiki version number * @since 1.2 */ -$wgVersion = '1.25.3'; +$wgVersion = '1.26.0'; /** * Name of the site. It must be changed in LocalSettings.php @@ -83,6 +83,14 @@ $wgVersion = '1.25.3'; $wgSitename = 'MediaWiki'; /** + * When the wiki is running behind a proxy and this is set to true, assumes that the proxy exposes + * the wiki on the standard ports (443 for https and 80 for http). + * @var bool + * @since 1.26 + */ +$wgAssumeProxiesUseDefaultProtocolPorts = true; + +/** * URL of the server. * * @par Example: @@ -203,7 +211,7 @@ $wgLoadScript = false; /** * The URL path of the skins directory. - * Defaults to "{$wgScriptPath}/skins". + * Defaults to "{$wgResourceBasePath}/skins". * @since 1.3 */ $wgStylePath = false; @@ -218,7 +226,7 @@ $wgLocalStylePath = false; /** * The URL path of the extensions directory. - * Defaults to "{$wgScriptPath}/extensions". + * Defaults to "{$wgResourceBasePath}/extensions". * @since 1.16 */ $wgExtensionAssetsPath = false; @@ -472,13 +480,13 @@ $wgImgAuthUrlPathMap = array(); * * These settings describe a foreign MediaWiki installation. They are optional, and will be ignored * for local repositories: - * - descBaseUrl URL of image description pages, e.g. http://en.wikipedia.org/wiki/File: + * - descBaseUrl URL of image description pages, e.g. https://en.wikipedia.org/wiki/File: * - scriptDirUrl URL of the MediaWiki installation, equivalent to $wgScriptPath, e.g. - * http://en.wikipedia.org/w + * https://en.wikipedia.org/w * - scriptExtension Script extension of the MediaWiki installation, equivalent to * $wgScriptExtension, e.g. .php5 defaults to .php * - * - articleUrl Equivalent to $wgArticlePath, e.g. http://en.wikipedia.org/wiki/$1 + * - articleUrl Equivalent to $wgArticlePath, e.g. https://en.wikipedia.org/wiki/$1 * - fetchDescription Fetch the text of the remote file description page. Equivalent to * $wgFetchCommonsDescriptions. * - abbrvThreshold File names over this size will use the short form of thumbnail names. @@ -518,6 +526,16 @@ $wgForeignFileRepos = array(); $wgUseInstantCommons = false; /** + * Array of foreign file repo names (set in $wgForeignFileRepos above) that + * are allowable upload targets. These wikis must have some method of + * authentication (i.e. CentralAuth), and be CORS-enabled for this wiki. + * + * Example: + * $wgForeignUploadTargets = array( 'shared' ); + */ +$wgForeignUploadTargets = array(); + +/** * File backend structure configuration. * * This is an array of file backend configuration arrays. @@ -717,7 +735,7 @@ $wgMinUploadChunkSize = 1024; # 1KB * * @par Example: * @code - * $wgUploadNavigationUrl = 'http://commons.wikimedia.org/wiki/Special:Upload'; + * $wgUploadNavigationUrl = 'https://commons.wikimedia.org/wiki/Special:Upload'; * @endcode */ $wgUploadNavigationUrl = false; @@ -777,7 +795,7 @@ $wgHashedSharedUploadDirectory = true; * * Please specify the namespace, as in the example below. */ -$wgRepositoryBaseUrl = "http://commons.wikimedia.org/wiki/File:"; +$wgRepositoryBaseUrl = "https://commons.wikimedia.org/wiki/File:"; /** * This is the list of preferred extensions for uploading files. Uploading files @@ -884,6 +902,7 @@ $wgMediaHandlers = array( 'image/png' => 'PNGHandler', 'image/gif' => 'GIFHandler', 'image/tiff' => 'TiffHandler', + 'image/webp' => 'WebPHandler', 'image/x-ms-bmp' => 'BmpHandler', 'image/x-bmp' => 'BmpHandler', 'image/x-xcf' => 'XCFHandler', @@ -978,6 +997,14 @@ $wgJpegTran = '/usr/bin/jpegtran'; */ $wgExiv2Command = '/usr/bin/exiv2'; + +/** + * Path to exiftool binary. Used for lossless ICC profile swapping. + * + * @since 1.26 + */ +$wgExiftool = '/usr/bin/exiftool'; + /** * Scalable Vector Graphics (SVG) may be uploaded as images. * Since SVG support is not yet standard in browsers, it is @@ -1012,7 +1039,7 @@ $wgSVGConverterPath = ''; /** * Don't scale a SVG larger than this */ -$wgSVGMaxSize = 2048; +$wgSVGMaxSize = 5120; /** * Don't read SVG metadata beyond this point. @@ -1331,6 +1358,14 @@ $wgUploadThumbnailRenderHttpCustomHost = false; $wgUploadThumbnailRenderHttpCustomDomain = false; /** + * When this variable is true and JPGs use the sRGB ICC profile, swaps it for the more lightweight + * (and free) TinyRGB profile when generating thumbnails. + * + * @since 1.26 + */ +$wgUseTinyRGBForJPGThumbnails = false; + +/** * Default parameters for the "<gallery>" tag */ $wgGalleryOptions = array( @@ -1580,7 +1615,8 @@ $wgEnotifRevealEditorAddress = false; /** * Send notification mails on minor edits to watchlist pages. This is enabled - * by default. Does not affect user talk notifications. + * by default. User talk notifications are affected by this, $wgEnotifUserTalk, and + * the nominornewtalk user right. */ $wgEnotifMinorEdits = true; @@ -1843,12 +1879,6 @@ $wgDBservers = false; $wgLBFactoryConf = array( 'class' => 'LBFactorySimple' ); /** - * How long to wait for a slave to catch up to the master - * @deprecated since 1.24 - */ -$wgMasterWaitTimeout = 10; - -/** * File to log database errors to */ $wgDBerrorLog = false; @@ -1862,11 +1892,11 @@ $wgDBerrorLog = false; * * @par Examples: * @code - * $wgLocaltimezone = 'UTC'; - * $wgLocaltimezone = 'GMT'; - * $wgLocaltimezone = 'PST8PDT'; - * $wgLocaltimezone = 'Europe/Sweden'; - * $wgLocaltimezone = 'CET'; + * $wgDBerrorLogTZ = 'UTC'; + * $wgDBerrorLogTZ = 'GMT'; + * $wgDBerrorLogTZ = 'PST8PDT'; + * $wgDBerrorLogTZ = 'Europe/Sweden'; + * $wgDBerrorLogTZ = 'CET'; * @endcode * * @since 1.20 @@ -1874,13 +1904,6 @@ $wgDBerrorLog = false; $wgDBerrorLogTZ = false; /** - * Scale load balancer polling time so that under overload conditions, the - * database server receives a SHOW STATUS query at an average interval of this - * many microseconds - */ -$wgDBAvgStatusPoll = 2000; - -/** * Set to true to engage MySQL 4.1/5.0 charset-related features; * for now will just cause sending of 'SET NAMES=utf8' on connect. * @@ -2067,6 +2090,14 @@ $wgMaxArticleSize = 2048; */ $wgMemoryLimit = "50M"; +/** + * The minimum amount of time that MediaWiki needs for "slow" write request, + * particularly ones with multiple non-atomic writes that *should* be as + * transactional as possible; MediaWiki will call set_time_limit() if needed. + * @since 1.26 + */ +$wgTransactionalTimeLimit = 120; + /** @} */ # end performance hacks } /************************************************************************//** @@ -2086,8 +2117,8 @@ $wgCacheDirectory = false; /** * Main cache type. This should be a cache with fast access, but it may have - * limited space. By default, it is disabled, since the database is not fast - * enough to make it worthwhile. + * limited space. By default, it is disabled, since the stock database cache + * is not fast enough to make it worthwhile. * * The options are: * @@ -2157,6 +2188,19 @@ $wgObjectCaches = array( CACHE_ACCEL => array( 'factory' => 'ObjectCache::newAccelerator' ), CACHE_MEMCACHED => array( 'factory' => 'ObjectCache::newMemcached', 'loggroup' => 'memcached' ), + 'db-replicated' => array( + 'class' => 'ReplicatedBagOStuff', + 'readFactory' => array( + 'class' => 'SqlBagOStuff', + 'args' => array( array( 'slaveOnly' => true ) ) + ), + 'writeFactory' => array( + 'class' => 'SqlBagOStuff', + 'args' => array( array( 'slaveOnly' => false ) ) + ), + 'loggroup' => 'SQLBagOStuff' + ), + 'apc' => array( 'class' => 'APCBagOStuff' ), 'xcache' => array( 'class' => 'XCacheBagOStuff' ), 'wincache' => array( 'class' => 'WinCacheBagOStuff' ), @@ -2166,6 +2210,71 @@ $wgObjectCaches = array( ); /** + * Main Wide-Area-Network cache type. This should be a cache with fast access, + * but it may have limited space. By default, it is disabled, since the basic stock + * cache is not fast enough to make it worthwhile. For single data-center setups, this can + * simply be pointed to a cache in $wgWANObjectCaches that uses a local $wgObjectCaches + * cache with a relayer of type EventRelayerNull. + * + * The options are: + * - false: Configure the cache using $wgMainCacheType, without using + * a relayer (only matters if there are multiple data-centers) + * - CACHE_NONE: Do not cache + * - (other): A string may be used which identifies a cache + * configuration in $wgWANObjectCaches + * @since 1.26 + */ +$wgMainWANCache = false; + +/** + * Advanced WAN object cache configuration. + * + * Each WAN cache wraps a registered object cache (for the local cluster) + * and it must also be configured to point to a PubSub instance. Subscribers + * must be configured to relay purges to the actual cache servers. + * + * The format is an associative array where the key is a cache identifier, and + * the value is an associative array of parameters. The "cacheId" parameter is + * a cache identifier from $wgObjectCaches. The "relayerConfig" parameter is an + * array used to construct an EventRelayer object. The "pool" parameter is a + * string that is used as a PubSub channel prefix. + * + * @since 1.26 + */ +$wgWANObjectCaches = array( + CACHE_NONE => array( + 'class' => 'WANObjectCache', + 'cacheId' => CACHE_NONE, + 'pool' => 'mediawiki-main-none', + 'relayerConfig' => array( 'class' => 'EventRelayerNull' ) + ) + /* Example of a simple single data-center cache: + 'memcached-php' => array( + 'class' => 'WANObjectCache', + 'cacheId' => 'memcached-php', + 'pool' => 'mediawiki-main-memcached', + 'relayerConfig' => array( 'class' => 'EventRelayerNull' ) + ) + */ +); + +/** + * Main object stash type. This should be a fast storage system for storing + * lightweight data like hit counters and user activity. Sites with multiple + * data-centers should have this use a store that replicates all writes. The + * store should have enough consistency for CAS operations to be usable. + * Reads outside of those needed for merge() may be eventually consistent. + * + * The options are: + * - db: Store cache objects in the DB + * - (other): A string may be used which identifies a cache + * configuration in $wgObjectCaches + * + * @since 1.26 + */ +$wgMainStash = 'db-replicated'; + +/** * The expiry time for the parser cache, in seconds. * The default is 86400 (one day). */ @@ -2239,11 +2348,13 @@ $wgAdaptiveMessageCache = false; * Localisation cache configuration. Associative array with keys: * class: The class to use. May be overridden by extensions. * - * store: The location to store cache data. May be 'files', 'db' or + * store: The location to store cache data. May be 'files', 'array', 'db' or * 'detect'. If set to "files", data will be in CDB files. If set * to "db", data will be stored to the database. If set to * "detect", files will be used if $wgCacheDirectory is set, * otherwise the database will be used. + * "array" is an experimental option that uses PHP files that + * store static arrays. * * storeClass: The class name for the underlying storage. If set to a class * name, it overrides the "store" setting. @@ -2311,13 +2422,8 @@ $wgUseFileCache = false; $wgFileCacheDepth = 2; /** - * Keep parsed pages in a cache (objectcache table or memcached) - * to speed up output of the same page viewed by another user with the - * same options. - * - * This can provide a significant speedup for medium to large pages, - * so you probably want to keep it on. Extensions that conflict with the - * parser cache should disable the cache on a per-page basis instead. + * Kept for extension compatibility; see $wgParserCacheType + * @deprecated 1.26 */ $wgEnableParserCache = true; @@ -2447,13 +2553,16 @@ $wgInternalServer = false; /** * Cache timeout for the squid, will be sent as s-maxage (without ESI) or * Surrogate-Control (with ESI). Without ESI, you should strip out s-maxage in - * the Squid config. 18000 seconds = 5 hours, more cache hits with 2678400 = 31 - * days + * the Squid config. + * +* 18000 seconds = 5 hours, more cache hits with 2678400 = 31 days. */ $wgSquidMaxage = 18000; /** * Default maximum age for raw CSS/JS accesses + * + * 300 seconds = 5 minutes. */ $wgForcedRawSMaxage = 300; @@ -2742,14 +2851,14 @@ $wgBrowserBlackList = array( * - Mozilla/4.0 (compatible; MSIE 5.23; Mac_PowerPC) * - [...] * - * @link http://en.wikipedia.org/w/index.php?diff=12356041&oldid=12355864 - * @link http://en.wikipedia.org/wiki/Template%3AOS9 + * @link https://en.wikipedia.org/w/index.php?diff=12356041&oldid=12355864 + * @link https://en.wikipedia.org/wiki/Template%3AOS9 */ '/^Mozilla\/4\.0 \(compatible; MSIE \d+\.\d+; Mac_PowerPC\)/', /** * Google wireless transcoder, seems to eat a lot of chars alive - * http://it.wikipedia.org/w/index.php?title=Luciano_Ligabue&diff=prev&oldid=8857361 + * https://it.wikipedia.org/w/index.php?title=Luciano_Ligabue&diff=prev&oldid=8857361 */ '/^Mozilla\/4\.0 \(compatible; MSIE 6.0; Windows NT 5.0; Google Wireless Transcoder;\)/' ); @@ -3382,8 +3491,8 @@ $wgResourceModuleSkinStyles = array(); $wgResourceLoaderSources = array(); /** - * Default 'remoteBasePath' value for instances of ResourceLoaderFileModule. - * If not set, then $wgScriptPath will be used as a fallback. + * The default 'remoteBasePath' value for instances of ResourceLoaderFileModule. + * Defaults to $wgScriptPath. */ $wgResourceBasePath = null; @@ -3422,13 +3531,6 @@ $wgResourceLoaderMaxage = array( $wgResourceLoaderDebug = false; /** - * Enable embedding of certain resources using Edge Side Includes. This will - * improve performance but only works if there is something in front of the - * web server (e..g a Squid or Varnish server) configured to process the ESI. - */ -$wgResourceLoaderUseESI = false; - -/** * Put each statement on its own line when minifying JavaScript. This makes * debugging in non-debug mode a bit easier. */ @@ -3442,28 +3544,27 @@ $wgResourceLoaderMinifierStatementsOnOwnLine = false; $wgResourceLoaderMinifierMaxLineLength = 1000; /** - * Whether to include the mediawiki.legacy JS library (old wikibits.js), and its - * dependencies. + * Whether to ensure the mediawiki.legacy library is loaded before other modules. + * + * @deprecated since 1.26: Always declare dependencies. */ $wgIncludeLegacyJavaScript = true; /** - * Whether to preload the mediawiki.util module as blocking module in the top - * queue. + * Whether to ensure the mediawiki.util is loaded before other modules. * - * Before MediaWiki 1.19, modules used to load slower/less asynchronous which - * allowed modules to lack dependencies on 'popular' modules that were likely - * loaded already. + * Before MediaWiki 1.19, modules used to load less asynchronous which allowed + * modules to lack dependencies on 'popular' modules that were likely loaded already. * * This setting is to aid scripts during migration by providing mediawiki.util - * unconditionally (which was the most commonly missed dependency). - * It doesn't cover all missing dependencies obviously but should fix most of - * them. + * unconditionally (which was the most commonly missed dependency). It doesn't + * cover all missing dependencies obviously but should fix most of them. * * This should be removed at some point after site/user scripts have been fixed. * Enable this if your wiki has a large amount of user/site scripts that are * lacking dependencies. - * @todo Deprecate + * + * @deprecated since 1.26: Always declare dependencies. */ $wgPreloadJavaScriptMwUtil = false; @@ -3529,13 +3630,6 @@ $wgResourceLoaderValidateJS = true; $wgResourceLoaderValidateStaticJS = false; /** - * If set to true, asynchronous loading of bottom-queue scripts in the "<head>" - * will be enabled. This is an experimental feature that's supposed to make - * JavaScript load faster. - */ -$wgResourceLoaderExperimentalAsyncLoading = false; - -/** * Global LESS variables. An associative array binding variable names to * LESS code snippets representing their values. * @@ -3561,18 +3655,6 @@ $wgResourceLoaderExperimentalAsyncLoading = false; $wgResourceLoaderLESSVars = array(); /** - * Custom LESS functions. An associative array mapping function name to PHP - * callable. - * - * Changes to LESS functions do not trigger cache invalidation. - * - * @since 1.22 - * @deprecated since 1.24 Questionable usefulness and problematic to support, - * will be removed in the future. - */ -$wgResourceLoaderLESSFunctions = array(); - -/** * Default import paths for LESS modules. LESS files referenced in @import * statements will be looked up here first, and relative to the importing file * second. To avoid collisions, it's important for the LESS files in these @@ -3881,6 +3963,15 @@ $wgTrackingCategories = array(); $wgContentNamespaces = array( NS_MAIN ); /** + * Array of namespaces, in addition to the talk namespaces, where signatures + * (~~~~) are likely to be used. This determines whether to display the + * Signature button on the edit toolbar, and may also be used by extensions. + * For example, "traditional" style wikis, where content and discussion are + * intermixed, could place NS_MAIN and NS_PROJECT namespaces in this array. + */ +$wgExtraSignatureNamespaces = array(); + +/** * Max number of redirects to follow when resolving redirects. * 1 means only the first redirect is followed (default behavior). * 0 or less means no redirects are followed. @@ -4031,44 +4122,55 @@ $wgEnableImageWhitelist = true; $wgAllowImageTag = false; /** - * $wgUseTidy: use tidy to make sure HTML output is sane. - * Tidy is a free tool that fixes broken HTML. - * See http://www.w3.org/People/Raggett/tidy/ + * Configuration for HTML postprocessing tool. Set this to a configuration + * array to enable an external tool. Dave Raggett's "HTML Tidy" is typically + * used. See http://www.w3.org/People/Raggett/tidy/ * - * - $wgTidyBin should be set to the path of the binary and - * - $wgTidyConf to the path of the configuration file. - * - $wgTidyOpts can include any number of parameters. - * - $wgTidyInternal controls the use of the PECL extension or the - * libtidy (PHP >= 5) extension to use an in-process tidy library instead - * of spawning a separate program. - * Normally you shouldn't need to override the setting except for - * debugging. To install, use 'pear install tidy' and add a line - * 'extension=tidy.so' to php.ini. + * If this is null and $wgUseTidy is true, the deprecated configuration + * parameters will be used instead. + * + * If this is null and $wgUseTidy is false, a pure PHP fallback will be used. + * + * Keys are: + * - driver: May be: + * - RaggettInternalHHVM: Use the limited-functionality HHVM extension + * - RaggettInternalPHP: Use the PECL extension + * - RaggettExternal: Shell out to an external binary (tidyBin) + * + * - tidyConfigFile: Path to configuration file for any of the Raggett drivers + * - debugComment: True to add a comment to the output with warning messages + * - tidyBin: For RaggettExternal, the path to the tidy binary. + * - tidyCommandLine: For RaggettExternal, additional command line options. */ -$wgUseTidy = false; +$wgTidyConfig = null; /** - * @see $wgUseTidy + * Set this to true to use the deprecated tidy configuration parameters. + * @deprecated use $wgTidyConfig */ -$wgAlwaysUseTidy = false; +$wgUseTidy = false; /** - * @see $wgUseTidy + * The path to the tidy binary. + * @deprecated Use $wgTidyConfig['tidyBin'] */ $wgTidyBin = 'tidy'; /** - * @see $wgUseTidy + * The path to the tidy config file + * @deprecated Use $wgTidyConfig['tidyConfigFile'] */ -$wgTidyConf = $IP . '/includes/tidy.conf'; +$wgTidyConf = $IP . '/includes/tidy/tidy.conf'; /** - * @see $wgUseTidy + * The command line options to the tidy binary + * @deprecated Use $wgTidyConfig['tidyCommandLine'] */ $wgTidyOpts = ''; /** - * @see $wgUseTidy + * Set this to true to use the tidy extension + * @deprecated Use $wgTidyConfig['driver'] */ $wgTidyInternal = extension_loaded( 'tidy' ); @@ -4198,6 +4300,59 @@ $wgActiveUserDays = 30; */ /** + * Password policy for local wiki users. A user's effective policy + * is the superset of all policy statements from the policies for the + * groups where the user is a member. If more than one group policy + * include the same policy statement, the value is the max() of the + * values. Note true > false. The 'default' policy group is required, + * and serves as the minimum policy for all users. New statements can + * be added by appending to $wgPasswordPolicy['checks']. + * Statements: + * - MinimalPasswordLength - minimum length a user can set + * - MinimumPasswordLengthToLogin - passwords shorter than this will + * not be allowed to login, regardless if it is correct. + * - MaximalPasswordLength - maximum length password a user is allowed + * to attempt. Prevents DoS attacks with pbkdf2. + * - PasswordCannotMatchUsername - Password cannot match username to + * - PasswordCannotMatchBlacklist - Username/password combination cannot + * match a specific, hardcoded blacklist. + * @since 1.26 + */ +$wgPasswordPolicy = array( + 'policies' => array( + 'bureaucrat' => array( + 'MinimalPasswordLength' => 8, + 'MinimumPasswordLengthToLogin' => 1, + 'PasswordCannotMatchUsername' => true, + ), + 'sysop' => array( + 'MinimalPasswordLength' => 8, + 'MinimumPasswordLengthToLogin' => 1, + 'PasswordCannotMatchUsername' => true, + ), + 'bot' => array( + 'MinimalPasswordLength' => 8, + 'MinimumPasswordLengthToLogin' => 1, + 'PasswordCannotMatchUsername' => true, + ), + 'default' => array( + 'MinimalPasswordLength' => 1, + 'PasswordCannotMatchUsername' => true, + 'PasswordCannotMatchBlacklist' => true, + 'MaximalPasswordLength' => 4096, + ), + ), + 'checks' => array( + 'MinimalPasswordLength' => 'PasswordPolicyChecks::checkMinimalPasswordLength', + 'MinimumPasswordLengthToLogin' => 'PasswordPolicyChecks::checkMinimumPasswordLengthToLogin', + 'PasswordCannotMatchUsername' => 'PasswordPolicyChecks::checkPasswordCannotMatchUsername', + 'PasswordCannotMatchBlacklist' => 'PasswordPolicyChecks::checkPasswordCannotMatchBlacklist', + 'MaximalPasswordLength' => 'PasswordPolicyChecks::checkMaximalPasswordLength', + ), +); + + +/** * For compatibility with old installations set to false * @deprecated since 1.24 will be removed in future */ @@ -4206,8 +4361,9 @@ $wgPasswordSalt = true; /** * Specifies the minimal length of a user password. If set to 0, empty pass- * words are allowed. + * @deprecated since 1.26, use $wgPasswordPolicy's MinimalPasswordLength. */ -$wgMinimalPasswordLength = 1; +$wgMinimalPasswordLength = false; /** * Specifies the maximal length of a user password (T64685). @@ -4218,8 +4374,9 @@ $wgMinimalPasswordLength = 1; * * @warning Unlike other password settings, user with passwords greater than * the maximum will not be able to log in. + * @deprecated since 1.26, use $wgPasswordPolicy's MaximalPasswordLength. */ -$wgMaximalPasswordLength = 4096; +$wgMaximalPasswordLength = false; /** * Specifies if users should be sent to a password-reset form on login, if their @@ -4295,7 +4452,7 @@ $wgPasswordConfig = array( */ $wgPasswordResetRoutes = array( 'username' => true, - 'email' => false, + 'email' => true, ); /** @@ -4322,6 +4479,7 @@ $wgReservedUsernames = array( 'msg:double-redirect-fixer', // Automatic double redirect fix 'msg:usermessage-editor', // Default user for leaving user messages 'msg:proxyblocker', // For $wgProxyList and Special:Blockme (removed in 1.22) + 'msg:spambot_username', // Used by cleanupSpam.php ); /** @@ -4397,7 +4555,7 @@ $wgHiddenPrefs = array(); * This is used in a regular expression character class during * registration (regex metacharacters like / are escaped). */ -$wgInvalidUsernameCharacters = '@'; +$wgInvalidUsernameCharacters = '@:'; /** * Character used as a delimiter when testing for interwiki userrights @@ -4413,7 +4571,7 @@ $wgUserrightsInterwikiDelimiter = '@'; /** * This is to let user authenticate using https when they come from http. * Based on an idea by George Herbert on wikitech-l: - * http://lists.wikimedia.org/pipermail/wikitech-l/2010-October/050039.html + * https://lists.wikimedia.org/pipermail/wikitech-l/2010-October/050039.html * @since 1.17 */ $wgSecureLogin = false; @@ -4433,7 +4591,7 @@ $wgAutoblockExpiry = 86400; /** * Set this to true to allow blocked users to edit their own user talk page. */ -$wgBlockAllowsUTEdit = false; +$wgBlockAllowsUTEdit = true; /** * Allow sysops to ban users from accessing Emailuser @@ -4940,7 +5098,7 @@ $wgAccountCreationThrottle = 0; * There's no administrator override on-wiki, so be careful what you set. :) * May be an array of regexes or a single string for backwards compatibility. * - * @see http://en.wikipedia.org/wiki/Regular_expression + * @see https://en.wikipedia.org/wiki/Regular_expression * * @note Each regex needs a beginning/end delimiter, eg: # or / */ @@ -5145,6 +5303,22 @@ $wgProxyList = array(); $wgCookieExpiration = 180 * 86400; /** + * The identifiers of the login cookies that can have their lifetimes + * extended independently of all other login cookies. + * + * @var string[] + */ +$wgExtendedLoginCookies = array( 'UserID', 'Token' ); + +/** + * Default login cookie lifetime, in seconds. Setting + * $wgExtendLoginCookieExpiration to null will use $wgCookieExpiration to + * calculate the cookie lifetime. As with $wgCookieExpiration, 0 will make + * login cookies session-only. + */ +$wgExtendedLoginCookieExpiration = null; + +/** * Set to set an explicit domain on the login cookies eg, "justthis.domain.org" * or ".any.subdomain.net" */ @@ -5282,6 +5456,36 @@ $wgDebugDumpSql = false; $wgDebugDumpSqlLength = 500; /** + * Performance expectations for DB usage + * + * @since 1.26 + */ +$wgTrxProfilerLimits = array( + // Basic GET and POST requests + 'GET' => array( + 'masterConns' => 0, + 'writes' => 0, + 'readQueryTime' => 5 + ), + 'POST' => array( + 'readQueryTime' => 5, + 'writeQueryTime' => 1, + 'maxAffected' => 500 + ), + // Background job runner + 'JobRunner' => array( + 'readQueryTime' => 30, + 'writeQueryTime' => 5, + 'maxAffected' => 500 + ), + // Command-line scripts + 'Maintenance' => array( + 'writeQueryTime' => 5, + 'maxAffected' => 1000 + ) +); + +/** * Map of string log group names to log destinations. * * If set, wfDebugLog() output for that group will go to that file instead @@ -5447,7 +5651,7 @@ $wgProfilePerHost = null; * * The host should be running a daemon which can be obtained from MediaWiki * Git at: - * http://git.wikimedia.org/tree/operations%2Fsoftware.git/master/udpprofile + * https://git.wikimedia.org/tree/operations%2Fsoftware.git/master/udpprofile * * @deprecated set $wgProfiler['udphost'] instead */ @@ -5504,6 +5708,29 @@ $wgAggregateStatsID = false; $wgStatsFormatString = "stats/%s - %s 1 1 1 1 %s\n"; /** + * Destination of statsd metrics. + * + * A host or host:port of a statsd server. Port defaults to 8125. + * + * If not set, statsd metrics will not be collected. + * + * @see wfLogProfilingData + * @since 1.25 + */ +$wgStatsdServer = false; + +/** + * Prefix for metric names sent to wgStatsdServer. + * + * Defaults to "MediaWiki". + * + * @see RequestContext::getStats + * @see BufferingStatsdDataFactory + * @since 1.25 + */ +$wgStatsdMetricPrefix = false; + +/** * InfoAction retrieves a list of transclusion links (both to and from). * This number puts a limit on that query in the case of highly transcluded * templates. @@ -5836,6 +6063,21 @@ $wgGitRepositoryViewers = array( $wgRCMaxAge = 90 * 24 * 3600; /** + * Page watchers inactive for more than this many seconds are considered inactive. + * Used mainly by action=info. Default: 180 days = about six months. + * @since 1.26 + */ +$wgWatchersMaxAge = 180 * 24 * 3600; + +/** + * If active watchers (per above) are this number or less, do not disclose it. + * Left to 1, prevents unprivileged users from knowing for sure that there are 0. + * Set to -1 if you want to always complement watchers count with this info. + * @since 1.26 + */ +$wgUnwatchedPageSecret = 1; + +/** * Filter $wgRCLinkDays by $wgRCMaxAge to avoid showing links for numbers * higher than what will be stored. Note that this is disabled by default * because we sometimes do have RC data which is beyond the limit for some @@ -6453,6 +6695,7 @@ $wgJobClasses = array( 'ThumbnailRender' => 'ThumbnailRenderJob', 'recentChangesUpdate' => 'RecentChangesUpdateJob', 'refreshLinksPrioritized' => 'RefreshLinksJob', // for cascading protection + 'activityUpdateJob' => 'ActivityUpdateJob', 'enqueue' => 'EnqueueJob', // local queue for multi-DC setups 'null' => 'NullJob' ); @@ -6482,13 +6725,28 @@ $wgJobTypesExcludedFromDefaultQueue = array( 'AssembleUploadChunks', 'PublishSta $wgJobBackoffThrottling = array(); /** + * Make job runners commit changes for slave-lag prone jobs one job at a time. + * This is useful if there are many job workers that race on slave lag checks. + * If set, jobs taking this many seconds of DB write time have serialized commits. + * + * Note that affected jobs may have worse lock contention. Also, if they affect + * several DBs at once they may have a smaller chance of being atomic due to the + * possibility of connection loss while queueing up to commit. Affected jobs may + * also fail due to the commit lock acquisition timeout. + * + * @var float|bool + * @since 1.26 + */ +$wgJobSerialCommitThreshold = false; + +/** * Map of job types to configuration arrays. * This determines which queue class and storage system is used for each job type. * Job types that do not have explicit configuration will use the 'default' config. * These settings should be global to all wikis. */ $wgJobTypeConf = array( - 'default' => array( 'class' => 'JobQueueDB', 'order' => 'random' ), + 'default' => array( 'class' => 'JobQueueDB', 'order' => 'random', 'claimTTL' => 3600 ), ); /** @@ -6604,6 +6862,7 @@ $wgLogTypes = array( 'suppress', 'tag', 'managetags', + 'contentmodel', ); /** @@ -6679,15 +6938,15 @@ $wgLogNames = array( $wgLogHeaders = array( '' => 'alllogstext', 'block' => 'blocklogtext', - 'protect' => 'protectlogtext', - 'rights' => 'rightslogtext', 'delete' => 'dellogpagetext', - 'upload' => 'uploadlogpagetext', - 'move' => 'movelogpagetext', 'import' => 'importlogpagetext', - 'patrol' => 'patrol-log-header', 'merge' => 'mergelogpagetext', + 'move' => 'movelogpagetext', + 'patrol' => 'patrol-log-header', + 'protect' => 'protectlogtext', + 'rights' => 'rightslogtext', 'suppress' => 'suppressionlogtext', + 'upload' => 'uploadlogpagetext', ); /** @@ -6697,10 +6956,9 @@ $wgLogHeaders = array( * Extensions with custom log types may add to this array. */ $wgLogActions = array( - 'protect/protect' => 'protectedarticle', 'protect/modify' => 'modifiedarticleprotection', + 'protect/protect' => 'protectedarticle', 'protect/unprotect' => 'unprotectedarticle', - 'protect/move_prot' => 'movedarticleprotection', ); /** @@ -6710,34 +6968,36 @@ $wgLogActions = array( * @see LogFormatter */ $wgLogActionsHandlers = array( - 'move/move' => 'MoveLogFormatter', - 'move/move_redir' => 'MoveLogFormatter', + 'block/block' => 'BlockLogFormatter', + 'block/reblock' => 'BlockLogFormatter', + 'block/unblock' => 'BlockLogFormatter', + 'contentmodel/change' => 'ContentModelLogFormatter', 'delete/delete' => 'DeleteLogFormatter', + 'delete/event' => 'DeleteLogFormatter', 'delete/restore' => 'DeleteLogFormatter', 'delete/revision' => 'DeleteLogFormatter', - 'delete/event' => 'DeleteLogFormatter', - 'suppress/revision' => 'DeleteLogFormatter', - 'suppress/event' => 'DeleteLogFormatter', - 'suppress/delete' => 'DeleteLogFormatter', - 'patrol/patrol' => 'PatrolLogFormatter', - 'rights/rights' => 'RightsLogFormatter', - 'rights/autopromote' => 'RightsLogFormatter', - 'upload/upload' => 'UploadLogFormatter', - 'upload/overwrite' => 'UploadLogFormatter', - 'upload/revert' => 'UploadLogFormatter', - 'merge/merge' => 'MergeLogFormatter', - 'tag/update' => 'TagLogFormatter', - 'managetags/create' => 'LogFormatter', - 'managetags/delete' => 'LogFormatter', + 'import/interwiki' => 'LogFormatter', + 'import/upload' => 'LogFormatter', 'managetags/activate' => 'LogFormatter', + 'managetags/create' => 'LogFormatter', 'managetags/deactivate' => 'LogFormatter', - 'block/block' => 'BlockLogFormatter', - 'block/unblock' => 'BlockLogFormatter', - 'block/reblock' => 'BlockLogFormatter', + 'managetags/delete' => 'LogFormatter', + 'merge/merge' => 'MergeLogFormatter', + 'move/move' => 'MoveLogFormatter', + 'move/move_redir' => 'MoveLogFormatter', + 'patrol/patrol' => 'PatrolLogFormatter', + 'protect/move_prot' => 'ProtectLogFormatter', + 'rights/autopromote' => 'RightsLogFormatter', + 'rights/rights' => 'RightsLogFormatter', 'suppress/block' => 'BlockLogFormatter', + 'suppress/delete' => 'DeleteLogFormatter', + 'suppress/event' => 'DeleteLogFormatter', 'suppress/reblock' => 'BlockLogFormatter', - 'import/upload' => 'LogFormatter', - 'import/interwiki' => 'LogFormatter', + 'suppress/revision' => 'DeleteLogFormatter', + 'tag/update' => 'TagLogFormatter', + 'upload/overwrite' => 'UploadLogFormatter', + 'upload/revert' => 'UploadLogFormatter', + 'upload/upload' => 'UploadLogFormatter', ); /** @@ -6764,14 +7024,6 @@ $wgAllowSpecialInclusion = true; $wgDisableQueryPageUpdate = false; /** - * List of special pages, followed by what subtitle they should go under - * at Special:SpecialPages - * - * @deprecated since 1.21 Override SpecialPage::getGroupName instead - */ -$wgSpecialPageGroups = array(); - -/** * On Special:Unusedimages, consider images "used", if they are put * into a category. Default (false) is not to count those as used. */ @@ -7009,12 +7261,6 @@ $wgAPIPropModules = array(); $wgAPIListModules = array(); /** - * This variable is ignored. To add your module to the API, please add it to $wgAPI*Modules - * @deprecated since 1.21 - */ -$wgAPIGeneratorModules = array(); - -/** * Maximum amount of rows to scan in a DB query in the API * The default value is generally fine */ @@ -7464,6 +7710,7 @@ $wgUseLinkNamespaceDBFields = true; * $wgVirtualRestConfig['modules']['parsoid'] = array( * 'url' => 'http://localhost:8000', * 'prefix' => 'enwiki', + * 'domain' => 'en.wikipedia.org', * ); * * @var array @@ -7474,12 +7721,22 @@ $wgVirtualRestConfig = array( 'global' => array( # Timeout in seconds 'timeout' => 360, + # 'domain' is set to $wgCanonicalServer in Setup.php 'forwardCookies' => false, 'HTTPProxy' => null ) ); /** + * Controls whether zero-result search queries with suggestions should display results for + * these suggestions. + * + * @var bool + * @since 1.26 + */ +$wgSearchRunSuggestedQuery = true; + +/** * For really cool vim folding this needs to be at the end: * vim: foldmarker=@{,@} foldmethod=marker * @} diff --git a/includes/Defines.php b/includes/Defines.php index c9263da9..d55bbcf8 100644 --- a/includes/Defines.php +++ b/includes/Defines.php @@ -24,11 +24,6 @@ * @defgroup Constants MediaWiki constants */ -/** - * Version constants for the benefit of extensions - */ -define( 'MW_SPECIALPAGE_VERSION', 2 ); - /**@{ * Database related constants */ @@ -203,7 +198,7 @@ define( 'LIST_OR', 4 ); /** * Unicode and normalisation related */ -require_once __DIR__ . '/libs/normal/UtfNormalDefines.php'; +require_once __DIR__ . '/compat/normal/UtfNormalDefines.php'; /**@{ * Hook support constants diff --git a/includes/EditPage.php b/includes/EditPage.php index 8d27eac8..05e0ac0e 100644 --- a/includes/EditPage.php +++ b/includes/EditPage.php @@ -168,6 +168,12 @@ class EditPage { const AS_PARSE_ERROR = 240; /** + * Status: when changing the content model is disallowed due to + * $wgContentHandlerUseDB being false + */ + const AS_CANNOT_USE_CUSTOM_MODEL = 241; + + /** * HTML id and name for the beginning of the edit form. */ const EDITFORM_ID = 'editform'; @@ -380,13 +386,15 @@ class EditPage { public $suppressIntro = false; - /** @var bool Set to true to allow editing of non-text content types. */ - public $allowNonTextContent = false; - /** @var bool */ protected $edit; /** + * @var bool Set in ApiEditPage, based on ContentHandler::allowsDirectApiEditing + */ + private $enableApiEditOverride = false; + + /** * @param Article $article */ public function __construct( Article $article ) { @@ -447,8 +455,18 @@ class EditPage { * @throws MWException If $modelId has no known handler */ public function isSupportedContentModel( $modelId ) { - return $this->allowNonTextContent || - ContentHandler::getForModelID( $modelId ) instanceof TextContentHandler; + return $this->enableApiEditOverride === true || + ContentHandler::getForModelID( $modelId )->supportsDirectEditing(); + } + + /** + * Allow editing of content that supports API direct editing, but not general + * direct editing. Set to false by default. + * + * @param bool $enableOverride + */ + public function setApiEditOverride( $enableOverride ) { + $this->enableApiEditOverride = $enableOverride; } function submit() { @@ -509,7 +527,10 @@ class EditPage { if ( $permErrors ) { wfDebug( __METHOD__ . ": User can't edit\n" ); // Auto-block user's IP if the account was "hard" blocked - $wgUser->spreadAnyEditBlock(); + $user = $wgUser; + DeferredUpdates::addCallableUpdate( function() use ( $user ) { + $user->spreadAnyEditBlock(); + } ); $this->displayPermissionsError( $permErrors ); @@ -634,6 +655,9 @@ class EditPage { $this->getContextTitle()->getPrefixedText() ) ); $wgOut->addBacklinkSubtitle( $this->getContextTitle() ); + $wgOut->addHTML( $this->editFormPageTop ); + $wgOut->addHTML( $this->editFormTextTop ); + $wgOut->addWikiText( $wgOut->formatPermissionsErrorMessage( $permErrors, 'edit' ) ); $wgOut->addHTML( "<hr />\n" ); @@ -647,13 +671,16 @@ class EditPage { $wgOut->addWikiMsg( 'viewsourcetext' ); } + $wgOut->addHTML( $this->editFormTextBeforeContent ); $this->showTextbox( $text, 'wpTextbox1', array( 'readonly' ) ); + $wgOut->addHTML( $this->editFormTextAfterContent ); $wgOut->addHTML( Html::rawElement( 'div', array( 'class' => 'templatesUsed' ), Linker::formatTemplates( $this->getTemplates() ) ) ); $wgOut->addModules( 'mediawiki.action.edit.collapsibleFooter' ); + $wgOut->addHTML( $this->editFormTextBottom ); if ( $this->mTitle->exists() ) { $wgOut->returnToMain( null, $this->mTitle ); } @@ -1025,7 +1052,6 @@ class EditPage { $undo = $wgRequest->getInt( 'undo' ); if ( $undo > 0 && $undoafter > 0 ) { - $undorev = Revision::newFromId( $undo ); $oldrev = Revision::newFromId( $undoafter ); @@ -1034,8 +1060,8 @@ class EditPage { # Otherwise, $content will be left as-is. if ( !is_null( $undorev ) && !is_null( $oldrev ) && !$undorev->isDeleted( Revision::DELETED_TEXT ) && - !$oldrev->isDeleted( Revision::DELETED_TEXT ) ) { - + !$oldrev->isDeleted( Revision::DELETED_TEXT ) + ) { $content = $this->mArticle->getUndoContent( $undorev, $oldrev ); if ( $content === false ) { @@ -1230,9 +1256,9 @@ class EditPage { if ( !$converted ) { //TODO: somehow show a warning to the user! - wfDebug( "Attempt to preload incompatible content: " - . "can't convert " . $content->getModel() - . " to " . $handler->getModelID() ); + wfDebug( "Attempt to preload incompatible content: " . + "can't convert " . $content->getModel() . + " to " . $handler->getModelID() ); return $handler->makeEmptyContent(); } @@ -1350,6 +1376,7 @@ class EditPage { case self::AS_HOOK_ERROR: return false; + case self::AS_CANNOT_USE_CUSTOM_MODEL: case self::AS_PARSE_ERROR: $wgOut->addWikiText( '<div class="error">' . $status->getWikiText() . '</div>' ); return true; @@ -1532,6 +1559,7 @@ class EditPage { */ function internalAttemptSave( &$result, $bot = false ) { global $wgUser, $wgRequest, $wgParser, $wgMaxArticleSize; + global $wgContentHandlerUseDB; $status = Status::newGood(); @@ -1652,11 +1680,19 @@ class EditPage { } } - if ( $this->contentModel !== $this->mTitle->getContentModel() - && !$wgUser->isAllowed( 'editcontentmodel' ) - ) { - $status->setResult( false, self::AS_NO_CHANGE_CONTENT_MODEL ); - return $status; + $changingContentModel = false; + if ( $this->contentModel !== $this->mTitle->getContentModel() ) { + if ( !$wgContentHandlerUseDB ) { + $status->fatal( 'editpage-cannot-use-custom-model' ); + $status->value = self::AS_CANNOT_USE_CUSTOM_MODEL; + return $status; + } elseif ( !$wgUser->isAllowed( 'editcontentmodel' ) ) { + $status->setResult( false, self::AS_NO_CHANGE_CONTENT_MODEL ); + return $status; + + } + $changingContentModel = true; + $oldContentModel = $this->mTitle->getContentModel(); } if ( $this->changeTags ) { @@ -1916,7 +1952,7 @@ class EditPage { $this->summary, $flags, false, - null, + $wgUser, $content->getDefaultFormat() ); @@ -1946,10 +1982,22 @@ class EditPage { if ( $this->changeTags && isset( $doEditStatus->value['revision'] ) ) { // If a revision was created, apply any change tags that were requested - ChangeTags::addTags( - $this->changeTags, - isset( $doEditStatus->value['rc'] ) ? $doEditStatus->value['rc']->mAttribs['rc_id'] : null, - $doEditStatus->value['revision']->getId() + $addTags = $this->changeTags; + $revId = $doEditStatus->value['revision']->getId(); + // Defer this both for performance and so that addTags() sees the rc_id + // since the recentchange entry addition is deferred first (bug T100248) + DeferredUpdates::addCallableUpdate( function() use ( $addTags, $revId ) { + ChangeTags::addTags( $addTags, null, $revId ); + } ); + } + + // If the content model changed, add a log entry + if ( $changingContentModel ) { + $this->addContentModelChangeLogEntry( + $wgUser, + $oldContentModel, + $this->contentModel, + $this->summary ); } @@ -1957,6 +2005,26 @@ class EditPage { } /** + * @param Title $title + * @param string $oldModel + * @param string $newModel + * @param string $reason + */ + protected function addContentModelChangeLogEntry( User $user, $oldModel, $newModel, $reason ) { + $log = new ManualLogEntry( 'contentmodel', 'change' ); + $log->setPerformer( $user ); + $log->setTarget( $this->mTitle ); + $log->setComment( $reason ); + $log->setParameters( array( + '4::oldmodel' => $oldModel, + '5::newmodel' => $newModel + ) ); + $logid = $log->insert(); + $log->publish( $logid ); + } + + + /** * Register the change of watch status */ protected function updateWatchlist() { @@ -2486,7 +2554,7 @@ class EditPage { $wgOut->addHTML( $this->editFormTextBeforeContent ); if ( !$this->isCssJsSubpage && $showToolbar && $wgUser->getOption( 'showtoolbar' ) ) { - $wgOut->addHTML( EditPage::getEditToolbar() ); + $wgOut->addHTML( EditPage::getEditToolbar( $this->mTitle ) ); } if ( $this->blankArticle ) { @@ -3386,7 +3454,7 @@ HTML $this->deletedSinceEdit = false; - if ( $this->mTitle->isDeletedQuick() ) { + if ( !$this->mTitle->exists() && $this->mTitle->isDeletedQuick() ) { $this->lastDelete = $this->getLastDelete(); if ( $this->lastDelete ) { $deleteTime = wfTimestamp( TS_MW, $this->lastDelete->log_timestamp ); @@ -3450,6 +3518,8 @@ HTML global $wgOut, $wgUser, $wgRawHtml, $wgLang; global $wgAllowUserCss, $wgAllowUserJs; + $stats = $wgOut->getContext()->getStats(); + if ( $wgRawHtml && !$this->mTokenOk ) { // Could be an offsite preview attempt. This is very unsafe if // HTML is enabled, as it could be an attack. @@ -3461,6 +3531,7 @@ HTML $parsedNote = $wgOut->parse( "<div class='previewnote'>" . wfMessage( 'session_fail_preview_html' )->text() . "</div>", true, /* interface */true ); } + $stats->increment( 'edit.failures.session_loss' ); return $parsedNote; } @@ -3484,11 +3555,16 @@ HTML if ( $this->mTriedSave && !$this->mTokenOk ) { if ( $this->mTokenOkExceptSuffix ) { $note = wfMessage( 'token_suffix_mismatch' )->plain(); + $stats->increment( 'edit.failures.bad_token' ); } else { $note = wfMessage( 'session_fail_preview' )->plain(); + $stats->increment( 'edit.failures.session_loss' ); } } elseif ( $this->incompleteForm ) { $note = wfMessage( 'edit_form_incomplete' )->plain(); + if ( $this->mTriedSave ) { + $stats->increment( 'edit.failures.incomplete_form' ); + } } else { $note = wfMessage( 'previewnote' )->plain() . ' ' . $continueEditing; } @@ -3619,13 +3695,18 @@ HTML * Shows a bulletin board style toolbar for common editing functions. * It can be disabled in the user preferences. * + * @param $title Title object for the page being edited (optional) * @return string */ - static function getEditToolbar() { + static function getEditToolbar( $title = null ) { global $wgContLang, $wgOut; global $wgEnableUploads, $wgForeignFileRepos; $imagesAvailable = $wgEnableUploads || count( $wgForeignFileRepos ); + $showSignature = true; + if ( $title ) { + $showSignature = MWNamespace::wantSignatures( $title->getNamespace() ); + } /** * $toolarray is an array of arrays each of which includes the @@ -3693,13 +3774,13 @@ HTML 'sample' => wfMessage( 'nowiki_sample' )->text(), 'tip' => wfMessage( 'nowiki_tip' )->text(), ), - array( + $showSignature ? array( 'id' => 'mw-editbutton-signature', 'open' => '--~~~~', 'close' => '', 'sample' => '', 'tip' => wfMessage( 'sig_tip' )->text(), - ), + ) : false, array( 'id' => 'mw-editbutton-hr', 'open' => "\n----\n", @@ -3737,7 +3818,7 @@ HTML } $script .= '});'; - $wgOut->addScript( Html::inlineScript( ResourceLoader::makeLoaderConditionalScript( $script ) ) ); + $wgOut->addScript( ResourceLoader::makeInlineScript( $script ) ); $toolbar = '<div id="toolbar"></div>'; diff --git a/includes/Export.php b/includes/Export.php index 4600feb5..adab21c3 100644 --- a/includes/Export.php +++ b/includes/Export.php @@ -874,7 +874,7 @@ class XmlDumpWriter { } global $wgContLang; - $prefix = str_replace( '_', ' ', $wgContLang->getNsText( $title->getNamespace() ) ); + $prefix = $wgContLang->getFormattedNsText( $title->getNamespace() ); if ( $prefix !== '' ) { $prefix .= ':'; @@ -1191,7 +1191,7 @@ class Dump7ZipOutput extends DumpPipeOutput { * @return string */ function setup7zCommand( $file ) { - $command = "7za a -bd -si " . wfEscapeShellArg( $file ); + $command = "7za a -bd -si -mx=4 " . wfEscapeShellArg( $file ); // Suppress annoying useless crap from p7zip // Unfortunately this could suppress real error messages too $command .= ' >' . wfGetNull() . ' 2>&1'; diff --git a/includes/FileDeleteForm.php b/includes/FileDeleteForm.php index c1d14db0..bcd6db20 100644 --- a/includes/FileDeleteForm.php +++ b/includes/FileDeleteForm.php @@ -296,8 +296,8 @@ class FileDeleteForm { Xml::closeElement( 'form' ); if ( $wgUser->isAllowed( 'editinterface' ) ) { - $title = Title::makeTitle( NS_MEDIAWIKI, 'Filedelete-reason-dropdown' ); - $link = Linker::link( + $title = wfMessage( 'filedelete-reason-dropdown' )->inContentLanguage()->getTitle(); + $link = Linker::linkKnown( $title, wfMessage( 'filedelete-edit-reasonlist' )->escaped(), array(), diff --git a/includes/GitInfo.php b/includes/GitInfo.php index fb298cfe..7f05bb0f 100644 --- a/includes/GitInfo.php +++ b/includes/GitInfo.php @@ -281,9 +281,9 @@ class GitInfo { $config = "{$this->basedir}/config"; $url = false; if ( is_readable( $config ) ) { - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $configArray = parse_ini_file( $config, true ); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); $remote = false; // Use the "origin" remote repo if available or any other repo if not. diff --git a/includes/GlobalFunctions.php b/includes/GlobalFunctions.php index ab3f019f..64aa87ec 100644 --- a/includes/GlobalFunctions.php +++ b/includes/GlobalFunctions.php @@ -24,7 +24,6 @@ if ( !defined( 'MEDIAWIKI' ) ) { die( "This file is part of MediaWiki, it is not a valid entry point" ); } -use Liuggio\StatsdClient\StatsdClient; use Liuggio\StatsdClient\Sender\SocketSender; use MediaWiki\Logger\LoggerFactory; @@ -172,6 +171,7 @@ if ( !function_exists( 'hash_equals' ) ) { * * @param string $ext Name of the extension to load * @param string|null $path Absolute path of where to find the extension.json file + * @since 1.25 */ function wfLoadExtension( $ext, $path = null ) { if ( !$path ) { @@ -192,6 +192,7 @@ function wfLoadExtension( $ext, $path = null ) { * * @see wfLoadExtension * @param string[] $exts Array of extension names to load + * @since 1.25 */ function wfLoadExtensions( array $exts ) { global $wgExtensionDirectory; @@ -207,6 +208,7 @@ function wfLoadExtensions( array $exts ) { * @see wfLoadExtension * @param string $skin Name of the extension to load * @param string|null $path Absolute path of where to find the skin.json file + * @since 1.25 */ function wfLoadSkin( $skin, $path = null ) { if ( !$path ) { @@ -221,6 +223,7 @@ function wfLoadSkin( $skin, $path = null ) { * * @see wfLoadExtensions * @param string[] $skins Array of extension names to load + * @since 1.25 */ function wfLoadSkins( array $skins ) { global $wgStyleDirectory; @@ -402,12 +405,17 @@ function wfRandomString( $length = 32 ) { * * ;:@&=$-_.+!*'(), * + * RFC 1738 says ~ is unsafe, however RFC 3986 considers it an unreserved + * character which should not be encoded. More importantly, google chrome + * always converts %7E back to ~, and converting it in this function can + * cause a redirect loop (T105265). + * * But + is not safe because it's used to indicate a space; &= are only safe in * paths and not in queries (and we don't distinguish here); ' seems kind of * scary; and urlencode() doesn't touch -_. to begin with. Plus, although / * is reserved, we don't care. So the list we unescape is: * - * ;:@$!*(),/ + * ;:@$!*(),/~ * * However, IIS7 redirects fail when the url contains a colon (Bug 22709), * so no fancy : for IIS7. @@ -426,7 +434,7 @@ function wfUrlencode( $s ) { } if ( is_null( $needle ) ) { - $needle = array( '%3B', '%40', '%24', '%21', '%2A', '%28', '%29', '%2C', '%2F' ); + $needle = array( '%3B', '%40', '%24', '%21', '%2A', '%28', '%29', '%2C', '%2F', '%7E' ); if ( !isset( $_SERVER['SERVER_SOFTWARE'] ) || ( strpos( $_SERVER['SERVER_SOFTWARE'], 'Microsoft-IIS/7' ) === false ) ) { @@ -437,7 +445,7 @@ function wfUrlencode( $s ) { $s = urlencode( $s ); $s = str_ireplace( $needle, - array( ';', '@', '$', '!', '*', '(', ')', ',', '/', ':' ), + array( ';', '@', '$', '!', '*', '(', ')', ',', '/', '~', ':' ), $s ); @@ -860,9 +868,9 @@ function wfParseUrl( $url ) { if ( $wasRelative ) { $url = "http:$url"; } - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $bits = parse_url( $url ); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); // parse_url() returns an array without scheme for some invalid URLs, e.g. // parse_url("%0Ahttp://example.com") == array( 'host' => '%0Ahttp', 'path' => 'example.com' ) if ( !$bits || !isset( $bits['scheme'] ) ) { @@ -1248,13 +1256,17 @@ function wfLogProfilingData() { $profiler->logData(); $config = $context->getConfig(); - if ( $config->has( 'StatsdServer' ) ) { - $statsdServer = explode( ':', $config->get( 'StatsdServer' ) ); - $statsdHost = $statsdServer[0]; - $statsdPort = isset( $statsdServer[1] ) ? $statsdServer[1] : 8125; - $statsdSender = new SocketSender( $statsdHost, $statsdPort ); - $statsdClient = new StatsdClient( $statsdSender ); - $statsdClient->send( $context->getStats()->getBuffer() ); + if ( $config->get( 'StatsdServer' ) ) { + try { + $statsdServer = explode( ':', $config->get( 'StatsdServer' ) ); + $statsdHost = $statsdServer[0]; + $statsdPort = isset( $statsdServer[1] ) ? $statsdServer[1] : 8125; + $statsdSender = new SocketSender( $statsdHost, $statsdPort ); + $statsdClient = new SamplingStatsdClient( $statsdSender, true, false ); + $statsdClient->send( $context->getStats()->getBuffer() ); + } catch ( Exception $ex ) { + MWExceptionHandler::logException( $ex ); + } } # Profiling must actually be enabled... @@ -1344,6 +1356,17 @@ function wfReadOnlyReason() { } else { $wgReadOnly = false; } + // Callers use this method to be aware that data presented to a user + // may be very stale and thus allowing submissions can be problematic. + try { + if ( $wgReadOnly === false && wfGetLB()->getLaggedSlaveMode() ) { + $wgReadOnly = 'The database has been automatically locked ' . + 'while the slave database servers catch up to the master'; + } + } catch ( DBConnectionError $e ) { + $wgReadOnly = 'The database has been automatically locked ' . + 'until the slave database servers become available'; + } } return $wgReadOnly; @@ -1405,7 +1428,7 @@ function wfGetLangObj( $langcode = false ) { * * This function replaces all old wfMsg* functions. * - * @param string|string[] $key Message key, or array of keys + * @param string|string[]|MessageSpecifier $key Message key, or array of keys, or a MessageSpecifier * @param mixed $params,... Normal message parameters * @return Message * @@ -1745,7 +1768,7 @@ function wfMsgExt( $key, $options ) { } if ( in_array( 'escape', $options, true ) ) { - $string = htmlspecialchars ( $string ); + $string = htmlspecialchars( $string ); } elseif ( in_array( 'escapenoentities', $options, true ) ) { $string = Sanitizer::escapeHtmlAllowEntities( $string ); } @@ -2118,15 +2141,14 @@ function wfVarDump( $var ) { */ function wfHttpError( $code, $label, $desc ) { global $wgOut; - header( "HTTP/1.0 $code $label" ); - header( "Status: $code $label" ); + HttpStatus::header( $code ); if ( $wgOut ) { $wgOut->disable(); $wgOut->sendCacheControl(); } header( 'Content-type: text/html; charset=utf-8' ); - print "<!doctype html>" . + print '<!DOCTYPE html>' . '<html><head><title>' . htmlspecialchars( $label ) . '</title></head><body><h1>' . @@ -2161,14 +2183,24 @@ function wfResetOutputBuffers( $resetGzipEncoding = true ) { $wgDisableOutputCompression = true; } while ( $status = ob_get_status() ) { - if ( $status['type'] == 0 /* PHP_OUTPUT_HANDLER_INTERNAL */ ) { - // Probably from zlib.output_compression or other - // PHP-internal setting which can't be removed. - // + if ( isset( $status['flags'] ) ) { + $flags = PHP_OUTPUT_HANDLER_CLEANABLE | PHP_OUTPUT_HANDLER_REMOVABLE; + $deleteable = ( $status['flags'] & $flags ) === $flags; + } elseif ( isset( $status['del'] ) ) { + $deleteable = $status['del']; + } else { + // Guess that any PHP-internal setting can't be removed. + $deleteable = $status['type'] !== 0; /* PHP_OUTPUT_HANDLER_INTERNAL */ + } + if ( !$deleteable ) { // Give up, and hope the result doesn't break // output behavior. break; } + if ( $status['name'] === 'MediaWikiTestCase::wfResetOutputBuffersBarrier' ) { + // Unit testing barrier to prevent this function from breaking PHPUnit. + break; + } if ( !ob_end_clean() ) { // Could not remove output buffer handler; abort now // to avoid getting in some kind of infinite loop. @@ -2312,40 +2344,19 @@ function wfNegotiateType( $cprefs, $sprefs ) { /** * Reference-counted warning suppression * + * @deprecated since 1.26, use MediaWiki\suppressWarnings() directly * @param bool $end */ function wfSuppressWarnings( $end = false ) { - static $suppressCount = 0; - static $originalLevel = false; - - if ( $end ) { - if ( $suppressCount ) { - --$suppressCount; - if ( !$suppressCount ) { - error_reporting( $originalLevel ); - } - } - } else { - if ( !$suppressCount ) { - $originalLevel = error_reporting( E_ALL & ~( - E_WARNING | - E_NOTICE | - E_USER_WARNING | - E_USER_NOTICE | - E_DEPRECATED | - E_USER_DEPRECATED | - E_STRICT - ) ); - } - ++$suppressCount; - } + MediaWiki\suppressWarnings( $end ); } /** + * @deprecated since 1.26, use MediaWiki\restoreWarnings() directly * Restore error level to previous value */ function wfRestoreWarnings() { - wfSuppressWarnings( true ); + MediaWiki\suppressWarnings( true ); } # Autodetect, convert and provide timestamps of various types @@ -2453,7 +2464,7 @@ function wfTimestampNow() { function wfIsWindows() { static $isWindows = null; if ( $isWindows === null ) { - $isWindows = substr( php_uname(), 0, 7 ) == 'Windows'; + $isWindows = strtoupper( substr( PHP_OS, 0, 3 ) ) === 'WIN'; } return $isWindows; } @@ -2515,7 +2526,7 @@ function wfMkdirParents( $dir, $mode = null, $caller = null ) { wfDebug( "$caller: called wfMkdirParents($dir)\n" ); } - if ( strval( $dir ) === '' || ( file_exists( $dir ) && is_dir( $dir ) ) ) { + if ( strval( $dir ) === '' || is_dir( $dir ) ) { return true; } @@ -2526,9 +2537,9 @@ function wfMkdirParents( $dir, $mode = null, $caller = null ) { } // Turn off the normal warning, we're doing our own below - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $ok = mkdir( $dir, $mode, true ); // PHP5 <3 - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); if ( !$ok ) { //directory may have been created on another request since we last checked @@ -2769,7 +2780,7 @@ function wfShellExec( $cmd, &$retval = null, $environ = array(), $useLogPipe = false; if ( is_executable( '/bin/bash' ) ) { - $time = intval ( isset( $limits['time'] ) ? $limits['time'] : $wgMaxShellTime ); + $time = intval( isset( $limits['time'] ) ? $limits['time'] : $wgMaxShellTime ); if ( isset( $limits['walltime'] ) ) { $wallTime = intval( $limits['walltime'] ); } elseif ( isset( $limits['time'] ) ) { @@ -2777,8 +2788,8 @@ function wfShellExec( $cmd, &$retval = null, $environ = array(), } else { $wallTime = intval( $wgMaxShellWallClockTime ); } - $mem = intval ( isset( $limits['memory'] ) ? $limits['memory'] : $wgMaxShellMemory ); - $filesize = intval ( isset( $limits['filesize'] ) ? $limits['filesize'] : $wgMaxShellFileSize ); + $mem = intval( isset( $limits['memory'] ) ? $limits['memory'] : $wgMaxShellMemory ); + $filesize = intval( isset( $limits['filesize'] ) ? $limits['filesize'] : $wgMaxShellFileSize ); if ( $time > 0 || $mem > 0 || $filesize > 0 || $wallTime > 0 ) { $cmd = '/bin/bash ' . escapeshellarg( "$IP/includes/limit.sh" ) . ' ' . @@ -3023,9 +3034,9 @@ function wfMerge( $old, $mine, $yours, &$result ) { # This check may also protect against code injection in # case of broken installations. - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $haveDiff3 = $wgDiff3 && file_exists( $wgDiff3 ); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); if ( !$haveDiff3 ) { wfDebug( "diff3 not found\n" ); @@ -3102,9 +3113,9 @@ function wfDiff( $before, $after, $params = '-u' ) { } global $wgDiff; - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $haveDiff = $wgDiff && file_exists( $wgDiff ); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); # This check may also protect against code injection in # case of broken installations. @@ -3205,6 +3216,7 @@ function wfUsePHP( $req_ver ) { * * @see perldoc -f use * + * @deprecated since 1.26, use the "requires' property of extension.json * @param string|int|float $req_ver The version to check, can be a string, an integer, or a float * @throws MWException */ @@ -3455,7 +3467,6 @@ function wfResetSessionID() { $_SESSION = $tmp; } $newSessionId = session_id(); - Hooks::run( 'ResetSessionID', array( $oldSessionId, $newSessionId ) ); } /** @@ -3464,15 +3475,17 @@ function wfResetSessionID() { * @param bool $sessionId */ function wfSetupSession( $sessionId = false ) { - global $wgSessionsInMemcached, $wgSessionsInObjectCache, $wgCookiePath, $wgCookieDomain, - $wgCookieSecure, $wgCookieHttpOnly, $wgSessionHandler; - if ( $wgSessionsInObjectCache || $wgSessionsInMemcached ) { + global $wgSessionsInObjectCache, $wgSessionHandler; + global $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookieHttpOnly; + + if ( $wgSessionsInObjectCache ) { ObjectCacheSessionHandler::install(); } elseif ( $wgSessionHandler && $wgSessionHandler != ini_get( 'session.save_handler' ) ) { # Only set this if $wgSessionHandler isn't null and session.save_handler # hasn't already been set to the desired value (that causes errors) ini_set( 'session.save_handler', $wgSessionHandler ); } + session_set_cookie_params( 0, $wgCookiePath, $wgCookieDomain, $wgCookieSecure, $wgCookieHttpOnly ); session_cache_limiter( 'private, must-revalidate' ); @@ -3481,9 +3494,14 @@ function wfSetupSession( $sessionId = false ) { } else { wfFixSessionID(); } - wfSuppressWarnings(); + + MediaWiki\suppressWarnings(); session_start(); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); + + if ( $wgSessionsInObjectCache ) { + ObjectCacheSessionHandler::renewCurrentSession(); + } } /** @@ -3506,7 +3524,7 @@ function wfGetPrecompiledData( $name ) { } /** - * Get a cache key + * Make a cache key for the local wiki. * * @param string $args,... * @return string @@ -3516,12 +3534,13 @@ function wfMemcKey( /*...*/ ) { $prefix = $wgCachePrefix === false ? wfWikiID() : $wgCachePrefix; $args = func_get_args(); $key = $prefix . ':' . implode( ':', $args ); - $key = str_replace( ' ', '_', $key ); - return $key; + return strtr( $key, ' ', '_' ); } /** - * Get a cache key for a foreign DB + * Make a cache key for a foreign DB. + * + * Must match what wfMemcKey() would produce in context of the foreign wiki. * * @param string $db * @param string $prefix @@ -3531,11 +3550,29 @@ function wfMemcKey( /*...*/ ) { function wfForeignMemcKey( $db, $prefix /*...*/ ) { $args = array_slice( func_get_args(), 2 ); if ( $prefix ) { + // Match wfWikiID() logic $key = "$db-$prefix:" . implode( ':', $args ); } else { $key = $db . ':' . implode( ':', $args ); } - return str_replace( ' ', '_', $key ); + return strtr( $key, ' ', '_' ); +} + +/** + * Make a cache key with database-agnostic prefix. + * + * Doesn't have a wiki-specific namespace. Uses a generic 'global' prefix + * instead. Must have a prefix as otherwise keys that use a database name + * in the first segment will clash with wfMemcKey/wfForeignMemcKey. + * + * @since 1.26 + * @param string $args,... + * @return string + */ +function wfGlobalCacheKey( /*...*/ ) { + $args = func_get_args(); + $key = 'global:' . implode( ':', $args ); + return strtr( $key, ' ', '_' ); } /** @@ -3744,6 +3781,7 @@ function wfWaitForSlaves( } // Figure out which clusters need to be checked + /** @var LoadBalancer[] $lbs */ $lbs = array(); if ( $cluster === '*' ) { wfGetLBFactory()->forEachLB( function ( LoadBalancer $lb ) use ( &$lbs ) { @@ -3760,20 +3798,14 @@ function wfWaitForSlaves( // time needed to wait on the next clusters. $masterPositions = array_fill( 0, count( $lbs ), false ); foreach ( $lbs as $i => $lb ) { - // bug 27975 - Don't try to wait for slaves if there are none - // Prevents permission error when getting master position - if ( $lb->getServerCount() > 1 ) { - if ( $ifWritesSince && !$lb->hasMasterConnection() ) { - continue; // assume no writes done - } - // Use the empty string to not trigger selectDB() since the connection - // may have been to a server that does not have a DB for the current wiki. - $dbw = $lb->getConnection( DB_MASTER, array(), '' ); - if ( $ifWritesSince && $dbw->lastDoneWrites() < $ifWritesSince ) { - continue; // no writes since the last wait - } - $masterPositions[$i] = $dbw->getMasterPos(); + if ( $lb->getServerCount() <= 1 ) { + // Bug 27975 - Don't try to wait for slaves if there are none + // Prevents permission error when getting master position + continue; + } elseif ( $ifWritesSince && $lb->lastMasterChangeTimestamp() < $ifWritesSince ) { + continue; // no writes since the last wait } + $masterPositions[$i] = $lb->getMasterPos(); } $ok = true; @@ -3830,9 +3862,9 @@ function wfStripIllegalFilenameChars( $name ) { } /** - * Set PHP's memory limit to the larger of php.ini or $wgMemoryLimit; + * Set PHP's memory limit to the larger of php.ini or $wgMemoryLimit * - * @return int Value the memory limit was set to. + * @return int Resulting value of the memory limit. */ function wfMemoryLimit() { global $wgMemoryLimit; @@ -3841,15 +3873,15 @@ function wfMemoryLimit() { $conflimit = wfShorthandToInteger( $wgMemoryLimit ); if ( $conflimit == -1 ) { wfDebug( "Removing PHP's memory limit\n" ); - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); ini_set( 'memory_limit', $conflimit ); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); return $conflimit; } elseif ( $conflimit > $memlimit ) { wfDebug( "Raising PHP's memory limit to $conflimit bytes\n" ); - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); ini_set( 'memory_limit', $conflimit ); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); return $conflimit; } } @@ -3857,6 +3889,26 @@ function wfMemoryLimit() { } /** + * Set PHP's time limit to the larger of php.ini or $wgTransactionalTimeLimit + * + * @return int Prior time limit + * @since 1.26 + */ +function wfTransactionalTimeLimit() { + global $wgTransactionalTimeLimit; + + $timeLimit = ini_get( 'max_execution_time' ); + // Note that CLI scripts use 0 + if ( $timeLimit > 0 && $wgTransactionalTimeLimit > $timeLimit ) { + set_time_limit( $wgTransactionalTimeLimit ); + } + + ignore_user_abort( true ); // ignore client disconnects + + return $timeLimit; +} + +/** * Converts shorthand byte notation to integer form * * @param string $string @@ -3917,13 +3969,13 @@ function wfBCP47( $code ) { } /** - * Get a cache object. + * Get a specific cache object. * - * @param int $inputType Cache type, one of the CACHE_* constants. + * @param int|string $cacheType A CACHE_* constants, or other key in $wgObjectCaches * @return BagOStuff */ -function wfGetCache( $inputType ) { - return ObjectCache::getInstance( $inputType ); +function wfGetCache( $cacheType ) { + return ObjectCache::getInstance( $cacheType ); } /** @@ -3995,9 +4047,9 @@ function wfUnpack( $format, $data, $length = false ) { } } - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $result = unpack( $format, $data ); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); if ( $result === false ) { // If it cannot extract the packed data. @@ -4236,3 +4288,28 @@ function wfThumbIsStandard( File $file, array $params ) { return true; } + +/** + * Merges two (possibly) 2 dimensional arrays into the target array ($baseArray). + * + * Values that exist in both values will be combined with += (all values of the array + * of $newValues will be added to the values of the array of $baseArray, while values, + * that exists in both, the value of $baseArray will be used). + * + * @param array $baseArray The array where you want to add the values of $newValues to + * @param array $newValues An array with new values + * @return array The combined array + * @since 1.26 + */ +function wfArrayPlus2d( array $baseArray, array $newValues ) { + // First merge items that are in both arrays + foreach ( $baseArray as $name => &$groupVal ) { + if ( isset( $newValues[$name] ) ) { + $groupVal += $newValues[$name]; + } + } + // Now add items that didn't exist yet + $baseArray += $newValues; + + return $baseArray; +} diff --git a/includes/HistoryBlob.php b/includes/HistoryBlob.php index 69f1120d..494cbfaf 100644 --- a/includes/HistoryBlob.php +++ b/includes/HistoryBlob.php @@ -522,9 +522,9 @@ class DiffHistoryBlob implements HistoryBlob { function diff( $t1, $t2 ) { # Need to do a null concatenation with warnings off, due to bugs in the current version of xdiff # "String is not zero-terminated" - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $diff = xdiff_string_rabdiff( $t1, $t2 ) . ''; - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); return $diff; } @@ -535,9 +535,9 @@ class DiffHistoryBlob implements HistoryBlob { */ function patch( $base, $diff ) { if ( function_exists( 'xdiff_string_bpatch' ) ) { - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $text = xdiff_string_bpatch( $base, $diff ) . ''; - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); return $text; } diff --git a/includes/Hooks.php b/includes/Hooks.php index dffc7bcf..a4145624 100644 --- a/includes/Hooks.php +++ b/includes/Hooks.php @@ -135,9 +135,6 @@ class Hooks { * returning null) is equivalent to returning true. */ public static function run( $event, array $args = array(), $deprecatedVersion = null ) { - $profiler = Profiler::instance(); - $eventPS = $profiler->scopedProfileIn( 'hook: ' . $event ); - foreach ( self::getHandlers( $event ) as $hook ) { // Turn non-array values into an array. (Can't use casting because of objects.) if ( !is_array( $hook ) ) { @@ -196,8 +193,6 @@ class Hooks { $badhookmsg = null; $hook_args = array_merge( $hook, $args ); - // Profile first in case the Profiler causes errors - $funcPS = $profiler->scopedProfileIn( $func ); set_error_handler( 'Hooks::hookErrorHandler' ); // mark hook as deprecated, if deprecation version is specified @@ -215,7 +210,6 @@ class Hooks { } restore_error_handler(); - $profiler->scopedProfileOut( $funcPS ); // Process the return value. if ( is_string( $retval ) ) { @@ -237,22 +231,25 @@ class Hooks { } /** - * Handle PHP errors issued inside a hook. Catch errors that have to do with - * a function expecting a reference, and let all others pass through. - * - * This REALLY should be protected... but it's public for compatibility + * Handle PHP errors issued inside a hook. Catch errors that have to do + * with a function expecting a reference, and pass all others through to + * MWExceptionHandler::handleError() for default processing. * * @since 1.18 * * @param int $errno Error number (unused) * @param string $errstr Error message * @throws MWHookException If the error has to do with the function signature - * @return bool Always returns false + * @return bool */ public static function hookErrorHandler( $errno, $errstr ) { if ( strpos( $errstr, 'expected to be a reference, value given' ) !== false ) { throw new MWHookException( $errstr, $errno ); } - return false; + + // Delegate unhandled errors to the default MW handler + return call_user_func_array( + 'MWExceptionHandler::handleError', func_get_args() + ); } } diff --git a/includes/Html.php b/includes/Html.php index d312e0a6..62ae0b85 100644 --- a/includes/Html.php +++ b/includes/Html.php @@ -104,27 +104,26 @@ class Html { /** * Modifies a set of attributes meant for button elements * and apply a set of default attributes when $wgUseMediaWikiUIEverywhere enabled. - * @param array $attrs - * @param string[] $modifiers to add to the button + * @param array $attrs HTML attributes in an associative array + * @param string[] $modifiers classes to add to the button * @see https://tools.wmflabs.org/styleguide/desktop/index.html for guidance on available modifiers * @return array $attrs A modified attribute array */ - public static function buttonAttributes( $attrs, $modifiers = array() ) { + public static function buttonAttributes( array $attrs, array $modifiers = array() ) { global $wgUseMediaWikiUIEverywhere; if ( $wgUseMediaWikiUIEverywhere ) { if ( isset( $attrs['class'] ) ) { if ( is_array( $attrs['class'] ) ) { $attrs['class'][] = 'mw-ui-button'; - $attrs = array_merge( $attrs, $modifiers ); + $attrs['class'] = array_merge( $attrs['class'], $modifiers ); // ensure compatibility with Xml $attrs['class'] = implode( ' ', $attrs['class'] ); } else { $attrs['class'] .= ' mw-ui-button ' . implode( ' ', $modifiers ); } } else { - $attrs['class'] = array( 'mw-ui-button' ); // ensure compatibility with Xml - $attrs['class'] = implode( ' ', array_merge( $attrs['class'], $modifiers ) ); + $attrs['class'] = 'mw-ui-button ' . implode( ' ', $modifiers ); } } return $attrs; @@ -137,11 +136,8 @@ class Html { * @param array $attrs An attribute array. * @return array $attrs A modified attribute array */ - public static function getTextInputAttributes( $attrs ) { + public static function getTextInputAttributes( array $attrs ) { global $wgUseMediaWikiUIEverywhere; - if ( !$attrs ) { - $attrs = array(); - } if ( $wgUseMediaWikiUIEverywhere ) { if ( isset( $attrs['class'] ) ) { if ( is_array( $attrs['class'] ) ) { @@ -165,11 +161,11 @@ class Html { * @param array $attrs Associative array of attributes, e.g., array( * 'href' => 'http://www.mediawiki.org/' ). See expandAttributes() for * further documentation. - * @param string[] $modifiers to add to the button + * @param string[] $modifiers classes to add to the button * @see http://tools.wmflabs.org/styleguide/desktop/index.html for guidance on available modifiers * @return string Raw HTML */ - public static function linkButton( $contents, $attrs, $modifiers = array() ) { + public static function linkButton( $contents, array $attrs, array $modifiers = array() ) { return self::element( 'a', self::buttonAttributes( $attrs, $modifiers ), $contents @@ -185,11 +181,11 @@ class Html { * @param array $attrs Associative array of attributes, e.g., array( * 'href' => 'http://www.mediawiki.org/' ). See expandAttributes() for * further documentation. - * @param string[] $modifiers to add to the button + * @param string[] $modifiers classes to add to the button * @see http://tools.wmflabs.org/styleguide/desktop/index.html for guidance on available modifiers * @return string Raw HTML */ - public static function submitButton( $contents, $attrs, $modifiers = array() ) { + public static function submitButton( $contents, array $attrs, array $modifiers = array() ) { $attrs['type'] = 'submit'; $attrs['value'] = $contents; return self::element( 'input', self::buttonAttributes( $attrs, $modifiers ) ); @@ -337,8 +333,7 @@ class Html { * further documentation. * @return array An array of attributes functionally identical to $attribs */ - private static function dropDefaults( $element, $attribs ) { - + private static function dropDefaults( $element, array $attribs ) { // Whenever altering this array, please provide a covering test case // in HtmlTest::provideElementsWithAttributesHavingDefaultValues static $attribDefaults = array( @@ -485,11 +480,10 @@ class Html { * @return string HTML fragment that goes between element name and '>' * (starting with a space if at least one attribute is output) */ - public static function expandAttributes( $attribs ) { + public static function expandAttributes( array $attribs ) { global $wgWellFormedXml; $ret = ''; - $attribs = (array)$attribs; foreach ( $attribs as $key => $value ) { // Support intuitive array( 'checked' => true/false ) form if ( $value === false || is_null( $value ) ) { @@ -714,13 +708,16 @@ class Html { * attributes, passed to Html::element() * @return string Raw HTML */ - public static function input( $name, $value = '', $type = 'text', $attribs = array() ) { + public static function input( $name, $value = '', $type = 'text', array $attribs = array() ) { $attribs['type'] = $type; $attribs['value'] = $value; $attribs['name'] = $name; if ( in_array( $type, array( 'text', 'search', 'email', 'password', 'number' ) ) ) { $attribs = self::getTextInputAttributes( $attribs ); } + if ( in_array( $type, array( 'button', 'reset', 'submit' ) ) ) { + $attribs = self::buttonAttributes( $attribs ); + } return self::element( 'input', $attribs ); } @@ -794,7 +791,7 @@ class Html { * attributes, passed to Html::element() * @return string Raw HTML */ - public static function hidden( $name, $value, $attribs = array() ) { + public static function hidden( $name, $value, array $attribs = array() ) { return self::input( $name, $value, 'hidden', $attribs ); } @@ -810,7 +807,7 @@ class Html { * attributes, passed to Html::element() * @return string Raw HTML */ - public static function textarea( $name, $value = '', $attribs = array() ) { + public static function textarea( $name, $value = '', array $attribs = array() ) { $attribs['name'] = $name; if ( substr( $value, 0, 1 ) == "\n" ) { @@ -826,6 +823,47 @@ class Html { } /** + * Helper for Html::namespaceSelector(). + * @param array $params See Html::namespaceSelector() + * @return array + */ + public static function namespaceSelectorOptions( array $params = array() ) { + global $wgContLang; + + $options = array(); + + if ( !isset( $params['exclude'] ) || !is_array( $params['exclude'] ) ) { + $params['exclude'] = array(); + } + + if ( isset( $params['all'] ) ) { + // add an option that would let the user select all namespaces. + // Value is provided by user, the name shown is localized for the user. + $options[$params['all']] = wfMessage( 'namespacesall' )->text(); + } + // Add all namespaces as options (in the content language) + $options += $wgContLang->getFormattedNamespaces(); + + $optionsOut = array(); + // Filter out namespaces below 0 and massage labels + foreach ( $options as $nsId => $nsName ) { + if ( $nsId < NS_MAIN || in_array( $nsId, $params['exclude'] ) ) { + continue; + } + if ( $nsId === NS_MAIN ) { + // For other namespaces use the namespace prefix as label, but for + // main we don't use "" but the user message describing it (e.g. "(Main)" or "(Article)") + $nsName = wfMessage( 'blanknamespace' )->text(); + } elseif ( is_int( $nsId ) ) { + $nsName = $wgContLang->convertNamespace( $nsId ); + } + $optionsOut[ $nsId ] = $nsName; + } + + return $optionsOut; + } + + /** * Build a drop-down box for selecting a namespace * * @param array $params Params to set. @@ -844,8 +882,6 @@ class Html { public static function namespaceSelector( array $params = array(), array $selectAttribs = array() ) { - global $wgContLang; - ksort( $selectAttribs ); // Is a namespace selected? @@ -862,37 +898,16 @@ class Html { $params['selected'] = ''; } - if ( !isset( $params['exclude'] ) || !is_array( $params['exclude'] ) ) { - $params['exclude'] = array(); - } if ( !isset( $params['disable'] ) || !is_array( $params['disable'] ) ) { $params['disable'] = array(); } // Associative array between option-values and option-labels - $options = array(); - - if ( isset( $params['all'] ) ) { - // add an option that would let the user select all namespaces. - // Value is provided by user, the name shown is localized for the user. - $options[$params['all']] = wfMessage( 'namespacesall' )->text(); - } - // Add all namespaces as options (in the content language) - $options += $wgContLang->getFormattedNamespaces(); + $options = self::namespaceSelectorOptions( $params ); - // Convert $options to HTML and filter out namespaces below 0 + // Convert $options to HTML $optionsHtml = array(); foreach ( $options as $nsId => $nsName ) { - if ( $nsId < NS_MAIN || in_array( $nsId, $params['exclude'] ) ) { - continue; - } - if ( $nsId === NS_MAIN ) { - // For other namespaces use the namespace prefix as label, but for - // main we don't use "" but the user message describing it (e.g. "(Main)" or "(Article)") - $nsName = wfMessage( 'blanknamespace' )->text(); - } elseif ( is_int( $nsId ) ) { - $nsName = $wgContLang->convertNamespace( $nsId ); - } $optionsHtml[] = self::element( 'option', array( 'disabled' => in_array( $nsId, $params['disable'] ), @@ -937,7 +952,7 @@ class Html { * attributes, passed to Html::element() of html tag. * @return string Raw HTML */ - public static function htmlHeader( $attribs = array() ) { + public static function htmlHeader( array $attribs = array() ) { $ret = ''; global $wgHtml5Version, $wgMimeType, $wgXhtmlNamespaces; @@ -1047,7 +1062,7 @@ class Html { * @param string[] $urls * @return string */ - static function srcSet( $urls ) { + static function srcSet( array $urls ) { $candidates = array(); foreach ( $urls as $density => $url ) { // Cast density to float to strip 'x'. diff --git a/includes/HttpFunctions.php b/includes/HttpFunctions.php index fa54487a..bc5a9570 100644 --- a/includes/HttpFunctions.php +++ b/includes/HttpFunctions.php @@ -257,7 +257,7 @@ class MWHttpRequest { $this->parsedUrl = wfParseUrl( $this->url ); if ( !$this->parsedUrl || !Http::isValidURI( $this->url ) ) { - $this->status = Status::newFatal( 'http-invalid-url' ); + $this->status = Status::newFatal( 'http-invalid-url', $url ); } else { $this->status = Status::newGood( 100 ); // continue } @@ -797,14 +797,14 @@ class CurlHttpRequest extends MWHttpRequest { } if ( $this->followRedirects && $this->canFollowRedirects() ) { - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); if ( !curl_setopt( $curlHandle, CURLOPT_FOLLOWLOCATION, true ) ) { wfDebug( __METHOD__ . ": Couldn't set CURLOPT_FOLLOWLOCATION. " . "Probably safe_mode or open_basedir is set.\n" ); // Continue the processing. If it were in curl_setopt_array, // processing would have halted on its entry } - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); } if ( $this->profiler ) { diff --git a/includes/Import.php b/includes/Import.php index d31be43b..6a0bfd09 100644 --- a/includes/Import.php +++ b/includes/Import.php @@ -34,7 +34,7 @@ class WikiImporter { private $reader = null; private $foreignNamespaces = null; private $mLogItemCallback, $mUploadCallback, $mRevisionCallback, $mPageCallback; - private $mSiteInfoCallback, $mTargetNamespace, $mPageOutCallback; + private $mSiteInfoCallback, $mPageOutCallback; private $mNoticeCallback, $mDebug; private $mImportUploads, $mImageBasePath; private $mNoUpdates = false; @@ -49,8 +49,13 @@ class WikiImporter { * Creates an ImportXMLReader drawing from the source provided * @param ImportSource $source * @param Config $config + * @throws Exception */ function __construct( ImportSource $source, Config $config = null ) { + if ( !class_exists( 'XMLReader' ) ) { + throw new Exception( 'Import requires PHP to have been compiled with libxml support' ); + } + $this->reader = new XMLReader(); if ( !$config ) { wfDeprecated( __METHOD__ . ' without a Config instance', '1.25' ); @@ -62,11 +67,22 @@ class WikiImporter { stream_wrapper_register( 'uploadsource', 'UploadSourceAdapter' ); } $id = UploadSourceAdapter::registerSource( $source ); + + // Enable the entity loader, as it is needed for loading external URLs via + // XMLReader::open (T86036) + $oldDisable = libxml_disable_entity_loader( false ); if ( defined( 'LIBXML_PARSEHUGE' ) ) { - $this->reader->open( "uploadsource://$id", null, LIBXML_PARSEHUGE ); + $status = $this->reader->open( "uploadsource://$id", null, LIBXML_PARSEHUGE ); } else { - $this->reader->open( "uploadsource://$id" ); + $status = $this->reader->open( "uploadsource://$id" ); } + if ( !$status ) { + $error = libxml_get_last_error(); + libxml_disable_entity_loader( $oldDisable ); + throw new MWException( 'Encountered an internal error while initializing WikiImporter object: ' . + $error->message ); + } + libxml_disable_entity_loader( $oldDisable ); // Default callbacks $this->setPageCallback( array( $this, 'beforeImportPage' ) ); @@ -224,7 +240,6 @@ class WikiImporter { public function setTargetNamespace( $namespace ) { if ( is_null( $namespace ) ) { // Don't override namespaces - $this->mTargetNamespace = null; $this->setImportTitleFactory( new NaiveImportTitleFactory() ); return true; } elseif ( @@ -232,7 +247,6 @@ class WikiImporter { MWNamespace::exists( intval( $namespace ) ) ) { $namespace = intval( $namespace ); - $this->mTargetNamespace = $namespace; $this->setImportTitleFactory( new NamespaceImportTitleFactory( $namespace ) ); return true; } else { @@ -252,10 +266,7 @@ class WikiImporter { $this->setImportTitleFactory( new NaiveImportTitleFactory() ); } elseif ( $rootpage !== '' ) { $rootpage = rtrim( $rootpage, '/' ); //avoid double slashes - $title = Title::newFromText( $rootpage, !is_null( $this->mTargetNamespace ) - ? $this->mTargetNamespace - : NS_MAIN - ); + $title = Title::newFromText( $rootpage ); if ( !$title || $title->isExternal() ) { $status->fatal( 'import-rootpage-invalid' ); @@ -383,9 +394,9 @@ class WikiImporter { $countKey = 'title_' . $title->getPrefixedText(); $countable = $page->isCountable( $editInfo ); if ( array_key_exists( $countKey, $this->countableCache ) && - $countable != $this->countableCache[ $countKey ] ) { + $countable != $this->countableCache[$countKey] ) { DeferredUpdates::addUpdate( SiteStatsUpdate::factory( array( - 'articles' => ( (int)$countable - (int)$this->countableCache[ $countKey ] ) + 'articles' => ( (int)$countable - (int)$this->countableCache[$countKey] ) ) ) ); } } @@ -528,10 +539,10 @@ class WikiImporter { $oldDisable = libxml_disable_entity_loader( true ); $this->reader->read(); - if ( $this->reader->name != 'mediawiki' ) { + if ( $this->reader->localName != 'mediawiki' ) { libxml_disable_entity_loader( $oldDisable ); throw new MWException( "Expected <mediawiki> tag, got " . - $this->reader->name ); + $this->reader->localName ); } $this->debug( "<mediawiki> tag is correct." ); @@ -542,7 +553,7 @@ class WikiImporter { $rethrow = null; try { while ( $keepReading ) { - $tag = $this->reader->name; + $tag = $this->reader->localName; $type = $this->reader->nodeType; if ( !Hooks::run( 'ImportHandleToplevelXMLTag', array( $this ) ) ) { @@ -593,14 +604,14 @@ class WikiImporter { while ( $this->reader->read() ) { if ( $this->reader->nodeType == XmlReader::END_ELEMENT && - $this->reader->name == 'siteinfo' ) { + $this->reader->localName == 'siteinfo' ) { break; } - $tag = $this->reader->name; + $tag = $this->reader->localName; if ( $tag == 'namespace' ) { - $this->foreignNamespaces[ $this->nodeAttribute( 'key' ) ] = + $this->foreignNamespaces[$this->nodeAttribute( 'key' )] = $this->nodeContents(); } elseif ( in_array( $tag, $normalFields ) ) { $siteInfo[$tag] = $this->nodeContents(); @@ -621,11 +632,11 @@ class WikiImporter { while ( $this->reader->read() ) { if ( $this->reader->nodeType == XMLReader::END_ELEMENT && - $this->reader->name == 'logitem' ) { + $this->reader->localName == 'logitem' ) { break; } - $tag = $this->reader->name; + $tag = $this->reader->localName; if ( !Hooks::run( 'ImportHandleLogItemXMLTag', array( $this, $logInfo @@ -685,13 +696,13 @@ class WikiImporter { while ( $skip ? $this->reader->next() : $this->reader->read() ) { if ( $this->reader->nodeType == XMLReader::END_ELEMENT && - $this->reader->name == 'page' ) { + $this->reader->localName == 'page' ) { break; } $skip = false; - $tag = $this->reader->name; + $tag = $this->reader->localName; if ( $badTitle ) { // The title is invalid, bail out of this page @@ -758,11 +769,11 @@ class WikiImporter { while ( $skip ? $this->reader->next() : $this->reader->read() ) { if ( $this->reader->nodeType == XMLReader::END_ELEMENT && - $this->reader->name == 'revision' ) { + $this->reader->localName == 'revision' ) { break; } - $tag = $this->reader->name; + $tag = $this->reader->localName; if ( !Hooks::run( 'ImportHandleRevisionXMLTag', array( $this, $pageInfo, $revisionInfo @@ -850,11 +861,11 @@ class WikiImporter { while ( $skip ? $this->reader->next() : $this->reader->read() ) { if ( $this->reader->nodeType == XMLReader::END_ELEMENT && - $this->reader->name == 'upload' ) { + $this->reader->localName == 'upload' ) { break; } - $tag = $this->reader->name; + $tag = $this->reader->localName; if ( !Hooks::run( 'ImportHandleUploadXMLTag', array( $this, $pageInfo @@ -948,11 +959,11 @@ class WikiImporter { while ( $this->reader->read() ) { if ( $this->reader->nodeType == XMLReader::END_ELEMENT && - $this->reader->name == 'contributor' ) { + $this->reader->localName == 'contributor' ) { break; } - $tag = $this->reader->name; + $tag = $this->reader->localName; if ( in_array( $tag, $fields ) ) { $info[$tag] = $this->nodeContents(); @@ -1846,9 +1857,9 @@ class ImportStreamSource implements ImportSource { * @return Status */ static function newFromFile( $filename ) { - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $file = fopen( $filename, 'rt' ); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); if ( !$file ) { return Status::newFatal( "importcantopen" ); } diff --git a/includes/Linker.php b/includes/Linker.php index b58dabab..9b5ff27b 100644 --- a/includes/Linker.php +++ b/includes/Linker.php @@ -77,7 +77,7 @@ class Linker { wfDeprecated( __METHOD__, '1.25' ); $title = urldecode( $title ); - $title = str_replace( '_', ' ', $title ); + $title = strtr( $title, '_', ' ' ); return self::getLinkAttributesInternal( $title, $class ); } @@ -1276,9 +1276,11 @@ class Linker { * @param string $comment * @param Title|null $title Title object (to generate link to the section in autocomment) or null * @param bool $local Whether section links should refer to local page + * @param string|null $wikiId Id (as used by WikiMap) of the wiki to generate links to. For use with external changes. + * * @return mixed|string */ - public static function formatComment( $comment, $title = null, $local = false ) { + public static function formatComment( $comment, $title = null, $local = false, $wikiId = null ) { # Sanitize text a bit: $comment = str_replace( "\n", " ", $comment ); @@ -1286,8 +1288,8 @@ class Linker { $comment = Sanitizer::escapeHtmlAllowEntities( $comment ); # Render autocomments and make links: - $comment = self::formatAutocomments( $comment, $title, $local ); - $comment = self::formatLinksInComment( $comment, $title, $local ); + $comment = self::formatAutocomments( $comment, $title, $local, $wikiId ); + $comment = self::formatLinksInComment( $comment, $title, $local, $wikiId ); return $comment; } @@ -1304,9 +1306,11 @@ class Linker { * @param string $comment Comment text * @param Title|null $title An optional title object used to links to sections * @param bool $local Whether section links should refer to local page - * @return string Formatted comment + * @param string|null $wikiId Id of the wiki to link to (if not the local wiki), as used by WikiMap. + * + * @return string Formatted comment (wikitext) */ - private static function formatAutocomments( $comment, $title = null, $local = false ) { + private static function formatAutocomments( $comment, $title = null, $local = false, $wikiId = null ) { // @todo $append here is something of a hack to preserve the status // quo. Someone who knows more about bidi and such should decide // (1) what sane rendering even *is* for an LTR edit summary on an RTL @@ -1320,7 +1324,7 @@ class Linker { // zero-width assertions optional, so wrap them in a non-capturing // group. '!(?:(?<=(.)))?/\*\s*(.*?)\s*\*/(?:(?=(.)))?!', - function ( $match ) use ( $title, $local, &$append ) { + function ( $match ) use ( $title, $local, $wikiId, &$append ) { global $wgLang; // Ensure all match positions are defined @@ -1330,7 +1334,7 @@ class Linker { $auto = $match[2]; $post = $match[3] !== ''; $comment = null; - Hooks::run( 'FormatAutocomments', array( &$comment, $pre, $auto, $post, $title, $local ) ); + Hooks::run( 'FormatAutocomments', array( &$comment, $pre, $auto, $post, $title, $local, $wikiId ) ); if ( $comment === null ) { $link = ''; if ( $title ) { @@ -1349,9 +1353,7 @@ class Linker { $title->getDBkey(), $section ); } if ( $sectionTitle ) { - $link = Linker::link( $sectionTitle, - $wgLang->getArrow(), array(), array(), - 'noclasses' ); + $link = Linker::makeCommentLink( $sectionTitle, $wgLang->getArrow(), $wikiId, 'noclasses' ); } else { $link = ''; } @@ -1384,7 +1386,7 @@ class Linker { * @param string $comment Text to format links in * @param Title|null $title An optional title object used to links to sections * @param bool $local Whether section links should refer to local page - * @param string|null $wikiId Id of the wiki to link to (if not the local wiki), as used by WikiMap + * @param string|null $wikiId Id of the wiki to link to (if not the local wiki), as used by WikiMap. * * @return string */ @@ -1414,10 +1416,9 @@ class Linker { # fix up urlencoded title texts (copied from Parser::replaceInternalLinks) if ( strpos( $match[1], '%' ) !== false ) { - $match[1] = str_replace( - array( '<', '>' ), - array( '<', '>' ), - rawurldecode( $match[1] ) + $match[1] = strtr( + rawurldecode( $match[1] ), + array( '<' => '<', '>' => '>' ) ); } @@ -1460,22 +1461,9 @@ class Linker { $newTarget = clone ( $title ); $newTarget->setFragment( '#' . $target->getFragment() ); $target = $newTarget; - - } - - if ( $wikiId !== null ) { - $thelink = Linker::makeExternalLink( - WikiMap::getForeignURL( $wikiId, $target->getFullText() ), - $linkText . $inside, - /* escape = */ false // Already escaped - ) . $trail; - } else { - $thelink = Linker::link( - $target, - $linkText . $inside - ) . $trail; } + $thelink = Linker::makeCommentLink( $target, $linkText . $inside, $wikiId ) . $trail; } } if ( $thelink ) { @@ -1495,6 +1483,32 @@ class Linker { } /** + * Generates a link to the given Title + * + * @note This is only public for technical reasons. It's not intended for use outside Linker. + * + * @param Title $title + * @param string $text + * @param string|null $wikiId Id of the wiki to link to (if not the local wiki), as used by WikiMap. + * @param string|string[] $options See the $options parameter in Linker::link. + * + * @return string HTML link + */ + public static function makeCommentLink( Title $title, $text, $wikiId = null, $options = array() ) { + if ( $wikiId !== null && !$title->isExternal() ) { + $link = Linker::makeExternalLink( + WikiMap::getForeignURL( $wikiId, $title->getPrefixedText(), $title->getFragment() ), + $text, + /* escape = */ false // Already escaped + ); + } else { + $link = Linker::link( $title, $text, array(), array(), $options ); + } + + return $link; + } + + /** * @param Title $contextTitle * @param string $target * @param string $text @@ -1580,17 +1594,18 @@ class Linker { * @param string $comment * @param Title|null $title Title object (to generate link to section in autocomment) or null * @param bool $local Whether section links should refer to local page + * @param string|null $wikiId Id (as used by WikiMap) of the wiki to generate links to. For use with external changes. * * @return string */ - public static function commentBlock( $comment, $title = null, $local = false ) { + public static function commentBlock( $comment, $title = null, $local = false, $wikiId = null ) { // '*' used to be the comment inserted by the software way back // in antiquity in case none was provided, here for backwards // compatibility, acc. to brion -ævar if ( $comment == '' || $comment == '*' ) { return ''; } else { - $formatted = self::formatComment( $comment, $title, $local ); + $formatted = self::formatComment( $comment, $title, $local, $wikiId ); $formatted = wfMessage( 'parentheses' )->rawParams( $formatted )->escaped(); return " <span class=\"comment\">$formatted</span>"; } @@ -1705,13 +1720,13 @@ class Linker { } /** - * Generate a table of contents from a section tree - * Currently unused. + * Generate a table of contents from a section tree. * * @param array $tree Return value of ParserOutput::getSections() + * @param string|Language|bool $lang Language for the toc title, defaults to user language * @return string HTML fragment */ - public static function generateTOC( $tree ) { + public static function generateTOC( $tree, $lang = false ) { $toc = ''; $lastLevel = 0; foreach ( $tree as $section ) { @@ -1730,7 +1745,7 @@ class Linker { $lastLevel = $section['toclevel']; } $toc .= self::tocLineEnd(); - return self::tocList( $toc ); + return self::tocList( $toc, $lang ); } /** @@ -2383,6 +2398,7 @@ class Linker { 'title' => $tooltip ) ); } + } /** diff --git a/includes/MWNamespace.php b/includes/MWNamespace.php index e370bf10..8ca205ab 100644 --- a/includes/MWNamespace.php +++ b/includes/MWNamespace.php @@ -299,6 +299,18 @@ class MWNamespace { } /** + * Might pages in this namespace require the use of the Signature button on + * the edit toolbar? + * + * @param int $index Index to check + * @return bool + */ + public static function wantSignatures( $index ) { + global $wgExtraSignatureNamespaces; + return self::isTalk( $index ) || in_array( $index, $wgExtraSignatureNamespaces ); + } + + /** * Can pages in a namespace be watched? * * @param int $index diff --git a/includes/MWTimestamp.php b/includes/MWTimestamp.php index ea91470e..d28f88e5 100644 --- a/includes/MWTimestamp.php +++ b/includes/MWTimestamp.php @@ -56,7 +56,7 @@ class MWTimestamp { * * @since 1.20 * - * @param bool|string $timestamp Timestamp to set, or false for current time + * @param bool|string|int|float $timestamp Timestamp to set, or false for current time */ public function __construct( $timestamp = false ) { $this->setTimestamp( $timestamp ); @@ -74,6 +74,7 @@ class MWTimestamp { * @throws TimestampException */ public function setTimestamp( $ts = false ) { + $m = array(); $da = array(); $strtime = ''; @@ -87,9 +88,9 @@ class MWTimestamp { # TS_EXIF } elseif ( preg_match( '/^(\d{4})(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)$/D', $ts, $da ) ) { # TS_MW - } elseif ( preg_match( '/^-?\d{1,13}$/D', $ts ) ) { + } elseif ( preg_match( '/^(-?\d{1,13})(\.\d+)?$/D', $ts, $m ) ) { # TS_UNIX - $strtime = "@$ts"; // http://php.net/manual/en/datetime.formats.compound.php + $strtime = "@{$m[1]}"; // http://php.net/manual/en/datetime.formats.compound.php } elseif ( preg_match( '/^\d{2}-\d{2}-\d{4} \d{2}:\d{2}:\d{2}.\d{6}$/', $ts ) ) { # TS_ORACLE // session altered to DD-MM-YYYY HH24:MI:SS.FF6 $strtime = preg_replace( '/(\d\d)\.(\d\d)\.(\d\d)(\.(\d+))?/', "$1:$2:$3", @@ -199,42 +200,19 @@ class MWTimestamp { * * @since 1.20 * @since 1.22 Uses Language::getHumanTimestamp to produce the timestamp + * @deprecated since 1.26 Use Language::getHumanTimestamp directly * - * @param MWTimestamp|null $relativeTo The base timestamp to compare to - * (defaults to now). - * @param User|null $user User the timestamp is being generated for (or null - * to use main context's user). - * @param Language|null $lang Language to use to make the human timestamp - * (or null to use main context's language). + * @param MWTimestamp|null $relativeTo The base timestamp to compare to (defaults to now) + * @param User|null $user User the timestamp is being generated for (or null to use main context's user) + * @param Language|null $lang Language to use to make the human timestamp (or null to use main context's language) * @return string Formatted timestamp */ - public function getHumanTimestamp( MWTimestamp $relativeTo = null, - User $user = null, Language $lang = null - ) { - if ( $relativeTo === null ) { - $relativeTo = new self(); - } - if ( $user === null ) { - $user = RequestContext::getMain()->getUser(); - } + public function getHumanTimestamp( MWTimestamp $relativeTo = null, User $user = null, Language $lang = null ) { if ( $lang === null ) { $lang = RequestContext::getMain()->getLanguage(); } - // Adjust for the user's timezone. - $offsetThis = $this->offsetForUser( $user ); - $offsetRel = $relativeTo->offsetForUser( $user ); - - $ts = ''; - if ( Hooks::run( 'GetHumanTimestamp', array( &$ts, $this, $relativeTo, $user, $lang ) ) ) { - $ts = $lang->getHumanTimestamp( $this, $relativeTo, $user ); - } - - // Reset the timezone on the objects. - $this->timestamp->sub( $offsetThis ); - $relativeTo->timestamp->sub( $offsetRel ); - - return $ts; + return $lang->getHumanTimestamp( $this, $relativeTo, $user ); } /** diff --git a/includes/MagicWord.php b/includes/MagicWord.php index 186821de..2c7ba91b 100644 --- a/includes/MagicWord.php +++ b/includes/MagicWord.php @@ -718,9 +718,6 @@ class MagicWordArray { private $regex; - /** @todo Unused? */ - private $matches; - /** * @param array $names */ @@ -953,10 +950,12 @@ class MagicWordArray { if ( $regex === '' ) { continue; } - preg_match_all( $regex, $text, $matches, PREG_SET_ORDER ); - foreach ( $matches as $m ) { - list( $name, $param ) = $this->parseMatch( $m ); - $found[$name] = $param; + $matches = array(); + if ( preg_match_all( $regex, $text, $matches, PREG_SET_ORDER ) ) { + foreach ( $matches as $m ) { + list( $name, $param ) = $this->parseMatch( $m ); + $found[$name] = $param; + } } $text = preg_replace( $regex, '', $text ); } diff --git a/includes/MediaWiki.php b/includes/MediaWiki.php index ec2f40f6..fbacb250 100644 --- a/includes/MediaWiki.php +++ b/includes/MediaWiki.php @@ -51,6 +51,7 @@ class MediaWiki { /** * Parse the request to get the Title object * + * @throws MalformedTitleException If a title has been provided by the user, but is invalid. * @return Title Title object to be $wgTitle */ private function parseTitle() { @@ -110,7 +111,10 @@ class MediaWiki { } if ( $ret === null || ( $ret->getDBkey() == '' && !$ret->isExternal() ) ) { - $ret = SpecialPage::getTitleFor( 'Badtitle' ); + // If we get here, we definitely don't have a valid title; throw an exception. + // Try to get detailed invalid title exception first, fall back to MalformedTitleException. + Title::newFromTextThrow( $title ); + throw new MalformedTitleException( 'badtitletext', $title ); } return $ret; @@ -122,7 +126,11 @@ class MediaWiki { */ public function getTitle() { if ( !$this->context->hasTitle() ) { - $this->context->setTitle( $this->parseTitle() ); + try { + $this->context->setTitle( $this->parseTitle() ); + } catch ( MalformedTitleException $ex ) { + $this->context->setTitle( SpecialPage::getTitleFor( 'Badtitle' ) ); + } } return $this->context->getTitle(); } @@ -174,6 +182,11 @@ class MediaWiki { || $title->isSpecial( 'Badtitle' ) ) { $this->context->setTitle( SpecialPage::getTitleFor( 'Badtitle' ) ); + try { + $this->parseTitle(); + } catch ( MalformedTitleException $ex ) { + throw new BadTitleError( $ex ); + } throw new BadTitleError(); } @@ -219,65 +232,116 @@ class MediaWiki { $output->redirect( $url, 301 ); } else { $this->context->setTitle( SpecialPage::getTitleFor( 'Badtitle' ) ); - throw new BadTitleError(); - } - // Redirect loops, no title in URL, $wgUsePathInfo URLs, and URLs with a variant - } elseif ( $request->getVal( 'action', 'view' ) == 'view' && !$request->wasPosted() - && ( $request->getVal( 'title' ) === null - || $title->getPrefixedDBkey() != $request->getVal( 'title' ) ) - && !count( $request->getValueNames( array( 'action', 'title' ) ) ) - && Hooks::run( 'TestCanonicalRedirect', array( $request, $title, $output ) ) - ) { - if ( $title->isSpecialPage() ) { - list( $name, $subpage ) = SpecialPageFactory::resolveAlias( $title->getDBkey() ); - if ( $name ) { - $title = SpecialPage::getTitleFor( $name, $subpage ); + try { + $this->parseTitle(); + } catch ( MalformedTitleException $ex ) { + throw new BadTitleError( $ex ); } + throw new BadTitleError(); } - $targetUrl = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT ); - // Redirect to canonical url, make it a 301 to allow caching - if ( $targetUrl == $request->getFullRequestURL() ) { - $message = "Redirect loop detected!\n\n" . - "This means the wiki got confused about what page was " . - "requested; this sometimes happens when moving a wiki " . - "to a new server or changing the server configuration.\n\n"; - - if ( $this->config->get( 'UsePathInfo' ) ) { - $message .= "The wiki is trying to interpret the page " . - "title from the URL path portion (PATH_INFO), which " . - "sometimes fails depending on the web server. Try " . - "setting \"\$wgUsePathInfo = false;\" in your " . - "LocalSettings.php, or check that \$wgArticlePath " . - "is correct."; + // Handle any other redirects. + // Redirect loops, titleless URL, $wgUsePathInfo URLs, and URLs with a variant + } elseif ( !$this->tryNormaliseRedirect( $title ) ) { + + // Special pages + if ( NS_SPECIAL == $title->getNamespace() ) { + // Actions that need to be made when we have a special pages + SpecialPageFactory::executePath( $title, $this->context ); + } else { + // ...otherwise treat it as an article view. The article + // may still be a wikipage redirect to another article or URL. + $article = $this->initializeArticle(); + if ( is_object( $article ) ) { + $this->performAction( $article, $requestTitle ); + } elseif ( is_string( $article ) ) { + $output->redirect( $article ); } else { - $message .= "Your web server was detected as possibly not " . - "supporting URL path components (PATH_INFO) correctly; " . - "check your LocalSettings.php for a customized " . - "\$wgArticlePath setting and/or toggle \$wgUsePathInfo " . - "to true."; + throw new MWException( "Shouldn't happen: MediaWiki::initializeArticle()" + . " returned neither an object nor a URL" ); } - throw new HttpError( 500, $message ); - } else { - $output->setSquidMaxage( 1200 ); - $output->redirect( $targetUrl, '301' ); } - // Special pages - } elseif ( NS_SPECIAL == $title->getNamespace() ) { - // Actions that need to be made when we have a special pages - SpecialPageFactory::executePath( $title, $this->context ); - } else { - // ...otherwise treat it as an article view. The article - // may be a redirect to another article or URL. - $article = $this->initializeArticle(); - if ( is_object( $article ) ) { - $this->performAction( $article, $requestTitle ); - } elseif ( is_string( $article ) ) { - $output->redirect( $article ); + } + } + + /** + * Handle redirects for uncanonical title requests. + * + * Handles: + * - Redirect loops. + * - No title in URL. + * - $wgUsePathInfo URLs. + * - URLs with a variant. + * - Other non-standard URLs (as long as they have no extra query parameters). + * + * Behaviour: + * - Normalise title values: + * /wiki/Foo%20Bar -> /wiki/Foo_Bar + * - Normalise empty title: + * /wiki/ -> /wiki/Main + * /w/index.php?title= -> /wiki/Main + * - Normalise non-standard title urls: + * /w/index.php?title=Foo_Bar -> /wiki/Foo_Bar + * - Don't redirect anything with query parameters other than 'title' or 'action=view'. + * + * @param Title $title + * @return bool True if a redirect was set. + * @throws HttpError + */ + private function tryNormaliseRedirect( Title $title ) { + $request = $this->context->getRequest(); + $output = $this->context->getOutput(); + + if ( $request->getVal( 'action', 'view' ) != 'view' + || $request->wasPosted() + || count( $request->getValueNames( array( 'action', 'title' ) ) ) + || !Hooks::run( 'TestCanonicalRedirect', array( $request, $title, $output ) ) + ) { + return false; + } + + if ( $title->isSpecialPage() ) { + list( $name, $subpage ) = SpecialPageFactory::resolveAlias( $title->getDBkey() ); + if ( $name ) { + $title = SpecialPage::getTitleFor( $name, $subpage ); + } + } + // Redirect to canonical url, make it a 301 to allow caching + $targetUrl = wfExpandUrl( $title->getFullURL(), PROTO_CURRENT ); + + if ( $targetUrl != $request->getFullRequestURL() ) { + $output->setSquidMaxage( 1200 ); + $output->redirect( $targetUrl, '301' ); + return true; + } + + // If there is no title, or the title is in a non-standard encoding, we demand + // a redirect. If cgi somehow changed the 'title' query to be non-standard while + // the url is standard, the server is misconfigured. + if ( $request->getVal( 'title' ) === null + || $title->getPrefixedDBkey() != $request->getVal( 'title' ) + ) { + $message = "Redirect loop detected!\n\n" . + "This means the wiki got confused about what page was " . + "requested; this sometimes happens when moving a wiki " . + "to a new server or changing the server configuration.\n\n"; + + if ( $this->config->get( 'UsePathInfo' ) ) { + $message .= "The wiki is trying to interpret the page " . + "title from the URL path portion (PATH_INFO), which " . + "sometimes fails depending on the web server. Try " . + "setting \"\$wgUsePathInfo = false;\" in your " . + "LocalSettings.php, or check that \$wgArticlePath " . + "is correct."; } else { - throw new MWException( "Shouldn't happen: MediaWiki::initializeArticle()" - . " returned neither an object nor a URL" ); + $message .= "Your web server was detected as possibly not " . + "supporting URL path components (PATH_INFO) correctly; " . + "check your LocalSettings.php for a customized " . + "\$wgArticlePath setting and/or toggle \$wgUsePathInfo " . + "to true."; } + throw new HttpError( 500, $message ); } + return false; } /** @@ -301,9 +365,8 @@ class MediaWiki { $this->context->setWikiPage( $article->getPage() ); } - // NS_MEDIAWIKI has no redirects. - // It is also used for CSS/JS, so performance matters here... - if ( $title->getNamespace() == NS_MEDIAWIKI ) { + // Skip some unnecessary code if the content model doesn't support redirects + if ( !ContentHandler::getForTitle( $title )->supportsRedirects() ) { return $article; } @@ -404,8 +467,7 @@ class MediaWiki { } /** - * Run the current MediaWiki instance - * index.php just calls this + * Run the current MediaWiki instance; index.php just calls this */ public function run() { try { @@ -416,16 +478,71 @@ class MediaWiki { // Bug 62091: while exceptions are convenient to bubble up GUI errors, // they are not internal application faults. As with normal requests, this // should commit, print the output, do deferred updates, jobs, and profiling. - wfGetLBFactory()->commitMasterChanges(); + $this->doPreOutputCommit(); $e->report(); // display the GUI error } + } catch ( Exception $e ) { + MWExceptionHandler::handleException( $e ); + } + + $this->doPostOutputShutdown( 'normal' ); + } + + /** + * This function commits all DB changes as needed before + * the user can receive a response (in case commit fails) + * + * @since 1.26 + */ + public function doPreOutputCommit() { + // Either all DBs should commit or none + ignore_user_abort( true ); + + // Commit all changes and record ChronologyProtector positions + $factory = wfGetLBFactory(); + $factory->commitMasterChanges(); + $factory->shutdown(); + + wfDebug( __METHOD__ . ' completed; all transactions committed' ); + } + + /** + * This function does work that can be done *after* the + * user gets the HTTP response so they don't block on it + * + * This manages deferred updates, job insertion, + * final commit, and the logging of profiling data + * + * @param string $mode Use 'fast' to always skip job running + * @since 1.26 + */ + public function doPostOutputShutdown( $mode = 'normal' ) { + // Show visible profiling data if enabled (which cannot be post-send) + Profiler::instance()->logDataPageOutputOnly(); + + $that = $this; + $callback = function () use ( $that, $mode ) { + try { + $that->restInPeace( $mode ); + } catch ( Exception $e ) { + MWExceptionHandler::handleException( $e ); + } + }; + + // Defer everything else... + if ( function_exists( 'register_postsend_function' ) ) { + // https://github.com/facebook/hhvm/issues/1230 + register_postsend_function( $callback ); + } else { if ( function_exists( 'fastcgi_finish_request' ) ) { fastcgi_finish_request(); + } else { + // Either all DB and deferred updates should happen or none. + // The later should not be cancelled due to client disconnect. + ignore_user_abort( true ); } - $this->triggerJobs(); - $this->restInPeace(); - } catch ( Exception $e ) { - MWExceptionHandler::handleException( $e ); + + $callback(); } } @@ -440,7 +557,7 @@ class MediaWiki { list( $host, $lag ) = wfGetLB()->getMaxLag(); if ( $lag > $maxLag ) { $resp = $this->context->getRequest()->response(); - $resp->header( 'HTTP/1.1 503 Service Unavailable' ); + $resp->statusHeader( 503 ); $resp->header( 'Retry-After: ' . max( intval( $maxLag ), 5 ) ); $resp->header( 'X-Database-Lag: ' . intval( $lag ) ); $resp->header( 'Content-Type: text/plain' ); @@ -457,7 +574,7 @@ class MediaWiki { } private function main() { - global $wgTitle; + global $wgTitle, $wgTrxProfilerLimits; $request = $this->context->getRequest(); @@ -489,10 +606,9 @@ class MediaWiki { if ( !$request->wasPosted() && in_array( $action, array( 'view', 'edit', 'history' ) ) ) { - $trxProfiler->setExpectation( 'masterConns', 0, __METHOD__ ); - $trxProfiler->setExpectation( 'writes', 0, __METHOD__ ); + $trxProfiler->setExpectations( $wgTrxProfilerLimits['GET'], __METHOD__ ); } else { - $trxProfiler->setExpectation( 'maxAffected', 500, __METHOD__ ); + $trxProfiler->setExpectations( $wgTrxProfilerLimits['POST'], __METHOD__ ); } // If the user has forceHTTPS set to true, or if the user @@ -565,22 +681,23 @@ class MediaWiki { // Actually do the work of the request and build up any output $this->performRequest(); - // Either all DB and deferred updates should happen or none. - // The later should not be cancelled due to client disconnect. - ignore_user_abort( true ); // Now commit any transactions, so that unreported errors after - // output() don't roll back the whole DB transaction - wfGetLBFactory()->commitMasterChanges(); + // output() don't roll back the whole DB transaction and so that + // we avoid having both success and error text in the response + $this->doPreOutputCommit(); // Output everything! $this->context->getOutput()->output(); - } /** * Ends this task peacefully + * @param string $mode Use 'fast' to always skip job running */ - public function restInPeace() { + public function restInPeace( $mode = 'fast' ) { + // Assure deferred updates are not in the main transaction + wfGetLBFactory()->commitMasterChanges(); + // Ignore things like master queries/connections on GET requests // as long as they are in deferred updates (which catch errors). Profiler::instance()->getTransactionProfiler()->resetExpectations(); @@ -588,6 +705,15 @@ class MediaWiki { // Do any deferred jobs DeferredUpdates::doUpdates( 'commit' ); + // Make sure any lazy jobs are pushed + JobQueueGroup::pushLazyJobs(); + + // Now that everything specific to this request is done, + // try to occasionally run jobs (if enabled) from the queues + if ( $mode === 'normal' ) { + $this->triggerJobs(); + } + // Log profiling data, e.g. in the database or UDP wfLogProfilingData(); @@ -604,7 +730,7 @@ class MediaWiki { * to run a specified number of jobs. This registers a callback to cleanup * the socket once it's done. */ - protected function triggerJobs() { + public function triggerJobs() { $jobRunRate = $this->config->get( 'JobRunRate' ); if ( $jobRunRate <= 0 || wfReadOnly() ) { return; @@ -647,7 +773,7 @@ class MediaWiki { $errno = $errstr = null; $info = wfParseUrl( $this->config->get( 'Server' ) ); - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $sock = fsockopen( $info['host'], isset( $info['port'] ) ? $info['port'] : 80, @@ -657,7 +783,7 @@ class MediaWiki { // is a problem elsewhere. 0.1 ); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); if ( !$sock ) { $runJobsLogger->error( "Failed to start cron API (socket error $errno): $errstr" ); // Fall back to running the job here while the user waits diff --git a/includes/Message.php b/includes/Message.php index 134af0ed..54abfd15 100644 --- a/includes/Message.php +++ b/includes/Message.php @@ -156,7 +156,7 @@ * * @since 1.17 */ -class Message implements MessageSpecifier { +class Message implements MessageSpecifier, Serializable { /** * In which language to get this message. True, which is the default, @@ -226,8 +226,9 @@ class Message implements MessageSpecifier { /** * @since 1.17 * - * @param string|string[] $key Message key or array of message keys to try and use the first - * non-empty message for. + * @param string|string[]|MessageSpecifier $key Message key, or array of + * message keys to try and use the first non-empty message for, or a + * MessageSpecifier to copy from. * @param array $params Message parameters. * @param Language $language Optional language of the message, defaults to $wgLang. * @@ -236,6 +237,16 @@ class Message implements MessageSpecifier { public function __construct( $key, $params = array(), Language $language = null ) { global $wgLang; + if ( $key instanceof MessageSpecifier ) { + if ( $params ) { + throw new InvalidArgumentException( + '$params must be empty if $key is a MessageSpecifier' + ); + } + $params = $key->getParams(); + $key = $key->getKey(); + } + if ( !is_string( $key ) && !is_array( $key ) ) { throw new InvalidArgumentException( '$key must be a string or an array' ); } @@ -253,6 +264,41 @@ class Message implements MessageSpecifier { } /** + * @see Serializable::serialize() + * @since 1.26 + * @return string + */ + public function serialize() { + return serialize( array( + 'interface' => $this->interface, + 'language' => $this->language->getCode(), + 'key' => $this->key, + 'keysToTry' => $this->keysToTry, + 'parameters' => $this->parameters, + 'format' => $this->format, + 'useDatabase' => $this->useDatabase, + 'title' => $this->title, + ) ); + } + + /** + * @see Serializable::unserialize() + * @since 1.26 + * @param string $serialized + */ + public function unserialize( $serialized ) { + $data = unserialize( $serialized ); + $this->interface = $data['interface']; + $this->key = $data['key']; + $this->keysToTry = $data['keysToTry']; + $this->parameters = $data['parameters']; + $this->format = $data['format']; + $this->useDatabase = $data['useDatabase']; + $this->language = Language::factory( $data['language'] ); + $this->title = $data['title']; + } + + /** * @since 1.24 * * @return bool True if this is a multi-key message, that is, if the key provided to the @@ -327,7 +373,7 @@ class Message implements MessageSpecifier { * * @since 1.17 * - * @param string|string[] $key Message key or array of keys. + * @param string|string[]|MessageSpecifier $key * @param mixed $param,... Parameters as strings. * * @return Message @@ -365,6 +411,31 @@ class Message implements MessageSpecifier { } /** + * Get a title object for a mediawiki message, where it can be found in the mediawiki namespace. + * The title will be for the current language, if the message key is in + * $wgForceUIMsgAsContentMsg it will be append with the language code (except content + * language), because Message::inContentLanguage will also return in user language. + * + * @see $wgForceUIMsgAsContentMsg + * @return Title + * @since 1.26 + */ + public function getTitle() { + global $wgContLang, $wgForceUIMsgAsContentMsg; + + $code = $this->language->getCode(); + $title = $this->key; + if ( + $wgContLang->getCode() !== $code + && in_array( $this->key, (array)$wgForceUIMsgAsContentMsg ) + ) { + $title .= '/' . $code; + } + + return Title::makeTitle( NS_MEDIAWIKI, $wgContLang->ucfirst( strtr( $title, ' ', '_' ) ) ); + } + + /** * Adds parameters to the parameter list of this message. * * @since 1.17 @@ -597,7 +668,7 @@ class Message implements MessageSpecifier { if ( $lang instanceof Language || $lang instanceof StubUserLang ) { $this->language = $lang; } elseif ( is_string( $lang ) ) { - if ( $this->language->getCode() != $lang ) { + if ( !$this->language instanceof Language || $this->language->getCode() != $lang ) { $this->language = Language::factory( $lang ); } } else { diff --git a/includes/MimeMagic.php b/includes/MimeMagic.php index ebe98a3c..2b240c3b 100644 --- a/includes/MimeMagic.php +++ b/includes/MimeMagic.php @@ -617,16 +617,18 @@ class MimeMagic { /** * Guess the MIME type from the file contents. * + * @todo Remove $ext param + * * @param string $file * @param mixed $ext * @return bool|string * @throws MWException */ - private function doGuessMimeType( $file, $ext ) { // TODO: remove $ext param + private function doGuessMimeType( $file, $ext ) { // Read a chunk of the file - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $f = fopen( $file, 'rb' ); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); if ( !$f ) { return 'unknown/unknown'; @@ -693,7 +695,7 @@ class MimeMagic { } /* Look for WebP */ - if ( strncmp( $head, "RIFF", 4 ) == 0 && strncmp( substr( $head, 8, 8 ), "WEBPVP8 ", 8 ) == 0 ) { + if ( strncmp( $head, "RIFF", 4 ) == 0 && strncmp( substr( $head, 8, 7 ), "WEBPVP8", 7 ) == 0 ) { wfDebug( __METHOD__ . ": recognized file as image/webp\n" ); return "image/webp"; } @@ -780,9 +782,9 @@ class MimeMagic { return $this->detectZipType( $head, $tail, $ext ); } - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $gis = getimagesize( $file ); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); if ( $gis && isset( $gis['mime'] ) ) { $mime = $gis['mime']; diff --git a/includes/MovePage.php b/includes/MovePage.php index de7da3f9..2cd9698c 100644 --- a/includes/MovePage.php +++ b/includes/MovePage.php @@ -64,21 +64,9 @@ class MovePage { $status->fatal( 'spamprotectiontext' ); } - # The move is allowed only if (1) the target doesn't exist, or - # (2) the target is a redirect to the source, and has no history - # (so we can undo bad moves right after they're done). - - if ( $this->newTitle->getArticleID() ) { # Target exists; check for validity - if ( !$this->isValidMoveTarget() ) { - $status->fatal( 'articleexists' ); - } - } else { - $tp = $this->newTitle->getTitleProtection(); - if ( $tp !== false ) { - if ( !$user->isAllowed( $tp['permission'] ) ) { - $status->fatal( 'cantmove-titleprotected' ); - } - } + $tp = $this->newTitle->getTitleProtection(); + if ( $tp !== false && !$user->isAllowed( $tp['permission'] ) ) { + $status->fatal( 'cantmove-titleprotected' ); } Hooks::run( 'MovePageCheckPermissions', @@ -125,6 +113,13 @@ class MovePage { $status->fatal( 'badarticleerror' ); } + # The move is allowed only if (1) the target doesn't exist, or + # (2) the target is a redirect to the source, and has no history + # (so we can undo bad moves right after they're done). + if ( $this->newTitle->getArticleID() && !$this->isValidMoveTarget() ) { + $status->fatal( 'articleexists' ); + } + // Content model checks if ( !$wgContentHandlerUseDB && $this->oldTitle->getContentModel() !== $this->newTitle->getContentModel() ) { @@ -310,8 +305,8 @@ class MovePage { __METHOD__, array( 'IGNORE' ) ); - # Update the protection log - $log = new LogPage( 'protect' ); + + // Build comment for log $comment = wfMessage( 'prot_1movedto2', $this->oldTitle->getPrefixedText(), @@ -320,14 +315,6 @@ class MovePage { if ( $reason ) { $comment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason; } - // @todo FIXME: $params? - $logId = $log->addEntry( - 'move_prot', - $this->newTitle, - $comment, - array( $this->oldTitle->getPrefixedText() ), - $user - ); // reread inserted pr_ids for log relation $insertedPrIds = $dbw->select( @@ -340,7 +327,18 @@ class MovePage { foreach ( $insertedPrIds as $prid ) { $logRelationsValues[] = $prid->pr_id; } - $log->addRelations( 'pr_id', $logRelationsValues, $logId ); + + // Update the protection log + $logEntry = new ManualLogEntry( 'protect', 'move_prot' ); + $logEntry->setTarget( $this->newTitle ); + $logEntry->setComment( $comment ); + $logEntry->setPerformer( $user ); + $logEntry->setParameters( array( + '4::oldtitle' => $this->oldTitle->getPrefixedText(), + ) ); + $logEntry->setRelations( array( 'pr_id' => $logRelationsValues ) ); + $logId = $logEntry->insert(); + $logEntry->publish( $logId ); } // Update *_from_namespace fields as needed @@ -421,6 +419,13 @@ class MovePage { $redirectContent = null; } + // Figure out whether the content model is no longer the default + $oldDefault = ContentHandler::getDefaultModelFor( $this->oldTitle ); + $contentModel = $this->oldTitle->getContentModel(); + $newDefault = ContentHandler::getDefaultModelFor( $nt ); + $defaultContentModelChanging = ( $oldDefault !== $newDefault + && $oldDefault === $contentModel ); + // bug 57084: log_page should be the ID of the *moved* page $oldid = $this->oldTitle->getArticleID(); $logTitle = clone $this->oldTitle; @@ -498,6 +503,16 @@ class MovePage { $newpage->doEditUpdates( $nullRevision, $user, array( 'changed' => false, 'moved' => true, 'oldcountable' => $oldcountable ) ); + // If the default content model changes, we need to populate rev_content_model + if ( $defaultContentModelChanging ) { + $dbw->update( + 'revision', + array( 'rev_content_model' => $contentModel ), + array( 'rev_page' => $nt->getArticleID(), 'rev_content_model IS NULL' ), + __METHOD__ + ); + } + if ( !$moveOverRedirect ) { WikiPage::onArticleCreate( $nt ); } diff --git a/includes/OutputPage.php b/includes/OutputPage.php index 7e671878..552e1815 100644 --- a/includes/OutputPage.php +++ b/includes/OutputPage.php @@ -20,6 +20,9 @@ * @file */ +use MediaWiki\Logger\LoggerFactory; +use WrappedString\WrappedString; + /** * This class should be covered by a general architecture document which does * not exist as of January 2011. This is one of the Core classes and should @@ -139,9 +142,6 @@ class OutputPage extends ContextSource { /** @var string Inline CSS styles. Use addInlineStyle() sparingly */ protected $mInlineStyles = ''; - /** @todo Unused? */ - private $mLinkColours; - /** * @var string Used by skin template. * Example: $tpl->set( 'displaytitle', $out->mPageLinkTitle ); @@ -162,9 +162,6 @@ class OutputPage extends ContextSource { /** @var array */ protected $mModuleStyles = array(); - /** @var array */ - protected $mModuleMessages = array(); - /** @var ResourceLoader */ protected $mResourceLoader; @@ -306,6 +303,11 @@ class OutputPage extends ContextSource { private $mEnableSectionEditLinks = true; /** + * @var string|null The URL to send in a <link> element with rel=copyright + */ + private $copyrightUrl; + + /** * Constructor for OutputPage. This should not be called directly. * Instead a new RequestContext should be created and it will implicitly create * a OutputPage tied to that context. @@ -342,6 +344,18 @@ class OutputPage extends ContextSource { } /** + * Set the copyright URL to send with the output. + * Empty string to omit, null to reset. + * + * @since 1.26 + * + * @param string|null $url + */ + public function setCopyrightUrl( $url ) { + $this->copyrightUrl = $url; + } + + /** * Set the HTTP status code to send with the output. * * @param int $statusCode @@ -594,6 +608,20 @@ class OutputPage extends ContextSource { * @return array Array of module names */ public function getModuleStyles( $filter = false, $position = null ) { + // T97420 + $resourceLoader = $this->getResourceLoader(); + + foreach ( $this->mModuleStyles as $val ) { + $module = $resourceLoader->getModule( $val ); + + if ( $module instanceof ResourceLoaderModule && $module->isPositionDefault() ) { + $warning = __METHOD__ . ': style module should define its position explicitly: ' . + $val . ' ' . get_class( $module ); + wfDebugLog( 'resourceloader', $warning ); + wfLogWarning( $warning ); + } + } + return $this->getModules( $filter, $position, 'mModuleStyles' ); } @@ -613,24 +641,24 @@ class OutputPage extends ContextSource { /** * Get the list of module messages to include on this page * + * @deprecated since 1.26 Obsolete * @param bool $filter * @param string|null $position - * * @return array Array of module names */ public function getModuleMessages( $filter = false, $position = null ) { - return $this->getModules( $filter, $position, 'mModuleMessages' ); + wfDeprecated( __METHOD__, '1.26' ); + return array(); } /** - * Add only messages of one or more modules recognized by the resource loader. - * Module messages added through this function will be loaded by the resource - * loader when the page loads. + * Load messages of one or more ResourceLoader modules. * + * @deprecated since 1.26 Use addModules() instead * @param string|array $modules Module name (string) or array of module names */ public function addModuleMessages( $modules ) { - $this->mModuleMessages = array_merge( $this->mModuleMessages, (array)$modules ); + wfDeprecated( __METHOD__, '1.26' ); } /** @@ -797,9 +825,9 @@ class OutputPage extends ContextSource { # this breaks strtotime(). $clientHeader = preg_replace( '/;.*$/', '', $clientHeader ); - wfSuppressWarnings(); // E_STRICT system time bitching + MediaWiki\suppressWarnings(); // E_STRICT system time bitching $clientHeaderTime = strtotime( $clientHeader ); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); if ( !$clientHeaderTime ) { wfDebug( __METHOD__ . ": unable to parse the client's If-Modified-Since header: $clientHeader\n" ); @@ -826,10 +854,10 @@ class OutputPage extends ContextSource { } # Not modified - # Give a 304 response code and disable body output + # Give a 304 Not Modified response code and disable body output wfDebug( __METHOD__ . ": NOT MODIFIED, $info\n", 'log' ); ini_set( 'zlib.output_compression', 0 ); - $this->getRequest()->response()->header( "HTTP/1.1 304 Not Modified" ); + $this->getRequest()->response()->statusHeader( 304 ); $this->sendCacheControl(); $this->disable(); @@ -1761,7 +1789,6 @@ class OutputPage extends ContextSource { $this->addModules( $parserOutput->getModules() ); $this->addModuleScripts( $parserOutput->getModuleScripts() ); $this->addModuleStyles( $parserOutput->getModuleStyles() ); - $this->addModuleMessages( $parserOutput->getModuleMessages() ); $this->addJsConfigVars( $parserOutput->getJsConfigVars() ); $this->mPreventClickjacking = $this->mPreventClickjacking || $parserOutput->preventClickjacking(); @@ -1788,6 +1815,11 @@ class OutputPage extends ContextSource { } } + // enable OOUI if requested via ParserOutput + if ( $parserOutput->getEnableOOUI() ) { + $this->enableOOUI(); + } + // Link flags are ignored for now, but may in the future be // used to mark individual language links. $linkFlags = array(); @@ -1808,7 +1840,6 @@ class OutputPage extends ContextSource { $this->addModules( $parserOutput->getModules() ); $this->addModuleScripts( $parserOutput->getModuleScripts() ); $this->addModuleStyles( $parserOutput->getModuleStyles() ); - $this->addModuleMessages( $parserOutput->getModuleMessages() ); $this->addJsConfigVars( $parserOutput->getJsConfigVars() ); } @@ -1978,21 +2009,20 @@ class OutputPage extends ContextSource { * Add an HTTP header that will influence on the cache * * @param string $header Header name - * @param array|null $option - * @todo FIXME: Document the $option parameter; it appears to be for - * X-Vary-Options but what format is acceptable? + * @param string[]|null $option Options for X-Vary-Options. Possible options are: + * - "string-contains=$XXX" varies on whether the header value as a string + * contains $XXX as a substring. + * - "list-contains=$XXX" varies on whether the header value as a + * comma-separated list contains $XXX as one of the list items. */ - public function addVaryHeader( $header, $option = null ) { + public function addVaryHeader( $header, array $option = null ) { if ( !array_key_exists( $header, $this->mVaryHeader ) ) { - $this->mVaryHeader[$header] = (array)$option; - } elseif ( is_array( $option ) ) { - if ( is_array( $this->mVaryHeader[$header] ) ) { - $this->mVaryHeader[$header] = array_merge( $this->mVaryHeader[$header], $option ); - } else { - $this->mVaryHeader[$header] = $option; - } + $this->mVaryHeader[$header] = array(); } - $this->mVaryHeader[$header] = array_unique( (array)$this->mVaryHeader[$header] ); + if ( !is_array( $option ) ) { + $option = array(); + } + $this->mVaryHeader[$header] = array_unique( array_merge( $this->mVaryHeader[$header], $option ) ); } /** @@ -2210,8 +2240,7 @@ class OutputPage extends ContextSource { if ( Hooks::run( "BeforePageRedirect", array( $this, &$redirect, &$code ) ) ) { if ( $code == '301' || $code == '303' ) { if ( !$config->get( 'DebugRedirects' ) ) { - $message = HttpStatus::getMessage( $code ); - $response->header( "HTTP/1.1 $code $message" ); + $response->statusHeader( $code ); } $this->mLastModified = wfTimestamp( TS_RFC2822 ); } @@ -2233,10 +2262,7 @@ class OutputPage extends ContextSource { return; } elseif ( $this->mStatusCode ) { - $message = HttpStatus::getMessage( $this->mStatusCode ); - if ( $message ) { - $response->header( 'HTTP/1.1 ' . $this->mStatusCode . ' ' . $message ); - } + $response->statusHeader( $this->mStatusCode ); } # Buffer output; final headers may depend on later processing @@ -2258,14 +2284,14 @@ class OutputPage extends ContextSource { if ( $this->mArticleBodyOnly ) { echo $this->mBodytext; } else { - $sk = $this->getSkin(); // add skin specific modules $modules = $sk->getDefaultModules(); - // enforce various default modules for all skins + // Enforce various default modules for all skins $coreModules = array( - // keep this list as small as possible + // Keep this list as small as possible + 'site', 'mediawiki.page.startup', 'mediawiki.user', ); @@ -2672,16 +2698,14 @@ class OutputPage extends ContextSource { } $ret .= Html::element( 'title', null, $this->getHTMLTitle() ) . "\n"; + $ret .= $this->getInlineHeadScripts() . "\n"; + $ret .= $this->buildCssLinks() . "\n"; + $ret .= $this->getExternalHeadScripts() . "\n"; foreach ( $this->getHeadLinksArray() as $item ) { $ret .= $item . "\n"; } - // No newline after buildCssLinks since makeResourceLoaderLink did that already - $ret .= $this->buildCssLinks(); - - $ret .= $this->getHeadScripts() . "\n"; - foreach ( $this->mHeadItems as $item ) { $ret .= $item . "\n"; } @@ -2729,29 +2753,31 @@ class OutputPage extends ContextSource { */ public function getResourceLoader() { if ( is_null( $this->mResourceLoader ) ) { - $this->mResourceLoader = new ResourceLoader( $this->getConfig() ); + $this->mResourceLoader = new ResourceLoader( + $this->getConfig(), + LoggerFactory::getInstance( 'resourceloader' ) + ); } return $this->mResourceLoader; } /** - * @todo Document + * Construct neccecary html and loader preset states to load modules on a page. + * + * Use getHtmlFromLoaderLinks() to convert this array to HTML. + * * @param array|string $modules One or more module names * @param string $only ResourceLoaderModule TYPE_ class constant - * @param bool $useESI - * @param array $extraQuery Array with extra query parameters to add to each - * request. array( param => value ). - * @param bool $loadCall If true, output an (asynchronous) mw.loader.load() - * call rather than a "<script src='...'>" tag. - * @return string The html "<script>", "<link>" and "<style>" tags - */ - public function makeResourceLoaderLink( $modules, $only, $useESI = false, - array $extraQuery = array(), $loadCall = false - ) { + * @param array $extraQuery [optional] Array with extra query parameters for the request + * @return array A list of HTML strings and array of client loader preset states + */ + public function makeResourceLoaderLink( $modules, $only, array $extraQuery = array() ) { $modules = (array)$modules; $links = array( - 'html' => '', + // List of html strings + 'html' => array(), + // Associative array of module names and their states 'states' => array(), ); @@ -2768,8 +2794,8 @@ class OutputPage extends ContextSource { if ( ResourceLoader::inDebugMode() ) { // Recursively call us for every item foreach ( $modules as $name ) { - $link = $this->makeResourceLoaderLink( $name, $only, $useESI ); - $links['html'] .= $link['html']; + $link = $this->makeResourceLoaderLink( $name, $only, $extraQuery ); + $links['html'] = array_merge( $links['html'], $link['html'] ); $links['states'] += $link['states']; } return $links; @@ -2783,7 +2809,6 @@ class OutputPage extends ContextSource { // Create keyed-by-source and then keyed-by-group list of module objects from modules list $sortedModules = array(); $resourceLoader = $this->getResourceLoader(); - $resourceLoaderUseESI = $this->getConfig()->get( 'ResourceLoaderUseESI' ); foreach ( $modules as $name ) { $module = $resourceLoader->getModule( $name ); # Check that we're allowed to include this module on this page @@ -2849,21 +2874,18 @@ class OutputPage extends ContextSource { // Inline private modules. These can't be loaded through load.php for security // reasons, see bug 34907. Note that these modules should be loaded from - // getHeadScripts() before the first loader call. Otherwise other modules can't + // getExternalHeadScripts() before the first loader call. Otherwise other modules can't // properly use them as dependencies (bug 30914) if ( $group === 'private' ) { if ( $only == ResourceLoaderModule::TYPE_STYLES ) { - $links['html'] .= Html::inlineStyle( + $links['html'][] = Html::inlineStyle( $resourceLoader->makeModuleResponse( $context, $grpModules ) ); } else { - $links['html'] .= Html::inlineScript( - ResourceLoader::makeLoaderConditionalScript( - $resourceLoader->makeModuleResponse( $context, $grpModules ) - ) + $links['html'][] = ResourceLoader::makeInlineScript( + $resourceLoader->makeModuleResponse( $context, $grpModules ) ); } - $links['html'] .= "\n"; continue; } @@ -2874,65 +2896,44 @@ class OutputPage extends ContextSource { // and we shouldn't be putting timestamps in Squid-cached HTML $version = null; if ( $group === 'user' ) { - // Get the maximum timestamp - $timestamp = 1; - foreach ( $grpModules as $module ) { - $timestamp = max( $timestamp, $module->getModifiedTime( $context ) ); - } - // Add a version parameter so cache will break when things change - $query['version'] = wfTimestamp( TS_ISO_8601_BASIC, $timestamp ); + $query['version'] = $resourceLoader->getCombinedVersion( $context, array_keys( $grpModules ) ); } $query['modules'] = ResourceLoader::makePackedModulesString( array_keys( $grpModules ) ); $moduleContext = new ResourceLoaderContext( $resourceLoader, new FauxRequest( $query ) ); $url = $resourceLoader->createLoaderURL( $source, $moduleContext, $extraQuery ); - if ( $useESI && $resourceLoaderUseESI ) { - $esi = Xml::element( 'esi:include', array( 'src' => $url ) ); - if ( $only == ResourceLoaderModule::TYPE_STYLES ) { - $link = Html::inlineStyle( $esi ); - } else { - $link = Html::inlineScript( $esi ); - } + // Automatically select style/script elements + if ( $only === ResourceLoaderModule::TYPE_STYLES ) { + $link = Html::linkedStyle( $url ); } else { - // Automatically select style/script elements - if ( $only === ResourceLoaderModule::TYPE_STYLES ) { - $link = Html::linkedStyle( $url ); - } elseif ( $loadCall ) { - $link = Html::inlineScript( - ResourceLoader::makeLoaderConditionalScript( - Xml::encodeJsCall( 'mw.loader.load', array( $url, 'text/javascript', true ) ) - ) - ); + if ( $context->getRaw() || $isRaw ) { + // Startup module can't load itself, needs to use <script> instead of mw.loader.load + $link = Html::element( 'script', array( + // In SpecialJavaScriptTest, QUnit must load synchronous + 'async' => !isset( $extraQuery['sync'] ), + 'src' => $url + ) ); } else { - $link = Html::linkedScript( $url ); - if ( !$context->getRaw() && !$isRaw ) { - // Wrap only=script / only=combined requests in a conditional as - // browsers not supported by the startup module would unconditionally - // execute this module. Otherwise users will get "ReferenceError: mw is - // undefined" or "jQuery is undefined" from e.g. a "site" module. - $link = Html::inlineScript( - ResourceLoader::makeLoaderConditionalScript( - Xml::encodeJsCall( 'document.write', array( $link ) ) - ) - ); - } + $link = ResourceLoader::makeInlineScript( + Xml::encodeJsCall( 'mw.loader.load', array( $url ) ) + ); + } - // For modules requested directly in the html via <link> or <script>, - // tell mw.loader they are being loading to prevent duplicate requests. - foreach ( $grpModules as $key => $module ) { - // Don't output state=loading for the startup module.. - if ( $key !== 'startup' ) { - $links['states'][$key] = 'loading'; - } + // For modules requested directly in the html via <script> or mw.loader.load + // tell mw.loader they are being loading to prevent duplicate requests. + foreach ( $grpModules as $key => $module ) { + // Don't output state=loading for the startup module. + if ( $key !== 'startup' ) { + $links['states'][$key] = 'loading'; } } } if ( $group == 'noscript' ) { - $links['html'] .= Html::rawElement( 'noscript', array(), $link ) . "\n"; + $links['html'][] = Html::rawElement( 'noscript', array(), $link ); } else { - $links['html'] .= $link . "\n"; + $links['html'][] = $link; } } } @@ -2946,26 +2947,26 @@ class OutputPage extends ContextSource { * @return string HTML */ protected static function getHtmlFromLoaderLinks( array $links ) { - $html = ''; + $html = array(); $states = array(); foreach ( $links as $link ) { if ( !is_array( $link ) ) { - $html .= $link; + $html[] = $link; } else { - $html .= $link['html']; + $html = array_merge( $html, $link['html'] ); $states += $link['states']; } } + // Filter out empty values + $html = array_filter( $html, 'strlen' ); if ( count( $states ) ) { - $html = Html::inlineScript( - ResourceLoader::makeLoaderConditionalScript( - ResourceLoader::makeLoaderStateScript( $states ) - ) - ) . "\n" . $html; + array_unshift( $html, ResourceLoader::makeInlineScript( + ResourceLoader::makeLoaderStateScript( $states ) + ) ); } - return $html; + return WrappedString::join( "\n", $html ); } /** @@ -2975,127 +2976,149 @@ class OutputPage extends ContextSource { * @return string HTML fragment */ function getHeadScripts() { - // Startup - this will immediately load jquery and mediawiki modules + return $this->getInlineHeadScripts() . "\n" . $this->getExternalHeadScripts(); + } + + /** + * <script src="..."> tags for "<head>". This is the startup module + * and other modules marked with position 'top'. + * + * @return string HTML fragment + */ + function getExternalHeadScripts() { $links = array(); - $links[] = $this->makeResourceLoaderLink( 'startup', ResourceLoaderModule::TYPE_SCRIPTS, true ); - // Load config before anything else + // Startup - this provides the client with the module manifest and loads jquery and mediawiki base modules + $links[] = $this->makeResourceLoaderLink( 'startup', ResourceLoaderModule::TYPE_SCRIPTS ); + + return self::getHtmlFromLoaderLinks( $links ); + } + + /** + * <script>...</script> tags to put in "<head>". + * + * @return string HTML fragment + */ + function getInlineHeadScripts() { + $links = array(); + + // Client profile classes for <html>. Allows for easy hiding/showing of UI components. + // Must be done synchronously on every page to avoid flashes of wrong content. + // Note: This class distinguishes MediaWiki-supported JavaScript from the rest. + // The "rest" includes browsers that support JavaScript but not supported by our runtime. + // For the performance benefit of the majority, this is added unconditionally here and is + // then fixed up by the startup module for unsupported browsers. $links[] = Html::inlineScript( - ResourceLoader::makeLoaderConditionalScript( - ResourceLoader::makeConfigSetScript( $this->getJSVars() ) - ) + 'document.documentElement.className = document.documentElement.className' + . '.replace( /(^|\s)client-nojs(\s|$)/, "$1client-js$2" );' + ); + + // Load config before anything else + $links[] = ResourceLoader::makeInlineScript( + ResourceLoader::makeConfigSetScript( $this->getJSVars() ) ); // Load embeddable private modules before any loader links // This needs to be TYPE_COMBINED so these modules are properly wrapped // in mw.loader.implement() calls and deferred until mw.user is available - $embedScripts = array( 'user.options', 'user.tokens' ); + $embedScripts = array( 'user.options' ); $links[] = $this->makeResourceLoaderLink( $embedScripts, ResourceLoaderModule::TYPE_COMBINED ); - - // Scripts and messages "only" requests marked for top inclusion - // Messages should go first - $links[] = $this->makeResourceLoaderLink( - $this->getModuleMessages( true, 'top' ), - ResourceLoaderModule::TYPE_MESSAGES - ); - $links[] = $this->makeResourceLoaderLink( - $this->getModuleScripts( true, 'top' ), - ResourceLoaderModule::TYPE_SCRIPTS - ); + // Separate user.tokens as otherwise caching will be allowed (T84960) + $links[] = $this->makeResourceLoaderLink( 'user.tokens', ResourceLoaderModule::TYPE_COMBINED ); // Modules requests - let the client calculate dependencies and batch requests as it likes // Only load modules that have marked themselves for loading at the top $modules = $this->getModules( true, 'top' ); if ( $modules ) { - $links[] = Html::inlineScript( - ResourceLoader::makeLoaderConditionalScript( - Xml::encodeJsCall( 'mw.loader.load', array( $modules ) ) - ) + $links[] = ResourceLoader::makeInlineScript( + Xml::encodeJsCall( 'mw.loader.load', array( $modules ) ) ); } - if ( $this->getConfig()->get( 'ResourceLoaderExperimentalAsyncLoading' ) ) { - $links[] = $this->getScriptsForBottomQueue( true ); - } + // "Scripts only" modules marked for top inclusion + $links[] = $this->makeResourceLoaderLink( + $this->getModuleScripts( true, 'top' ), + ResourceLoaderModule::TYPE_SCRIPTS + ); return self::getHtmlFromLoaderLinks( $links ); } /** - * JS stuff to put at the 'bottom', which can either be the bottom of the - * "<body>" or the bottom of the "<head>" depending on - * $wgResourceLoaderExperimentalAsyncLoading: modules marked with position - * 'bottom', legacy scripts ($this->mScripts), user preferences, site JS - * and user JS. + * JS stuff to put at the 'bottom', which goes at the bottom of the `<body>`. + * These are modules marked with position 'bottom', legacy scripts ($this->mScripts), + * site JS, and user JS. * - * @param bool $inHead If true, this HTML goes into the "<head>", - * if false it goes into the "<body>". + * @param bool $unused Previously used to let this method change its output based + * on whether it was called by getExternalHeadScripts() or getBottomScripts(). * @return string */ - function getScriptsForBottomQueue( $inHead ) { - // Scripts and messages "only" requests marked for bottom inclusion + function getScriptsForBottomQueue( $unused = null ) { + // Scripts "only" requests marked for bottom inclusion // If we're in the <head>, use load() calls rather than <script src="..."> tags - // Messages should go first $links = array(); - $links[] = $this->makeResourceLoaderLink( $this->getModuleMessages( true, 'bottom' ), - ResourceLoaderModule::TYPE_MESSAGES, /* $useESI = */ false, /* $extraQuery = */ array(), - /* $loadCall = */ $inHead - ); + $links[] = $this->makeResourceLoaderLink( $this->getModuleScripts( true, 'bottom' ), - ResourceLoaderModule::TYPE_SCRIPTS, /* $useESI = */ false, /* $extraQuery = */ array(), - /* $loadCall = */ $inHead + ResourceLoaderModule::TYPE_SCRIPTS + ); + + $links[] = $this->makeResourceLoaderLink( $this->getModuleStyles( true, 'bottom' ), + ResourceLoaderModule::TYPE_STYLES ); // Modules requests - let the client calculate dependencies and batch requests as it likes // Only load modules that have marked themselves for loading at the bottom $modules = $this->getModules( true, 'bottom' ); if ( $modules ) { - $links[] = Html::inlineScript( - ResourceLoader::makeLoaderConditionalScript( - Xml::encodeJsCall( 'mw.loader.load', array( $modules, null, true ) ) - ) + $links[] = ResourceLoader::makeInlineScript( + Xml::encodeJsCall( 'mw.loader.load', array( $modules ) ) ); } // Legacy Scripts - $links[] = "\n" . $this->mScripts; - - // Add site JS if enabled - $links[] = $this->makeResourceLoaderLink( 'site', ResourceLoaderModule::TYPE_SCRIPTS, - /* $useESI = */ false, /* $extraQuery = */ array(), /* $loadCall = */ $inHead - ); + $links[] = $this->mScripts; // Add user JS if enabled + // This must use TYPE_COMBINED instead of only=scripts so that its request is handled by + // mw.loader.implement() which ensures that execution is scheduled after the "site" module. if ( $this->getConfig()->get( 'AllowUserJs' ) && $this->getUser()->isLoggedIn() && $this->getTitle() && $this->getTitle()->isJsSubpage() && $this->userCanPreview() ) { - # XXX: additional security check/prompt? - // We're on a preview of a JS subpage - // Exclude this page from the user module in case it's in there (bug 26283) - $links[] = $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_SCRIPTS, false, - array( 'excludepage' => $this->getTitle()->getPrefixedDBkey() ), $inHead + // We're on a preview of a JS subpage. Exclude this page from the user module (T28283) + // and include the draft contents as a raw script instead. + $links[] = $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_COMBINED, + array( 'excludepage' => $this->getTitle()->getPrefixedDBkey() ) ); // Load the previewed JS - $links[] = Html::inlineScript( "\n" - . $this->getRequest()->getText( 'wpTextbox1' ) . "\n" ) . "\n"; + $links[] = ResourceLoader::makeInlineScript( + Xml::encodeJsCall( 'mw.loader.using', array( + array( 'user', 'site' ), + new XmlJsCode( + 'function () {' + . Xml::encodeJsCall( '$.globalEval', array( + $this->getRequest()->getText( 'wpTextbox1' ) + ) ) + . '}' + ) + ) ) + ); // FIXME: If the user is previewing, say, ./vector.js, his ./common.js will be loaded // asynchronously and may arrive *after* the inline script here. So the previewed code - // may execute before ./common.js runs. Normally, ./common.js runs before ./vector.js... + // may execute before ./common.js runs. Normally, ./common.js runs before ./vector.js. + // Similarly, when previewing ./common.js and the user module does arrive first, it will + // arrive without common.js and the inline script runs after. Thus running common after + // the excluded subpage. } else { // Include the user module normally, i.e., raw to avoid it being wrapped in a closure. - $links[] = $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_SCRIPTS, - /* $useESI = */ false, /* $extraQuery = */ array(), /* $loadCall = */ $inHead - ); + $links[] = $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_COMBINED ); } // Group JS is only enabled if site JS is enabled. - $links[] = $this->makeResourceLoaderLink( 'user.groups', ResourceLoaderModule::TYPE_COMBINED, - /* $useESI = */ false, /* $extraQuery = */ array(), /* $loadCall = */ $inHead - ); + $links[] = $this->makeResourceLoaderLink( 'user.groups', ResourceLoaderModule::TYPE_COMBINED ); return self::getHtmlFromLoaderLinks( $links ); } @@ -3105,17 +3128,10 @@ class OutputPage extends ContextSource { * @return string */ function getBottomScripts() { - // Optimise jQuery ready event cross-browser. - // This also enforces $.isReady to be true at </body> which fixes the - // mw.loader bug in Firefox with using document.write between </body> - // and the DOMContentReady event (bug 47457). - $html = Html::inlineScript( 'if(window.jQuery)jQuery.ready();' ); - - if ( !$this->getConfig()->get( 'ResourceLoaderExperimentalAsyncLoading' ) ) { - $html .= $this->getScriptsForBottomQueue( false ); - } + // In case the skin wants to add bottom CSS + $this->getSkin()->setupSkinUserCss( $this ); - return $html; + return $this->getScriptsForBottomQueue(); } /** @@ -3426,33 +3442,37 @@ class OutputPage extends ContextSource { $lang = $this->getTitle()->getPageLanguage(); if ( $lang->hasVariants() ) { $variants = $lang->getVariants(); - foreach ( $variants as $_v ) { - $tags["variant-$_v"] = Html::element( 'link', array( + foreach ( $variants as $variant ) { + $tags["variant-$variant"] = Html::element( 'link', array( 'rel' => 'alternate', - 'hreflang' => wfBCP47( $_v ), - 'href' => $this->getTitle()->getLocalURL( array( 'variant' => $_v ) ) ) + 'hreflang' => wfBCP47( $variant ), + 'href' => $this->getTitle()->getLocalURL( array( 'variant' => $variant ) ) ) ); } + # x-default link per https://support.google.com/webmasters/answer/189077?hl=en + $tags["variant-x-default"] = Html::element( 'link', array( + 'rel' => 'alternate', + 'hreflang' => 'x-default', + 'href' => $this->getTitle()->getLocalURL() ) ); } - # x-default link per https://support.google.com/webmasters/answer/189077?hl=en - $tags["variant-x-default"] = Html::element( 'link', array( - 'rel' => 'alternate', - 'hreflang' => 'x-default', - 'href' => $this->getTitle()->getLocalURL() ) ); } # Copyright - $copyright = ''; - if ( $config->get( 'RightsPage' ) ) { - $copy = Title::newFromText( $config->get( 'RightsPage' ) ); + if ( $this->copyrightUrl !== null ) { + $copyright = $this->copyrightUrl; + } else { + $copyright = ''; + if ( $config->get( 'RightsPage' ) ) { + $copy = Title::newFromText( $config->get( 'RightsPage' ) ); - if ( $copy ) { - $copyright = $copy->getLocalURL(); + if ( $copy ) { + $copyright = $copy->getLocalURL(); + } } - } - if ( !$copyright && $config->get( 'RightsUrl' ) ) { - $copyright = $config->get( 'RightsUrl' ); + if ( !$copyright && $config->get( 'RightsUrl' ) ) { + $copyright = $config->get( 'RightsUrl' ); + } } if ( $copyright ) { @@ -3513,8 +3533,25 @@ class OutputPage extends ContextSource { if ( $canonicalUrl !== false ) { $canonicalUrl = wfExpandUrl( $canonicalUrl, PROTO_CANONICAL ); } else { - $reqUrl = $this->getRequest()->getRequestURL(); - $canonicalUrl = wfExpandUrl( $reqUrl, PROTO_CANONICAL ); + if ( $this->isArticleRelated() ) { + // This affects all requests where "setArticleRelated" is true. This is + // typically all requests that show content (query title, curid, oldid, diff), + // and all wikipage actions (edit, delete, purge, info, history etc.). + // It does not apply to File pages and Special pages. + // 'history' and 'info' actions address page metadata rather than the page + // content itself, so they may not be canonicalized to the view page url. + // TODO: this ought to be better encapsulated in the Action class. + $action = Action::getActionName( $this->getContext() ); + if ( in_array( $action, array( 'history', 'info' ) ) ) { + $query = "action={$action}"; + } else { + $query = ''; + } + $canonicalUrl = $this->getTitle()->getCanonicalURL( $query ); + } else { + $reqUrl = $this->getRequest()->getRequestURL(); + $canonicalUrl = wfExpandUrl( $reqUrl, PROTO_CANONICAL ); + } } } if ( $canonicalUrl !== false ) { @@ -3613,10 +3650,10 @@ class OutputPage extends ContextSource { 'noscript' => array() ); $links = array(); - $otherTags = ''; // Tags to append after the normal <link> tags + $otherTags = array(); // Tags to append after the normal <link> tags $resourceLoader = $this->getResourceLoader(); - $moduleStyles = $this->getModuleStyles(); + $moduleStyles = $this->getModuleStyles( true, 'top' ); // Per-site custom styles $moduleStyles[] = 'site'; @@ -3629,10 +3666,10 @@ class OutputPage extends ContextSource { ) { // We're on a preview of a CSS subpage // Exclude this page from the user module in case it's in there (bug 26283) - $link = $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_STYLES, false, + $link = $this->makeResourceLoaderLink( 'user', ResourceLoaderModule::TYPE_STYLES, array( 'excludepage' => $this->getTitle()->getPrefixedDBkey() ) ); - $otherTags .= $link['html']; + $otherTags = array_merge( $otherTags, $link['html'] ); // Load the previewed CSS // If needed, Janus it first. This is user-supplied CSS, so it's @@ -3641,7 +3678,7 @@ class OutputPage extends ContextSource { if ( $this->getLanguage()->getDir() !== $wgContLang->getDir() ) { $previewedCSS = CSSJanus::transform( $previewedCSS, true, false ); } - $otherTags .= Html::inlineStyle( $previewedCSS ) . "\n"; + $otherTags[] = Html::inlineStyle( $previewedCSS ) . "\n"; } else { // Load the user styles normally $moduleStyles[] = 'user'; @@ -3655,9 +3692,17 @@ class OutputPage extends ContextSource { if ( !$module ) { continue; } + if ( $name === 'site' ) { + // HACK: The site module shouldn't be fragmented with a cache group and + // http request. But in order to ensure its styles are separated and after the + // ResourceLoaderDynamicStyles marker, pretend it is in a group called 'site'. + // The scripts remain ungrouped and rides the bottom queue. + $styles['site'][] = $name; + continue; + } $group = $module->getGroup(); - // Modules in groups different than the ones listed on top (see $styles assignment) - // will be placed in the "other" group + // Modules in groups other than the ones needing special treatment (see $styles assignment) + // will be placed in the "other" style category. $styles[isset( $styles[$group] ) ? $group : 'other'][] = $name; } @@ -3674,9 +3719,9 @@ class OutputPage extends ContextSource { $links[] = Html::element( 'meta', array( 'name' => 'ResourceLoaderDynamicStyles', 'content' => '' ) - ) . "\n"; + ); - // Add site, private and user styles + // Add site-specific and user-specific styles // 'private' at present only contains user.options, so put that before 'user' // Any future private modules will likely have a similar user-specific character foreach ( array( 'site', 'noscript', 'private', 'user' ) as $group ) { @@ -3686,7 +3731,7 @@ class OutputPage extends ContextSource { } // Add stuff in $otherTags (previewed user CSS if applicable) - return self::getHtmlFromLoaderLinks( $links ) . $otherTags; + return self::getHtmlFromLoaderLinks( $links ) . implode( '', $otherTags ); } /** @@ -3918,14 +3963,40 @@ class OutputPage extends ContextSource { } /** + * Helper function to setup the PHP implementation of OOUI to use in this request. + * + * @since 1.26 + * @param String $skinName The Skin name to determine the correct OOUI theme + * @param String $dir Language direction + */ + public static function setupOOUI( $skinName = '', $dir = 'ltr' ) { + $themes = ExtensionRegistry::getInstance()->getAttribute( 'SkinOOUIThemes' ); + // Make keys (skin names) lowercase for case-insensitive matching. + $themes = array_change_key_case( $themes, CASE_LOWER ); + $theme = isset( $themes[ $skinName ] ) ? $themes[ $skinName ] : 'MediaWiki'; + // For example, 'OOUI\MediaWikiTheme'. + $themeClass = "OOUI\\{$theme}Theme"; + OOUI\Theme::setSingleton( new $themeClass() ); + OOUI\Element::setDefaultDir( $dir ); + } + + /** * Add ResourceLoader module styles for OOUI and set up the PHP implementation of it for use with * MediaWiki and this OutputPage instance. * * @since 1.25 */ public function enableOOUI() { - OOUI\Theme::setSingleton( new OOUI\MediaWikiTheme() ); - OOUI\Element::setDefaultDir( $this->getLanguage()->getDir() ); - $this->addModuleStyles( 'oojs-ui.styles' ); + self::setupOOUI( + strtolower( $this->getSkin()->getSkinName() ), + $this->getLanguage()->getDir() + ); + $this->addModuleStyles( array( + 'oojs-ui.styles', + 'oojs-ui.styles.icons', + 'oojs-ui.styles.indicators', + 'oojs-ui.styles.textures', + 'mediawiki.widgets.styles', + ) ); } } diff --git a/includes/PHPVersionCheck.php b/includes/PHPVersionCheck.php index eee9aa9c..ba3ff1ab 100644 --- a/includes/PHPVersionCheck.php +++ b/includes/PHPVersionCheck.php @@ -25,13 +25,23 @@ /** * Check php version and that external dependencies are installed, and * display an informative error if either condition is not satisfied. + * + * @note Since we can't rely on anything, the minimum PHP versions and MW current + * version are hardcoded here */ function wfEntryPointCheck( $entryPoint ) { + $mwVersion = '1.26'; + $minimumVersionPHP = '5.3.3'; + $phpVersion = PHP_VERSION; + if ( !function_exists( 'version_compare' ) - || version_compare( PHP_VERSION, '5.3.3' ) < 0 - || !file_exists( dirname( __FILE__ ) . '/../vendor/autoload.php' ) + || version_compare( $phpVersion, $minimumVersionPHP ) < 0 ) { - wfPHPVersionError( $entryPoint ); + wfPHPVersionError( $entryPoint, $mwVersion, $minimumVersionPHP, $phpVersion ); + } + + if ( !file_exists( dirname( __FILE__ ) . '/../vendor/autoload.php' ) ) { + wfMissingVendorError( $entryPoint, $mwVersion ); } } @@ -49,47 +59,39 @@ function wfEntryPointCheck( $entryPoint ) { * - api.php * - mw-config/index.php * - cli - * - * @note Since we can't rely on anything, the minimum PHP versions and MW current - * version are hardcoded here + * @param string $mwVersion The number of the MediaWiki version used + * @param string $title HTML code to be put within an <h2> tag + * @param string $shortText + * @param string $longText + * @param string $longHtml */ -function wfPHPVersionError( $type ) { - $mwVersion = '1.25'; - $minimumVersionPHP = '5.3.3'; - - $phpVersion = PHP_VERSION; +function wfGenericError( $type, $mwVersion, $title, $shortText, $longText, $longHtml ) { $protocol = isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0'; - $message = "MediaWiki $mwVersion requires at least " - . "PHP version $minimumVersionPHP, you are using PHP $phpVersion. Installing some " - . " external dependencies (e.g. via composer) is also required."; if ( $type == 'cli' ) { - $finalOutput = "Error: You are missing some external dependencies or are using on older PHP version. \n" - . "MediaWiki $mwVersion needs PHP $minimumVersionPHP or higher.\n\n" - . "Check if you have a newer php executable with a different name, such as php5.\n\n" - . "MediaWiki now also has some external dependencies that need to be installed\n" - . "via composer or from a separate git repo. Please see\n" - . "https://www.mediawiki.org/wiki/Download_from_Git#Fetch_external_libraries\n" - . "for help on installing the required components."; - } elseif ( $type == 'index.php' || $type == 'mw-config/index.php' ) { - $pathinfo = pathinfo( $_SERVER['SCRIPT_NAME'] ); - if ( $type == 'mw-config/index.php' ) { - $dirname = dirname( $pathinfo['dirname'] ); - } else { - $dirname = $pathinfo['dirname']; - } - $encLogo = htmlspecialchars( - str_replace( '//', '/', $dirname . '/' ) . - 'resources/assets/mediawiki.png' - ); - + $finalOutput = $longText; + } else { header( "$protocol 500 MediaWiki configuration Error" ); - header( 'Content-type: text/html; charset=UTF-8' ); // Don't cache error pages! They cause no end of trouble... header( 'Cache-control: none' ); header( 'Pragma: no-cache' ); - $finalOutput = <<<HTML + if ( $type == 'index.php' || $type == 'mw-config/index.php' ) { + $pathinfo = pathinfo( $_SERVER['SCRIPT_NAME'] ); + if ( $type == 'mw-config/index.php' ) { + $dirname = dirname( $pathinfo['dirname'] ); + } else { + $dirname = $pathinfo['dirname']; + } + $encLogo = htmlspecialchars( + str_replace( '//', '/', $dirname . '/' ) . + 'resources/assets/mediawiki.png' + ); + $shortHtml = htmlspecialchars( $shortText ); + + header( 'Content-type: text/html; charset=UTF-8' ); + + $finalOutput = <<<HTML <!DOCTYPE html> <html lang="en" dir="ltr"> <head> @@ -120,10 +122,43 @@ function wfPHPVersionError( $type ) { <h1>MediaWiki {$mwVersion} internal error</h1> <div class='error'> <p> - {$message} + {$shortHtml} </p> - <h2>Supported PHP versions</h2> + <h2>{$title}</h2> <p> + {$longHtml} + </p> + </div> + </body> +</html> +HTML; + // Handle everything that's not index.php + } else { + // So nothing thinks this is JS or CSS + $finalOutput = ( $type == 'load.php' ) ? "/* $shortText */" : $shortText; + } + } + echo "$finalOutput\n"; + die( 1 ); +} + +/** + * Display an error for the minimum PHP version requirement not being satisfied. + * + * @param string $type See wfGenericError + * @param string $mwVersion See wfGenericError + * @param string $minimumVersionPHP The minimum PHP version supported by MediaWiki + * @param string $phpVersion The current PHP version + */ +function wfPHPVersionError( $type, $mwVersion, $minimumVersionPHP, $phpVersion ) { + $shortText = "MediaWiki $mwVersion requires at least " + . "PHP version $minimumVersionPHP, you are using PHP $phpVersion."; + + $longText = "Error: You might be using on older PHP version. \n" + . "MediaWiki $mwVersion needs PHP $minimumVersionPHP or higher.\n\n" + . "Check if you have a newer php executable with a different name, such as php5.\n\n"; + + $longHtml = <<<HTML Please consider <a href="http://www.php.net/downloads.php">upgrading your copy of PHP</a>. PHP versions less than 5.3.0 are no longer supported by the PHP Group and will not receive security or bugfix updates. @@ -134,24 +169,31 @@ function wfPHPVersionError( $type ) { of MediaWiki from our website. See our <a href="https://www.mediawiki.org/wiki/Compatibility#PHP">compatibility page</a> for details of which versions are compatible with prior versions of PHP. - </p> - <h2>External dependencies</h2> - <p> +HTML; + wfGenericError( $type, $mwVersion, 'Supported PHP versions', $shortText, $longText, $longHtml ); +} + +/** + * Display an error for the vendor/autoload.php file not being found. + * + * @param string $type See wfGenericError + * @param string $mwVersion See wfGenericError + */ +function wfMissingVendorError( $type, $mwVersion ) { + $shortText = "Installing some external dependencies (e.g. via composer) is required."; + + $longText = "Error: You are missing some external dependencies. \n" + . "MediaWiki now also has some external dependencies that need to be installed\n" + . "via composer or from a separate git repo. Please see\n" + . "https://www.mediawiki.org/wiki/Download_from_Git#Fetch_external_libraries\n" + . "for help on installing the required components."; + + $longHtml = <<<HTML MediaWiki now also has some external dependencies that need to be installed via composer or from a separate git repo. Please see <a href="https://www.mediawiki.org/wiki/Download_from_Git#Fetch_external_libraries">mediawiki.org</a> for help on installing the required components. - </p> - </div> - </body> -</html> HTML; - // Handle everything that's not index.php - } else { - // So nothing thinks this is JS or CSS - $finalOutput = ( $type == 'load.php' ) ? "/* $message */" : $message; - header( "$protocol 500 MediaWiki configuration Error" ); - } - echo "$finalOutput\n"; - die( 1 ); + + wfGenericError( $type, $mwVersion, 'External dependencies', $shortText, $longText, $longHtml ); } diff --git a/includes/Preferences.php b/includes/Preferences.php index a5239331..d0475c17 100644 --- a/includes/Preferences.php +++ b/includes/Preferences.php @@ -124,6 +124,7 @@ class Preferences { $disable = !$user->isAllowed( 'editmyoptions' ); + $defaultOptions = User::getDefaultOptions(); ## Prod in defaults from the user foreach ( $defaultPreferences as $name => &$info ) { $prefFromUser = self::getOptionFromUser( $name, $info, $user ); @@ -131,7 +132,6 @@ class Preferences { $info['disabled'] = 'disabled'; } $field = HTMLForm::loadInputFromParameters( $name, $info, $dummyForm ); // For validation - $defaultOptions = User::getDefaultOptions(); $globalDefault = isset( $defaultOptions[$name] ) ? $defaultOptions[$name] : null; @@ -657,8 +657,9 @@ class Preferences { $now = wfTimestampNow(); $lang = $context->getLanguage(); $nowlocal = Xml::element( 'span', array( 'id' => 'wpLocalTime' ), - $lang->time( $now, true ) ); - $nowserver = $lang->time( $now, false ) . + $lang->userTime( $now, $user ) ); + $nowserver = $lang->userTime( $now, $user, + array( 'format' => false, 'timecorrection' => false ) ) . Html::hidden( 'wpServerTime', (int)substr( $now, 8, 2 ) * 60 + (int)substr( $now, 10, 2 ) ); $defaultPreferences['nowserver'] = array( @@ -750,7 +751,11 @@ class Preferences { 'type' => 'select', 'section' => 'rendering/advancedrendering', 'options' => $stubThresholdOptions, - 'label-raw' => $context->msg( 'stub-threshold' )->text(), // Raw HTML message. Yay? + // This is not a raw HTML message; label-raw is needed for the manual <a></a> + 'label-raw' => $context->msg( 'stub-threshold' )->rawParams( + '<a href="#" class="stub">' . + $context->msg( 'stub-threshold-sample-link' )->parse() . + '</a>' )->parse(), ); $defaultPreferences['showhiddencats'] = array( @@ -1293,12 +1298,19 @@ class Preferences { $opt = array(); $localTZoffset = $context->getConfig()->get( 'LocalTZoffset' ); + $timeZoneList = self::getTimeZoneList( $context->getLanguage() ); + $timestamp = MWTimestamp::getLocalInstance(); // Check that the LocalTZoffset is the same as the local time zone offset if ( $localTZoffset == $timestamp->format( 'Z' ) / 60 ) { + $timezoneName = $timestamp->getTimezone()->getName(); + // Localize timezone + if ( isset( $timeZoneList[$timezoneName] ) ) { + $timezoneName = $timeZoneList[$timezoneName]['name']; + } $server_tz_msg = $context->msg( 'timezoneuseserverdefault', - $timestamp->getTimezone()->getName() + $timezoneName )->text(); } else { $tzstring = sprintf( @@ -1312,49 +1324,12 @@ class Preferences { $opt[$context->msg( 'timezoneuseoffset' )->text()] = 'other'; $opt[$context->msg( 'guesstimezone' )->text()] = 'guess'; - if ( function_exists( 'timezone_identifiers_list' ) ) { - # Read timezone list - $tzs = timezone_identifiers_list(); - sort( $tzs ); - - $tzRegions = array(); - $tzRegions['Africa'] = $context->msg( 'timezoneregion-africa' )->text(); - $tzRegions['America'] = $context->msg( 'timezoneregion-america' )->text(); - $tzRegions['Antarctica'] = $context->msg( 'timezoneregion-antarctica' )->text(); - $tzRegions['Arctic'] = $context->msg( 'timezoneregion-arctic' )->text(); - $tzRegions['Asia'] = $context->msg( 'timezoneregion-asia' )->text(); - $tzRegions['Atlantic'] = $context->msg( 'timezoneregion-atlantic' )->text(); - $tzRegions['Australia'] = $context->msg( 'timezoneregion-australia' )->text(); - $tzRegions['Europe'] = $context->msg( 'timezoneregion-europe' )->text(); - $tzRegions['Indian'] = $context->msg( 'timezoneregion-indian' )->text(); - $tzRegions['Pacific'] = $context->msg( 'timezoneregion-pacific' )->text(); - asort( $tzRegions ); - - $prefill = array_fill_keys( array_values( $tzRegions ), array() ); - $opt = array_merge( $opt, $prefill ); - - $now = date_create( 'now' ); - - foreach ( $tzs as $tz ) { - $z = explode( '/', $tz, 2 ); - - # timezone_identifiers_list() returns a number of - # backwards-compatibility entries. This filters them out of the - # list presented to the user. - if ( count( $z ) != 2 || !array_key_exists( $z[0], $tzRegions ) ) { - continue; - } - - # Localize region - $z[0] = $tzRegions[$z[0]]; - - $minDiff = floor( timezone_offset_get( timezone_open( $tz ), $now ) / 60 ); - - $display = str_replace( '_', ' ', $z[0] . '/' . $z[1] ); - $value = "ZoneInfo|$minDiff|$tz"; - - $opt[$z[0]][$display] = $value; + foreach ( $timeZoneList as $timeZoneInfo ) { + $region = $timeZoneInfo['region']; + if ( !isset( $opt[$region] ) ) { + $opt[$region] = array(); } + $opt[$region][$timeZoneInfo['name']] = $timeZoneInfo['timecorrection']; } return $opt; } @@ -1393,7 +1368,7 @@ class Preferences { } # Max is +14:00 and min is -12:00, see: - # http://en.wikipedia.org/wiki/Timezone + # https://en.wikipedia.org/wiki/Timezone $minDiff = min( $minDiff, 840 ); # 14:00 $minDiff = max( $minDiff, - 720 ); # -12:00 return 'Offset|' . $minDiff; @@ -1458,10 +1433,10 @@ class Preferences { } Hooks::run( 'PreferencesFormPreSave', array( $formData, $form, $user, &$result ) ); - $user->saveSettings(); } $wgAuth->updateExternalDB( $user ); + $user->saveSettings(); return $result; } @@ -1490,6 +1465,68 @@ class Preferences { return Status::newGood(); } + + /** + * Get a list of all time zones + * @param Language $language Language used for the localized names + * @return array A list of all time zones. The system name of the time zone is used as key and + * the value is an array which contains localized name, the timecorrection value used for + * preferences and the region + * @since 1.26 + */ + public static function getTimeZoneList( Language $language ) { + $identifiers = DateTimeZone::listIdentifiers(); + if ( $identifiers === false ) { + return array(); + } + sort( $identifiers ); + + $tzRegions = array( + 'Africa' => wfMessage( 'timezoneregion-africa' )->inLanguage( $language )->text(), + 'America' => wfMessage( 'timezoneregion-america' )->inLanguage( $language )->text(), + 'Antarctica' => wfMessage( 'timezoneregion-antarctica' )->inLanguage( $language )->text(), + 'Arctic' => wfMessage( 'timezoneregion-arctic' )->inLanguage( $language )->text(), + 'Asia' => wfMessage( 'timezoneregion-asia' )->inLanguage( $language )->text(), + 'Atlantic' => wfMessage( 'timezoneregion-atlantic' )->inLanguage( $language )->text(), + 'Australia' => wfMessage( 'timezoneregion-australia' )->inLanguage( $language )->text(), + 'Europe' => wfMessage( 'timezoneregion-europe' )->inLanguage( $language )->text(), + 'Indian' => wfMessage( 'timezoneregion-indian' )->inLanguage( $language )->text(), + 'Pacific' => wfMessage( 'timezoneregion-pacific' )->inLanguage( $language )->text(), + ); + asort( $tzRegions ); + + $timeZoneList = array(); + + $now = new DateTime(); + + foreach ( $identifiers as $identifier ) { + $parts = explode( '/', $identifier, 2 ); + + // DateTimeZone::listIdentifiers() returns a number of + // backwards-compatibility entries. This filters them out of the + // list presented to the user. + if ( count( $parts ) !== 2 || !array_key_exists( $parts[0], $tzRegions ) ) { + continue; + } + + // Localize region + $parts[0] = $tzRegions[$parts[0]]; + + $dateTimeZone = new DateTimeZone( $identifier ); + $minDiff = floor( $dateTimeZone->getOffset( $now ) / 60 ); + + $display = str_replace( '_', ' ', $parts[0] . '/' . $parts[1] ); + $value = "ZoneInfo|$minDiff|$identifier"; + + $timeZoneList[$identifier] = array( + 'name' => $display, + 'timecorrection' => $value, + 'region' => $parts[0], + ); + } + + return $timeZoneList; + } } /** Some tweaks to allow js prefs to work */ diff --git a/includes/PrefixSearch.php b/includes/PrefixSearch.php index 55a4f49b..430b4b89 100644 --- a/includes/PrefixSearch.php +++ b/includes/PrefixSearch.php @@ -362,7 +362,11 @@ abstract class PrefixSearch { $ns = NS_MAIN; // if searching on many always default to main } - $t = Title::newFromText( $search, $ns ); + $t = null; + if ( is_string( $search ) ) { + $t = Title::newFromText( $search, $ns ); + } + $prefix = $t ? $t->getDBkey() : ''; $dbr = wfGetDB( DB_SLAVE ); $res = $dbr->select( 'page', diff --git a/includes/ProtectionForm.php b/includes/ProtectionForm.php index 1219da51..4cad7b74 100644 --- a/includes/ProtectionForm.php +++ b/includes/ProtectionForm.php @@ -157,7 +157,7 @@ class ProtectionForm { $value = $this->mExpirySelection[$action]; } if ( wfIsInfinity( $value ) ) { - $time = wfGetDB( DB_SLAVE )->getInfinity(); + $time = 'infinity'; } else { $unix = strtotime( $value ); @@ -384,7 +384,12 @@ class ProtectionForm { "mwProtect-$action-expires" ); - $expiryFormOptions = ''; + $expiryFormOptions = new XmlSelect( "wpProtectExpirySelection-$action", "mwProtectExpirySelection-$action", $this->mExpirySelection[$action] ); + $expiryFormOptions->setAttribute( 'tabindex', '2' ); + if ( $this->disabled ) { + $expiryFormOptions->setAttribute( 'disabled', 'disabled' ); + } + if ( $this->mExistingExpiry[$action] ) { if ( $this->mExistingExpiry[$action] == 'infinity' ) { $existingExpiryMessage = $context->msg( 'protect-existing-expiry-infinity' ); @@ -394,29 +399,17 @@ class ProtectionForm { $t = $lang->userTime( $this->mExistingExpiry[$action], $user ); $existingExpiryMessage = $context->msg( 'protect-existing-expiry', $timestamp, $d, $t ); } - $expiryFormOptions .= - Xml::option( - $existingExpiryMessage->text(), - 'existing', - $this->mExpirySelection[$action] == 'existing' - ) . "\n"; + $expiryFormOptions->addOption( $existingExpiryMessage->text(), 'existing' ); } - $expiryFormOptions .= Xml::option( - $context->msg( 'protect-othertime-op' )->text(), - "othertime" - ) . "\n"; + $expiryFormOptions->addOption( $context->msg( 'protect-othertime-op' )->text(), 'othertime' ); foreach ( explode( ',', $scExpiryOptions ) as $option ) { if ( strpos( $option, ":" ) === false ) { $show = $value = $option; } else { list( $show, $value ) = explode( ":", $option ); } - $expiryFormOptions .= Xml::option( - $show, - htmlspecialchars( $value ), - $this->mExpirySelection[$action] === $value - ) . "\n"; + $expiryFormOptions->addOption( $show, htmlspecialchars( $value ) ); } # Add expiry dropdown if ( $showProtectOptions && !$this->disabled ) { @@ -426,12 +419,7 @@ class ProtectionForm { {$mProtectexpiry} </td> <td class='mw-input'>" . - Xml::tags( 'select', - array( - 'id' => "mwProtectExpirySelection-$action", - 'name' => "wpProtectExpirySelection-$action", - 'tabindex' => '2' ) + $this->disabledAttrib, - $expiryFormOptions ) . + $expiryFormOptions->getHTML() . "</td> </tr></table>"; } @@ -541,9 +529,8 @@ class ProtectionForm { $out .= Xml::closeElement( 'fieldset' ); if ( $user->isAllowed( 'editinterface' ) ) { - $title = Title::makeTitle( NS_MEDIAWIKI, 'Protect-dropdown' ); - $link = Linker::link( - $title, + $link = Linker::linkKnown( + $context->msg( 'protect-dropdown' )->inContentLanguage()->getTitle(), $context->msg( 'protect-edit-reasonlist' )->escaped(), array(), array( 'action' => 'edit' ) @@ -577,18 +564,18 @@ class ProtectionForm { ); $id = 'mwProtect-level-' . $action; - $attribs = array( - 'id' => $id, - 'name' => $id, - 'size' => count( $levels ), - ) + $this->disabledAttrib; - $out = Xml::openElement( 'select', $attribs ); + $select = new XmlSelect( $id, $id, $selected ); + $select->setAttribute( 'size', count( $levels ) ); + if ( $this->disabled ) { + $select->setAttribute( 'disabled', 'disabled' ); + } + foreach ( $levels as $key ) { - $out .= Xml::option( $this->getOptionLabel( $key ), $key, $key == $selected ); + $select->addOption( $this->getOptionLabel( $key ), $key ); } - $out .= Xml::closeElement( 'select' ); - return $out; + + return $select->getHTML(); } /** diff --git a/includes/Revision.php b/includes/Revision.php index 3ba6157c..32ee259f 100644 --- a/includes/Revision.php +++ b/includes/Revision.php @@ -194,8 +194,8 @@ class Revision implements IDBAccessObject { if ( !isset( $attribs['title'] ) && isset( $row->ar_namespace ) - && isset( $row->ar_title ) ) { - + && isset( $row->ar_title ) + ) { $attribs['title'] = Title::makeTitle( $row->ar_namespace, $row->ar_title ); } @@ -1087,7 +1087,7 @@ class Revision implements IDBAccessObject { /** * Returns the content model for this revision. * - * If no content model was stored in the database, $this->getTitle()->getContentModel() is + * If no content model was stored in the database, the default content model for the title is * used to determine the content model to use. If no title is know, CONTENT_MODEL_WIKITEXT * is used as a last resort. * @@ -1097,7 +1097,11 @@ class Revision implements IDBAccessObject { public function getContentModel() { if ( !$this->mContentModel ) { $title = $this->getTitle(); - $this->mContentModel = ( $title ? $title->getContentModel() : CONTENT_MODEL_WIKITEXT ); + if ( $title ) { + $this->mContentModel = ContentHandler::getDefaultModelFor( $title ); + } else { + $this->mContentModel = CONTENT_MODEL_WIKITEXT; + } assert( !empty( $this->mContentModel ) ); } @@ -1284,8 +1288,14 @@ class Revision implements IDBAccessObject { if ( $wgCompressRevisions ) { if ( function_exists( 'gzdeflate' ) ) { - $text = gzdeflate( $text ); - $flags[] = 'gzip'; + $deflated = gzdeflate( $text ); + + if ( $deflated === false ) { + wfLogWarning( __METHOD__ . ': gzdeflate() failed' ); + } else { + $text = $deflated; + $flags[] = 'gzip'; + } } else { wfDebug( __METHOD__ . " -- no zlib support, not compressing\n" ); } @@ -1306,6 +1316,11 @@ class Revision implements IDBAccessObject { # This can be done periodically via maintenance/compressOld.php, and # as pages are saved if $wgCompressRevisions is set. $text = gzinflate( $text ); + + if ( $text === false ) { + wfLogWarning( __METHOD__ . ': gzinflate() failed' ); + return false; + } } if ( in_array( 'object', $flags ) ) { @@ -1626,8 +1641,9 @@ class Revision implements IDBAccessObject { $row['content_format'] = $current->rev_content_format; } + $row['title'] = Title::makeTitle( $current->page_namespace, $current->page_title ); + $revision = new Revision( $row ); - $revision->setTitle( Title::makeTitle( $current->page_namespace, $current->page_title ) ); } else { $revision = null; } diff --git a/includes/RevisionList.php b/includes/RevisionList.php index 6844dadc..e4174730 100644 --- a/includes/RevisionList.php +++ b/includes/RevisionList.php @@ -30,6 +30,7 @@ abstract class RevisionListBase extends ContextSource { /** @var array */ protected $ids; + /** @var ResultWrapper|bool */ protected $res; /** @var bool|object */ @@ -340,7 +341,8 @@ class RevisionItem extends RevisionItemBase { * @return string */ protected function getRevisionLink() { - $date = $this->list->getLanguage()->timeanddate( $this->revision->getTimestamp(), true ); + $date = htmlspecialchars( $this->list->getLanguage()->userTimeAndDate( + $this->revision->getTimestamp(), $this->list->getUser() ) ); if ( $this->isDeleted() && !$this->canViewContent() ) { return $date; diff --git a/includes/Sanitizer.php b/includes/Sanitizer.php index 96193a74..de63af79 100644 --- a/includes/Sanitizer.php +++ b/includes/Sanitizer.php @@ -346,12 +346,9 @@ class Sanitizer { ($space*=$space* (?: # The attribute value: quoted or alone - \"([^<\"]*)\" - | '([^<']*)' + \"([^<\"]*)(?:\"|\$) + | '([^<']*)(?:'|\$) | ([a-zA-Z0-9!#$%&()*,\\-.\\/:;<>?@[\\]^_`{|}~]+) - | (\#[0-9a-fA-F]+) # Technically wrong, but lots of - # colors are specified like this. - # We'll be normalizing it. ) )?(?=$space|\$)/sx"; } @@ -359,20 +356,13 @@ class Sanitizer { } /** - * Cleans up HTML, removes dangerous tags and attributes, and - * removes HTML comments - * @param string $text - * @param callable $processCallback Callback to do any variable or parameter - * replacements in HTML attribute values - * @param array|bool $args Arguments for the processing callback + * Return the various lists of recognized tags * @param array $extratags For any extra tags to include * @param array $removetags For any tags (default or extra) to exclude - * @return string + * @return array */ - public static function removeHTMLtags( $text, $processCallback = null, - $args = array(), $extratags = array(), $removetags = array() - ) { - global $wgUseTidy, $wgAllowMicrodataAttributes, $wgAllowImageTag; + public static function getRecognizedTagData( $extratags = array(), $removetags = array() ) { + global $wgAllowMicrodataAttributes, $wgAllowImageTag; static $htmlpairsStatic, $htmlsingle, $htmlsingleonly, $htmlnest, $tabletags, $htmllist, $listtags, $htmlsingleallowed, $htmlelementsStatic, $staticInitialised; @@ -381,7 +371,6 @@ class Sanitizer { // are changed (like in the screwed up test system) we will re-initialise the settings. $globalContext = implode( '-', compact( 'wgAllowMicrodataAttributes', 'wgAllowImageTag' ) ); if ( !$staticInitialised || $staticInitialised != $globalContext ) { - $htmlpairsStatic = array( # Tags that must be closed 'b', 'bdi', 'del', 'i', 'ins', 'u', 'font', 'big', 'small', 'sub', 'sup', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'cite', 'code', 'em', 's', @@ -431,17 +420,47 @@ class Sanitizer { } $staticInitialised = $globalContext; } + # Populate $htmlpairs and $htmlelements with the $extratags and $removetags arrays $extratags = array_flip( $extratags ); $removetags = array_flip( $removetags ); $htmlpairs = array_merge( $extratags, $htmlpairsStatic ); $htmlelements = array_diff_key( array_merge( $extratags, $htmlelementsStatic ), $removetags ); + return array( + 'htmlpairs' => $htmlpairs, + 'htmlsingle' => $htmlsingle, + 'htmlsingleonly' => $htmlsingleonly, + 'htmlnest' => $htmlnest, + 'tabletags' => $tabletags, + 'htmllist' => $htmllist, + 'listtags' => $listtags, + 'htmlsingleallowed' => $htmlsingleallowed, + 'htmlelements' => $htmlelements, + ); + } + + /** + * Cleans up HTML, removes dangerous tags and attributes, and + * removes HTML comments + * @param string $text + * @param callable $processCallback Callback to do any variable or parameter + * replacements in HTML attribute values + * @param array|bool $args Arguments for the processing callback + * @param array $extratags For any extra tags to include + * @param array $removetags For any tags (default or extra) to exclude + * @return string + */ + public static function removeHTMLtags( $text, $processCallback = null, + $args = array(), $extratags = array(), $removetags = array() + ) { + extract( self::getRecognizedTagData( $extratags, $removetags ) ); + # Remove HTML comments $text = Sanitizer::removeHTMLcomments( $text ); $bits = explode( '<', $text ); $text = str_replace( '>', '>', array_shift( $bits ) ); - if ( !$wgUseTidy ) { + if ( !MWTidy::isEnabled() ) { $tagstack = $tablestack = array(); foreach ( $bits as $x ) { $regs = array(); @@ -463,9 +482,9 @@ class Sanitizer { $badtag = true; } elseif ( $slash ) { # Closing a tag... is it the one we just opened? - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $ot = array_pop( $tagstack ); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); if ( $ot != $t ) { if ( isset( $htmlsingleallowed[$ot] ) ) { @@ -473,32 +492,32 @@ class Sanitizer { # and see if we find a match below them $optstack = array(); array_push( $optstack, $ot ); - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $ot = array_pop( $tagstack ); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); while ( $ot != $t && isset( $htmlsingleallowed[$ot] ) ) { array_push( $optstack, $ot ); - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $ot = array_pop( $tagstack ); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); } if ( $t != $ot ) { # No match. Push the optional elements back again $badtag = true; - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $ot = array_pop( $optstack ); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); while ( $ot ) { array_push( $tagstack, $ot ); - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $ot = array_pop( $optstack ); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); } } } else { - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); array_push( $tagstack, $ot ); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); # <li> can be nested in <ul> or <ol>, skip those cases: if ( !isset( $htmllist[$ot] ) || !isset( $listtags[$t] ) ) { @@ -729,7 +748,7 @@ class Sanitizer { } # Allow any attribute beginning with "data-" - if ( !preg_match( '/^data-/i', $attribute ) && !isset( $whitelist[$attribute] ) ) { + if ( !preg_match( '/^data-(?!ooui)/i', $attribute ) && !isset( $whitelist[$attribute] ) ) { continue; } @@ -942,7 +961,8 @@ class Sanitizer { $value = self::normalizeCss( $value ); // Reject problematic keywords and control characters - if ( preg_match( '/[\000-\010\013\016-\037\177]/', $value ) ) { + if ( preg_match( '/[\000-\010\013\016-\037\177]/', $value ) || + strpos( $value, UtfNormal\Constants::UTF8_REPLACEMENT ) !== false ) { return '/* invalid control char */'; } elseif ( preg_match( '! expression @@ -1239,10 +1259,7 @@ class Sanitizer { * @return string */ private static function getTagAttributeCallback( $set ) { - if ( isset( $set[6] ) ) { - # Illegal #XXXXXX color with no quotes. - return $set[6]; - } elseif ( isset( $set[5] ) ) { + if ( isset( $set[5] ) ) { # No quotes. return $set[5]; } elseif ( isset( $set[4] ) ) { @@ -1252,9 +1269,10 @@ class Sanitizer { # Double-quoted return $set[3]; } elseif ( !isset( $set[2] ) ) { - # In XHTML, attributes must have a value. - # For 'reduced' form, return explicitly the attribute name here. - return $set[1]; + # In XHTML, attributes must have a value so return an empty string. + # See "Empty attribute syntax", + # http://www.w3.org/TR/html5/syntax.html#syntax-attribute-name + return ""; } else { throw new MWException( "Tag conditions not met. This should never happen and is a bug." ); } @@ -1374,15 +1392,19 @@ class Sanitizer { } /** - * Returns true if a given Unicode codepoint is a valid character in XML. + * Returns true if a given Unicode codepoint is a valid character in + * both HTML5 and XML. * @param int $codepoint * @return bool */ private static function validateCodepoint( $codepoint ) { + # U+000C is valid in HTML5 but not allowed in XML. + # U+000D is valid in XML but not allowed in HTML5. + # U+007F - U+009F are disallowed in HTML5 (control characters). return $codepoint == 0x09 || $codepoint == 0x0a - || $codepoint == 0x0d - || ( $codepoint >= 0x20 && $codepoint <= 0xd7ff ) + || ( $codepoint >= 0x20 && $codepoint <= 0x7e ) + || ( $codepoint >= 0xa0 && $codepoint <= 0xd7ff ) || ( $codepoint >= 0xe000 && $codepoint <= 0xfffd ) || ( $codepoint >= 0x10000 && $codepoint <= 0x10ffff ); } @@ -1784,6 +1806,11 @@ class Sanitizer { $host = preg_replace( $strip, '', $host ); + // IPv6 host names are bracketed with []. Url-decode these. + if ( substr_compare( "//%5B", $host, 0, 5 ) === 0 && preg_match( '!^//%5B([0-9A-Fa-f:.]+)%5D((:\d+)?)$!', $host, $matches ) ) { + $host = '//[' . $matches[1] . ']' . $matches[2]; + } + // @todo FIXME: Validate hostnames here return $protocol . $host . $rest; diff --git a/includes/Setup.php b/includes/Setup.php index 1b6d66c0..70e8cde4 100644 --- a/includes/Setup.php +++ b/includes/Setup.php @@ -68,17 +68,18 @@ if ( !empty( $wgActionPaths ) && !isset( $wgActionPaths['view'] ) ) { $wgActionPaths['view'] = $wgArticlePath; } +if ( $wgResourceBasePath === null ) { + $wgResourceBasePath = $wgScriptPath; +} if ( $wgStylePath === false ) { - $wgStylePath = "$wgScriptPath/skins"; + $wgStylePath = "$wgResourceBasePath/skins"; } if ( $wgLocalStylePath === false ) { + // Avoid wgResourceBasePath here since that may point to a different domain (e.g. CDN) $wgLocalStylePath = "$wgScriptPath/skins"; } if ( $wgExtensionAssetsPath === false ) { - $wgExtensionAssetsPath = "$wgScriptPath/extensions"; -} -if ( $wgResourceBasePath === null ) { - $wgResourceBasePath = $wgScriptPath; + $wgExtensionAssetsPath = "$wgResourceBasePath/extensions"; } if ( $wgLogo === false ) { @@ -105,6 +106,10 @@ if ( $wgGitInfoCacheDirectory === false && $wgCacheDirectory !== false ) { $wgGitInfoCacheDirectory = "{$wgCacheDirectory}/gitinfo"; } +if ( $wgEnableParserCache === false ) { + $wgParserCacheType = CACHE_NONE; +} + // Fix path to icon images after they were moved in 1.24 if ( $wgRightsIcon ) { $wgRightsIcon = str_replace( @@ -359,13 +364,13 @@ if ( $wgMetaNamespace === false ) { // Default value is 2000 or the suhosin limit if it is between 1 and 2000 if ( $wgResourceLoaderMaxQueryLength === false ) { - $suhosinMaxValueLength = (int) ini_get( 'suhosin.get.max_value_length' ); + $suhosinMaxValueLength = (int)ini_get( 'suhosin.get.max_value_length' ); if ( $suhosinMaxValueLength > 0 && $suhosinMaxValueLength < 2000 ) { $wgResourceLoaderMaxQueryLength = $suhosinMaxValueLength; } else { $wgResourceLoaderMaxQueryLength = 2000; } - unset($suhosinMaxValueLength); + unset( $suhosinMaxValueLength ); } // Ensure the minimum chunk size is less than PHP upload limits or the maximum @@ -434,12 +439,12 @@ if ( !$wgHtml5Version && $wgAllowRdfaAttributes ) { } // Blacklisted file extensions shouldn't appear on the "allowed" list -$wgFileExtensions = array_values( array_diff ( $wgFileExtensions, $wgFileBlacklist ) ); +$wgFileExtensions = array_values( array_diff( $wgFileExtensions, $wgFileBlacklist ) ); if ( $wgInvalidateCacheOnLocalSettingsChange ) { - // @codingStandardsIgnoreStart Generic.PHP.NoSilencedErrors.Discouraged - No GlobalFunction here yet. - $wgCacheEpoch = max( $wgCacheEpoch, gmdate( 'YmdHis', @filemtime( "$IP/LocalSettings.php" ) ) ); - // @codingStandardsIgnoreEnd + MediaWiki\suppressWarnings(); + $wgCacheEpoch = max( $wgCacheEpoch, gmdate( 'YmdHis', filemtime( "$IP/LocalSettings.php" ) ) ); + MediaWiki\restoreWarnings(); } if ( $wgNewUserLog ) { @@ -473,6 +478,21 @@ if ( $wgProfileOnly ) { $wgDebugLogFile = ''; } +// Backwards compatibility with old password limits +if ( $wgMinimalPasswordLength !== false ) { + $wgPasswordPolicy['policies']['default']['MinimalPasswordLength'] = $wgMinimalPasswordLength; +} + +if ( $wgMaximalPasswordLength !== false ) { + $wgPasswordPolicy['policies']['default']['MaximalPasswordLength'] = $wgMaximalPasswordLength; +} + +// Backwards compatibility with deprecated alias +// Must be before call to wfSetupSession() +if ( $wgSessionsInMemcached ) { + $wgSessionsInObjectCache = true; +} + Profiler::instance()->scopedProfileOut( $ps_default ); // Disable MWDebug for command line mode, this prevents MWDebug from eating up @@ -488,7 +508,7 @@ if ( !class_exists( 'AutoLoader' ) ) { MWExceptionHandler::installHandler(); -require_once "$IP/includes/libs/normal/UtfNormalUtil.php"; +require_once "$IP/includes/compat/normal/UtfNormalUtil.php"; $ps_default2 = Profiler::instance()->scopedProfileIn( $fname . '-defaults2' ); @@ -525,11 +545,11 @@ if ( $wgSecureLogin && substr( $wgServer, 0, 2 ) !== '//' ) { . 'HTTP or HTTPS. Disabling secure login.' ); } +$wgVirtualRestConfig['global']['domain'] = $wgCanonicalServer; + // Now that GlobalFunctions is loaded, set defaults that depend on it. if ( $wgTmpDirectory === false ) { - $ps_tmpdir = Profiler::instance()->scopedProfileIn( $fname . '-tempDir' ); $wgTmpDirectory = wfTempDir(); - Profiler::instance()->scopedProfileOut( $ps_tmpdir ); } // We don't use counters anymore. Left here for extensions still @@ -538,6 +558,18 @@ if ( !isset( $wgDisableCounters ) ) { $wgDisableCounters = true; } +if ( $wgMainWANCache === false ) { + // Setup a WAN cache from $wgMainCacheType with no relayer. + // Sites using multiple datacenters can configure a relayer. + $wgMainWANCache = 'mediawiki-main-default'; + $wgWANObjectCaches[$wgMainWANCache] = array( + 'class' => 'WANObjectCache', + 'cacheId' => $wgMainCacheType, + 'pool' => 'mediawiki-main-default', + 'relayerConfig' => array( 'class' => 'EventRelayerNull' ) + ); +} + Profiler::instance()->scopedProfileOut( $ps_default2 ); $ps_misc = Profiler::instance()->scopedProfileIn( $fname . '-misc1' ); @@ -551,9 +583,9 @@ wfMemoryLimit(); * explicitly set. Inspired by phpMyAdmin's treatment of the problem. */ if ( is_null( $wgLocaltimezone ) ) { - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $wgLocaltimezone = date_default_timezone_get(); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); } date_default_timezone_set( $wgLocaltimezone ); @@ -561,6 +593,10 @@ if ( is_null( $wgLocalTZoffset ) ) { $wgLocalTZoffset = date( 'Z' ) / 60; } +if ( !$wgDBerrorLogTZ ) { + $wgDBerrorLogTZ = $wgLocaltimezone; +} + // Useful debug output if ( $wgCommandLineMode ) { $wgRequest = new FauxRequest( array() ); @@ -654,12 +690,6 @@ if ( !is_object( $wgAuth ) ) { */ $wgTitle = null; -/** - * @deprecated since 1.24 Use DeferredUpdates::addUpdate instead - * @var array - */ -$wgDeferredUpdateList = array(); - Profiler::instance()->scopedProfileOut( $ps_globals ); $ps_extensions = Profiler::instance()->scopedProfileIn( $fname . '-extensions' ); diff --git a/includes/SiteStats.php b/includes/SiteStats.php index 15c18f35..81172a14 100644 --- a/includes/SiteStats.php +++ b/includes/SiteStats.php @@ -68,6 +68,8 @@ class SiteStats { * @return bool|ResultWrapper */ static function loadAndLazyInit() { + global $wgMiserMode; + wfDebug( __METHOD__ . ": reading site_stats from slave\n" ); $row = self::doLoad( wfGetDB( DB_SLAVE ) ); @@ -77,7 +79,7 @@ class SiteStats { $row = self::doLoad( wfGetDB( DB_MASTER ) ); } - if ( !self::isSane( $row ) ) { + if ( !$wgMiserMode && !self::isSane( $row ) ) { // Normally the site_stats table is initialized at install time. // Some manual construction scenarios may leave the table empty or // broken, however, for instance when importing from a dump into a diff --git a/includes/SquidPurgeClient.php b/includes/SquidPurgeClient.php index 824dd06b..ca8f11ae 100644 --- a/includes/SquidPurgeClient.php +++ b/includes/SquidPurgeClient.php @@ -95,9 +95,9 @@ class SquidPurgeClient { } $this->socket = socket_create( AF_INET, SOCK_STREAM, SOL_TCP ); socket_set_nonblock( $this->socket ); - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $ok = socket_connect( $this->socket, $ip, $this->port ); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); if ( !$ok ) { $error = socket_last_error( $this->socket ); if ( $error !== self::EINPROGRESS ) { @@ -153,12 +153,12 @@ class SquidPurgeClient { } elseif ( IP::isIPv6( $this->host ) ) { throw new MWException( '$wgSquidServers does not support IPv6' ); } else { - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $this->ip = gethostbyname( $this->host ); if ( $this->ip === $this->host ) { $this->ip = false; } - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); } } return $this->ip; @@ -178,11 +178,11 @@ class SquidPurgeClient { */ public function close() { if ( $this->socket ) { - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); socket_set_block( $this->socket ); socket_shutdown( $this->socket ); socket_close( $this->socket ); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); } $this->socket = null; $this->readBuffer = ''; @@ -252,9 +252,9 @@ class SquidPurgeClient { $buf = substr( $this->writeBuffer, 0, self::BUFFER_SIZE ); $flags = 0; } - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $bytesSent = socket_send( $socket, $buf, strlen( $buf ), $flags ); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); if ( $bytesSent === false ) { $error = socket_last_error( $socket ); @@ -278,9 +278,9 @@ class SquidPurgeClient { } $buf = ''; - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $bytesRead = socket_recv( $socket, $buf, self::BUFFER_SIZE, 0 ); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); if ( $bytesRead === false ) { $error = socket_last_error( $socket ); if ( $error != self::EAGAIN && $error != self::EINTR ) { @@ -442,9 +442,9 @@ class SquidPurgeClientPool { } $exceptSockets = null; $timeout = min( $startTime + $this->timeout - microtime( true ), 1 ); - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $numReady = socket_select( $readSockets, $writeSockets, $exceptSockets, $timeout ); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); if ( $numReady === false ) { wfDebugLog( 'squid', __METHOD__ . ': Error in stream_select: ' . socket_strerror( socket_last_error() ) . "\n" ); diff --git a/includes/Status.php b/includes/Status.php index cd10258d..28af7f53 100644 --- a/includes/Status.php +++ b/includes/Status.php @@ -69,9 +69,9 @@ class Status { * Succinct helper method to wrap a StatusValue * * This is is useful when formatting StatusValue objects: - * <code> + * @code * $this->getOutput()->addHtml( Status::wrap( $sv )->getHTML() ); - * </code> + * @endcode * * @param StatusValue|Status $sv * @return Status @@ -281,7 +281,7 @@ class Status { * Otherwise, if its an array, just use the first value as the * message and the remaining items as the params. * - * @return string + * @return Message */ protected function getErrorMessage( $error ) { if ( is_array( $error ) ) { @@ -316,9 +316,9 @@ class Status { } /** - * Return an array with the wikitext for each item in the array. + * Return an array with a Message object for each error. * @param array $errors - * @return array + * @return Message[] */ protected function getErrorMessageArray( $errors ) { return array_map( array( $this, 'getErrorMessage' ), $errors ); diff --git a/includes/StreamFile.php b/includes/StreamFile.php index a52b25b0..3f73ae3c 100644 --- a/includes/StreamFile.php +++ b/includes/StreamFile.php @@ -44,9 +44,9 @@ class StreamFile { throw new MWException( __FUNCTION__ . " given storage path '$fname'." ); } - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $stat = stat( $fname ); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); $res = self::prepareForStream( $fname, $stat, $headers, $sendErrors ); if ( $res == self::NOT_MODIFIED ) { @@ -78,7 +78,7 @@ class StreamFile { ) { if ( !is_array( $info ) ) { if ( $sendErrors ) { - header( 'HTTP/1.0 404 Not Found' ); + HttpStatus::header( 404 ); header( 'Cache-Control: no-cache' ); header( 'Content-Type: text/html; charset=utf-8' ); $encFile = htmlspecialchars( $path ); @@ -126,7 +126,7 @@ class StreamFile { $modsince = preg_replace( '/;.*$/', '', $_SERVER['HTTP_IF_MODIFIED_SINCE'] ); if ( wfTimestamp( TS_UNIX, $info['mtime'] ) <= strtotime( $modsince ) ) { ini_set( 'zlib.output_compression', 0 ); - header( "HTTP/1.0 304 Not Modified" ); + HttpStatus::header( 304 ); return self::NOT_MODIFIED; // ok } } diff --git a/includes/StubObject.php b/includes/StubObject.php index 2dfcdc2f..49155d6d 100644 --- a/includes/StubObject.php +++ b/includes/StubObject.php @@ -194,7 +194,7 @@ class StubUserLang extends StubObject { public function findVariantLink( &$link, &$nt, $ignoreOtherCond = false ) { global $wgLang; $this->_unstub( 'findVariantLink', 3 ); - return $wgLang->findVariantLink( $link, $nt, $ignoreOtherCond ); + $wgLang->findVariantLink( $link, $nt, $ignoreOtherCond ); } /** diff --git a/includes/TemplateParser.php b/includes/TemplateParser.php index 3de70fa2..d6b101b2 100644 --- a/includes/TemplateParser.php +++ b/includes/TemplateParser.php @@ -103,7 +103,7 @@ class TemplateParser { // See if the compiled PHP code is stored in cache. // CACHE_ACCEL throws an exception if no suitable object cache is present, so fall // back to CACHE_ANYTHING. - $cache = ObjectCache::newAccelerator( array(), CACHE_ANYTHING ); + $cache = ObjectCache::newAccelerator( CACHE_ANYTHING ); $key = wfMemcKey( 'template', $templateName, $fastHash ); $code = $this->forceRecompile ? null : $cache->get( $key ); @@ -130,7 +130,8 @@ class TemplateParser { if ( !is_callable( $renderer ) ) { throw new RuntimeException( "Requested template, {$templateName}, is not callable" ); } - return $this->renderers[$templateName] = $renderer; + $this->renderers[$templateName] = $renderer; + return $renderer; } /** @@ -172,7 +173,9 @@ class TemplateParser { array( // Do not add more flags here without discussion. // If you do add more flags, be sure to update unit tests as well. - 'flags' => LightnCandy::FLAG_ERROR_EXCEPTION + 'flags' => LightnCandy::FLAG_ERROR_EXCEPTION, + 'basedir' => $this->templateDir, + 'fileext' => '.mustache', ) ); } diff --git a/includes/Title.php b/includes/Title.php index d8976635..b347edbb 100644 --- a/includes/Title.php +++ b/includes/Title.php @@ -96,7 +96,7 @@ class Title { /** @var array Array of groups allowed to edit this article */ public $mRestrictions = array(); - /** @var bool */ + /** @var string|bool */ protected $mOldRestrictions = false; /** @var bool Cascade restrictions on this page to included templates and images? */ @@ -225,9 +225,11 @@ class Title { public static function newFromDBkey( $key ) { $t = new Title(); $t->mDbkeyform = $key; - if ( $t->secureAndSplit() ) { + + try { + $t->secureAndSplit(); return $t; - } else { + } catch ( MalformedTitleException $ex ) { return null; } } @@ -263,7 +265,34 @@ class Title { if ( is_object( $text ) ) { throw new InvalidArgumentException( '$text must be a string.' ); } elseif ( !is_string( $text ) ) { - wfWarn( __METHOD__ . ': $text must be a string. This will throw an InvalidArgumentException in future.' ); + wfDebugLog( 'T76305', wfGetAllCallers( 5 ) ); + wfWarn( __METHOD__ . ': $text must be a string. This will throw an InvalidArgumentException in future.', 2 ); + } + + try { + return Title::newFromTextThrow( $text, $defaultNamespace ); + } catch ( MalformedTitleException $ex ) { + return null; + } + } + + /** + * Like Title::newFromText(), but throws MalformedTitleException when the title is invalid, + * rather than returning null. + * + * The exception subclasses encode detailed information about why the title is invalid. + * + * @see Title::newFromText + * + * @since 1.25 + * @param string $text Title text to check + * @param int $defaultNamespace + * @throws MalformedTitleException If the title is invalid + * @return Title + */ + public static function newFromTextThrow( $text, $defaultNamespace = NS_MAIN ) { + if ( is_object( $text ) ) { + throw new MWException( 'Title::newFromTextThrow given an object' ); } $cache = self::getTitleCache(); @@ -284,17 +313,14 @@ class Title { $filteredText = Sanitizer::decodeCharReferencesAndNormalize( $text ); $t = new Title(); - $t->mDbkeyform = str_replace( ' ', '_', $filteredText ); + $t->mDbkeyform = strtr( $filteredText, ' ', '_' ); $t->mDefaultNamespace = intval( $defaultNamespace ); - if ( $t->secureAndSplit() ) { - if ( $defaultNamespace == NS_MAIN ) { - $cache->set( $text, $t ); - } - return $t; - } else { - return null; + $t->secureAndSplit(); + if ( $defaultNamespace == NS_MAIN ) { + $cache->set( $text, $t ); } + return $t; } /** @@ -319,13 +345,15 @@ class Title { # but some URLs used it as a space replacement and they still come # from some external search tools. if ( strpos( self::legalChars(), '+' ) === false ) { - $url = str_replace( '+', ' ', $url ); + $url = strtr( $url, '+', ' ' ); } - $t->mDbkeyform = str_replace( ' ', '_', $url ); - if ( $t->secureAndSplit() ) { + $t->mDbkeyform = strtr( $url, ' ', '_' ); + + try { + $t->secureAndSplit(); return $t; - } else { + } catch ( MalformedTitleException $ex ) { return null; } } @@ -451,6 +479,9 @@ class Title { if ( isset( $row->page_lang ) ) { $this->mDbPageLanguage = (string)$row->page_lang; } + if ( isset( $row->page_restrictions ) ) { + $this->mOldRestrictions = $row->page_restrictions; + } } else { // page not found $this->mArticleID = 0; $this->mLength = 0; @@ -478,10 +509,10 @@ class Title { $t->mInterwiki = $interwiki; $t->mFragment = $fragment; $t->mNamespace = $ns = intval( $ns ); - $t->mDbkeyform = str_replace( ' ', '_', $title ); + $t->mDbkeyform = strtr( $title, ' ', '_' ); $t->mArticleID = ( $ns >= 0 ) ? -1 : 0; $t->mUrlform = wfUrlencode( $t->mDbkeyform ); - $t->mTextform = str_replace( '_', ' ', $title ); + $t->mTextform = strtr( $title, '_', ' ' ); $t->mContentModel = false; # initialized lazily in getContentModel() return $t; } @@ -504,9 +535,11 @@ class Title { $t = new Title(); $t->mDbkeyform = Title::makeName( $ns, $title, $fragment, $interwiki, true ); - if ( $t->secureAndSplit() ) { + + try { + $t->secureAndSplit(); return $t; - } else { + } catch ( MalformedTitleException $ex ) { return null; } } @@ -937,7 +970,6 @@ class Title { /** * Get the page's content model id, see the CONTENT_MODEL_XXX constants. * - * @throws MWException * @param int $flags A bit field; may be Title::GAID_FOR_UPDATE to select for update * @return string Content model id */ @@ -952,10 +984,6 @@ class Title { $this->mContentModel = ContentHandler::getDefaultModelFor( $this ); } - if ( !$this->mContentModel ) { - throw new MWException( 'Failed to determine content model!' ); - } - return $this->mContentModel; } @@ -1391,7 +1419,7 @@ class Title { * @param string $fragment Text */ public function setFragment( $fragment ) { - $this->mFragment = str_replace( '_', ' ', substr( $fragment, 1 ) ); + $this->mFragment = strtr( substr( $fragment, 1 ), '_', ' ' ); } /** @@ -1421,7 +1449,7 @@ class Title { */ public function getPrefixedDBkey() { $s = $this->prefix( $this->mDbkeyform ); - $s = str_replace( ' ', '_', $s ); + $s = strtr( $s, ' ', '_' ); return $s; } @@ -1434,7 +1462,7 @@ class Title { public function getPrefixedText() { if ( $this->mPrefixedText === null ) { $s = $this->prefix( $this->mTextform ); - $s = str_replace( '_', ' ', $s ); + $s = strtr( $s, '_', ' ' ); $this->mPrefixedText = $s; } return $this->mPrefixedText; @@ -1582,7 +1610,7 @@ class Title { */ public function getSubpageUrlForm() { $text = $this->getSubpageText(); - $text = wfUrlencode( str_replace( ' ', '_', $text ) ); + $text = wfUrlencode( strtr( $text, ' ', '_' ) ); return $text; } @@ -1593,7 +1621,7 @@ class Title { */ public function getPrefixedURL() { $s = $this->prefix( $this->mDbkeyform ); - $s = wfUrlencode( str_replace( ' ', '_', $s ) ); + $s = wfUrlencode( strtr( $s, ' ', '_' ) ); return $s; } @@ -1911,7 +1939,6 @@ class Title { * - quick : does cheap permission checks from slaves (usable for GUI creation) * - full : does cheap and expensive checks possibly from a slave * - secure : does cheap and expensive checks, using the master as needed - * @param bool $short Set this to true to stop after the first permission error. * @param array $ignoreErrors Array of Strings Set this to a list of message keys * whose corresponding errors may be ignored. * @return array Array of arguments to wfMessage to explain permissions problems. @@ -2574,6 +2601,7 @@ class Title { if ( $row['permission'] == 'autoconfirmed' ) { $row['permission'] = 'editsemiprotected'; // B/C } + $row['expiry'] = $dbr->decodeExpiry( $row['expiry'] ); } $this->mTitleProtection = $row; } @@ -2711,7 +2739,6 @@ class Title { * false. */ public function getCascadeProtectionSources( $getPages = true ) { - global $wgContLang; $pagerestrictions = array(); if ( $this->mCascadeSources !== null && $getPages ) { @@ -2754,7 +2781,7 @@ class Title { $now = wfTimestampNow(); foreach ( $res as $row ) { - $expiry = $wgContLang->formatExpiry( $row->pr_expiry, TS_MW ); + $expiry = $dbr->decodeExpiry( $row->pr_expiry ); if ( $expiry > $now ) { if ( $getPages ) { $page_id = $row->pr_page; @@ -2887,28 +2914,29 @@ class Title { * restrictions from page table (pre 1.10) */ public function loadRestrictionsFromRows( $rows, $oldFashionedRestrictions = null ) { - global $wgContLang; $dbr = wfGetDB( DB_SLAVE ); $restrictionTypes = $this->getRestrictionTypes(); foreach ( $restrictionTypes as $type ) { $this->mRestrictions[$type] = array(); - $this->mRestrictionsExpiry[$type] = $wgContLang->formatExpiry( '', TS_MW ); + $this->mRestrictionsExpiry[$type] = 'infinity'; } $this->mCascadeRestriction = false; # Backwards-compatibility: also load the restrictions from the page record (old format). + if ( $oldFashionedRestrictions !== null ) { + $this->mOldRestrictions = $oldFashionedRestrictions; + } - if ( $oldFashionedRestrictions === null ) { - $oldFashionedRestrictions = $dbr->selectField( 'page', 'page_restrictions', + if ( $this->mOldRestrictions === false ) { + $this->mOldRestrictions = $dbr->selectField( 'page', 'page_restrictions', array( 'page_id' => $this->getArticleID() ), __METHOD__ ); } - if ( $oldFashionedRestrictions != '' ) { - - foreach ( explode( ':', trim( $oldFashionedRestrictions ) ) as $restrict ) { + if ( $this->mOldRestrictions != '' ) { + foreach ( explode( ':', trim( $this->mOldRestrictions ) ) as $restrict ) { $temp = explode( '=', trim( $restrict ) ); if ( count( $temp ) == 1 ) { // old old format should be treated as edit/move restriction @@ -2921,9 +2949,6 @@ class Title { } } } - - $this->mOldRestrictions = true; - } if ( count( $rows ) ) { @@ -2940,7 +2965,7 @@ class Title { // This code should be refactored, now that it's being used more generally, // But I don't really see any harm in leaving it in Block for now -werdna - $expiry = $wgContLang->formatExpiry( $row->pr_expiry, TS_MW ); + $expiry = $dbr->decodeExpiry( $row->pr_expiry ); // Only apply the restrictions if they haven't expired! if ( !$expiry || $expiry > $now ) { @@ -2962,11 +2987,9 @@ class Title { * restrictions from page table (pre 1.10) */ public function loadRestrictions( $oldFashionedRestrictions = null ) { - global $wgContLang; if ( !$this->mRestrictionsLoaded ) { + $dbr = wfGetDB( DB_SLAVE ); if ( $this->exists() ) { - $dbr = wfGetDB( DB_SLAVE ); - $res = $dbr->select( 'page_restrictions', array( 'pr_type', 'pr_expiry', 'pr_level', 'pr_cascade' ), @@ -2980,7 +3003,7 @@ class Title { if ( $title_protection ) { $now = wfTimestampNow(); - $expiry = $wgContLang->formatExpiry( $title_protection['expiry'], TS_MW ); + $expiry = $dbr->decodeExpiry( $title_protection['expiry'] ); if ( !$expiry || $expiry > $now ) { // Apply the restrictions @@ -2990,7 +3013,7 @@ class Title { $this->mTitleProtection = false; } } else { - $this->mRestrictionsExpiry['create'] = $wgContLang->formatExpiry( '', TS_MW ); + $this->mRestrictionsExpiry['create'] = 'infinity'; } $this->mRestrictionsLoaded = true; } @@ -3274,6 +3297,7 @@ class Title { } $this->mRestrictionsLoaded = false; $this->mRestrictions = array(); + $this->mOldRestrictions = false; $this->mRedirect = null; $this->mLength = -1; $this->mLatestID = false; @@ -3318,6 +3342,7 @@ class Title { * namespace prefixes, sets the other forms, and canonicalizes * everything. * + * @throws MalformedTitleException On invalid titles * @return bool True on success */ private function secureAndSplit() { @@ -3328,15 +3353,12 @@ class Title { $dbkey = $this->mDbkeyform; - try { - // @note: splitTitleString() is a temporary hack to allow MediaWikiTitleCodec to share - // the parsing code with Title, while avoiding massive refactoring. - // @todo: get rid of secureAndSplit, refactor parsing code. - $titleParser = self::getTitleParser(); - $parts = $titleParser->splitTitleString( $dbkey, $this->getDefaultNamespace() ); - } catch ( MalformedTitleException $ex ) { - return false; - } + // @note: splitTitleString() is a temporary hack to allow MediaWikiTitleCodec to share + // the parsing code with Title, while avoiding massive refactoring. + // @todo: get rid of secureAndSplit, refactor parsing code. + $titleParser = self::getTitleParser(); + // MalformedTitleException can be thrown here + $parts = $titleParser->splitTitleString( $dbkey, $this->getDefaultNamespace() ); # Fill fields $this->setFragment( '#' . $parts['fragment'] ); @@ -3347,7 +3369,7 @@ class Title { $this->mDbkeyform = $parts['dbkey']; $this->mUrlform = wfUrlencode( $this->mDbkeyform ); - $this->mTextform = str_replace( '_', ' ', $this->mDbkeyform ); + $this->mTextform = strtr( $this->mDbkeyform, '_', ' ' ); # We already know that some pages won't be in the database! if ( $this->isExternal() || $this->mNamespace == NS_SPECIAL ) { @@ -3428,8 +3450,6 @@ class Title { * @return array Array of Title objects linking here */ public function getLinksFrom( $options = array(), $table = 'pagelinks', $prefix = 'pl' ) { - global $wgContentHandlerUseDB; - $id = $this->getArticleID(); # If the page doesn't exist; there can't be any link from this page @@ -3443,49 +3463,36 @@ class Title { $db = wfGetDB( DB_SLAVE ); } - $namespaceFiled = "{$prefix}_namespace"; - $titleField = "{$prefix}_title"; - - $fields = array( - $namespaceFiled, - $titleField, - 'page_id', - 'page_len', - 'page_is_redirect', - 'page_latest' - ); - - if ( $wgContentHandlerUseDB ) { - $fields[] = 'page_content_model'; - } + $blNamespace = "{$prefix}_namespace"; + $blTitle = "{$prefix}_title"; $res = $db->select( array( $table, 'page' ), - $fields, + array_merge( + array( $blNamespace, $blTitle ), + WikiPage::selectFields() + ), array( "{$prefix}_from" => $id ), __METHOD__, $options, array( 'page' => array( 'LEFT JOIN', - array( "page_namespace=$namespaceFiled", "page_title=$titleField" ) + array( "page_namespace=$blNamespace", "page_title=$blTitle" ) ) ) ); $retVal = array(); - if ( $res->numRows() ) { - $linkCache = LinkCache::singleton(); - foreach ( $res as $row ) { - $titleObj = Title::makeTitle( $row->$namespaceFiled, $row->$titleField ); - if ( $titleObj ) { - if ( $row->page_id ) { - $linkCache->addGoodLinkObjFromRow( $titleObj, $row ); - } else { - $linkCache->addBadLinkObj( $titleObj ); - } - $retVal[] = $titleObj; - } + $linkCache = LinkCache::singleton(); + foreach ( $res as $row ) { + if ( $row->page_id ) { + $titleObj = Title::newFromRow( $row ); + } else { + $titleObj = Title::makeTitle( $row->$blNamespace, $row->$blTitle ); + $linkCache->addBadLinkObj( $titleObj ); } + $retVal[] = $titleObj; } + return $retVal; } @@ -3624,7 +3631,7 @@ class Title { ); } - return $errors ? : true; + return $errors ?: true; } /** @@ -4236,10 +4243,12 @@ class Title { * If you want to know if a title can be meaningfully viewed, you should * probably call the isKnown() method instead. * + * @param int $flags An optional bit field; may be Title::GAID_FOR_UPDATE to check + * from master/for update * @return bool */ - public function exists() { - $exists = $this->getArticleID() != 0; + public function exists( $flags = 0 ) { + $exists = $this->getArticleID( $flags ) != 0; Hooks::run( 'TitleExists', array( $this, &$exists ) ); return $exists; } @@ -4370,9 +4379,10 @@ class Title { /** * Updates page_touched for this page; called from LinksUpdate.php * + * @param integer $purgeTime TS_MW timestamp [optional] * @return bool True if the update succeeded */ - public function invalidateCache() { + public function invalidateCache( $purgeTime = null ) { if ( wfReadOnly() ) { return false; } @@ -4384,11 +4394,13 @@ class Title { $method = __METHOD__; $dbw = wfGetDB( DB_MASTER ); $conds = $this->pageCond(); - $dbw->onTransactionIdle( function () use ( $dbw, $conds, $method ) { + $dbw->onTransactionIdle( function () use ( $dbw, $conds, $method, $purgeTime ) { + $dbTimestamp = $dbw->timestamp( $purgeTime ?: time() ); + $dbw->update( 'page', - array( 'page_touched' => $dbw->timestamp() ), - $conds, + array( 'page_touched' => $dbTimestamp ), + $conds + array( 'page_touched < ' . $dbw->addQuotes( $dbTimestamp ) ), $method ); } ); @@ -4432,35 +4444,29 @@ class Title { * @return string|null */ public function getNotificationTimestamp( $user = null ) { - global $wgUser, $wgShowUpdatedMarker; + global $wgUser; + // Assume current user if none given if ( !$user ) { $user = $wgUser; } // Check cache first $uid = $user->getId(); + if ( !$uid ) { + return false; + } // avoid isset here, as it'll return false for null entries if ( array_key_exists( $uid, $this->mNotificationTimestamp ) ) { return $this->mNotificationTimestamp[$uid]; } - if ( !$uid || !$wgShowUpdatedMarker || !$user->isAllowed( 'viewmywatchlist' ) ) { - $this->mNotificationTimestamp[$uid] = false; - return $this->mNotificationTimestamp[$uid]; - } // Don't cache too much! if ( count( $this->mNotificationTimestamp ) >= self::CACHE_MAX ) { $this->mNotificationTimestamp = array(); } - $dbr = wfGetDB( DB_SLAVE ); - $this->mNotificationTimestamp[$uid] = $dbr->selectField( 'watchlist', - 'wl_notificationtimestamp', - array( - 'wl_user' => $user->getId(), - 'wl_namespace' => $this->getNamespace(), - 'wl_title' => $this->getDBkey(), - ), - __METHOD__ - ); + + $watchedItem = WatchedItem::fromUserTitle( $user, $this ); + $this->mNotificationTimestamp[$uid] = $watchedItem->getNotificationTimestamp(); + return $this->mNotificationTimestamp[$uid]; } @@ -4540,15 +4546,17 @@ class Title { public function isValidRedirectTarget() { global $wgInvalidRedirectTargets; - // invalid redirect targets are stored in a global array, but explicitly disallow Userlogout here - if ( $this->isSpecial( 'Userlogout' ) ) { - return false; - } - - foreach ( $wgInvalidRedirectTargets as $target ) { - if ( $this->isSpecial( $target ) ) { + if ( $this->isSpecialPage() ) { + // invalid redirect targets are stored in a global array, but explicitly disallow Userlogout here + if ( $this->isSpecial( 'Userlogout' ) ) { return false; } + + foreach ( $wgInvalidRedirectTargets as $target ) { + if ( $this->isSpecial( $target ) ) { + return false; + } + } } return true; @@ -4731,7 +4739,7 @@ class Title { } } else { // Even if there are no subpages in namespace, we still don't want "/" in MediaWiki message keys - $editnoticeText = $editnotice_ns . '-' . str_replace( '/', '-', $this->getDBkey() ); + $editnoticeText = $editnotice_ns . '-' . strtr( $this->getDBkey(), '/', '-' ); $msg = wfMessage( $editnoticeText ); if ( $msg->exists() ) { $html = $msg->parseAsBlock(); @@ -4752,4 +4760,26 @@ class Title { Hooks::run( 'TitleGetEditNotices', array( $this, $oldid, &$notices ) ); return $notices; } + + /** + * @return array + */ + public function __sleep() { + return array( + 'mNamespace', + 'mDbkeyform', + 'mFragment', + 'mInterwiki', + 'mLocalInterwiki', + 'mUserCaseDBKey', + 'mDefaultNamespace', + ); + } + + public function __wakeup() { + $this->mArticleID = ( $this->mNamespace >= 0 ) ? -1 : 0; + $this->mUrlform = wfUrlencode( $this->mDbkeyform ); + $this->mTextform = strtr( $this->mDbkeyform, '_', ' ' ); + } + } diff --git a/includes/User.php b/includes/User.php index 663a80b7..22c90cdd 100644 --- a/includes/User.php +++ b/includes/User.php @@ -183,50 +183,50 @@ class User implements IDBAccessObject { */ protected static $mAllRights = false; - /** @name Cache variables */ + /** Cache variables */ //@{ public $mId; - + /** @var string */ public $mName; - + /** @var string */ public $mRealName; - /** * @todo Make this actually private * @private + * @var Password */ public $mPassword; - /** * @todo Make this actually private * @private + * @var Password */ public $mNewpassword; - + /** @var string */ public $mNewpassTime; - + /** @var string */ public $mEmail; /** @var string TS_MW timestamp from the DB */ public $mTouched; /** @var string TS_MW timestamp from cache */ protected $mQuickTouched; - + /** @var string */ protected $mToken; - + /** @var string */ public $mEmailAuthenticated; - + /** @var string */ protected $mEmailToken; - + /** @var string */ protected $mEmailTokenExpires; - + /** @var string */ protected $mRegistration; - + /** @var int */ protected $mEditCount; - + /** @var array */ public $mGroups; - + /** @var array */ protected $mOptionOverrides; - + /** @var string */ protected $mPasswordExpires; //@} @@ -257,29 +257,29 @@ class User implements IDBAccessObject { * Lazy-initialized variables, invalidated with clearInstanceCache */ protected $mNewtalk; - + /** @var string */ protected $mDatePreference; - + /** @var string */ public $mBlockedby; - + /** @var string */ protected $mHash; - + /** @var array */ public $mRights; - + /** @var string */ protected $mBlockreason; - + /** @var array */ protected $mEffectiveGroups; - + /** @var array */ protected $mImplicitGroups; - + /** @var array */ protected $mFormerGroups; - + /** @var bool */ protected $mBlockedGlobally; - + /** @var bool */ protected $mLocked; - + /** @var bool */ public $mHideName; - + /** @var array */ public $mOptions; /** @@ -330,7 +330,7 @@ class User implements IDBAccessObject { * * @param integer $flags User::READ_* constant bitfield */ - public function load( $flags = self::READ_LATEST ) { + public function load( $flags = self::READ_NORMAL ) { if ( $this->mLoadedItems === true ) { return; } @@ -344,9 +344,13 @@ class User implements IDBAccessObject { $this->loadDefaults(); break; case 'name': - // @TODO: this gets the ID from a slave, assuming renames - // are rare. This should be controllable and more consistent. - $this->mId = self::idFromName( $this->mName ); + // Make sure this thread sees its own changes + if ( wfGetLB()->hasOrMadeRecentMasterChanges() ) { + $flags |= self::READ_LATEST; + $this->queryFlagsUsed = $flags; + } + + $this->mId = self::idFromName( $this->mName, $flags ); if ( !$this->mId ) { // Nonexistent user placeholder object $this->loadDefaults( $this->mName ); @@ -365,7 +369,8 @@ class User implements IDBAccessObject { Hooks::run( 'UserLoadAfterLoadFromSession', array( $this ) ); break; default: - throw new MWException( "Unrecognised value for User->mFrom: \"{$this->mFrom}\"" ); + throw new UnexpectedValueException( + "Unrecognised value for User->mFrom: \"{$this->mFrom}\"" ); } } @@ -374,27 +379,26 @@ class User implements IDBAccessObject { * @param integer $flags User::READ_* constant bitfield * @return bool False if the ID does not exist, true otherwise */ - public function loadFromId( $flags = self::READ_LATEST ) { + public function loadFromId( $flags = self::READ_NORMAL ) { if ( $this->mId == 0 ) { $this->loadDefaults(); return false; } - // Try cache - $cache = $this->loadFromCache(); - if ( !$cache ) { + // Try cache (unless this needs to lock the DB). + // NOTE: if this thread called saveSettings(), the cache was cleared. + $locking = ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING ); + if ( $locking || !$this->loadFromCache() ) { wfDebug( "User: cache miss for user {$this->mId}\n" ); - // Load from DB + // Load from DB (make sure this thread sees its own changes) + if ( wfGetLB()->hasOrMadeRecentMasterChanges() ) { + $flags |= self::READ_LATEST; + } if ( !$this->loadFromDatabase( $flags ) ) { // Can't load from ID, user is anonymous return false; } - if ( $flags & self::READ_LATEST ) { - // Only save master data back to the cache to keep it consistent. - // @TODO: save it anyway and have callers specifiy $flags and have - // load() called as needed. That requires updating MANY callers... - $this->saveToCache(); - } + $this->saveToCache(); } $this->mLoadedItems = true; @@ -410,15 +414,13 @@ class User implements IDBAccessObject { * @since 1.25 */ protected function loadFromCache() { - global $wgMemc; - if ( $this->mId == 0 ) { $this->loadDefaults(); return false; } $key = wfMemcKey( 'user', 'id', $this->mId ); - $data = $wgMemc->get( $key ); + $data = ObjectCache::getMainWANInstance()->get( $key ); if ( !is_array( $data ) || $data['mVersion'] < self::VERSION ) { // Object is expired return false; @@ -440,8 +442,6 @@ class User implements IDBAccessObject { * This method should not be called outside the User class */ public function saveToCache() { - global $wgMemc; - $this->load(); $this->loadGroups(); $this->loadOptions(); @@ -451,13 +451,6 @@ class User implements IDBAccessObject { return; } - // The cache needs good consistency due to its high TTL, so the user - // should have been loaded from the master to avoid lag amplification. - if ( !( $this->queryFlagsUsed & self::READ_LATEST ) ) { - wfWarn( "Cannot cache slave-loaded User object with ID '{$this->mId}'." ); - return; - } - $data = array(); foreach ( self::$mCacheVars as $name ) { $data[$name] = $this->$name; @@ -465,7 +458,7 @@ class User implements IDBAccessObject { $data['mVersion'] = self::VERSION; $key = wfMemcKey( 'user', 'id', $this->mId ); - $wgMemc->set( $key, $data ); + ObjectCache::getMainWANInstance()->set( $key, $data, 3600 ); } /** @name newFrom*() static factory methods */ @@ -604,9 +597,10 @@ class User implements IDBAccessObject { /** * Get database id given a user name * @param string $name Username + * @param integer $flags User::READ_* constant bitfield * @return int|null The corresponding user's ID, or null if user is nonexistent */ - public static function idFromName( $name ) { + public static function idFromName( $name, $flags = self::READ_NORMAL ) { $nt = Title::makeTitleSafe( NS_USER, $name ); if ( is_null( $nt ) ) { // Illegal name @@ -617,8 +611,11 @@ class User implements IDBAccessObject { return self::$idCacheByName[$name]; } - $dbr = wfGetDB( DB_SLAVE ); - $s = $dbr->selectRow( + $db = ( $flags & self::READ_LATEST ) + ? wfGetDB( DB_MASTER ) + : wfGetDB( DB_SLAVE ); + + $s = $db->selectRow( 'user', array( 'user_id' ), array( 'user_name' => $nt->getText() ), @@ -846,19 +843,19 @@ class User implements IDBAccessObject { * able to set their password to this. * * @param string $password Desired password + * @param string $purpose one of 'login', 'create', 'reset' * @return Status * @since 1.23 */ - public function checkPasswordValidity( $password ) { - global $wgMinimalPasswordLength, $wgMaximalPasswordLength, $wgContLang; + public function checkPasswordValidity( $password, $purpose = 'login' ) { + global $wgPasswordPolicy; - static $blockedLogins = array( - 'Useruser' => 'Passpass', 'Useruser1' => 'Passpass1', # r75589 - 'Apitestsysop' => 'testpass', 'Apitestuser' => 'testpass' # r75605 + $upp = new UserPasswordPolicy( + $wgPasswordPolicy['policies'], + $wgPasswordPolicy['checks'] ); $status = Status::newGood(); - $result = false; //init $result to false for the internal checks if ( !Hooks::run( 'isValidPassword', array( $password, &$result, $this ) ) ) { @@ -867,28 +864,8 @@ class User implements IDBAccessObject { } if ( $result === false ) { - if ( strlen( $password ) < $wgMinimalPasswordLength ) { - $status->error( 'passwordtooshort', $wgMinimalPasswordLength ); - return $status; - } elseif ( strlen( $password ) > $wgMaximalPasswordLength ) { - // T64685: Password too long, might cause DoS attack - $status->fatal( 'passwordtoolong', $wgMaximalPasswordLength ); - return $status; - } elseif ( $wgContLang->lc( $password ) == $wgContLang->lc( $this->mName ) ) { - $status->error( 'password-name-match' ); - return $status; - } elseif ( isset( $blockedLogins[$this->getName()] ) - && $password == $blockedLogins[$this->getName()] - ) { - $status->error( 'password-login-forbidden' ); - return $status; - } else { - //it seems weird returning a Good status here, but this is because of the - //initialization of $result to false above. If the hook is never run or it - //doesn't modify $result, then we will likely get down into this if with - //a valid password. - return $status; - } + $status->merge( $upp->checkUserPassword( $this, $password, $purpose ) ); + return $status; } elseif ( $result === true ) { return $status; } else { @@ -974,7 +951,7 @@ class User implements IDBAccessObject { * - 'usable' Valid for batch processes and login * - 'creatable' Valid for batch processes, login and account creation * - * @throws MWException + * @throws InvalidArgumentException * @return bool|string */ public static function getCanonicalName( $name, $validate = 'valid' ) { @@ -1021,7 +998,8 @@ class User implements IDBAccessObject { } break; default: - throw new MWException( 'Invalid parameter value for $validate in ' . __METHOD__ ); + throw new InvalidArgumentException( + 'Invalid parameter value for $validate in ' . __METHOD__ ); } return $name; } @@ -1168,7 +1146,6 @@ class User implements IDBAccessObject { } $proposedUser = User::newFromId( $sId ); - $proposedUser->load( self::READ_LATEST ); if ( !$proposedUser->isLoggedIn() ) { // Not a valid ID return false; @@ -1235,7 +1212,7 @@ class User implements IDBAccessObject { self::selectFields(), array( 'user_id' => $this->mId ), __METHOD__, - ( $flags & self::READ_LOCKING == self::READ_LOCKING ) + ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING ) ? array( 'LOCK IN SHARE MODE' ) : array() ); @@ -1436,38 +1413,86 @@ class User implements IDBAccessObject { public function addAutopromoteOnceGroups( $event ) { global $wgAutopromoteOnceLogInRC, $wgAuth; - $toPromote = array(); - if ( !wfReadOnly() && $this->getId() ) { - $toPromote = Autopromote::getAutopromoteOnceGroups( $this, $event ); - if ( count( $toPromote ) ) { - $oldGroups = $this->getGroups(); // previous groups + if ( wfReadOnly() || !$this->getId() ) { + return array(); + } - foreach ( $toPromote as $group ) { - $this->addGroup( $group ); - } - // update groups in external authentication database - $wgAuth->updateExternalDBGroups( $this, $toPromote ); + $toPromote = Autopromote::getAutopromoteOnceGroups( $this, $event ); + if ( !count( $toPromote ) ) { + return array(); + } - $newGroups = array_merge( $oldGroups, $toPromote ); // all groups + if ( !$this->checkAndSetTouched() ) { + return array(); // raced out (bug T48834) + } - $logEntry = new ManualLogEntry( 'rights', 'autopromote' ); - $logEntry->setPerformer( $this ); - $logEntry->setTarget( $this->getUserPage() ); - $logEntry->setParameters( array( - '4::oldgroups' => $oldGroups, - '5::newgroups' => $newGroups, - ) ); - $logid = $logEntry->insert(); - if ( $wgAutopromoteOnceLogInRC ) { - $logEntry->publish( $logid ); - } - } + $oldGroups = $this->getGroups(); // previous groups + foreach ( $toPromote as $group ) { + $this->addGroup( $group ); + } + // update groups in external authentication database + Hooks::run( 'UserGroupsChanged', array( $this, $toPromote, array(), false ) ); + $wgAuth->updateExternalDBGroups( $this, $toPromote ); + + $newGroups = array_merge( $oldGroups, $toPromote ); // all groups + + $logEntry = new ManualLogEntry( 'rights', 'autopromote' ); + $logEntry->setPerformer( $this ); + $logEntry->setTarget( $this->getUserPage() ); + $logEntry->setParameters( array( + '4::oldgroups' => $oldGroups, + '5::newgroups' => $newGroups, + ) ); + $logid = $logEntry->insert(); + if ( $wgAutopromoteOnceLogInRC ) { + $logEntry->publish( $logid ); } return $toPromote; } /** + * Bump user_touched if it didn't change since this object was loaded + * + * On success, the mTouched field is updated. + * The user serialization cache is always cleared. + * + * @return bool Whether user_touched was actually updated + * @since 1.26 + */ + protected function checkAndSetTouched() { + $this->load(); + + if ( !$this->mId ) { + return false; // anon + } + + // Get a new user_touched that is higher than the old one + $oldTouched = $this->mTouched; + $newTouched = $this->newTouchedTimestamp(); + + $dbw = wfGetDB( DB_MASTER ); + $dbw->update( 'user', + array( 'user_touched' => $dbw->timestamp( $newTouched ) ), + array( + 'user_id' => $this->mId, + 'user_touched' => $dbw->timestamp( $oldTouched ) // CAS check + ), + __METHOD__ + ); + $success = ( $dbw->affectedRows() > 0 ); + + if ( $success ) { + $this->mTouched = $newTouched; + } + + // Clears on failure too since that is desired if the cache is stale + $this->clearSharedCache(); + + return $success; + } + + /** * Clear various cached data stored in this object. The cache of the user table * data (i.e. self::$mCacheVars) is not cleared unless $reloadFrom is given. * @@ -1566,7 +1591,7 @@ class User implements IDBAccessObject { # We only need to worry about passing the IP address to the Block generator if the # user is not immune to autoblocks/hardblocks, and they are the current user so we # know which IP address they're actually coming from - if ( !$this->isAllowed( 'ipblock-exempt' ) && $this->getID() == $wgUser->getID() ) { + if ( !$this->isAllowed( 'ipblock-exempt' ) && $this->equals( $wgUser ) ) { $ip = $this->getRequest()->getIP(); } else { $ip = null; @@ -1968,6 +1993,7 @@ class User implements IDBAccessObject { global $wgAuth; $authUser = $wgAuth->getUserInstance( $this ); $this->mLocked = (bool)$authUser->isLocked(); + Hooks::run( 'UserIsLocked', array( $this, &$this->mLocked ) ); return $this->mLocked; } @@ -1985,6 +2011,7 @@ class User implements IDBAccessObject { global $wgAuth; $authUser = $wgAuth->getUserInstance( $this ); $this->mHideName = (bool)$authUser->isHidden(); + Hooks::run( 'UserIsHidden', array( $this, &$this->mHideName ) ); } return $this->mHideName; } @@ -2133,6 +2160,7 @@ class User implements IDBAccessObject { && $newMessageLinks[0]['wiki'] === wfWikiID() && $newMessageLinks[0]['rev'] ) { + /** @var Revision $newMessageRevision */ $newMessageRevision = $newMessageLinks[0]['rev']; $newMessageRevisionId = $newMessageRevision->getId(); } @@ -2209,8 +2237,6 @@ class User implements IDBAccessObject { * page. Ignored if null or !$val. */ public function setNewtalk( $val, $curRev = null ) { - global $wgMemc; - if ( wfReadOnly() ) { return; } @@ -2232,12 +2258,6 @@ class User implements IDBAccessObject { $changed = $this->deleteNewtalk( $field, $id ); } - if ( $this->isAnon() ) { - // Anons have a separate memcached space, since - // user records aren't kept for them. - $key = wfMemcKey( 'newtalk', 'ip', $id ); - $wgMemc->set( $key, $val ? 1 : 0, 1800 ); - } if ( $changed ) { $this->invalidateCache(); } @@ -2267,11 +2287,10 @@ class User implements IDBAccessObject { * Called implicitly from invalidateCache() and saveSettings(). */ public function clearSharedCache() { - global $wgMemc; - - $this->load(); - if ( $this->mId ) { - $wgMemc->delete( wfMemcKey( 'user', 'id', $this->mId ) ); + $id = $this->getId(); + if ( $id ) { + $key = wfMemcKey( 'user', 'id', $id ); + ObjectCache::getMainWANInstance()->delete( $key ); } } @@ -2298,15 +2317,11 @@ class User implements IDBAccessObject { * @since 1.25 */ public function touch() { - global $wgMemc; - - $this->load(); - - if ( $this->mId ) { - $key = wfMemcKey( 'user-quicktouched', 'id', $this->mId ); - $timestamp = $this->newTouchedTimestamp(); - $wgMemc->set( $key, $timestamp ); - $this->mQuickTouched = $timestamp; + $id = $this->getId(); + if ( $id ) { + $key = wfMemcKey( 'user-quicktouched', 'id', $id ); + ObjectCache::getMainWANInstance()->touchCheckKey( $key ); + $this->mQuickTouched = null; } } @@ -2321,23 +2336,21 @@ class User implements IDBAccessObject { /** * Get the user touched timestamp + * + * Use this value only to validate caches via inequalities + * such as in the case of HTTP If-Modified-Since response logic + * * @return string TS_MW Timestamp */ public function getTouched() { - global $wgMemc; - $this->load(); if ( $this->mId ) { if ( $this->mQuickTouched === null ) { $key = wfMemcKey( 'user-quicktouched', 'id', $this->mId ); - $timestamp = $wgMemc->get( $key ); - if ( $timestamp ) { - $this->mQuickTouched = $timestamp; - } else { - # Set the timestamp to get HTTP 304 cache hits - $this->touch(); - } + $cache = ObjectCache::getMainWANInstance(); + + $this->mQuickTouched = wfTimestamp( TS_MW, $cache->getCheckKeyTime( $key ) ); } return max( $this->mTouched, $this->mQuickTouched ); @@ -2347,6 +2360,17 @@ class User implements IDBAccessObject { } /** + * Get the user_touched timestamp field (time of last DB updates) + * @return string TS_MW Timestamp + * @since 1.26 + */ + public function getDBTouched() { + $this->load(); + + return $this->mTouched; + } + + /** * @return Password * @since 1.24 */ @@ -2416,6 +2440,7 @@ class User implements IDBAccessObject { */ public function setInternalPassword( $str ) { $this->setToken(); + $this->setOption( 'watchlisttoken', false ); $passwordFactory = self::getPasswordFactory(); $this->mPassword = $passwordFactory->newFromPlaintext( $str ); @@ -2693,20 +2718,24 @@ class User implements IDBAccessObject { * @return string|bool User's current value for the option, or false if this option is disabled. * @see resetTokenFromOption() * @see getOption() + * @deprecated 1.26 Applications should use the OAuth extension */ public function getTokenFromOption( $oname ) { global $wgHiddenPrefs; - if ( in_array( $oname, $wgHiddenPrefs ) ) { + + $id = $this->getId(); + if ( !$id || in_array( $oname, $wgHiddenPrefs ) ) { return false; } $token = $this->getOption( $oname ); if ( !$token ) { - $token = $this->resetTokenFromOption( $oname ); - if ( !wfReadOnly() ) { - $this->saveSettings(); - } + // Default to a value based on the user token to avoid space + // wasted on storing tokens for all users. When this option + // is set manually by the user, only then is it stored. + $token = hash_hmac( 'sha1', "$oname:$id", $this->getToken() ); } + return $token; } @@ -3186,10 +3215,10 @@ class User implements IDBAccessObject { /** * Check if user is allowed to access a feature / make an action * - * @param string $permissions,... Permissions to test + * @param string ... Permissions to test * @return bool True if user is allowed to perform *any* of the given actions */ - public function isAllowedAny( /*...*/ ) { + public function isAllowedAny() { $permissions = func_get_args(); foreach ( $permissions as $permission ) { if ( $this->isAllowed( $permission ) ) { @@ -3201,10 +3230,10 @@ class User implements IDBAccessObject { /** * - * @param string $permissions,... Permissions to test + * @param string ... Permissions to test * @return bool True if the user is allowed to perform *all* of the given actions */ - public function isAllowedAll( /*...*/ ) { + public function isAllowedAll() { $permissions = func_get_args(); foreach ( $permissions as $permission ) { if ( !$this->isAllowed( $permission ) ) { @@ -3368,19 +3397,24 @@ class User implements IDBAccessObject { return; } - $nextid = $oldid ? $title->getNextRevisionID( $oldid ) : null; + $that = $this; + // Try to update the DB post-send and only if needed... + DeferredUpdates::addCallableUpdate( function() use ( $that, $title, $oldid ) { + if ( !$that->getNewtalk() ) { + return; // no notifications to clear + } - if ( !$oldid || !$nextid ) { - // If we're looking at the latest revision, we should definitely clear it - $this->setNewtalk( false ); - } else { - // Otherwise we should update its revision, if it's present - if ( $this->getNewtalk() ) { - // Naturally the other one won't clear by itself - $this->setNewtalk( false ); - $this->setNewtalk( true, Revision::newFromId( $nextid ) ); + // Delete the last notifications (they stack up) + $that->setNewtalk( false ); + + // If there is a new, unseen, revision, use its timestamp + $nextid = $oldid + ? $title->getNextRevisionID( $oldid, Title::GAID_FOR_UPDATE ) + : null; + if ( $nextid ) { + $that->setNewtalk( true, Revision::newFromId( $nextid ) ); } - } + } ); } if ( !$wgUseEnotif && !$wgShowUpdatedMarker ) { @@ -3401,7 +3435,9 @@ class User implements IDBAccessObject { $force = 'force'; } - $this->getWatchedItem( $title )->resetNotificationTimestamp( $force, $oldid ); + $this->getWatchedItem( $title )->resetNotificationTimestamp( + $force, $oldid, WatchedItem::DEFERRED + ); } /** @@ -3430,7 +3466,7 @@ class User implements IDBAccessObject { $dbw = wfGetDB( DB_MASTER ); $dbw->update( 'watchlist', array( /* SET */ 'wl_notificationtimestamp' => null ), - array( /* WHERE */ 'wl_user' => $id ), + array( /* WHERE */ 'wl_user' => $id, 'wl_notificationtimestamp IS NOT NULL' ), __METHOD__ ); // We also need to clear here the "you have new message" notification for the own user_talk page; @@ -3477,6 +3513,31 @@ class User implements IDBAccessObject { } /** + * Set an extended login cookie on the user's client. The expiry of the cookie + * is controlled by the $wgExtendedLoginCookieExpiration configuration + * variable. + * + * @see User::setCookie + * + * @param string $name Name of the cookie to set + * @param string $value Value to set + * @param bool $secure + * true: Force setting the secure attribute when setting the cookie + * false: Force NOT setting the secure attribute when setting the cookie + * null (default): Use the default ($wgCookieSecure) to set the secure attribute + */ + protected function setExtendedLoginCookie( $name, $value, $secure ) { + global $wgExtendedLoginCookieExpiration, $wgCookieExpiration; + + $exp = time(); + $exp += $wgExtendedLoginCookieExpiration !== null + ? $wgExtendedLoginCookieExpiration + : $wgCookieExpiration; + + $this->setCookie( $name, $value, $exp, $secure ); + } + + /** * Set the default cookies for this session on the user's client. * * @param WebRequest|null $request WebRequest object to use; $wgRequest will be used if null @@ -3485,6 +3546,8 @@ class User implements IDBAccessObject { * @param bool $rememberMe Whether to add a Token cookie for elongated sessions */ public function setCookies( $request = null, $secure = null, $rememberMe = false ) { + global $wgExtendedLoginCookies; + if ( $request === null ) { $request = $this->getRequest(); } @@ -3526,6 +3589,8 @@ class User implements IDBAccessObject { foreach ( $cookies as $name => $value ) { if ( $value === false ) { $this->clearCookie( $name ); + } elseif ( $rememberMe && in_array( $name, $wgExtendedLoginCookies ) ) { + $this->setExtendedLoginCookie( $name, $value, $secure ); } else { $this->setCookie( $name, $value, 0, $secure, array(), $request ); } @@ -3598,17 +3663,11 @@ class User implements IDBAccessObject { return; // anon } - // This method is for updating existing users, so the user should - // have been loaded from the master to begin with to avoid problems. - if ( !( $this->queryFlagsUsed & self::READ_LATEST ) ) { - wfWarn( "Attempting to save slave-loaded User object with ID '{$this->mId}'." ); - } - // Get a new user_touched that is higher than the old one. // This will be used for a CAS check as a last-resort safety // check against race conditions and slave lag. $oldTouched = $this->mTouched; - $this->mTouched = $this->newTouchedTimestamp(); + $newTouched = $this->newTouchedTimestamp(); if ( !$wgAuth->allowSetLocalPassword() ) { $this->mPassword = self::getPasswordFactory()->newFromCiphertext( null ); @@ -3624,7 +3683,7 @@ class User implements IDBAccessObject { 'user_real_name' => $this->mRealName, 'user_email' => $this->mEmail, 'user_email_authenticated' => $dbw->timestampOrNull( $this->mEmailAuthenticated ), - 'user_touched' => $dbw->timestamp( $this->mTouched ), + 'user_touched' => $dbw->timestamp( $newTouched ), 'user_token' => strval( $this->mToken ), 'user_email_token' => $this->mEmailToken, 'user_email_token_expires' => $dbw->timestampOrNull( $this->mEmailTokenExpires ), @@ -3636,16 +3695,17 @@ class User implements IDBAccessObject { ); if ( !$dbw->affectedRows() ) { - // User was changed in the meantime or loaded with stale data - MWExceptionHandler::logException( new MWException( - "CAS update failed on user_touched for user ID '{$this->mId}'." - ) ); // Maybe the problem was a missed cache update; clear it to be safe $this->clearSharedCache(); - - return; + // User was changed in the meantime or loaded with stale data + $from = ( $this->queryFlagsUsed & self::READ_LATEST ) ? 'master' : 'slave'; + throw new MWException( + "CAS update failed on user_touched for user ID '{$this->mId}' (read from $from);" . + " the version of the user to be saved is older than the current version." + ); } + $this->mTouched = $newTouched; $this->saveOptions(); Hooks::run( 'UserSaveSettings', array( $this ) ); @@ -3655,20 +3715,28 @@ class User implements IDBAccessObject { /** * If only this user's username is known, and it exists, return the user ID. + * + * @param int $flags Bitfield of User:READ_* constants; useful for existence checks * @return int */ - public function idForName() { + public function idForName( $flags = 0 ) { $s = trim( $this->getName() ); if ( $s === '' ) { return 0; } - $dbr = wfGetDB( DB_SLAVE ); - $id = $dbr->selectField( 'user', 'user_id', array( 'user_name' => $s ), __METHOD__ ); - if ( $id === false ) { - $id = 0; - } - return $id; + $db = ( ( $flags & self::READ_LATEST ) == self::READ_LATEST ) + ? wfGetDB( DB_MASTER ) + : wfGetDB( DB_SLAVE ); + + $options = ( ( $flags & self::READ_LOCKING ) == self::READ_LOCKING ) + ? array( 'LOCK IN SHARE MODE' ) + : array(); + + $id = $db->selectField( 'user', + 'user_id', array( 'user_name' => $s ), __METHOD__, $options ); + + return (int)$id; } /** @@ -4172,22 +4240,25 @@ class User implements IDBAccessObject { * * @param string $subject Message subject * @param string $body Message body - * @param string $from Optional From address; if unspecified, default + * @param User|null $from Optional sending user; if unspecified, default * $wgPasswordSender will be used. * @param string $replyto Reply-To address * @return Status */ public function sendMail( $subject, $body, $from = null, $replyto = null ) { - if ( is_null( $from ) ) { - global $wgPasswordSender; + global $wgPasswordSender; + + if ( $from instanceof User ) { + $sender = MailAddress::newFromUser( $from ); + } else { $sender = new MailAddress( $wgPasswordSender, wfMessage( 'emailsender' )->inContentLanguage()->text() ); - } else { - $sender = MailAddress::newFromUser( $from ); } - $to = MailAddress::newFromUser( $this ); - return UserMailer::send( $to, $sender, $subject, $body, $replyto ); + + return UserMailer::send( $to, $sender, $subject, $body, array( + 'replyTo' => $replyto, + ) ); } /** @@ -4745,37 +4816,50 @@ class User implements IDBAccessObject { } /** + * Deferred version of incEditCountImmediate() + */ + public function incEditCount() { + $that = $this; + wfGetDB( DB_MASTER )->onTransactionPreCommitOrIdle( function() use ( $that ) { + $that->incEditCountImmediate(); + } ); + } + + /** * Increment the user's edit-count field. * Will have no effect for anonymous users. + * @since 1.26 */ - public function incEditCount() { - if ( !$this->isAnon() ) { - $dbw = wfGetDB( DB_MASTER ); - $dbw->update( - 'user', - array( 'user_editcount=user_editcount+1' ), - array( 'user_id' => $this->getId() ), - __METHOD__ - ); + public function incEditCountImmediate() { + if ( $this->isAnon() ) { + return; + } - // Lazy initialization check... - if ( $dbw->affectedRows() == 0 ) { - // Now here's a goddamn hack... - $dbr = wfGetDB( DB_SLAVE ); - if ( $dbr !== $dbw ) { - // If we actually have a slave server, the count is - // at least one behind because the current transaction - // has not been committed and replicated. - $this->initEditCount( 1 ); - } else { - // But if DB_SLAVE is selecting the master, then the - // count we just read includes the revision that was - // just added in the working transaction. - $this->initEditCount(); - } + $dbw = wfGetDB( DB_MASTER ); + // No rows will be "affected" if user_editcount is NULL + $dbw->update( + 'user', + array( 'user_editcount=user_editcount+1' ), + array( 'user_id' => $this->getId() ), + __METHOD__ + ); + // Lazy initialization check... + if ( $dbw->affectedRows() == 0 ) { + // Now here's a goddamn hack... + $dbr = wfGetDB( DB_SLAVE ); + if ( $dbr !== $dbw ) { + // If we actually have a slave server, the count is + // at least one behind because the current transaction + // has not been committed and replicated. + $this->initEditCount( 1 ); + } else { + // But if DB_SLAVE is selecting the master, then the + // count we just read includes the revision that was + // just added in the working transaction. + $this->initEditCount(); } } - // edit count in user cache too + // Edit count in user cache too $this->invalidateCache(); } diff --git a/includes/UserRightsProxy.php b/includes/UserRightsProxy.php index 1b9e4b69..a19f6984 100644 --- a/includes/UserRightsProxy.php +++ b/includes/UserRightsProxy.php @@ -278,8 +278,8 @@ class UserRightsProxy { array( 'user_id' => $this->id ), __METHOD__ ); - global $wgMemc; + $cache = ObjectCache::getMainWANInstance(); $key = wfForeignMemcKey( $this->database, false, 'user', 'id', $this->id ); - $wgMemc->delete( $key ); + $cache->delete( $key ); } } diff --git a/includes/WatchedItem.php b/includes/WatchedItem.php index 4d226924..adee1264 100644 --- a/includes/WatchedItem.php +++ b/includes/WatchedItem.php @@ -27,20 +27,6 @@ * @ingroup Watchlist */ class WatchedItem { - /** - * Constant to specify that user rights 'editmywatchlist' and - * 'viewmywatchlist' should not be checked. - * @since 1.22 - */ - const IGNORE_USER_RIGHTS = 0; - - /** - * Constant to specify that user rights 'editmywatchlist' and - * 'viewmywatchlist' should be checked. - * @since 1.22 - */ - const CHECK_USER_RIGHTS = 1; - /** @var Title */ public $mTitle; @@ -60,6 +46,31 @@ class WatchedItem { private $timestamp; /** + * Constant to specify that user rights 'editmywatchlist' and + * 'viewmywatchlist' should not be checked. + * @since 1.22 + */ + const IGNORE_USER_RIGHTS = 0; + + /** + * Constant to specify that user rights 'editmywatchlist' and + * 'viewmywatchlist' should be checked. + * @since 1.22 + */ + const CHECK_USER_RIGHTS = 1; + + /** + * Do DB master updates right now + * @since 1.26 + */ + const IMMEDIATE = 0; + /** + * Do DB master updates via the job queue + * @since 1.26 + */ + const DEFERRED = 1; + + /** * Create a WatchedItem object with the given user and title * @since 1.22 $checkRights parameter added * @param User $user The user to use for (un)watching @@ -208,8 +219,11 @@ class WatchedItem { * @param bool $force Whether to force the write query to be executed even if the * page is not watched or the notification timestamp is already NULL. * @param int $oldid The revision id being viewed. If not given or 0, latest revision is assumed. + * @mode int $mode WatchedItem::DEFERRED/IMMEDIATE */ - public function resetNotificationTimestamp( $force = '', $oldid = 0 ) { + public function resetNotificationTimestamp( + $force = '', $oldid = 0, $mode = self::IMMEDIATE + ) { // Only loggedin user can have a watchlist if ( wfReadOnly() || $this->mUser->isAnon() || !$this->isAllowed( 'editmywatchlist' ) ) { return; @@ -240,11 +254,7 @@ class WatchedItem { } else { // Oldid given and isn't the latest; update the timestamp. // This will result in no further notification emails being sent! - $dbr = wfGetDB( DB_SLAVE ); - $notificationTimestamp = $dbr->selectField( - 'revision', 'rev_timestamp', - array( 'rev_page' => $title->getArticleID(), 'rev_id' => $oldid ) - ); + $notificationTimestamp = Revision::getTimestampFromId( $title, $oldid ); // We need to go one second to the future because of various strict comparisons // throughout the codebase $ts = new MWTimestamp( $notificationTimestamp ); @@ -262,11 +272,30 @@ class WatchedItem { } } - // If the page is watched by the user (or may be watched), update the timestamp on any - // any matching rows - $dbw = wfGetDB( DB_MASTER ); - $dbw->update( 'watchlist', array( 'wl_notificationtimestamp' => $notificationTimestamp ), - $this->dbCond(), __METHOD__ ); + // If the page is watched by the user (or may be watched), update the timestamp + if ( $mode === self::DEFERRED ) { + $job = new ActivityUpdateJob( + $title, + array( + 'type' => 'updateWatchlistNotification', + 'userid' => $this->getUserId(), + 'notifTime' => $notificationTimestamp, + 'curTime' => time() + ) + ); + // Try to run this post-send + DeferredUpdates::addCallableUpdate( function() use ( $job ) { + $job->run(); + } ); + } else { + $dbw = wfGetDB( DB_MASTER ); + $dbw->update( 'watchlist', + array( 'wl_notificationtimestamp' => $notificationTimestamp ), + $this->dbCond(), + __METHOD__ + ); + } + $this->timestamp = null; } diff --git a/includes/WebRequest.php b/includes/WebRequest.php index 054eceb9..b4b8be9b 100644 --- a/includes/WebRequest.php +++ b/includes/WebRequest.php @@ -39,6 +39,12 @@ class WebRequest { protected $data, $headers = array(); /** + * Flag to make WebRequest::getHeader return an array of values. + * @since 1.26 + */ + const GETHEADER_LIST = 1; + + /** * Lazy-init response object * @var WebResponse */ @@ -98,9 +104,9 @@ class WebRequest { if ( !preg_match( '!^https?://!', $url ) ) { $url = 'http://unused' . $url; } - wfSuppressWarnings(); + MediaWiki\suppressWarnings(); $a = parse_url( $url ); - wfRestoreWarnings(); + MediaWiki\restoreWarnings(); if ( $a ) { $path = isset( $a['path'] ) ? $a['path'] : ''; @@ -170,6 +176,8 @@ class WebRequest { * @return string */ public static function detectServer() { + global $wgAssumeProxiesUseDefaultProtocolPorts; + $proto = self::detectProtocol(); $stdPort = $proto === 'https' ? 443 : 80; @@ -180,13 +188,15 @@ class WebRequest { if ( !isset( $_SERVER[$varName] ) ) { continue; } + $parts = IP::splitHostAndPort( $_SERVER[$varName] ); if ( !$parts ) { // Invalid, do not use continue; } + $host = $parts[0]; - if ( isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) ) { + if ( $wgAssumeProxiesUseDefaultProtocolPorts && isset( $_SERVER['HTTP_X_FORWARDED_PROTO'] ) ) { // Bug 70021: Assume that upstream proxy is running on the default // port based on the protocol. We have no reliable way to determine // the actual port in use upstream. @@ -685,7 +695,7 @@ class WebRequest { // This shouldn't happen! throw new MWException( "Web server doesn't provide either " . "REQUEST_URI, HTTP_X_ORIGINAL_URL or SCRIPT_NAME. Report details " . - "of your web server configuration to http://bugzilla.wikimedia.org/" ); + "of your web server configuration to https://phabricator.wikimedia.org/" ); } // User-agents should not send a fragment with the URI, but // if they do, and the web server passes it on to us, we @@ -768,7 +778,7 @@ class WebRequest { * * @param int $deflimit Limit to use if no input and the user hasn't set the option. * @param string $optionname To specify an option other than rclimit to pull from. - * @return array First element is limit, second is offset + * @return int[] First element is limit, second is offset */ public function getLimitOffset( $deflimit = 50, $optionname = 'rclimit' ) { global $wgUser; @@ -861,7 +871,7 @@ class WebRequest { /** * Initialise the header list */ - private function initHeaders() { + protected function initHeaders() { if ( count( $this->headers ) ) { return; } @@ -894,19 +904,28 @@ class WebRequest { } /** - * Get a request header, or false if it isn't set - * @param string $name Case-insensitive header name + * Get a request header, or false if it isn't set. * - * @return string|bool False on failure - */ - public function getHeader( $name ) { + * @param string $name Case-insensitive header name + * @param int $flags Bitwise combination of: + * WebRequest::GETHEADER_LIST Treat the header as a comma-separated list + * of values, as described in RFC 2616 § 4.2. + * (since 1.26). + * @return string|array|bool False if header is unset; otherwise the + * header value(s) as either a string (the default) or an array, if + * WebRequest::GETHEADER_LIST flag was set. + */ + public function getHeader( $name, $flags = 0 ) { $this->initHeaders(); $name = strtoupper( $name ); - if ( isset( $this->headers[$name] ) ) { - return $this->headers[$name]; - } else { + if ( !isset( $this->headers[$name] ) ) { return false; } + $value = $this->headers[$name]; + if ( $flags & self::GETHEADER_LIST ) { + $value = array_map( 'trim', explode( ',', $value ) ); + } + return $value; } /** @@ -1278,6 +1297,7 @@ class FauxRequest extends WebRequest { private $wasPosted = false; private $session = array(); private $requestUrl; + protected $cookies = array(); /** * @param array $data Array of *non*-urlencoded key => value pairs, the @@ -1305,11 +1325,10 @@ class FauxRequest extends WebRequest { } /** - * @param string $method - * @throws MWException + * Initialise the header list */ - private function notImplemented( $method ) { - throw new MWException( "{$method}() not implemented" ); + protected function initHeaders() { + // Nothing to init } /** @@ -1352,7 +1371,38 @@ class FauxRequest extends WebRequest { } public function getCookie( $key, $prefix = null, $default = null ) { - return $default; + if ( $prefix === null ) { + global $wgCookiePrefix; + $prefix = $wgCookiePrefix; + } + $name = $prefix . $key; + return isset( $this->cookies[$name] ) ? $this->cookies[$name] : $default; + } + + /** + * @since 1.26 + * @param string $name Unprefixed name of the cookie to set + * @param string|null $value Value of the cookie to set + * @param string|null $prefix Cookie prefix. Defaults to $wgCookiePrefix + */ + public function setCookie( $key, $value, $prefix = null ) { + $this->setCookies( array( $key => $value ), $prefix ); + } + + /** + * @since 1.26 + * @param array $cookies + * @param string|null $prefix Cookie prefix. Defaults to $wgCookiePrefix + */ + public function setCookies( $cookies, $prefix = null ) { + if ( $prefix === null ) { + global $wgCookiePrefix; + $prefix = $wgCookiePrefix; + } + foreach ( $cookies as $key => $value ) { + $name = $prefix . $key; + $this->cookies[$name] = $value; + } } public function checkSessionCookie() { @@ -1375,21 +1425,22 @@ class FauxRequest extends WebRequest { } /** - * @param string $name The name of the header to get (case insensitive). - * @return bool|string + * @param string $name + * @param string $val */ - public function getHeader( $name ) { - $name = strtoupper( $name ); - return isset( $this->headers[$name] ) ? $this->headers[$name] : false; + public function setHeader( $name, $val ) { + $this->setHeaders( array( $name => $val ) ); } /** - * @param string $name - * @param string $val + * @since 1.26 + * @param array $headers */ - public function setHeader( $name, $val ) { - $name = strtoupper( $name ); - $this->headers[$name] = $val; + public function setHeaders( $headers ) { + foreach ( $headers as $name => $val ) { + $name = strtoupper( $name ); + $this->headers[$name] = $val; + } } /** @@ -1488,8 +1539,8 @@ class DerivativeRequest extends FauxRequest { return $this->base->checkSessionCookie(); } - public function getHeader( $name ) { - return $this->base->getHeader( $name ); + public function getHeader( $name, $flags = 0 ) { + return $this->base->getHeader( $name, $flags ); } public function getAllHeaders() { diff --git a/includes/WebResponse.php b/includes/WebResponse.php index ab34931c..1b6947cd 100644 --- a/includes/WebResponse.php +++ b/includes/WebResponse.php @@ -28,7 +28,7 @@ class WebResponse { /** - * Output a HTTP header, wrapper for PHP's header() + * Output an HTTP header, wrapper for PHP's header() * @param string $string Header to output * @param bool $replace Replace current similar header * @param null|int $http_response_code Forces the HTTP response code to the specified value. @@ -54,6 +54,15 @@ class WebResponse { } /** + * Output an HTTP status code header + * @since 1.26 + * @param int $code Status code + */ + public function statusHeader( $code ) { + HttpStatus::header( $code ); + } + + /** * Set the browser cookie * @param string $name The name of the cookie. * @param string $value The value to be stored in the cookie. @@ -163,6 +172,14 @@ class FauxResponse extends WebResponse { } /** + * @since 1.26 + * @param int $code Status code + */ + public function statusHeader( $code ) { + $this->code = intval( $code ); + } + + /** * @param string $key The name of the header to get (case insensitive). * @return string|null The header value (if set); null otherwise. */ diff --git a/includes/WebStart.php b/includes/WebStart.php index 9c71f3e1..f5a4f93b 100644 --- a/includes/WebStart.php +++ b/includes/WebStart.php @@ -78,7 +78,6 @@ if ( $IP === false ) { # Grab profiling functions require_once "$IP/includes/profiler/ProfilerFunctions.php"; -$wgRUstart = wfGetRusage() ?: array(); # Start the autoloader, so that extensions can derive classes from core files require_once "$IP/includes/AutoLoader.php"; @@ -137,3 +136,8 @@ if ( ob_get_level() == 0 ) { if ( !defined( 'MW_NO_SETUP' ) ) { require_once "$IP/includes/Setup.php"; } + +# Multiple DBs or commits might be used; keep the request as transactional as possible +if ( isset( $_SERVER['REQUEST_METHOD'] ) && $_SERVER['REQUEST_METHOD'] === 'POST' ) { + ignore_user_abort( true ); +} diff --git a/includes/WikiMap.php b/includes/WikiMap.php index f16f5aa7..027ff72f 100644 --- a/includes/WikiMap.php +++ b/includes/WikiMap.php @@ -108,13 +108,15 @@ class WikiMap { * * @param string $wikiID Wiki'd id (generally database name) * @param string $page Page name (must be normalised before calling this function!) + * @param string|null $fragmentId + * * @return string|bool URL or false if the wiki was not found */ - public static function getForeignURL( $wikiID, $page ) { + public static function getForeignURL( $wikiID, $page, $fragmentId = null ) { $wiki = WikiMap::getWiki( $wikiID ); if ( $wiki ) { - return $wiki->getFullUrl( $page ); + return $wiki->getFullUrl( $page, $fragmentId ); } return false; @@ -147,33 +149,18 @@ class WikiReference { } /** - * @return string - * @throws MWException - */ - public function getHostname() { - $prefixes = array( 'http://', 'https://' ); - foreach ( $prefixes as $prefix ) { - if ( substr( $this->mCanonicalServer, 0, strlen( $prefix ) ) ) { - return substr( $this->mCanonicalServer, strlen( $prefix ) ); - } - } - throw new MWException( "Invalid hostname for wiki {$this->mMinor}.{$this->mMajor}" ); - } - - /** * Get the URL in a way to be displayed to the user * More or less Wikimedia specific * * @return string */ public function getDisplayName() { - $url = $this->getUrl( '' ); - $parsed = wfParseUrl( $url ); + $parsed = wfParseUrl( $this->mCanonicalServer ); if ( $parsed ) { return $parsed['host']; } else { - // Invalid URL. There's no sane thing to do here, so just return it - return $url; + // Invalid server spec. There's no sane thing to do here, so just return the canonical server name in full + return $this->mCanonicalServer; } } @@ -181,21 +168,32 @@ class WikiReference { * Helper function for getUrl() * * @todo FIXME: This may be generalized... - * @param string $page Page name (must be normalised before calling this function!) - * @return string Url fragment + * + * @param string $page Page name (must be normalised before calling this function! May contain a section part.) + * @param string|null $fragmentId + * + * @return string relative URL, without the server part. */ - private function getLocalUrl( $page ) { - return str_replace( '$1', wfUrlEncode( str_replace( ' ', '_', $page ) ), $this->mPath ); + private function getLocalUrl( $page, $fragmentId = null ) { + $page = wfUrlEncode( str_replace( ' ', '_', $page ) ); + + if ( is_string( $fragmentId ) && $fragmentId !== '' ) { + $page .= '#' . wfUrlEncode( $fragmentId ); + } + + return str_replace( '$1', $page, $this->mPath ); } /** * Get a canonical (i.e. based on $wgCanonicalServer) URL to a page on this foreign wiki * * @param string $page Page name (must be normalised before calling this function!) + * @param string|null $fragmentId + * * @return string Url */ - public function getCanonicalUrl( $page ) { - return $this->mCanonicalServer . $this->getLocalUrl( $page ); + public function getCanonicalUrl( $page, $fragmentId = null ) { + return $this->mCanonicalServer . $this->getLocalUrl( $page, $fragmentId ); } /** @@ -209,10 +207,12 @@ class WikiReference { /** * Alias for getCanonicalUrl(), for backwards compatibility. * @param string $page + * @param string|null $fragmentId + * * @return string */ - public function getUrl( $page ) { - return $this->getCanonicalUrl( $page ); + public function getUrl( $page, $fragmentId = null ) { + return $this->getCanonicalUrl( $page, $fragmentId ); } /** @@ -220,10 +220,12 @@ class WikiReference { * when called locally on the wiki. * * @param string $page Page name (must be normalized before calling this function!) + * @param string|null $fragmentId + * * @return string URL */ - public function getFullUrl( $page ) { + public function getFullUrl( $page, $fragmentId = null ) { return $this->mServer . - $this->getLocalUrl( $page ); + $this->getLocalUrl( $page, $fragmentId ); } } diff --git a/includes/Xml.php b/includes/Xml.php index f0bd70b2..37cffdef 100644 --- a/includes/Xml.php +++ b/includes/Xml.php @@ -144,26 +144,19 @@ class Xml { public static function monthSelector( $selected = '', $allmonths = null, $id = 'month' ) { global $wgLang; $options = array(); + $data = new XmlSelect( 'month', $id, $selected ); if ( is_null( $selected ) ) { $selected = ''; } if ( !is_null( $allmonths ) ) { - $options[] = self::option( - wfMessage( 'monthsall' )->text(), - $allmonths, - $selected === $allmonths - ); + $options[wfMessage( 'monthsall' )->text()] = $allmonths; } for ( $i = 1; $i < 13; $i++ ) { - $options[] = self::option( $wgLang->getMonthName( $i ), $i, $selected === $i ); + $options[$wgLang->getMonthName( $i )] = $i; } - return self::openElement( 'select', array( - 'id' => $id, - 'name' => 'month', - 'class' => 'mw-month-selector' - ) ) - . implode( "\n", $options ) - . self::closeElement( 'select' ); + $data->addOptions( $options ); + $data->setAttribute( 'class', 'mw-month-selector' ); + return $data->getHTML(); } /** @@ -871,112 +864,6 @@ class Xml { } } -class XmlSelect { - protected $options = array(); - protected $default = false; - protected $attributes = array(); - - public function __construct( $name = false, $id = false, $default = false ) { - if ( $name ) { - $this->setAttribute( 'name', $name ); - } - - if ( $id ) { - $this->setAttribute( 'id', $id ); - } - - if ( $default !== false ) { - $this->default = $default; - } - } - - /** - * @param string $default - */ - public function setDefault( $default ) { - $this->default = $default; - } - - /** - * @param string $name - * @param array $value - */ - public function setAttribute( $name, $value ) { - $this->attributes[$name] = $value; - } - - /** - * @param string $name - * @return array|null - */ - public function getAttribute( $name ) { - if ( isset( $this->attributes[$name] ) ) { - return $this->attributes[$name]; - } else { - return null; - } - } - - /** - * @param string $name - * @param bool $value - */ - public function addOption( $name, $value = false ) { - // Stab stab stab - $value = $value !== false ? $value : $name; - - $this->options[] = array( $name => $value ); - } - - /** - * This accepts an array of form - * label => value - * label => ( label => value, label => value ) - * - * @param array $options - */ - public function addOptions( $options ) { - $this->options[] = $options; - } - - /** - * This accepts an array of form - * label => value - * label => ( label => value, label => value ) - * - * @param array $options - * @param bool $default - * @return string - */ - static function formatOptions( $options, $default = false ) { - $data = ''; - - foreach ( $options as $label => $value ) { - if ( is_array( $value ) ) { - $contents = self::formatOptions( $value, $default ); - $data .= Html::rawElement( 'optgroup', array( 'label' => $label ), $contents ) . "\n"; - } else { - $data .= Xml::option( $label, $value, $value === $default ) . "\n"; - } - } - - return $data; - } - - /** - * @return string - */ - public function getHTML() { - $contents = ''; - - foreach ( $this->options as $options ) { - $contents .= self::formatOptions( $options, $this->default ); - } - - return Html::rawElement( 'select', $this->attributes, rtrim( $contents ) ); - } -} - /** * A wrapper class which causes Xml::encodeJsVar() and Xml::encodeJsCall() to * interpret a given string as being a JavaScript expression, instead of string diff --git a/includes/XmlSelect.php b/includes/XmlSelect.php new file mode 100644 index 00000000..78f47645 --- /dev/null +++ b/includes/XmlSelect.php @@ -0,0 +1,132 @@ +<?php +/** + * Class for generating HTML <select> elements. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * http://www.gnu.org/copyleft/gpl.html + * + * @file + */ + +/** + * Class for generating HTML <select> elements. + */ +class XmlSelect { + protected $options = array(); + protected $default = false; + protected $attributes = array(); + + public function __construct( $name = false, $id = false, $default = false ) { + if ( $name ) { + $this->setAttribute( 'name', $name ); + } + + if ( $id ) { + $this->setAttribute( 'id', $id ); + } + + if ( $default !== false ) { + $this->default = $default; + } + } + + /** + * @param string|array $default + */ + public function setDefault( $default ) { + $this->default = $default; + } + + /** + * @param string $name + * @param string $value + */ + public function setAttribute( $name, $value ) { + $this->attributes[$name] = $value; + } + + /** + * @param string $name + * @return string|null + */ + public function getAttribute( $name ) { + if ( isset( $this->attributes[$name] ) ) { + return $this->attributes[$name]; + } else { + return null; + } + } + + /** + * @param string $label + * @param string $value If not given, assumed equal to $label + */ + public function addOption( $label, $value = false ) { + $value = $value !== false ? $value : $label; + $this->options[] = array( $label => $value ); + } + + /** + * This accepts an array of form + * label => value + * label => ( label => value, label => value ) + * + * @param array $options + */ + public function addOptions( $options ) { + $this->options[] = $options; + } + + /** + * This accepts an array of form: + * label => value + * label => ( label => value, label => value ) + * + * @param array $options + * @param string|array $default + * @return string + */ + static function formatOptions( $options, $default = false ) { + $data = ''; + + foreach ( $options as $label => $value ) { + if ( is_array( $value ) ) { + $contents = self::formatOptions( $value, $default ); + $data .= Html::rawElement( 'optgroup', array( 'label' => $label ), $contents ) . "\n"; + } else { + // If $default is an array, then the <select> probably has the multiple attribute, + // so we should check if each $value is in $default, rather than checking if + // $value is equal to $default. + $selected = is_array( $default ) ? in_array( $value, $default ) : $value === $default; + $data .= Xml::option( $label, $value, $selected ) . "\n"; + } + } + + return $data; + } + + /** + * @return string + */ + public function getHTML() { + $contents = ''; + + foreach ( $this->options as $options ) { + $contents .= self::formatOptions( $options, $this->default ); + } + + return Html::rawElement( 'select', $this->attributes, rtrim( $contents ) ); + } +} diff --git a/includes/ZhConversion.php b/includes/ZhConversion.php index 4be27513..893ae040 100644 --- a/includes/ZhConversion.php +++ b/includes/ZhConversion.php @@ -46,7 +46,6 @@ $zh2Hant = array( '㱮' => '殨', '㲿' => '瀇', '㳔' => '濧', -'㳕' => '灡', '㳠' => '澾', '㳡' => '濄', '㳢' => '𣾷', @@ -79,7 +78,6 @@ $zh2Hant = array( '䍁' => '繸', '䎬' => '䎱', '䏝' => '膞', -'䓕' => '薳', '䓖' => '藭', '䗖' => '螮', '䘛' => '𧝞', @@ -375,6 +373,7 @@ $zh2Hant = array( '啰' => '囉', '啴' => '嘽', '啸' => '嘯', +'喂' => '餵', '喷' => '噴', '喽' => '嘍', '喾' => '嚳', @@ -415,7 +414,6 @@ $zh2Hant = array( '垭' => '埡', '垱' => '壋', '垲' => '塏', -'垴' => '堖', '埘' => '塒', '埙' => '塤', '埚' => '堝', @@ -504,7 +502,6 @@ $zh2Hant = array( '岖' => '嶇', '岗' => '崗', '岘' => '峴', -'岙' => '嶴', '岚' => '嵐', '岛' => '島', '岭' => '嶺', @@ -1373,6 +1370,7 @@ $zh2Hant = array( '脶' => '腡', '脸' => '臉', '腊' => '臘', +'腌' => '醃', '腘' => '膕', '腭' => '齶', '腻' => '膩', @@ -1477,7 +1475,6 @@ $zh2Hant = array( '虑' => '慮', '虚' => '虛', '虫' => '蟲', -'虬' => '虯', '虮' => '蟣', '虽' => '雖', '虾' => '蝦', @@ -2889,7 +2886,6 @@ $zh2Hant = array( '𩧿' => '䮠', '𩨀' => '騔', '𩨁' => '䮞', -'𩨂' => '驄', '𩨃' => '騝', '𩨄' => '騪', '𩨅' => '𩤸', @@ -3002,8 +2998,8 @@ $zh2Hant = array( '𫛢' => '鸋', '𫛶' => '鶒', '𫛸' => '鶗', -'0出現' => '0出現', '0出现' => '0出現', +'0出現' => '0出現', '0出線' => '0出線', '0出线' => '0出線', '0只支持' => '0只支持', @@ -3079,6 +3075,8 @@ $zh2Hant = array( '9余' => '9餘', '·范' => '·范', '’s' => '’s', +'、面点' => '、麵點', +'。个中' => '。箇中', '〇周后' => '〇周後', '〇年' => '〇年', '〇只' => '〇隻', @@ -3116,6 +3114,7 @@ $zh2Hant = array( '一争两丑' => '一爭兩醜', '一物克一物' => '一物剋一物', '一目了然' => '一目了然', +'一碗面' => '一碗麵', '一扎' => '一紮', '一冲' => '一衝', '一厘一毫' => '一釐一毫', @@ -3188,6 +3187,7 @@ $zh2Hant = array( '下签' => '下籤', '下课钟' => '下課鐘', '不干不净' => '不乾不淨', +'不干胶' => '不乾膠', '不克自制' => '不克自制', '不加自制' => '不加自制', '不占凶吉' => '不占凶吉', @@ -3201,26 +3201,11 @@ $zh2Hant = array( '不好干预' => '不好干預', '不嫌母丑' => '不嫌母醜', '不寒而栗' => '不寒而慄', -'不干事' => '不干事', -'不干他' => '不干他', -'不干休' => '不干休', -'不干你' => '不干你', -'不干她' => '不干她', -'不干它' => '不干它', -'不干我' => '不干我', -'不干擾' => '不干擾', -'不干扰' => '不干擾', -'不干涉' => '不干涉', -'不干牠' => '不干牠', -'不干犯' => '不干犯', -'不干預' => '不干預', -'不干预' => '不干預', -'不干' => '不幹', '不吊' => '不弔', '不卷' => '不捲', '不采' => '不採', -'不斗膽' => '不斗膽', '不斗胆' => '不斗膽', +'不斗膽' => '不斗膽', '不断发' => '不斷發', '不每只' => '不每只', '不谷' => '不穀', @@ -3228,8 +3213,8 @@ $zh2Hant = array( '不负所托' => '不負所托', '不通吊庆' => '不通弔慶', '不丑' => '不醜', -'不采聲' => '不采聲', '不采声' => '不采聲', +'不采聲' => '不采聲', '不锈钢' => '不鏽鋼', '不食干腊' => '不食乾腊', '不斗' => '不鬥', @@ -3260,6 +3245,7 @@ $zh2Hant = array( '中型钟表面' => '中型鐘表面', '中型钟表' => '中型鐘錶', '中型钟面' => '中型鐘面', +'中境里' => '中境里', '中岳' => '中嶽', '中庄子' => '中庄子', '中文里' => '中文裡', @@ -3275,31 +3261,36 @@ $zh2Hant = array( '中签订' => '中簽訂', '中签' => '中籤', '中风后' => '中風後', -'丰儀' => '丰儀', '丰仪' => '丰儀', +'丰儀' => '丰儀', '丰南' => '丰南', '丰姿' => '丰姿', '丰容' => '丰容', -'丰度' => '丰度', '丰情' => '丰情', '丰标' => '丰標', -'丰標不凡' => '丰標不凡', '丰标不凡' => '丰標不凡', +'丰標不凡' => '丰標不凡', '丰神' => '丰神', '丰茸' => '丰茸', '丰采' => '丰采', -'丰韻' => '丰韻', '丰韵' => '丰韻', +'丰韻' => '丰韻', +'丹棱' => '丹稜', '主仆' => '主僕', '主干' => '主幹', '主钟差' => '主鐘差', '主钟曲线' => '主鐘曲線', '乃系' => '乃係', '么么唱唱' => '么么唱唱', +'么九' => '么九', '么儿' => '么兒', +'么半' => '么半', '么喝' => '么喝', +'么女' => '么女', '么妹' => '么妹', +'么子' => '么子', '么弟' => '么弟', +'么正' => '么正', '么爷' => '么爺', '么雞' => '么雞', '么么小丑' => '么麼小丑', @@ -3321,12 +3312,8 @@ $zh2Hant = array( '九扎' => '九紮', '九只' => '九隻', '九余' => '九餘', -'也斗了胆' => '也斗了膽', -'干上' => '乾上', '干干' => '乾乾', -'干干儿的' => '乾乾兒的', '干干净净' => '乾乾淨淨', -'干了' => '乾了', '干井' => '乾井', '干个够' => '乾個夠', '干儿' => '乾兒', @@ -3349,12 +3336,12 @@ $zh2Hant = array( '干回付' => '乾回付', '干圆洁净' => '乾圓潔淨', '干地' => '乾地', -'干坤' => '乾坤', '干坞' => '乾塢', '干女' => '乾女', '干奴才' => '乾奴才', '干妹' => '乾妹', '干姊' => '乾姊', +'干姐' => '乾姐', '干娘' => '乾娘', '干妈' => '乾媽', '干子' => '乾子', @@ -3364,7 +3351,6 @@ $zh2Hant = array( '干巴' => '乾巴', '干式' => '乾式', '干弟' => '乾弟', -'干得' => '乾得', '干急' => '乾急', '干性' => '乾性', '干打雷' => '乾打雷', @@ -3374,7 +3360,6 @@ $zh2Hant = array( '干擦' => '乾擦', '干支剌' => '乾支剌', '干支支' => '乾支支', -'干敲梆子不卖油' => '乾敲梆子不賣油', '干料' => '乾料', '干旱' => '乾旱', '干暖' => '乾暖', @@ -3418,6 +3403,7 @@ $zh2Hant = array( '干癣' => '乾癬', '干瘾' => '乾癮', '干白儿' => '乾白兒', +'干白葡萄酒' => '乾白葡萄酒', '干的' => '乾的', '干眼' => '乾眼', '干瞪眼' => '乾瞪眼', @@ -3428,6 +3414,7 @@ $zh2Hant = array( '干篾片' => '乾篾片', '干粉' => '乾粉', '干粮' => '乾糧', +'干红葡萄酒' => '乾紅葡萄酒', '干结' => '乾結', '干丝' => '乾絲', '干纲' => '乾綱', @@ -3464,13 +3451,14 @@ $zh2Hant = array( '干醋' => '乾醋', '干重' => '乾重', '干量' => '乾量', +'干锅' => '乾鍋', '干阿奶' => '乾阿奶', -'干隆' => '乾隆', '干雷' => '乾雷', '干电' => '乾電', '干霍乱' => '乾霍亂', '干颡' => '乾顙', '干台' => '乾颱', +'干食' => '乾食', '干饭' => '乾飯', '干馆' => '乾館', '干糇' => '乾餱', @@ -3481,13 +3469,10 @@ $zh2Hant = array( '乱发生' => '亂發生', '乱发脾气' => '亂發脾氣', '乱发' => '亂髮', -'乱哄' => '亂鬨', -'乱哄不过来' => '亂鬨不過來', +'乱哄哄' => '亂鬨鬨', '了然后' => '了然後', -'事情干脆' => '事情干脆', '事有斗巧' => '事有鬥巧', '事里' => '事裡', -'事都干脆' => '事都干脆', '二不棱登' => '二不稜登', '二个' => '二個', '二只得' => '二只得', @@ -3507,11 +3492,11 @@ $zh2Hant = array( '于仲文' => '于仲文', '于佳卉' => '于佳卉', '于来山' => '于來山', -'于偉國' => '于偉國', '于伟国' => '于偉國', +'于偉國' => '于偉國', '于光新' => '于光新', -'于光遠' => '于光遠', '于光远' => '于光遠', +'于光遠' => '于光遠', '于克-兰多县' => '于克-蘭多縣', '于克-蘭多縣' => '于克-蘭多縣', '于克勒' => '于克勒', @@ -3530,12 +3515,12 @@ $zh2Hant = array( '于吉' => '于吉', '于和伟' => '于和偉', '于品海' => '于品海', -'于國楨' => '于國楨', '于国桢' => '于國楨', +'于國楨' => '于國楨', '于国治' => '于國治', '于國治' => '于國治', -'于堅' => '于堅', '于坚' => '于堅', +'于堅' => '于堅', '于大宝' => '于大寶', '于大寶' => '于大寶', '于天仁' => '于天仁', @@ -3558,12 +3543,12 @@ $zh2Hant = array( '于小惠' => '于小惠', '于少保' => '于少保', '于山' => '于山', -'于山國' => '于山國', '于山国' => '于山國', +'于山國' => '于山國', '于帅' => '于帥', '于帥' => '于帥', -'于幼軍' => '于幼軍', '于幼军' => '于幼軍', +'于幼軍' => '于幼軍', '于康震' => '于康震', '于广洲' => '于廣洲', '于廣洲' => '于廣洲', @@ -3571,14 +3556,14 @@ $zh2Hant = array( '于从濂' => '于從濂', '于從濂' => '于從濂', '于德海' => '于德海', -'于志寧' => '于志寧', '于志宁' => '于志寧', +'于志寧' => '于志寧', '于忠肃集' => '于忠肅集', '于思' => '于思', '于慎行' => '于慎行', '于慧' => '于慧', -'于成龙' => '于成龍', '于成龍' => '于成龍', +'于成龙' => '于成龍', '于振' => '于振', '于振武' => '于振武', '于敏' => '于敏', @@ -3587,24 +3572,24 @@ $zh2Hant = array( '于斯塔德' => '于斯塔德', '于斯納爾斯貝里' => '于斯納爾斯貝里', '于斯纳尔斯贝里' => '于斯納爾斯貝里', -'于斯達爾' => '于斯達爾', '于斯达尔' => '于斯達爾', -'于明濤' => '于明濤', +'于斯達爾' => '于斯達爾', '于明涛' => '于明濤', +'于明濤' => '于明濤', '于是之' => '于是之', '于晨楠' => '于晨楠', '于晴' => '于晴', '于会泳' => '于會泳', '于會泳' => '于會泳', -'于根偉' => '于根偉', '于根伟' => '于根偉', +'于根偉' => '于根偉', '于格' => '于格', '于枫' => '于楓', '于楓' => '于楓', '于荣光' => '于榮光', '于樂' => '于樂', -'于樹潔' => '于樹潔', '于树洁' => '于樹潔', +'于樹潔' => '于樹潔', '于欣' => '于欣', '于欣源' => '于欣源', '于正昇' => '于正昇', @@ -3615,18 +3600,18 @@ $zh2Hant = array( '于江震' => '于江震', '于波' => '于波', '于洋' => '于洋', -'于洪區' => '于洪區', '于洪区' => '于洪區', +'于洪區' => '于洪區', '于浩威' => '于浩威', '于海' => '于海', '于海洋' => '于海洋', -'于湘蘭' => '于湘蘭', '于湘兰' => '于湘蘭', -'于漢超' => '于漢超', +'于湘蘭' => '于湘蘭', '于汉超' => '于漢超', +'于漢超' => '于漢超', '于澄' => '于澄', -'于澤爾' => '于澤爾', '于泽尔' => '于澤爾', +'于澤爾' => '于澤爾', '于涛' => '于濤', '于濤' => '于濤', '于熙珍' => '于熙珍', @@ -3655,10 +3640,9 @@ $zh2Hant = array( '于谨' => '于謹', '于貝爾' => '于貝爾', '于贝尔' => '于貝爾', -'于赠' => '于贈', '于贈' => '于贈', +'于赠' => '于贈', '于越' => '于越', -'于军' => '于軍', '于軍' => '于軍', '于道泉' => '于道泉', '于远伟' => '于遠偉', @@ -3679,23 +3663,23 @@ $zh2Hant = array( '于非闇' => '于非闇', '于韋斯屈萊' => '于韋斯屈萊', '于韦斯屈莱' => '于韋斯屈萊', -'于风政' => '于風政', '于風政' => '于風政', +'于风政' => '于風政', +'于飛' => '于飛', '于飞' => '于飛', -'于飛島' => '于飛島', -'于飞岛' => '于飛島', '于余曲折' => '于餘曲折', '于鬯' => '于鬯', '于魁智' => '于魁智', -'于鳳桐' => '于鳳桐', '于凤桐' => '于鳳桐', -'于鳳至' => '于鳳至', +'于鳳桐' => '于鳳桐', '于凤至' => '于鳳至', -'于默奧' => '于默奧', +'于鳳至' => '于鳳至', '于默奥' => '于默奧', +'于默奧' => '于默奧', '云乎' => '云乎', '云云' => '云云', '云何' => '云何', +'云敞' => '云敞', '云为' => '云為', '云為' => '云為', '云然' => '云然', @@ -3704,6 +3688,7 @@ $zh2Hant = array( '五个' => '五個', '五周后' => '五周後', '五天后' => '五天後', +'五峰县' => '五峯縣', '五岳' => '五嶽', '五年' => '五年', '五谷' => '五穀', @@ -3715,7 +3700,6 @@ $zh2Hant = array( '五只' => '五隻', '五余' => '五餘', '井干' => '井幹', -'井干摧败' => '井榦摧敗', '井里' => '井裡', '亚于' => '亞於', '亚美尼亚历' => '亞美尼亞曆', @@ -3723,27 +3707,31 @@ $zh2Hant = array( '交游' => '交遊', '交哄' => '交鬨', '亦云' => '亦云', +'京沈' => '京瀋', '亮丑' => '亮醜', '亮钟' => '亮鐘', '人云' => '人云', '人如风后入江云' => '人如風後入江雲', +'人干的' => '人幹的', '人欲' => '人慾', '人数只' => '人數只', '人数里' => '人數裡', '人物志' => '人物誌', '人生天里' => '人生天里', +'人发指' => '人髮指', '什锦面' => '什錦麵', '仇仇' => '仇讎', '介胄' => '介冑', +'他干的' => '他幹的', '他钟' => '他鐘', '付托' => '付託', '仙后' => '仙后', '仙后座' => '仙后座', +'仙游' => '仙遊', '代数里' => '代數裡', '代理发行' => '代理發行', '代码表' => '代碼表', '代表' => '代表', -'令人发指' => '令人髮指', '以自制' => '以自制', '仲裁制' => '仲裁制', '件钟' => '件鐘', @@ -3767,6 +3755,7 @@ $zh2Hant = array( '伊郁' => '伊鬱', '伏几' => '伏几', '伐罪吊民' => '伐罪弔民', +'休克期' => '休克期', '休征' => '休徵', '伙头' => '伙頭', '伴游' => '伴遊', @@ -3791,15 +3780,13 @@ $zh2Hant = array( '佛罗棱萨' => '佛羅稜薩', '佛钟' => '佛鐘', '作品里' => '作品裡', -'作奸犯科' => '作姦犯科', '作准' => '作準', -'你斗了胆' => '你斗了膽', '你夸' => '你誇', '佣金' => '佣金', '佣鈿' => '佣鈿', '佣钿' => '佣鈿', -'佣钱' => '佣錢', '佣錢' => '佣錢', +'佣钱' => '佣錢', '佳肴' => '佳肴', '佳里鎮' => '佳里鎮', '并一不二' => '併一不二', @@ -3808,7 +3795,7 @@ $zh2Hant = array( '并到' => '併到', '并合' => '併合', '并名' => '併名', -'并吞' => '併吞', +'并吞下' => '併吞下', '并拢' => '併攏', '并案' => '併案', '并流' => '併流', @@ -3834,6 +3821,7 @@ $zh2Hant = array( '依托' => '依託', '侵并' => '侵併', '局促' => '侷促', +'便于' => '便於', '系数' => '係數', '系为' => '係為', '保险柜' => '保險柜', @@ -3843,17 +3831,17 @@ $zh2Hant = array( '修杰麟' => '修杰麟', '修胡刀' => '修鬍刀', '俯冲' => '俯衝', +'个月里' => '個月裡', '个里' => '個裡', '个钟' => '個鐘', '个钟表' => '個鐘錶', -'们斗了胆' => '們斗了膽', +'们干的' => '們幹的', '幸免' => '倖免', '幸存' => '倖存', '幸幸' => '倖幸', '候复' => '候覆', '倚闲' => '倚閑', '倛丑' => '倛醜', -'借听于聋' => '借聽於聾', '借鉴' => '借鑑', '倦游' => '倦遊', '假里' => '假裡', @@ -3870,11 +3858,12 @@ $zh2Hant = array( '佣仆' => '傭僕', '傲游' => '傲遊', '傲霜斗雪' => '傲霜鬥雪', -'傳位于四太子' => '傳位于四太子', '传位于四太子' => '傳位于四太子', +'傳位于四太子' => '傳位于四太子', '传于' => '傳於', '债累累' => '債纍纍', '傻里傻气' => '傻裡傻氣', +'仅余' => '僅餘', '仆人' => '僕人', '仆使' => '僕使', '仆仆' => '僕僕', @@ -3956,14 +3945,15 @@ $zh2Hant = array( '凶险' => '兇險', '先采' => '先採', '光致致' => '光緻緻', +'克期间' => '克期間', '免征' => '免徵', '党太尉' => '党太尉', '党姓' => '党姓', '党家' => '党家', '党怀英' => '党懷英', '党进' => '党進', -'党项' => '党項', '党項' => '党項', +'党项' => '党項', '内脏' => '內臟', '内制' => '內製', '内面包' => '內面包', @@ -3971,8 +3961,6 @@ $zh2Hant = array( '内斗' => '內鬥', '内哄' => '內鬨', '全干' => '全乾', -'全面包围' => '全面包圍', -'全面包裹' => '全面包裹', '两个' => '兩個', '两周后' => '兩周後', '两天后' => '兩天後', @@ -4032,10 +4020,10 @@ $zh2Hant = array( '冷面相' => '冷面相', '冷面' => '冷麵', '准三后' => '准三后', -'准保護' => '准保護', '准保护' => '准保護', -'准保釋' => '准保釋', +'准保護' => '准保護', '准保释' => '准保釋', +'准保釋' => '准保釋', '凌蒙初' => '凌濛初', '凝炼' => '凝鍊', '几上' => '几上', @@ -4065,8 +4053,8 @@ $zh2Hant = array( '分子钟' => '分子鐘', '分子云' => '分子雲', '分布于' => '分布於', -'分散于' => '分散於', '分钟' => '分鐘', +'分钟里' => '分鐘裡', '刑余' => '刑餘', '划一桨' => '划一槳', '划上' => '划上', @@ -4074,8 +4062,8 @@ $zh2Hant = array( '划不來' => '划不來', '划不来' => '划不來', '划了一会' => '划了一會', -'划来划去' => '划來划去', '划來划去' => '划來划去', +'划来划去' => '划來划去', '划具' => '划具', '划到岸' => '划到岸', '划到江心' => '划到江心', @@ -4086,29 +4074,30 @@ $zh2Hant = array( '划得來' => '划得來', '划得来' => '划得來', '划拳' => '划拳', -'划槳' => '划槳', '划桨' => '划槳', +'划槳' => '划槳', '划水' => '划水', +'划着独木舟' => '划着獨木舟', +'划着竹筏' => '划着竹筏', +'划着船' => '划着船', '划算' => '划算', '划船' => '划船', '划艇' => '划艇', -'划著' => '划著', '划行' => '划行', '划走' => '划走', '划起' => '划起', -'划進' => '划進', '划进' => '划進', +'划進' => '划進', '划过' => '划過', '划過' => '划過', -'划龙舟' => '划龍舟', '划龍舟' => '划龍舟', +'划龙舟' => '划龍舟', '判断发' => '判斷發', '别辟' => '別闢', '利欲' => '利慾', '利于' => '利於', '刮来刮去' => '刮來刮去', '刮起来' => '刮起來', -'刮风下雪倒便宜' => '刮風下雪倒便宜', '刮胡' => '刮鬍', '到山里' => '到山裡', '制冷机' => '制冷機', @@ -4136,7 +4125,6 @@ $zh2Hant = array( '剥制' => '剝製', '剩余' => '剩餘', '剪其发' => '剪其髮', -'剪牡丹喂牛' => '剪牡丹喂牛', '剪发' => '剪髮', '割舍' => '割捨', '创获' => '創穫', @@ -4148,8 +4136,9 @@ $zh2Hant = array( '铲头' => '剷頭', '划入' => '劃入', '划为' => '劃為', -'劉佳怜' => '劉佳怜', +'划著' => '劃著名', '刘佳怜' => '劉佳怜', +'劉佳怜' => '劉佳怜', '刘芸后' => '劉芸后', '力拼' => '力拚', '力拼众敌' => '力拼眾敵', @@ -4157,7 +4146,6 @@ $zh2Hant = array( '功勋' => '功勳', '功致' => '功緻', '加氢精制' => '加氫精制', -'加注' => '加註', '劣于' => '劣於', '助于' => '助於', '劫余' => '劫餘', @@ -4179,6 +4167,7 @@ $zh2Hant = array( '包扎' => '包紮', '匏系' => '匏繫', '北山索面' => '北山索麵', +'北仑河' => '北崙河', '北岳' => '北嶽', '北回线' => '北迴線', '北回铁路' => '北迴鐵路', @@ -4214,6 +4203,7 @@ $zh2Hant = array( '千钧一发' => '千鈞一髮', '千只' => '千隻', '千余' => '千餘', +'升高后' => '升高後', '半制品' => '半制品', '半只可' => '半只可', '半只够' => '半只夠', @@ -4230,21 +4220,17 @@ $zh2Hant = array( '南回线' => '南迴線', '南回铁路' => '南迴鐵路', '南游' => '南遊', -'博汇' => '博彙', '博采' => '博採', '博尔术' => '博爾朮', '卜云吉' => '卜云吉', '占了卜' => '占了卜', -'占便宜的是呆' => '占便宜的是獃', '印累绶若' => '印纍綬若', '印制' => '印製', '印鉴' => '印鑑', '危于' => '危於', '卵与石斗' => '卵與石鬥', -'卷发' => '卷髮', '卷须' => '卷鬚', '厂部' => '厂部', -'厝薪于火' => '厝薪於火', '原子钟' => '原子鐘', '原钟' => '原鐘', '历物之意' => '厤物之意', @@ -4254,9 +4240,10 @@ $zh2Hant = array( '反朴' => '反樸', '反冲' => '反衝', '反复制' => '反複製', -'反覆' => '反覆', '反复' => '反覆', +'反覆' => '反覆', '取舍' => '取捨', +'取决于' => '取決於', '受雇' => '受僱', '受托' => '受託', '丛林里' => '叢林裡', @@ -4270,12 +4257,13 @@ $zh2Hant = array( '口腹之欲' => '口腹之慾', '口里' => '口裡', '口钟' => '口鐘', -'古書云' => '古書云', +'古人有云' => '古人有云', '古书云' => '古書云', +'古書云' => '古書云', '古柯咸' => '古柯鹹', '古朴' => '古樸', -'古语云' => '古語云', '古語云' => '古語云', +'古语云' => '古語云', '古迹' => '古蹟', '古钟' => '古鐘', '古钟表' => '古鐘錶', @@ -4294,8 +4282,8 @@ $zh2Hant = array( '只身上有' => '只身上有', '只身上沒' => '只身上沒', '只身上没' => '只身上沒', -'只身上無' => '只身上無', '只身上无' => '只身上無', +'只身上無' => '只身上無', '只身上的' => '只身上的', '只身世' => '只身世', '只身份' => '只身份', @@ -4304,19 +4292,19 @@ $zh2Hant = array( '只身子' => '只身子', '只身形' => '只身形', '只身影' => '只身影', -'只身後' => '只身後', '只身后' => '只身後', +'只身後' => '只身後', '只身心' => '只身心', '只身旁' => '只身旁', '只身材' => '只身材', '只身段' => '只身段', '只身为' => '只身為', '只身為' => '只身為', -'只身邊' => '只身邊', '只身边' => '只身邊', +'只身邊' => '只身邊', '只身首' => '只身首', -'只身體' => '只身體', '只身体' => '只身體', +'只身體' => '只身體', '只身高' => '只身高', '只采声' => '只采聲', '叮叮当当' => '叮叮噹噹', @@ -4339,9 +4327,11 @@ $zh2Hant = array( '叶音' => '叶音', '叶韵' => '叶韻', '吃板刀面' => '吃板刀麵', +'吃碗面' => '吃碗麵', '吃姜' => '吃薑', '吃里扒外' => '吃裡扒外', '吃里爬外' => '吃裡爬外', +'吃面' => '吃麵', '各辟' => '各闢', '各类钟' => '各類鐘', '合伙人' => '合伙人', @@ -4359,6 +4349,7 @@ $zh2Hant = array( '同伙' => '同夥', '同于' => '同於', '同余' => '同餘', +'名单于' => '名單於', '后冠' => '后冠', '后北街' => '后北街', '后土' => '后土', @@ -4369,19 +4360,19 @@ $zh2Hant = array( '后庄' => '后庄', '后座' => '后座', '后母戊' => '后母戊', -'后海灣' => '后海灣', '后海湾' => '后海灣', +'后海灣' => '后海灣', '后瑞站' => '后瑞站', '后稷' => '后稷', '后綜' => '后綜', '后羿' => '后羿', '后街' => '后街', '后角' => '后角', -'后豐' => '后豐', '后丰' => '后豐', +'后豐' => '后豐', '后里' => '后里', -'后髮FK型星' => '后髮FK型星', '后发FK型星' => '后髮FK型星', +'后髮FK型星' => '后髮FK型星', '后发座' => '后髮座', '后髮座' => '后髮座', '后发星系团' => '后髮星系團', @@ -4402,19 +4393,21 @@ $zh2Hant = array( '吾为之范我驰驱' => '吾爲之範我馳驅', '吕后' => '呂后', '呂后' => '呂后', -'呆呆兽' => '呆呆獸', '呆致致' => '呆緻緻', '呆里呆气' => '呆裡呆氣', '告札' => '告劄', +'呦喂' => '呦喂', '周后' => '周后', +'周惠后' => '周惠后', '周历' => '周曆', -'周杰倫' => '周杰倫', -'周杰伦' => '周杰倫', +'周杰' => '周杰', '周历史' => '周歷史', -'周游' => '周遊', +'周游列国' => '周遊列國', +'呵喂' => '呵喂', '呼吁' => '呼籲', '命中注定' => '命中注定', '和奸' => '和姦', +'和制汉' => '和製漢', '咎征' => '咎徵', '咕咕钟' => '咕咕鐘', '咪表' => '咪錶', @@ -4422,7 +4415,6 @@ $zh2Hant = array( '咯当' => '咯噹', '哀吊' => '哀弔', '哀挽' => '哀輓', -'品汇' => '品彙', '品鉴' => '品鑑', '哄堂大笑' => '哄堂大笑', '員山庄' => '員山庄', @@ -4437,9 +4429,14 @@ $zh2Hant = array( '商历' => '商曆', '商标准许' => '商標准許', '商历史' => '商歷史', +'啊喂' => '啊喂', '启发式' => '啟發式', '啷当' => '啷噹', '喂了一声' => '喂了一聲', +'喂喂' => '喂喂', +'喂哟' => '喂喲', +'喂!' => '喂!', +'喂,' => '喂,', '善于' => '善於', '喜向往' => '喜向往', '喜欢表' => '喜歡錶', @@ -4450,11 +4447,13 @@ $zh2Hant = array( '喧哄' => '喧鬨', '丧钟' => '喪鐘', '乔岳' => '喬嶽', -'單于' => '單于', '单于' => '單于', +'單于' => '單于', '单单于' => '單單於', '单干' => '單幹', '单打独斗' => '單打獨鬥', +'哟喂' => '喲喂', +'喲喂' => '喲喂', '嘉谷' => '嘉穀', '嘉肴' => '嘉肴', '嘴里' => '嘴裡', @@ -4507,6 +4506,7 @@ $zh2Hant = array( '回游' => '回遊', '因于' => '因於', '困倦起来' => '困倦起來', +'困于' => '困於', '困兽之斗' => '困獸之鬥', '困兽犹斗' => '困獸猶鬥', '困斗' => '困鬥', @@ -4558,6 +4558,7 @@ $zh2Hant = array( '埃及历史' => '埃及歷史', '埃及艳后' => '埃及豔后', '埃荣冲' => '埃榮衝', +'城市里' => '城市裡', '城里' => '城裡', '埔子里' => '埔子里', '埔里社' => '埔裏社', @@ -4570,13 +4571,18 @@ $zh2Hant = array( '堡子里' => '堡子里', '场里' => '場裡', '塞耳盗钟' => '塞耳盜鐘', +'境里' => '境裡', +'境里程' => '境里程', '墓志铭' => '墓志銘', '墓志' => '墓誌', '增辟' => '增闢', '墨子里' => '墨子里', -'墨沈' => '墨沈', -'墨沈未干' => '墨瀋未乾', +'墨斗' => '墨斗', +'墨沈沈' => '墨沈沈', +'墨沈' => '墨瀋', '垦辟' => '墾闢', +'压制出' => '壓製出', +'压制机' => '壓製機', '壮游' => '壯遊', '壮面' => '壯麵', '壹郁' => '壹鬱', @@ -4597,16 +4603,16 @@ $zh2Hant = array( '多只含' => '多只含', '多只在' => '多只在', '多只是' => '多只是', -'多只會' => '多只會', '多只会' => '多只會', +'多只會' => '多只會', '多只有' => '多只有', '多只比' => '多只比', '多只用' => '多只用', '多只能' => '多只能', '多只限' => '多只限', '多只需' => '多只需', -'多只须' => '多只須', '多只須' => '多只須', +'多只须' => '多只須', '多周后' => '多周後', '多天后' => '多天後', '多于' => '多於', @@ -4665,7 +4671,6 @@ $zh2Hant = array( '大钟' => '大鐘', '大只' => '大隻', '大风后' => '大風後', -'大曲酒' => '大麴酒', '天克地冲' => '天克地衝', '天后' => '天后', '天后宫' => '天后宮', @@ -4691,15 +4696,10 @@ $zh2Hant = array( '太后' => '太后', '太丑' => '太醜', '太阁' => '太閤', -'夯干' => '夯幹', -'夸人' => '夸人', '夸克' => '夸克', -'夸姣' => '夸姣', -'夸容' => '夸容', '夸父' => '夸父', '夸特' => '夸特', '夸脱' => '夸脫', -'夸丽' => '夸麗', '奇勋' => '奇勳', '奇迹' => '奇蹟', '奇丑' => '奇醜', @@ -4710,7 +4710,6 @@ $zh2Hant = array( '女仆' => '女僕', '奴仆' => '奴僕', '奸淫掳掠' => '奸淫擄掠', -'好干' => '好乾', '好家伙' => '好傢夥', '好凶' => '好兇', '好勇斗狠' => '好勇鬥狠', @@ -4734,7 +4733,6 @@ $zh2Hant = array( '始于' => '始於', '委托' => '委託', '委托书' => '委託書', -'姜文杰' => '姜文杰', '奸夫' => '姦夫', '奸妇' => '姦婦', '奸宄' => '姦宄', @@ -4761,11 +4759,12 @@ $zh2Hant = array( '字汇' => '字彙', '字码表' => '字碼表', '字里行间' => '字裡行間', -'存十一于千百' => '存十一於千百', '存折' => '存摺', '存于' => '存於', '孛里海' => '孛里海', -'孤寡不谷' => '孤寡不穀', +'孝惠后' => '孝惠后', +'孙杰' => '孫杰', +'孫杰' => '孫杰', '学家' => '學家', '学里' => '學裡', '宇宙志' => '宇宙誌', @@ -4805,10 +4804,10 @@ $zh2Hant = array( '实干' => '實幹', '实累累' => '實纍纍', '写字台' => '寫字檯', -'宽宽松松' => '寬寬鬆鬆', '宽于' => '寬於', '宽余' => '寬餘', '宽松' => '寬鬆', +'宽松松' => '寬鬆鬆', '寮采' => '寮寀', '寶山庄' => '寶山庄', '宝历' => '寶曆', @@ -4832,6 +4831,7 @@ $zh2Hant = array( '对准表' => '對準錶', '对准钟' => '對準鐘', '对准钟表' => '對準鐘錶', +'对着干' => '對着幹', '对华发' => '對華發', '对表中' => '對表中', '对表扬' => '對表揚', @@ -4841,6 +4841,7 @@ $zh2Hant = array( '对表达' => '對表達', '导游' => '導遊', '小丑' => '小丑', +'小井里' => '小井里', '小价' => '小价', '小仆' => '小僕', '小几' => '小几', @@ -4856,13 +4857,16 @@ $zh2Hant = array( '小型钟表面' => '小型鐘表面', '小型钟表' => '小型鐘錶', '小型钟面' => '小型鐘面', +'小时里' => '小時裡', '小米面' => '小米麵', '小只' => '小隻', '少采' => '少採', '就范' => '就範', '就里' => '就裡', '尸位素餐' => '尸位素餐', +'尸佼' => '尸佼', '尸利' => '尸利', +'尸子' => '尸子', '尸居余气' => '尸居餘氣', '尸弃佛' => '尸棄佛', '尸祝' => '尸祝', @@ -4892,26 +4896,29 @@ $zh2Hant = array( '山羊胡' => '山羊鬍', '山里有' => '山裡有', '山里的' => '山裡的', -'山谷道' => '山谷道', +'山谷' => '山谷', '山重水复' => '山重水複', +'岫岩' => '岫巖', '岱岳' => '岱嶽', '峇里海' => '峇里海', '峰回' => '峰迴', '峻岭' => '峻岭', -'昆剧' => '崑劇', '崑剧' => '崑劇', +'昆剧' => '崑劇', '崑山' => '崑山', '昆山' => '崑山', '昆冈' => '崑岡', '昆仑' => '崑崙', +'昆嵛' => '崑嵛', +'昆承湖' => '崑承湖', '崑曲' => '崑曲', '昆曲' => '崑曲', -'昆腔' => '崑腔', '崑腔' => '崑腔', -'昆苏' => '崑蘇', +'昆腔' => '崑腔', '崑苏' => '崑蘇', -'昆调' => '崑調', +'昆苏' => '崑蘇', '崑调' => '崑調', +'昆调' => '崑調', '崖广' => '崖广', '嶒棱' => '嶒稜', '岳岳' => '嶽嶽', @@ -4923,7 +4930,6 @@ $zh2Hant = array( '工作台' => '工作檯', '工致' => '工緻', '左冲右突' => '左衝右突', -'巧妇做不得无面馎饦' => '巧婦做不得無麵餺飥', '巧干' => '巧幹', '巧历' => '巧曆', '巧历史' => '巧歷史', @@ -4935,7 +4941,7 @@ $zh2Hant = array( '已占算' => '已占算', '巴尔干' => '巴爾幹', '巷里' => '巷裡', -'市里' => '市裡', +'市里的' => '市裡的', '布谷' => '布穀', '布谷鸟' => '布穀鳥', '布谷鸟钟' => '布穀鳥鐘', @@ -4948,6 +4954,7 @@ $zh2Hant = array( '师范' => '師範', '席卷' => '席捲', '带征' => '帶徵', +'带余' => '帶餘', '带发修行' => '帶髮修行', '幅图里' => '幅圖裡', '干系' => '干係', @@ -4960,19 +4967,22 @@ $zh2Hant = array( '年里' => '年裡', '年鉴' => '年鑑', '并力' => '并力', +'并吞' => '并吞', '并州' => '并州', '并日而食' => '并日而食', '并迭' => '并迭', '幸免于难' => '幸免於難', '幸于' => '幸於', '幸运胡' => '幸運鬍', +'干上' => '幹上', '干下去' => '幹下去', '干不了' => '幹不了', '干不成' => '幹不成', +'干了' => '幹了', '干事' => '幹事', '干些' => '幹些', -'干人' => '幹人', '干什么' => '幹什麼', +'干仗' => '幹仗', '干个' => '幹個', '干劲' => '幹勁', '干吏' => '幹吏', @@ -4981,10 +4991,10 @@ $zh2Hant = array( '干吗' => '幹嗎', '干嘛' => '幹嘛', '干坏事' => '幹壞事', +'干大事' => '幹大事', '干完' => '幹完', '干家' => '幹家', -'干将' => '幹將', -'干得了' => '幹得了', +'干得' => '幹得', '干性油' => '幹性油', '干才' => '幹才', '干掉' => '幹掉', @@ -4999,7 +5009,8 @@ $zh2Hant = array( '干甚么' => '幹甚麼', '干略' => '幹略', '干当' => '幹當', -'干的停当' => '幹的停當', +'干的事' => '幹的事', +'干的好事' => '幹的好事', '干细胞' => '幹細胞', '干线' => '幹線', '干练' => '幹練', @@ -5010,8 +5021,7 @@ $zh2Hant = array( '干起来' => '幹起來', '干路' => '幹路', '干办' => '幹辦', -'干这一行' => '幹這一行', -'干这种事' => '幹這種事', +'干这' => '幹這', '干道' => '幹道', '干部' => '幹部', '干革命' => '幹革命', @@ -5027,8 +5037,8 @@ $zh2Hant = array( '床席' => '床蓆', '店里' => '店裡', '府干卿' => '府干卿', -'府干擾' => '府干擾', '府干扰' => '府干擾', +'府干擾' => '府干擾', '府干政' => '府干政', '府干涉' => '府干涉', '府干犯' => '府干犯', @@ -5041,8 +5051,8 @@ $zh2Hant = array( '厨余' => '廚餘', '厮斗' => '廝鬥', '庙里' => '廟裡', -'廢后' => '廢后', '废后' => '廢后', +'廢后' => '廢后', '广征' => '廣徵', '广舍' => '廣捨', '延历' => '延曆', @@ -5093,9 +5103,11 @@ $zh2Hant = array( '弘历史' => '弘歷史', '弱于' => '弱於', '弱水三千只取一瓢' => '弱水三千只取一瓢', -'張三丰' => '張三丰', '张三丰' => '張三丰', +'張三丰' => '張三丰', '张勋' => '張勳', +'张杰' => '張杰', +'張杰' => '張杰', '张乐于张徐' => '張樂于張徐', '强制作用' => '強制作用', '强奸' => '強姦', @@ -5109,13 +5121,9 @@ $zh2Hant = array( '弹子台' => '彈子檯', '弹珠台' => '彈珠檯', '汇刊' => '彙刊', -'汇报' => '彙報', -'汇整' => '彙整', '汇算' => '彙算', -'汇编' => '彙編', '汇纂' => '彙纂', '汇辑' => '彙輯', -'汇集' => '彙集', '形单影只' => '形單影隻', '形于' => '形於', '彭于晏' => '彭于晏', @@ -5129,15 +5137,18 @@ $zh2Hant = array( '很凶' => '很兇', '很准' => '很準', '很丑' => '很醜', +'很松' => '很鬆', '律历志' => '律曆志', '后印' => '後印', '后台老板' => '後台老板', '后天' => '後天', +'後庄' => '後庄', '后面店' => '後面店', '徐干' => '徐幹', '徒杠' => '徒杠', '徒托空言' => '徒託空言', '得到回复' => '得到回覆', +'得力干将' => '得力幹將', '从仆' => '從僕', '从图里' => '從圖裡', '从山里' => '從山裡', @@ -5326,9 +5337,9 @@ $zh2Hant = array( '忠人之托' => '忠人之托', '忠仆' => '忠僕', '忠于' => '忠於', -'快干' => '快乾', '快快当当' => '快快當當', '快冲' => '快衝', +'怎么干' => '怎麼幹', '怒于' => '怒於', '怒气冲天' => '怒氣衝天', '怒火冲天' => '怒火衝天', @@ -5343,7 +5354,9 @@ $zh2Hant = array( '怪里怪气' => '怪裡怪氣', '怫郁' => '怫鬱', '恂栗' => '恂慄', +'恒基' => '恒基', '恒生' => '恒生', +'恒隆' => '恒隆', '恕乏价催' => '恕乏价催', '息交绝游' => '息交絕遊', '息谷' => '息穀', @@ -5381,6 +5394,7 @@ $zh2Hant = array( '愿而恭' => '愿而恭', '栗冽' => '慄冽', '栗栗' => '慄慄', +'慈溪' => '慈谿', '慌里慌张' => '慌裡慌張', '惨淡' => '慘澹', '庆吊' => '慶弔', @@ -5406,9 +5420,8 @@ $zh2Hant = array( '应征' => '應徵', '应钟' => '應鐘', '懔栗' => '懍慄', -'蒙懂' => '懞懂', -'蒙蒙懂懂' => '懞懞懂懂', -'蒙直' => '懞直', +'懞懞懂懂' => '懞懞懂懂', +'懞直' => '懞直', '惩忿窒欲' => '懲忿窒欲', '怀里' => '懷裡', '怀钟' => '懷鐘', @@ -5424,6 +5437,7 @@ $zh2Hant = array( '截发' => '截髮', '战天斗地' => '戰天鬥地', '战栗' => '戰慄', +'战于' => '戰於', '战斗' => '戰鬥', '戏里' => '戲裡', '戲院里' => '戲院里', @@ -5437,20 +5451,19 @@ $zh2Hant = array( '所占算' => '所占算', '所托' => '所託', '扁拟谷盗虫' => '扁擬穀盜蟲', -'手冢治虫' => '手塚治虫', '手塚治虫' => '手塚治虫', '手折' => '手摺', -'手表態' => '手表態', '手表态' => '手表態', +'手表態' => '手表態', '手表明' => '手表明', -'手表決' => '手表決', '手表决' => '手表決', +'手表決' => '手表決', '手表演' => '手表演', -'手表現' => '手表現', '手表现' => '手表現', +'手表現' => '手表現', '手表示' => '手表示', -'手表達' => '手表達', '手表达' => '手表達', +'手表達' => '手表達', '手表露' => '手表露', '手表面' => '手表面', '手里剑' => '手裏劍', @@ -5484,7 +5497,6 @@ $zh2Hant = array( '打斗' => '打鬥', '托管国' => '托管國', '扛大梁' => '扛大樑', -'捍御' => '扞禦', '扯面' => '扯麵', '扶余' => '扶餘', '批准的' => '批准的', @@ -5492,7 +5504,6 @@ $zh2Hant = array( '批复' => '批覆', '批注' => '批註', '批斗' => '批鬥', -'承制' => '承製', '抑制作用' => '抑制作用', '抑制剂' => '抑制劑', '抑郁' => '抑鬱', @@ -5573,6 +5584,7 @@ $zh2Hant = array( '捉奸党' => '捉奸黨', '捉奸' => '捉姦', '捉发' => '捉髮', +'捍御' => '捍禦', '捏面人' => '捏麵人', '舍不得' => '捨不得', '舍入' => '捨入', @@ -5620,7 +5632,6 @@ $zh2Hant = array( '卷纸' => '捲紙', '卷缩' => '捲縮', '卷舌' => '捲舌', -'卷铺盖' => '捲舖蓋', '卷烟' => '捲菸', '卷叶蛾' => '捲葉蛾', '卷袖' => '捲袖', @@ -5628,9 +5639,10 @@ $zh2Hant = array( '卷起' => '捲起', '卷轴' => '捲軸', '卷逃' => '捲逃', +'卷铺盖' => '捲鋪蓋', '卷云' => '捲雲', '卷风' => '捲風', -'卷发器' => '捲髮器', +'卷发' => '捲髮', '捵面' => '捵麵', '捶炼' => '捶鍊', '扫荡' => '掃蕩', @@ -5686,8 +5698,8 @@ $zh2Hant = array( '采种' => '採種', '采空区' => '採空區', '采空采穗' => '採空採穗', -'采纳' => '採納', '采納' => '採納', +'采纳' => '採納', '采给' => '採給', '采花' => '採花', '采芹人' => '採芹人', @@ -5697,6 +5709,7 @@ $zh2Hant = array( '采薇' => '採薇', '采薪' => '採薪', '采药' => '採藥', +'采血' => '採血', '采行' => '採行', '采补' => '採補', '采访' => '採訪', @@ -5719,11 +5732,11 @@ $zh2Hant = array( '控制' => '控制', '推情准理' => '推情準理', '推托之词' => '推托之詞', -'推舟于陆' => '推舟於陸', '推托' => '推託', '提子干' => '提子乾', '提心吊胆' => '提心弔膽', '提摩太后书' => '提摩太後書', +'提高后' => '提高後', '插于' => '插於', '换签' => '換籤', '换只' => '換隻', @@ -5734,8 +5747,8 @@ $zh2Hant = array( '揪发' => '揪髮', '揪须' => '揪鬚', '揭丑' => '揭醜', -'揮手表' => '揮手表', '挥手表' => '揮手表', +'揮手表' => '揮手表', '搋面' => '搋麵', '损于' => '損於', '搏斗' => '搏鬥', @@ -5773,7 +5786,8 @@ $zh2Hant = array( '撩斗' => '撩鬥', '播于' => '播於', '扑冬' => '撲鼕', -'扑冬冬' => '撲鼕鼕', +'扑咚' => '撲鼕', +'扑咚咚' => '撲鼕鼕', '擀面' => '擀麵', '击扑' => '擊扑', '击钟' => '擊鐘', @@ -5781,7 +5795,6 @@ $zh2Hant = array( '担仔面' => '擔仔麵', '担担面' => '擔擔麵', '据云' => '據云', -'据干而窥井底' => '據榦而窺井底', '擢发' => '擢髮', '擦干' => '擦乾', '擦干净' => '擦乾淨', @@ -5791,9 +5804,10 @@ $zh2Hant = array( '支干' => '支幹', '支配欲' => '支配慾', '收获' => '收穫', +'改制成' => '改制成', '改征' => '改徵', '改采' => '改採', -'放蒙挣' => '放懞掙', +'放懞挣' => '放懞掙', '放荡' => '放蕩', '放松' => '放鬆', '政斗' => '政鬥', @@ -5840,14 +5854,15 @@ $zh2Hant = array( '数与虏确' => '數與虜确', '数只' => '數隻', '文丑' => '文丑', -'文汇报' => '文匯報', '文学志' => '文學誌', '文征明' => '文徵明', '文思泉涌' => '文思泉湧', +'文杰' => '文杰', '文采郁郁' => '文采郁郁', '斗牛星' => '斗牛星', '斫雕为朴' => '斫雕為樸', '新井里美' => '新井里美', +'新干县' => '新幹縣', '新历' => '新曆', '新历史' => '新歷史', '新扎' => '新紮', @@ -5855,123 +5870,23 @@ $zh2Hant = array( '断发' => '斷髮', '断发文身' => '斷髮文身', '方便面' => '方便麵', -'方几' => '方几', '方向往' => '方向往', '方志恒' => '方志恒', '方法里' => '方法裡', '方志' => '方誌', -'方面' => '方面', -'于0' => '於0', -'于1' => '於1', -'于2' => '於2', -'于3' => '於3', -'于4' => '於4', -'于5' => '於5', -'于6' => '於6', -'于7' => '於7', -'于8' => '於8', -'于9' => '於9', -'于一' => '於一', -'于一役' => '於一役', -'于七' => '於七', -'于三' => '於三', -'于世' => '於世', -'于之' => '於之', -'于乎' => '於乎', -'于九' => '於九', -'于事' => '於事', -'于二' => '於二', -'于五' => '於五', -'于人' => '於人', -'于今' => '於今', -'于他' => '於他', -'于伏' => '於伏', -'于何' => '於何', -'于你' => '於你', -'于八' => '於八', -'于六' => '於六', -'于前' => '於前', -'于劣' => '於劣', -'于勤' => '於勤', -'于十' => '於十', -'于半' => '於半', -'于呼哀哉' => '於呼哀哉', -'于四' => '於四', -'于国' => '於國', -'于坏' => '於坏', -'于垂' => '於垂', -'于夫罗' => '於夫羅', -'於夫罗' => '於夫羅', -'於夫羅' => '於夫羅', -'于她' => '於她', -'于好' => '於好', -'于始' => '於始', -'於姓' => '於姓', -'于它' => '於它', -'于家' => '於家', -'于密' => '於密', -'于差' => '於差', -'于己' => '於己', -'于市' => '於市', -'于幕' => '於幕', -'于弱' => '於弱', -'于强' => '於強', '于后' => '於後', '于征' => '於徵', -'于心' => '於心', -'于怀' => '於懷', -'于我' => '於我', -'于戏' => '於戲', -'于敝' => '於敝', -'于斯' => '於斯', -'于是' => '於是', -'于是乎' => '於是乎', -'于时' => '於時', -'于梨华' => '於梨華', -'於梨華' => '於梨華', -'于乐' => '於樂', -'于此' => '於此', -'於氏' => '於氏', -'于民' => '於民', -'于水' => '於水', -'于法' => '於法', '于海上' => '於海上', '于海边' => '於海邊', -'于潜县' => '於潛縣', -'于火' => '於火', -'于焉' => '於焉', -'于墙' => '於牆', -'于物' => '於物', -'于毕' => '於畢', -'于尽' => '於盡', -'于盲' => '於盲', -'于祂' => '於祂', -'于穆' => '於穆', -'于终' => '於終', -'于美' => '於美', -'于色' => '於色', -'于菟' => '於菟', -'于蓝' => '於藍', -'于行' => '於行', -'于衷' => '於衷', -'于该' => '於該', -'于农' => '於農', -'于途' => '於途', -'于过' => '於過', -'于邑' => '於邑', -'于丑' => '於醜', -'于野' => '於野', -'于陆' => '於陸', '于震中' => '於震中', '于震前' => '於震前', -'于震后' => '於震后', +'于震后' => '於震後', '施舍' => '施捨', '施于' => '施於', '施舍之道' => '施舍之道', '旁征博引' => '旁徵博引', '旁注' => '旁註', '旅游' => '旅遊', -'旋干转坤' => '旋乾轉坤', '旋回' => '旋迴', '族里' => '族裡', '日心历表' => '日心曆表', @@ -5991,11 +5906,13 @@ $zh2Hant = array( '明范' => '明範', '明鉴' => '明鑑', '易于' => '易於', +'昔人有云' => '昔人有云', '星历' => '星曆', '星期后' => '星期後', '星历史' => '星歷史', '春游' => '春遊', '春香斗学' => '春香鬥學', +'昭惠后' => '昭惠后', '是发小' => '是髮小', '时钟' => '時鐘', '时间不准' => '時間不準', @@ -6004,7 +5921,7 @@ $zh2Hant = array( '晚钟' => '晚鐘', '晞发' => '晞髮', '晨钟' => '晨鐘', -'普冬冬' => '普鼕鼕', +'普咚咚' => '普鼕鼕', '晾干' => '晾乾', '暗地里' => '暗地裡', '暗沟里' => '暗溝裡', @@ -6018,7 +5935,7 @@ $zh2Hant = array( '历始' => '曆始', '历室' => '曆室', '历尾' => '曆尾', -'历数' => '曆數', +'历数书' => '曆數書', '历日' => '曆日', '历书' => '曆書', '历本' => '曆本', @@ -6030,34 +5947,32 @@ $zh2Hant = array( '晒谷' => '曬穀', '曰云' => '曰云', '更仆难数' => '更僕難數', -'更加注' => '更加注', '更签' => '更籤', '更钟' => '更鐘', '书签' => '書籤', '书面' => '書面', '曹子里' => '曹子里', -'曼谷人' => '曼谷人', +'曼谷' => '曼谷', '曾朴' => '曾樸', '最多' => '最多', '最多只' => '最多只', -'會干擾' => '會干擾', '会干扰' => '會干擾', +'會干擾' => '會干擾', '会干' => '會幹', '会吊' => '會弔', '会里' => '會裡', '月历' => '月曆', '月历史' => '月歷史', '月球历表' => '月球曆表', -'月离于毕' => '月離於畢', +'月里来' => '月裡來', '月面' => '月面', -'月丽于箕' => '月麗於箕', '有事之无范' => '有事之無範', '有仆' => '有僕', '有只不' => '有只不', '有只允' => '有只允', '有只容' => '有只容', -'有只采' => '有只採', '有只採' => '有只採', +'有只采' => '有只採', '有只是' => '有只是', '有只用' => '有只用', '有回复' => '有回覆', @@ -6066,8 +5981,8 @@ $zh2Hant = array( '有征战' => '有征戰', '有征戰' => '有征戰', '有征服' => '有征服', -'有征讨' => '有征討', '有征討' => '有征討', +'有征讨' => '有征討', '有征' => '有徵', '有恒街' => '有恒街', '有栖川' => '有栖川', @@ -6081,6 +5996,7 @@ $zh2Hant = array( '望后石' => '望后石', '朝乾夕惕' => '朝乾夕惕', '朝钟' => '朝鐘', +'朝鲜于' => '朝鮮於', '朦胧' => '朦朧', '蒙胧' => '朦朧', '木偶戏扎' => '木偶戲紮', @@ -6093,6 +6009,7 @@ $zh2Hant = array( '未干涉' => '未干涉', '未干預' => '未干預', '未干预' => '未干預', +'本庄' => '本庄', '本征' => '本徵', '本出戏' => '本齣戲', '术赤' => '朮赤', @@ -6101,6 +6018,7 @@ $zh2Hant = array( '朱理安历史' => '朱理安歷史', '朴子里' => '朴子里', '李志喜' => '李志喜', +'李适' => '李适', '李连杰' => '李連杰', '李連杰' => '李連杰', '材干' => '材幹', @@ -6113,20 +6031,22 @@ $zh2Hant = array( '束发' => '束髮', '杠人' => '杠人', '杠梁' => '杠梁', -'杠轂' => '杠轂', '杠毂' => '杠轂', +'杠轂' => '杠轂', '杯干' => '杯乾', '杯面' => '杯麵', '杰伦' => '杰倫', -'杰威爾音樂' => '杰威爾音樂', -'杰威尔音乐' => '杰威爾音樂', -'杰特' => '杰特', +'杰倫' => '杰倫', +'杰威尔' => '杰威爾', +'杰威爾' => '杰威爾', '东周钟' => '東周鐘', '东岳' => '東嶽', '東湖里' => '東湖里', '东冲西突' => '東衝西突', '东游' => '東遊', +'松口镇' => '松口鎮', '松山庄' => '松山庄', +'松溪县' => '松谿縣', '板荡' => '板蕩', '林宏岳' => '林宏嶽', '林杰樑' => '林杰樑', @@ -6146,7 +6066,9 @@ $zh2Hant = array( '柜上' => '柜上', '柜子' => '柜子', '柜柳' => '柜柳', +'查封后' => '查封後', '柱梁' => '柱樑', +'柳斌杰' => '柳斌杰', '柳诒征' => '柳詒徵', '栖栖皇皇' => '栖栖皇皇', '栗栖溪' => '栗栖溪', @@ -6160,6 +6082,7 @@ $zh2Hant = array( '格里高利历' => '格里高利曆', '格斗' => '格鬥', '桂圆干' => '桂圓乾', +'框里' => '框裡', '桌几' => '桌几', '桌历' => '桌曆', '桌历史' => '桌歷史', @@ -6169,6 +6092,7 @@ $zh2Hant = array( '杆秤' => '桿秤', '杆菌' => '桿菌', '梁上君子' => '梁上君子', +'梁启超' => '梁啓超', '条干' => '條幹', '梨干' => '梨乾', '梯冲' => '梯衝', @@ -6185,14 +6109,15 @@ $zh2Hant = array( '植发' => '植髮', '椒面' => '椒麵', '椰枣干' => '椰棗乾', -'楊雅筑' => '楊雅筑', '杨雅筑' => '楊雅筑', +'楊雅筑' => '楊雅筑', '桢干' => '楨幹', '业余' => '業餘', '榨干' => '榨乾', '枪杆' => '槍桿', '杠杆' => '槓桿', '乐器钟' => '樂器鐘', +'乐游原' => '樂遊原', '樊于期' => '樊於期', '梁上' => '樑上', '梁柱' => '樑柱', @@ -6208,8 +6133,8 @@ $zh2Hant = array( '模范14棒' => '模范14棒', '模范21棒' => '模范21棒', '模范七棒' => '模范七棒', -'模范三軍' => '模范三軍', '模范三军' => '模范三軍', +'模范三軍' => '模范三軍', '模范棒棒堂' => '模范棒棒堂', '模制' => '模製', '样范' => '樣範', @@ -6242,6 +6167,7 @@ $zh2Hant = array( '机械表' => '機械錶', '机械钟' => '機械鐘', '机械钟表' => '機械鐘錶', +'横峰县' => '橫峯縣', '横征暴敛' => '橫徵暴斂', '横梁' => '橫樑', '横冲' => '橫衝', @@ -6250,6 +6176,7 @@ $zh2Hant = array( '台灯' => '檯燈', '台球' => '檯球', '台面上' => '檯面上', +'台面化' => '檯面化', '柜台' => '櫃檯', '栉发工' => '櫛髮工', '欲海难填' => '欲海難填', @@ -6259,6 +6186,7 @@ $zh2Hant = array( '欧游' => '歐遊', '止于' => '止於', '正官庄' => '正官庄', +'正杰' => '正杰', '武丑' => '武丑', '武后' => '武后', '武斗' => '武鬥', @@ -6299,6 +6227,7 @@ $zh2Hant = array( '水来汤里去' => '水來湯裡去', '水准' => '水準', '水无怜奈' => '水無怜奈', +'水表面' => '水表面', '水里' => '水裡', '水里商工' => '水里商工', '水里溪' => '水里溪', @@ -6312,13 +6241,11 @@ $zh2Hant = array( '永志不忘' => '永誌不忘', '求知欲' => '求知慾', '求签' => '求籤', -'求道于盲' => '求道於盲', -'污蔑' => '汙衊', '池里' => '池裡', +'污蔑' => '污衊', '汤卤' => '汤滷', '汲于' => '汲於', '决斗' => '決鬥', -'沈海蓉' => '沈海蓉', '沈淀' => '沈澱', '沈郁' => '沈鬱', '沉淀' => '沉澱', @@ -6327,15 +6254,14 @@ $zh2Hant = array( '没事干' => '沒事幹', '没干' => '沒幹', '没折至' => '沒摺至', -'没梢干' => '沒梢幹', '没样范' => '沒樣範', '没准' => '沒準', '冲冠发怒' => '沖冠髮怒', '冲天' => '沖天', +'沙琅' => '沙瑯', '沙羡' => '沙羡', '沙里淘金' => '沙裡淘金', '河岳' => '河嶽', -'河流汇集' => '河流匯集', '河里' => '河裡', '油泼面' => '油潑麵', '油斗' => '油鬥', @@ -6356,7 +6282,7 @@ $zh2Hant = array( '泱郁' => '泱鬱', '泳气钟' => '泳氣鐘', '洄游' => '洄遊', -'洋河大曲' => '洋河大麴', +'洋河大曲' => '洋河大麯', '洒家' => '洒家', '洒扫' => '洒掃', '洒水' => '洒水', @@ -6371,6 +6297,8 @@ $zh2Hant = array( '洗发' => '洗髮', '洛钟东应' => '洛鐘東應', '洞里' => '洞裡', +'洞里萨' => '洞里薩', +'洞里薩' => '洞里薩', '泄欲' => '洩慾', '洪范' => '洪範', '洪谷子' => '洪谷子', @@ -6391,8 +6319,8 @@ $zh2Hant = array( '浮夸' => '浮誇', '浮松' => '浮鬆', '海干' => '海乾', -'海淀山後' => '海淀山後', '海淀山后' => '海淀山後', +'海淀山後' => '海淀山後', '浸卤' => '浸滷', '涂善妮' => '涂善妮', '涂坤' => '涂坤', @@ -6413,8 +6341,9 @@ $zh2Hant = array( '涂醒哲' => '涂醒哲', '涂長望' => '涂長望', '涂长望' => '涂長望', -'涂鸿钦' => '涂鴻欽', '涂鴻欽' => '涂鴻欽', +'涂鸿钦' => '涂鴻欽', +'涌水塘' => '涌水塘', '涳蒙' => '涳濛', '涸干' => '涸乾', '凉席' => '涼蓆', @@ -6425,7 +6354,6 @@ $zh2Hant = array( '泪如泉涌' => '淚如泉湧', '淡于' => '淡於', '淡蒙蒙' => '淡濛濛', -'淡朱' => '淡硃', '净余' => '淨餘', '净发' => '淨髮', '淫欲' => '淫慾', @@ -6442,7 +6370,6 @@ $zh2Hant = array( '渠冲' => '渠衝', '测不准' => '測不準', '港制' => '港製', -'游牧民族' => '游牧民族', '游离' => '游離', '浑朴' => '渾樸', '浑个' => '渾箇', @@ -6453,6 +6380,7 @@ $zh2Hant = array( '涌入' => '湧入', '涌出' => '湧出', '涌向' => '湧向', +'涌水' => '湧水', '涌泉' => '湧泉', '涌现' => '湧現', '涌起' => '湧起', @@ -6495,13 +6423,17 @@ $zh2Hant = array( '准军事' => '準軍事', '准头' => '準頭', '准点' => '準點', +'沟大曲' => '溝大麯', +'沟谷' => '溝谷', '溟蒙' => '溟濛', '溢于' => '溢於', +'温洛克期' => '溫洛克期', '溲面' => '溲麵', '溺于' => '溺於', '滃郁' => '滃鬱', '滑借' => '滑藉', '汇丰' => '滙豐', +'渗漓' => '滲灕', '卤了' => '滷了', '卤五花' => '滷五花', '卤味' => '滷味', @@ -6542,9 +6474,9 @@ $zh2Hant = array( '潮涌' => '潮湧', '溃于' => '潰於', '涩谷区' => '澀谷區', +'澄江县' => '澂江縣', '澄澹精致' => '澄澹精致', '澒蒙' => '澒濛', -'泽渗漓而下降' => '澤滲灕而下降', '淀乃不耕之地' => '澱乃不耕之地', '淀北片' => '澱北片', '淀山' => '澱山', @@ -6560,19 +6492,29 @@ $zh2Hant = array( '蒙汜' => '濛汜', '蒙蒙细雨' => '濛濛細雨', '蒙雾' => '濛霧', -'蒙松雨' => '濛鬆雨', '蒙鸿' => '濛鴻', +'浚州' => '濬州', +'浚县' => '濬縣', '滨田里佳子' => '濱田里佳子', -'沈吉线' => '瀋吉線', +'沈丹客运' => '瀋丹客運', +'沈丹线' => '瀋丹線', +'沈丹铁路' => '瀋丹鐵路', +'沈北' => '瀋北', +'沈吉' => '瀋吉', +'沈大线' => '瀋大線', +'沈大铁路' => '瀋大鐵路', +'沈大高速' => '瀋大高速', '沈山线' => '瀋山線', +'沈山铁路' => '瀋山鐵路', '沈州' => '瀋州', '沈抚' => '瀋撫', '沈水' => '瀋水', '沈河' => '瀋河', -'沈海' => '瀋海', '沈海铁路' => '瀋海鐵路', +'沈海高速' => '瀋海高速', '沈阳' => '瀋陽', '泸州大曲' => '瀘州大麯', +'沥干' => '瀝乾', '潇洒' => '瀟洒', '弥山遍野' => '瀰山遍野', '弥漫' => '瀰漫', @@ -6604,6 +6546,7 @@ $zh2Hant = array( '烘制' => '烘製', '烤干' => '烤乾', '烤卤' => '烤滷', +'烹制' => '烹製', '焙干' => '焙乾', '无征不信' => '無徵不信', '无业游民' => '無業游民', @@ -6613,6 +6556,7 @@ $zh2Hant = array( '炼制' => '煉製', '煎面' => '煎麵', '烟卷' => '煙捲', +'烟台' => '煙臺', '照入签' => '照入籤', '照相干片' => '照相乾片', '煨干' => '煨乾', @@ -6622,8 +6566,6 @@ $zh2Hant = array( '燎发' => '燎髮', '烧干' => '燒乾', '燕几' => '燕几', -'燕巢于幕' => '燕巢於幕', -'燕燕于飞' => '燕燕于飛', '燕游' => '燕遊', '烫一个发' => '燙一個髮', '烫一次发' => '燙一次髮', @@ -6674,7 +6616,6 @@ $zh2Hant = array( '犹如表' => '猶如錶', '犹如钟' => '猶如鐘', '犹如钟表' => '猶如鐘錶', -'呆串了皮' => '獃串了皮', '狱里' => '獄裡', '奖杯' => '獎盃', '独裁制' => '獨裁制', @@ -6687,6 +6628,7 @@ $zh2Hant = array( '玉米面' => '玉米面', '王侯后' => '王侯后', '王后' => '王后', +'王添灯' => '王添灯', '王田里' => '王田里', '王鉴' => '王鑑', '王余鱼' => '王餘魚', @@ -6701,6 +6643,7 @@ $zh2Hant = array( '理次发' => '理次髮', '理发' => '理髮', '琴钟' => '琴鐘', +'珐琅' => '琺瑯', '瑞城里' => '瑞城里', '瑞征' => '瑞徵', '瑶签' => '瑤籤', @@ -6722,6 +6665,7 @@ $zh2Hant = array( '生发' => '生髮', '产卵洄游' => '產卵洄游', '苏醒' => '甦醒', +'用于' => '用於', '用法里' => '用法裡', '甩发' => '甩髮', '田子里' => '田子里', @@ -6732,10 +6676,10 @@ $zh2Hant = array( '由于' => '由於', '甲胄' => '甲冑', '甲后路' => '甲后路', -'电影后' => '电影後', '男仆' => '男僕', '界里' => '界裡', '畏于' => '畏於', +'留长发' => '留長髮', '留发' => '留髮', '毕于' => '畢於', '毕业于' => '畢業於', @@ -6765,13 +6709,11 @@ $zh2Hant = array( '癸丑' => '癸丑', '发干' => '發乾', '发呆' => '發獃', -'发蒙' => '發矇', '发签' => '發籤', '发松' => '發鬆', '发面' => '發麵', -'白干' => '白乾', +'白干儿' => '白乾兒', '白子里' => '白子里', -'白干儿' => '白干兒', '白术' => '白朮', '白朴' => '白樸', '白净面皮' => '白淨面皮', @@ -6785,11 +6727,11 @@ $zh2Hant = array( '白霉' => '白黴', '百个' => '百個', '百只可' => '百只可', -'百只夠' => '百只夠', '百只够' => '百只夠', +'百只夠' => '百只夠', '百只怕' => '百只怕', -'百只足夠' => '百只足夠', '百只足够' => '百只足夠', +'百只足夠' => '百只足夠', '百周后' => '百周後', '百天后' => '百天後', '百年' => '百年', @@ -6805,6 +6747,8 @@ $zh2Hant = array( '的回复' => '的回覆', '的图里' => '的圖裡', '的山里' => '的山裡', +'的干将' => '的幹將', +'的个中' => '的箇中', '的钟' => '的鐘', '的长发' => '的長髮', '的发小' => '的髮小', @@ -6857,11 +6801,13 @@ $zh2Hant = array( '看钟' => '看鐘', '真凶' => '真兇', '真个' => '真箇', +'真丑' => '真醜', '眼干' => '眼乾', '眼帘' => '眼帘', '眼眶里' => '眼眶裡', '眼睛里' => '眼睛裡', '眼里' => '眼裡', +'着眼于' => '着眼於', '困乏' => '睏乏', '困了' => '睏了', '困倦' => '睏倦', @@ -6877,6 +6823,7 @@ $zh2Hant = array( '瞳蒙' => '瞳矇', '蒙事' => '矇事', '蒙昧无知' => '矇昧無知', +'蒙松雨' => '矇松雨', '蒙混' => '矇混', '蒙瞍' => '矇瞍', '蒙眬' => '矇矓', @@ -6887,6 +6834,7 @@ $zh2Hant = array( '矜夸' => '矜誇', '短几' => '短几', '短于' => '短於', +'短发生' => '短發生', '短发' => '短髮', '矮几' => '矮几', '石几' => '石几', @@ -6895,20 +6843,14 @@ $zh2Hant = array( '石英钟' => '石英鐘', '石英钟表' => '石英鐘錶', '石钟' => '石鐘', -'石钟山' => '石鐘山', '研制' => '研製', '砰当' => '砰噹', '破鉴' => '破鑑', -'朱唇皓齿' => '硃唇皓齒', -'朱批' => '硃批', '朱砂' => '硃砂', -'朱笔' => '硃筆', -'朱红色' => '硃紅色', -'朱色' => '硃色', -'朱谕' => '硃諭', '硬干' => '硬幹', '确瘠' => '确瘠', '碑志' => '碑誌', +'碗里' => '碗裡', '碰钟' => '碰鐘', '确系' => '確係', '码表' => '碼錶', @@ -6952,7 +6894,6 @@ $zh2Hant = array( '私欲' => '私慾', '私斗' => '私鬥', '秋游' => '秋遊', -'秋阴入井干' => '秋陰入井幹', '秋发' => '秋髮', '种丹妮' => '种丹妮', '种师中' => '种師中', @@ -6965,6 +6906,7 @@ $zh2Hant = array( '秒表示' => '秒表示', '秒钟' => '秒鐘', '秤杆' => '秤桿', +'秦沈客运' => '秦瀋客運', '移祸于' => '移禍於', '稀松' => '稀鬆', '棱台' => '稜台', @@ -7006,10 +6948,10 @@ $zh2Hant = array( '谷草' => '穀草', '谷贵饿农' => '穀貴餓農', '谷贱伤农' => '穀賤傷農', -'谷道' => '穀道', '谷雨' => '穀雨', '谷类' => '穀類', '谷食' => '穀食', +'穆棱' => '穆稜', '穆罕默德历' => '穆罕默德曆', '穆罕默德历史' => '穆罕默德歷史', '积淀' => '積澱', @@ -7021,7 +6963,6 @@ $zh2Hant = array( '空蒙' => '空濛', '空荡' => '空蕩', '空荡荡' => '空蕩蕩', -'空谷回音' => '空谷回音', '空钟' => '空鐘', '空余' => '空餘', '窒欲' => '窒慾', @@ -7035,7 +6976,6 @@ $zh2Hant = array( '窃钟掩耳' => '竊鐘掩耳', '立于' => '立於', '立范' => '立範', -'站干岸儿' => '站乾岸兒', '童仆' => '童僕', '竞斗' => '競鬥', '竹几' => '竹几', @@ -7043,6 +6983,7 @@ $zh2Hant = array( '竹签' => '竹籤', '竹席' => '竹蓆', '竹制' => '竹製', +'竹溪县' => '竹谿縣', '笑里藏刀' => '笑裡藏刀', '第一出现' => '第一出現', '第一出現' => '第一出現', @@ -7053,8 +6994,8 @@ $zh2Hant = array( '第三出局' => '第三出局', '第三出' => '第三齣', '第九出' => '第九齣', -'第二出线' => '第二出線', '第二出線' => '第二出線', +'第二出线' => '第二出線', '第二出' => '第二齣', '第五出局' => '第五出局', '第五出' => '第五齣', @@ -7076,21 +7017,19 @@ $zh2Hant = array( '筑肥' => '筑肥', '筑西' => '筑西', '筑邦' => '筑邦', -'筑陽' => '筑陽', '筑阳' => '筑陽', +'筑陽' => '筑陽', '答复' => '答覆', '筵几' => '筵几', '个中原因' => '箇中原因', -'个中奥妙' => '箇中奧妙', -'个中奥秘' => '箇中奧秘', +'个中奥' => '箇中奧', '个中好手' => '箇中好手', '个中强手' => '箇中強手', -'个中消息' => '箇中消息', '个中滋味' => '箇中滋味', '个中玄机' => '箇中玄機', '个中理由' => '箇中理由', -'个中讯息' => '箇中訊息', -'个中资讯' => '箇中資訊', +'个中翘楚' => '箇中翹楚', +'个中道理' => '箇中道理', '个中高手' => '箇中高手', '个旧' => '箇舊', '算历' => '算曆', @@ -7103,6 +7042,7 @@ $zh2Hant = array( '节欲' => '節慾', '节目里' => '節目裡', '节余' => '節餘', +'范亭' => '範亭', '范例' => '範例', '范围' => '範圍', '范字' => '範字', @@ -7115,8 +7055,9 @@ $zh2Hant = array( '范金' => '範金', '简并' => '簡併', '简朴' => '簡樸', -'簡筑翎' => '簡筑翎', +'简短发' => '簡短發', '简筑翎' => '簡筑翎', +'簡筑翎' => '簡筑翎', '簸荡' => '簸蕩', '签幐' => '籤幐', '签押' => '籤押', @@ -7198,13 +7139,12 @@ $zh2Hant = array( '绝于' => '絕於', '绞干' => '絞乾', '络腮胡' => '絡腮鬍', -'給我干脆' => '給我干脆', -'给我干脆' => '給我干脆', '给于' => '給於', '丝恩发怨' => '絲恩髮怨', '丝制' => '絲製', '丝发' => '絲髮', '绑扎' => '綁紮', +'绥棱' => '綏稜', '捆扎' => '綑紮', '經有云' => '經有云', '经有云' => '經有云', @@ -7213,6 +7153,9 @@ $zh2Hant = array( '维系' => '維繫', '绾发' => '綰髮', '纲鉴' => '綱鑑', +'網球台' => '網球台', +'网球台' => '網球台', +'网站里' => '網站裡', '网里' => '網裡', '网志' => '網誌', '网游' => '網遊', @@ -7237,6 +7180,7 @@ $zh2Hant = array( '缝里' => '縫裡', '缝制' => '縫製', '缩栗' => '縮慄', +'缩短发' => '縮短發', '纵欲' => '縱慾', '纤夫' => '縴夫', '纤手' => '縴手', @@ -7247,7 +7191,6 @@ $zh2Hant = array( '繁复' => '繁複', '繁钟' => '繁鐘', '绷扒吊拷' => '繃扒弔拷', -'穗帏飘井干' => '繐幃飄井幹', '绕梁' => '繞樑', '绘制' => '繪製', '系上。' => '繫上。', @@ -7273,11 +7216,11 @@ $zh2Hant = array( '系紧' => '繫緊', '系绳' => '繫繩', '系累' => '繫纍', +'系舟' => '繫舟', '系船' => '繫船', '系辞' => '繫辭', '系鞋带' => '繫鞋帶', '系风捕影' => '繫風捕影', -'继承制' => '繼承制', '累囚' => '纍囚', '累堆' => '纍堆', '累瓦结绳' => '纍瓦結繩', @@ -7296,7 +7239,6 @@ $zh2Hant = array( '羁系' => '羈繫', '美容美发' => '美容美髮', '美于' => '美於', -'美制' => '美製', '美丑' => '美醜', '美发学' => '美髮學', '美发师' => '美髮師', @@ -7317,9 +7259,10 @@ $zh2Hant = array( '老干' => '老乾', '老仆' => '老僕', '老干部' => '老幹部', -'老蒙' => '老懞', +'老懞' => '老懞', '老于' => '老於', '老爷钟' => '老爺鐘', +'老白干' => '老白乾', '老姜' => '老薑', '老板' => '老闆', '老面皮' => '老面皮', @@ -7331,6 +7274,8 @@ $zh2Hant = array( '聊斋志异' => '聊齋志異', '圣人历' => '聖人曆', '圣后' => '聖后', +'圣马尔谷日' => '聖馬爾谷日', +'聖馬爾谷日' => '聖馬爾谷日', '聘雇' => '聘僱', '聚药雄蕊' => '聚葯雄蕊', '闻风后' => '聞風後', @@ -7356,7 +7301,9 @@ $zh2Hant = array( '胜肽' => '胜肽', '胜键' => '胜鍵', '胡云' => '胡云', +'胡子婴' => '胡子嬰', '胡子昂' => '胡子昂', +'胡杰' => '胡杰', '胡朴安' => '胡樸安', '胡里胡涂' => '胡裡胡塗', '胰脏' => '胰臟', @@ -7380,6 +7327,7 @@ $zh2Hant = array( '腊味' => '腊味', '腊毒' => '腊毒', '腊笔' => '腊筆', +'腌臜' => '腌臢', '肾脏' => '腎臟', '腐干' => '腐乾', '腐余' => '腐餘', @@ -7401,6 +7349,7 @@ $zh2Hant = array( '卧游' => '臥遊', '臧谷亡羊' => '臧穀亡羊', '临潼斗宝' => '臨潼鬥寶', +'自干五' => '自乾五', '自制一下' => '自制一下', '自制下来' => '自制下來', '自制不' => '自制不', @@ -7419,6 +7368,7 @@ $zh2Hant = array( '自制能力' => '自制能力', '自于' => '自於', '自然数里' => '自然數裡', +'自由钟' => '自由鐘', '自制' => '自製', '自觉自愿' => '自覺自愿', '自夸' => '自誇', @@ -7431,8 +7381,8 @@ $zh2Hant = array( '台静农' => '臺靜農', '臻于' => '臻於', '舂谷' => '舂穀', -'舉手表' => '舉手表', '举手表' => '舉手表', +'舉手表' => '舉手表', '舊庄' => '舊庄', '旧历' => '舊曆', '旧历史' => '舊歷史', @@ -7468,14 +7418,15 @@ $zh2Hant = array( '苑里' => '苑裡', '若干' => '若干', '苦干' => '苦幹', +'苦于' => '苦於', '苦里' => '苦裡', '苦斗' => '苦鬥', '苎麻' => '苧麻', '茂都淀' => '茂都澱', '范文同' => '范文同', '范文正公' => '范文正公', -'范文瀾' => '范文瀾', '范文澜' => '范文瀾', +'范文瀾' => '范文瀾', '范文照' => '范文照', '范文程' => '范文程', '范文芳' => '范文芳', @@ -7484,8 +7435,8 @@ $zh2Hant = array( '范登堡' => '范登堡', '范賢惠' => '范賢惠', '范贤惠' => '范賢惠', -'茅于轼' => '茅于軾', '茅于軾' => '茅于軾', +'茅于轼' => '茅于軾', '茶几' => '茶几', '茶余' => '茶餘', '茶面' => '茶麵', @@ -7502,12 +7453,12 @@ $zh2Hant = array( '莽荡' => '莽蕩', '菜干' => '菜乾', '菜坛' => '菜罈', -'菜肴' => '菜肴', +'菜肴' => '菜餚', '菠棱菜' => '菠稜菜', '菠萝干' => '菠蘿乾', '华严钟' => '華嚴鐘', -'萬一只' => '萬一只', '万一只' => '萬一只', +'萬一只' => '萬一只', '万个' => '萬個', '万周后' => '萬周後', '万天后' => '萬天後', @@ -7521,6 +7472,7 @@ $zh2Hant = array( '万象' => '萬象', '万只' => '萬隻', '万余' => '萬餘', +'落于' => '落於', '落腮胡' => '落腮鬍', '落发' => '落髮', '叶叶琴' => '葉叶琴', @@ -7528,6 +7480,7 @@ $zh2Hant = array( '葡萄干' => '葡萄乾', '董氏封发' => '董氏封髮', '葫芦里卖甚么药' => '葫蘆裡賣甚麼藥', +'葬于' => '葬於', '蒙雾露' => '蒙霧露', '蒜发' => '蒜髮', '蒲席' => '蒲蓆', @@ -7541,9 +7494,9 @@ $zh2Hant = array( '蓄须' => '蓄鬚', '席子' => '蓆子', '蓊郁' => '蓊鬱', -'蓬蓬松松' => '蓬蓬鬆鬆', '蓬发' => '蓬髮', '蓬松' => '蓬鬆', +'蓬松松' => '蓬鬆鬆', '参绥' => '蔘綏', '葱郁' => '蔥鬱', '荞麦面' => '蕎麥麵', @@ -7589,8 +7542,8 @@ $zh2Hant = array( '熏风' => '薰風', '熏香' => '薰香', '苧悴' => '薴悴', -'薴烯' => '薴烯', '苧烯' => '薴烯', +'薴烯' => '薴烯', '借以' => '藉以', '借助' => '藉助', '借口' => '藉口', @@ -7612,14 +7565,16 @@ $zh2Hant = array( '藤制' => '藤製', '药签' => '藥籤', '药面儿' => '藥麵兒', -'苏昆' => '蘇崑', '苏崑' => '蘇崑', +'苏昆' => '蘇崑', '苹果' => '蘋果', '苹果干' => '蘋果乾', +'兰溪市' => '蘭谿市', '萝卜' => '蘿蔔', '萝卜干' => '蘿蔔乾', '虎须' => '虎鬚', '虎斗' => '虎鬥', +'处于' => '處於', '虚夸' => '虛誇', '号志' => '號誌', '虫部' => '虫部', @@ -7634,6 +7589,7 @@ $zh2Hant = array( '蝎虎' => '蝎虎', '蝎蝎螫螫' => '蝎蝎螫螫', '蝎谮' => '蝎譖', +'虾面' => '蝦麵', '虮虱相吊' => '蟣蝨相弔', '蛏干' => '蟶乾', '蚁后' => '蟻后', @@ -7645,8 +7601,8 @@ $zh2Hant = array( '行事历' => '行事曆', '行事历史' => '行事歷史', '行凶' => '行兇', +'行家里手' => '行家裡手', '行于' => '行於', -'行百里者半于九十' => '行百里者半於九十', '卫后庄公' => '衛後莊公', '卫星钟' => '衛星鐘', '冲上' => '衝上', @@ -7698,6 +7654,7 @@ $zh2Hant = array( '冲头阵' => '衝頭陣', '冲风' => '衝風', '衡鉴' => '衡鑑', +'表面包' => '表面包', '衷于' => '衷於', '袋杆' => '袋桿', '袋里' => '袋裡', @@ -7716,9 +7673,9 @@ $zh2Hant = array( '夹裙' => '袷裙', '裁并' => '裁併', '裁制' => '裁製', -'里手' => '裏手', '里水镇' => '裏水鎮', '里海' => '裏海', +'里运河' => '裏運河', '补于' => '補於', '补注' => '補註', '装折' => '裝摺', @@ -7771,6 +7728,7 @@ $zh2Hant = array( '复利' => '複利', '复印' => '複印', '复句' => '複句', +'复合' => '複合', '复壁' => '複壁', '复姓' => '複姓', '复字键' => '複字鍵', @@ -7780,6 +7738,7 @@ $zh2Hant = array( '复平面' => '複平面', '复式' => '複式', '复数' => '複數', +'复方' => '複方', '复本' => '複本', '复查' => '複查', '复次' => '複次', @@ -7813,6 +7772,7 @@ $zh2Hant = array( '复韵' => '複韻', '褒赞' => '褒讚', '衬里' => '襯裡', +'西井里' => '西井里', '西周钟' => '西周鐘', '西昆' => '西崑', '西岳' => '西嶽', @@ -7840,12 +7800,14 @@ $zh2Hant = array( '角落里' => '角落裡', '觚棱' => '觚稜', '解雇' => '解僱', +'解封后' => '解封後', '解铃仍须系铃人' => '解鈴仍須繫鈴人', '解铃还须系铃人' => '解鈴還須繫鈴人', '解发佯狂' => '解髮佯狂', '触须' => '觸鬚', '言云' => '言云', '言大而夸' => '言大而夸', +'言里' => '言裡', '言辩而确' => '言辯而确', '订制' => '訂製', '计划' => '計劃', @@ -7855,6 +7817,7 @@ $zh2Hant = array( '托交' => '託交', '托人' => '託人', '托付' => '託付', +'托克逊' => '託克遜', '托儿' => '託兒', '托古讽今' => '託古諷今', '托名' => '託名', @@ -7876,9 +7839,10 @@ $zh2Hant = array( '托辞' => '託辭', '托运' => '託運', '托过' => '託過', +'托里县' => '託里縣', '托附' => '託附', '许愿起经' => '許愿起經', -'许虬' => '許虬', +'許聖杰' => '許聖杰', '注上' => '註上', '注册' => '註冊', '注失' => '註失', @@ -7901,7 +7865,6 @@ $zh2Hant = array( '词汇' => '詞彙', '词余' => '詞餘', '询于' => '詢於', -'询于刍荛' => '詢於芻蕘', '试制' => '試製', '詩云' => '詩云', '诗云' => '詩云', @@ -7915,6 +7878,7 @@ $zh2Hant = array( '诔赞' => '誄讚', '夸下海口' => '誇下海口', '夸了' => '誇了', +'夸人' => '誇人', '夸他' => '誇他', '夸你' => '誇你', '夸来夸去' => '誇來誇去', @@ -7926,7 +7890,9 @@ $zh2Hant = array( '夸多斗靡' => '誇多鬥靡', '夸大' => '誇大', '夸她' => '誇她', +'夸姣' => '誇姣', '夸官' => '誇官', +'夸容' => '誇容', '夸张' => '誇張', '夸强说会' => '誇強說會', '夸得' => '誇得', @@ -7951,6 +7917,7 @@ $zh2Hant = array( '夸辩' => '誇辯', '夸过' => '誇過', '夸饰' => '誇飾', +'夸丽' => '誇麗', '志哀' => '誌哀', '志喜' => '誌喜', '志庆' => '誌慶', @@ -7980,13 +7947,14 @@ $zh2Hant = array( '咨询' => '諮詢', '诸余' => '諸餘', '谋干' => '謀幹', +'謝杰' => '謝杰', +'谢杰' => '謝杰', '谢华后' => '謝華后', '谬采虚声' => '謬採虛聲', '谬赞' => '謬讚', '謷丑' => '謷醜', '謹愿' => '謹愿', '谨愿' => '謹愿', -'谨于心' => '謹於心', '哗噪' => '譁噪', '哗嚣' => '譁囂', '哗然' => '譁然', @@ -8023,6 +7991,7 @@ $zh2Hant = array( '豆干' => '豆乾', '豆腐干' => '豆腐乾', '竖起脊梁' => '豎起脊梁', +'丰度' => '豐度', '丰滨' => '豐濱', '丰滨乡' => '豐濱鄉', '丰台' => '豐臺', @@ -8043,8 +8012,8 @@ $zh2Hant = array( '賢后' => '賢后', '贤后' => '賢后', '卖断发' => '賣斷發', -'赋范' => '賦范', '賦范' => '賦范', +'赋范' => '賦范', '质数里' => '質數裡', '质朴' => '質樸', '赌后' => '賭后', @@ -8065,6 +8034,7 @@ $zh2Hant = array( '赵治勋' => '趙治勳', '趱干' => '趲幹', '足于' => '足於', +'足球台' => '足球台', '跌扑' => '跌扑', '路图里' => '路圖裡', '路签' => '路籤', @@ -8098,8 +8068,8 @@ $zh2Hant = array( '挽输' => '輓輸', '挽辞' => '輓辭', '轻于' => '輕於', -'轻轻松松' => '輕輕鬆鬆', '轻松' => '輕鬆', +'轻松松' => '輕鬆鬆', '轮奸' => '輪姦', '轮回' => '輪迴', '转向往' => '轉向往', @@ -8112,6 +8082,7 @@ $zh2Hant = array( '辞汇' => '辭彙', '辫发' => '辮髮', '辩斗' => '辯鬥', +'辰溪县' => '辰谿縣', '农历' => '農曆', '农历史' => '農歷史', '农民历' => '農民曆', @@ -8122,11 +8093,16 @@ $zh2Hant = array( '迥然回异' => '迥然迴異', '迫于' => '迫於', '回光返照' => '迴光返照', -'回向' => '迴向', '回圈' => '迴圈', '回廊' => '迴廊', '回形夹' => '迴形夾', -'回文' => '迴文', +'回文序列' => '迴文序列', +'回文数' => '迴文數', +'回文构词' => '迴文構詞', +'回文结构' => '迴文結構', +'回文联' => '迴文聯', +'回文诗' => '迴文詩', +'回文锦' => '迴文錦', '回旋' => '迴旋', '回环' => '迴環', '回纹针' => '迴紋針', @@ -8141,14 +8117,12 @@ $zh2Hant = array( '回递性' => '迴遞性', '回避' => '迴避', '回銮' => '迴鑾', -'回音' => '迴音', '回响' => '迴響', '回风' => '迴風', '迷于' => '迷於', '迷蒙' => '迷濛', '追凶' => '追兇', '退伙' => '退夥', -'退藏于密' => '退藏於密', '逆钟' => '逆鐘', '逆钟向' => '逆鐘向', '逆风后' => '逆風後', @@ -8185,6 +8159,7 @@ $zh2Hant = array( '这里' => '這裡', '这钟' => '這鐘', '这只' => '這隻', +'这么干' => '這麼幹', '这出' => '這齣', '通奸' => '通姦', '通心面' => '通心麵', @@ -8196,11 +8171,12 @@ $zh2Hant = array( '造钟' => '造鐘', '连三并四' => '連三併四', '连采' => '連採', +'连发式' => '連發式', '连系' => '連繫', -'周游世界' => '週遊世界', +'周游' => '週遊', '进两出' => '進兩出', -'進制' => '進制', '进制' => '進制', +'進制' => '進制', '逼并' => '逼併', '遇风后' => '遇風後', '游了' => '遊了', @@ -8234,6 +8210,7 @@ $zh2Hant = array( '游目骋怀' => '遊目騁懷', '游程' => '遊程', '游丝' => '遊絲', +'游美学务' => '遊美學務', '游兴' => '遊興', '游船' => '遊船', '游艇' => '遊艇', @@ -8259,6 +8236,7 @@ $zh2Hant = array( '递回' => '遞迴', '远游' => '遠遊', '遨游' => '遨遊', +'适于' => '適於', '遮丑' => '遮醜', '迁于' => '遷於', '选手表明' => '選手表明', @@ -8301,6 +8279,7 @@ $zh2Hant = array( '部子里' => '部子里', '部落发' => '部落發', '郭后' => '郭后', +'都市里' => '都市裡', '都于' => '都於', '乡愿' => '鄉愿', '鄉愿' => '鄉愿', @@ -8308,16 +8287,16 @@ $zh2Hant = array( '鄭凱云' => '鄭凱云', '配制饲料' => '配制飼料', '配图里' => '配圖裡', -'配水干管' => '配水幹管', '配制' => '配製', '酒帘' => '酒帘', '酒气冲天' => '酒氣衝天', '酒坛' => '酒罈', '酒肴' => '酒肴', -'酒麹' => '酒麴', '酒曲' => '酒麴', +'酒麹' => '酒麴', '酥松' => '酥鬆', '酸姜' => '酸薑', +'腌制' => '醃製', '醇朴' => '醇樸', '醉于' => '醉於', '醋坛' => '醋罈', @@ -8337,7 +8316,6 @@ $zh2Hant = array( '丑女' => '醜女', '丑女效颦' => '醜女效顰', '丑奴儿' => '醜奴兒', -'丑婆子' => '醜婆子', '丑妇' => '醜婦', '丑媳' => '醜媳', '丑媳妇' => '醜媳婦', @@ -8417,6 +8395,7 @@ $zh2Hant = array( '金表露' => '金表露', '金表面' => '金表面', '金装玉里' => '金裝玉裡', +'金溪县' => '金谿縣', '金链' => '金鍊', '金钟' => '金鐘', '金发' => '金髮', @@ -8511,6 +8490,7 @@ $zh2Hant = array( '钟罩' => '鐘罩', '钟声' => '鐘聲', '钟腰' => '鐘腰', +'钟花' => '鐘花', '钟螺' => '鐘螺', '钟行' => '鐘行', '钟表面' => '鐘表面', @@ -8620,8 +8600,10 @@ $zh2Hant = array( '陳冲' => '陳冲', '陳士杰' => '陳士杰', '陈升' => '陳昇', -'陳有后' => '陳有后', '陈有后' => '陳有后', +'陳有后' => '陳有后', +'陈杰' => '陳杰', +'陳杰' => '陳杰', '陈炼' => '陳鍊', '陆游' => '陸遊', '阳春面' => '陽春麵', @@ -8649,7 +8631,6 @@ $zh2Hant = array( '雕梁画栋' => '雕樑畫棟', '双折射' => '雙折射', '双折' => '雙摺', -'双沟大曲' => '雙溝大麯', '双胜类' => '雙胜類', '双雕' => '雙鵰', '杂合面儿' => '雜合麵兒', @@ -8671,8 +8652,9 @@ $zh2Hant = array( '雪里' => '雪裡', '雪里红' => '雪裡紅', '雪里蕻' => '雪裡蕻', -'云吞面' => '雲吞麵', +'云吞' => '雲吞', '云笈七签' => '雲笈七籤', +'云里雾里' => '雲裡霧裡', '云游' => '雲遊', '云须' => '雲鬚', '零个' => '零個', @@ -8684,16 +8666,22 @@ $zh2Hant = array( '电子表格' => '電子表格', '电子钟' => '電子鐘', '电子钟表' => '電子鐘錶', +'电影后' => '電影後', +'电梯里' => '電梯裡', '电波钟' => '電波鐘', '电码表' => '電碼表', '电冲' => '電衝', +'电视台风' => '電視台風', '电表' => '電錶', '电钟' => '電鐘', '震栗' => '震慄', '霉气冲天' => '霉氣衝天', +'沾化' => '霑化', +'沾益' => '霑益', '雾里' => '霧裡', '露丑' => '露醜', '霁范' => '霽範', +'灵昆' => '靈崑', '青山一发' => '青山一髮', '青霉' => '青黴', '非常准' => '非常準', @@ -8710,18 +8698,18 @@ $zh2Hant = array( '面包管' => '面包管', '面包扎' => '面包紮', '面包罗' => '面包羅', +'面包着' => '面包著', '面包藏' => '面包藏', '面包装' => '面包裝', '面包裹' => '面包裹', '面包起' => '面包起', '面包办' => '面包辦', '面店铺' => '面店鋪', -'面條目' => '面條目', '面条目' => '面條目', +'面條目' => '面條目', '面粉碎' => '面粉碎', '面粉红' => '面粉紅', '面食饭' => '面食飯', -'面食面' => '面食麵', '鞋里' => '鞋裡', '鞣制' => '鞣製', '秋千' => '鞦韆', @@ -8737,8 +8725,8 @@ $zh2Hant = array( '頁面' => '頁面', '页面' => '頁面', '顶凶' => '頂兇', -'顶多' => '頂多', '頂多' => '頂多', +'顶多' => '頂多', '项链' => '項鍊', '顺于' => '順於', '顺钟向' => '順鐘向', @@ -8749,7 +8737,6 @@ $zh2Hant = array( '预报不准' => '預報不準', '预制' => '預製', '领袖欲' => '領袖慾', -'头儿干' => '頭兒幹', '头里' => '頭裡', '头长发' => '頭長髮', '头发' => '頭髮', @@ -8783,11 +8770,13 @@ $zh2Hant = array( '风起云涌' => '風起雲湧', '風采' => '風采', '风采' => '風采', +'风刮' => '風颳', '台风' => '颱風', '台风后' => '颱風後', '刮了' => '颳了', '刮倒' => '颳倒', '刮去' => '颳去', +'刮大风' => '颳大風', '刮得' => '颳得', '刮走' => '颳走', '刮起' => '颳起', @@ -8797,6 +8786,7 @@ $zh2Hant = array( '飘荡' => '飄蕩', '飘游' => '飄遊', '飘飘荡荡' => '飄飄蕩蕩', +'飘发自由女神' => '飄髮自由女神', '飞扎' => '飛紮', '飞刍挽粟' => '飛芻輓粟', '飞行钟' => '飛行鐘', @@ -8854,6 +8844,7 @@ $zh2Hant = array( '余子' => '餘子', '余存' => '餘存', '余孽' => '餘孽', +'余干' => '餘干', '余年' => '餘年', '余式' => '餘式', '余弦' => '餘弦', @@ -8876,6 +8867,7 @@ $zh2Hant = array( '余殃' => '餘殃', '余毒' => '餘毒', '余气' => '餘氣', +'余江' => '餘江', '余波' => '餘波', '余温' => '餘溫', '余泽' => '餘澤', @@ -8924,7 +8916,6 @@ $zh2Hant = array( '馄饨面' => '餛飩麵', '馆谷' => '館穀', '馆里' => '館裡', -'餵驴' => '餵驢', '饥寒' => '饑寒', '饥民' => '饑民', '饥渴' => '饑渴', @@ -8940,13 +8931,14 @@ $zh2Hant = array( '香山庄' => '香山庄', '马干' => '馬乾', '馬占山' => '馬占山', +'马德钟' => '馬德鐘', '马斯垂克期' => '馬斯垂克期', '馬格里布' => '馬格里布', '马格里布' => '馬格里布', '驻扎' => '駐紮', '骀荡' => '駘蕩', -'騰格里' => '騰格里', '腾格里' => '騰格里', +'騰格里' => '騰格里', '腾涌' => '騰湧', '腾冲' => '騰衝', '惊栗' => '驚慄', @@ -8959,6 +8951,7 @@ $zh2Hant = array( '体范' => '體範', '体系' => '體系', '高几' => '高几', +'高后' => '高后', '高干扰' => '高干擾', '高干预' => '高干預', '高干' => '高幹', @@ -8973,6 +8966,7 @@ $zh2Hant = array( '发乳' => '髮乳', '发光可鉴' => '髮光可鑑', '发匪' => '髮匪', +'发及腰' => '髮及腰', '发型' => '髮型', '发夹' => '髮夾', '发妻' => '髮妻', @@ -8983,7 +8977,7 @@ $zh2Hant = array( '发廊' => '髮廊', '发式' => '髮式', '发引千钧' => '髮引千鈞', -'发指' => '髮指', +'发披肩' => '髮披肩', '发卷' => '髮捲', '发根' => '髮根', '发油' => '髮油', @@ -9048,7 +9042,7 @@ $zh2Hant = array( '松通' => '鬆通', '松开' => '鬆開', '松饼' => '鬆餅', -'松松' => '鬆鬆', +'松松地' => '鬆鬆地', '鬈发' => '鬈髮', '胡子' => '鬍子', '胡梢' => '鬍梢', @@ -9075,11 +9069,13 @@ $zh2Hant = array( '斗剑' => '鬥劍', '斗力' => '鬥力', '斗劲' => '鬥勁', +'斗勇' => '鬥勇', '斗胜' => '鬥勝', '斗口' => '鬥口', '斗合' => '鬥合', '斗嘴' => '鬥嘴', '斗地主' => '鬥地主', +'斗垮' => '鬥垮', '斗士' => '鬥士', '斗富' => '鬥富', '斗巧' => '鬥巧', @@ -9092,9 +9088,11 @@ $zh2Hant = array( '斗志' => '鬥志', '斗闷' => '鬥悶', '斗成' => '鬥成', +'斗战' => '鬥戰', '斗打' => '鬥打', '斗批改' => '鬥批改', '斗技' => '鬥技', +'斗败' => '鬥敗', '斗文' => '鬥文', '斗智' => '鬥智', '斗暴' => '鬥暴', @@ -9110,7 +9108,9 @@ $zh2Hant = array( '斗牛' => '鬥牛', '斗犀台' => '鬥犀臺', '斗犬' => '鬥犬', +'斗狗' => '鬥狗', '斗狠' => '鬥狠', +'斗兽' => '鬥獸', '斗叠' => '鬥疊', '斗百草' => '鬥百草', '斗眼' => '鬥眼', @@ -9123,6 +9123,7 @@ $zh2Hant = array( '斗草' => '鬥草', '斗叶儿' => '鬥葉兒', '斗叶子' => '鬥葉子', +'斗蛐' => '鬥蛐', '斗蟋蟀' => '鬥蟋蟀', '斗话' => '鬥話', '斗艳' => '鬥豔', @@ -9140,6 +9141,7 @@ $zh2Hant = array( '斗鸭' => '鬥鴨', '斗鹌鹑' => '鬥鵪鶉', '斗丽' => '鬥麗', +'斗龙' => '鬥龍', '闹表' => '鬧錶', '闹钟' => '鬧鐘', '哄动' => '鬨動', @@ -9189,14 +9191,14 @@ $zh2Hant = array( '鬼谷子' => '鬼谷子', '魂牵梦系' => '魂牽夢繫', '魏征' => '魏徵', -'魔杰座' => '魔杰座', '魔表' => '魔錶', '鱼干' => '魚乾', '鱼松' => '魚鬆', -'鲜于枢' => '鮮于樞', -'鮮于樞' => '鮮于樞', +'鮮于' => '鮮于', +'鲜于' => '鮮于', '鲸须' => '鯨鬚', -'鳳凰于飛' => '鳳凰于飛', +'鳥栖' => '鳥栖', +'鸟栖市' => '鳥栖市', '凤梨干' => '鳳梨乾', '鸣钟' => '鳴鐘', '鸿范' => '鴻範', @@ -9206,6 +9208,7 @@ $zh2Hant = array( '雕悍' => '鵰悍', '雕翎' => '鵰翎', '雕鹗' => '鵰鶚', +'鹤峰县' => '鶴峯縣', '鹤吊' => '鶴弔', '鹤发' => '鶴髮', '鸾鉴' => '鸞鑑', @@ -9230,7 +9233,7 @@ $zh2Hant = array( '咸菜' => '鹹菜', '咸菜干' => '鹹菜乾', '咸蛋' => '鹹蛋', -'咸猪肉' => '鹹豬肉', +'咸猪' => '鹹豬', '咸类' => '鹹類', '咸食' => '鹹食', '咸鱼' => '鹹魚', @@ -9242,16 +9245,17 @@ $zh2Hant = array( '盐余' => '鹽餘', '鹿場里' => '鹿場里', '丽于' => '麗於', +'麟游' => '麟遊', +'曲酒' => '麯酒', '曲尘' => '麴塵', '曲櫱' => '麴櫱', '曲秀才' => '麴秀才', '曲车' => '麴車', '曲道士' => '麴道士', '曲钱' => '麴錢', -'麹霉' => '麴黴', '曲霉' => '麴黴', +'麹霉' => '麴黴', '面人儿' => '麵人兒', -'面价' => '麵價', '面包' => '麵包', '面坊' => '麵坊', '面坯儿' => '麵坯兒', @@ -9263,7 +9267,6 @@ $zh2Hant = array( '面条' => '麵條', '面汤' => '麵湯', '面浆' => '麵漿', -'面灰' => '麵灰', '面疙瘩' => '麵疙瘩', '面皮' => '麵皮', '面码儿' => '麵碼兒', @@ -9273,15 +9276,23 @@ $zh2Hant = array( '面团' => '麵糰', '面缸' => '麵缸', '面茶' => '麵茶', +'面制品' => '麵製品', '面食' => '麵食', '面饺' => '麵餃', '面饼' => '麵餅', '面馆' => '麵館', +'面点、' => '麵點、', +'面点师' => '麵點師', '麻将席' => '麻將蓆', '麻酱面' => '麻醬麵', '黄干黑瘦' => '黃乾黑瘦', +'黄岩区' => '黃巖區', +'黄岩县' => '黃巖縣', '黄历' => '黃曆', +'黃杰' => '黃杰', +'黄杰' => '黃杰', '黄历史' => '黃歷史', +'黄白术' => '黃白術', '黃詩杰' => '黃詩杰', '黄诗杰' => '黃詩杰', '黄金表' => '黃金表', @@ -9291,14 +9302,18 @@ $zh2Hant = array( '黄发' => '黃髮', '黄曲毒素' => '黃麴毒素', '黎克特制' => '黎克特制', -'黎吉雲' => '黎吉雲', '黎吉云' => '黎吉雲', +'黎吉雲' => '黎吉雲', '黑奴吁天录' => '黑奴籲天錄', +'黑干将' => '黑幹將', +'黑长发' => '黑長髮', '黑发' => '黑髮', +'点个赞' => '點個讚', '点札' => '點劄', '点半钟' => '點半鐘', '点多钟' => '點多鐘', '点里' => '點裡', +'点赞' => '點讚', '点里程' => '點里程', '点钟' => '點鐘', '霉毒' => '黴毒', @@ -9309,6 +9324,7 @@ $zh2Hant = array( '鼓里' => '鼓裡', '鼓噪' => '鼓譟', '冬冬鼓' => '鼕鼕鼓', +'咚咚鼓' => '鼕鼕鼓', '鼠曲草' => '鼠麴草', '鼻梁儿' => '鼻梁兒', '鼻梁' => '鼻樑', @@ -9318,6 +9334,7 @@ $zh2Hant = array( '齿落发白' => '齒落髮白', '齿发' => '齒髮', '出儿' => '齣兒', +'龙岩' => '龍巖', '龙卷' => '龍捲', '龙眼干' => '龍眼乾', '龙须' => '龍鬚', @@ -9325,13 +9342,13 @@ $zh2Hant = array( '龙斗虎伤' => '龍鬥虎傷', '龜山庄' => '龜山庄', '龟鉴' => '龜鑑', +',个中' => ',箇中', ); $zh2Hans = array( '㑯' => '㑔', '㑳' => '㑇', '㑶' => '㐹', -'㑺' => '俊', '㒓' => '𠉂', '㒺' => '罔', '㓂' => '寇', @@ -9359,10 +9376,8 @@ $zh2Hans = array( '㢝' => '𢋈', '㤙' => '恩', '㥦' => '惬', -'㥫' => '惇', '㥮' => '㤘', '㦎' => '𢛯', -'㧱' => '拿', '㨗' => '捷', '㨪' => '晃', '㨿' => '据', @@ -9489,7 +9504,6 @@ $zh2Hans = array( '來' => '来', '侖' => '仑', '侶' => '侣', -'侷' => '局', '俁' => '俣', '係' => '系', '俔' => '伣', @@ -9766,7 +9780,6 @@ $zh2Hans = array( '執' => '执', '堅' => '坚', '堊' => '垩', -'堖' => '垴', '堝' => '埚', '堯' => '尧', '報' => '报', @@ -9776,7 +9789,6 @@ $zh2Hans = array( '塏' => '垲', '塒' => '埘', '塗' => '涂', -'塚' => '冢', '塟' => '葬', '塢' => '坞', '塤' => '埙', @@ -9933,7 +9945,6 @@ $zh2Hans = array( '嶧' => '峄', '嶨' => '峃', '嶮' => '崄', -'嶴' => '岙', '嶸' => '嵘', '嶺' => '岭', '嶼' => '屿', @@ -10075,7 +10086,6 @@ $zh2Hans = array( '應' => '应', '懌' => '怿', '懍' => '懔', -'懞' => '蒙', '懟' => '怼', '懣' => '懑', '懨' => '恹', @@ -10281,8 +10291,6 @@ $zh2Hans = array( '棧' => '栈', '棲' => '栖', '棶' => '梾', -'椀' => '碗', -'椉' => '乘', '椏' => '桠', '椗' => '碇', '椲' => '㭏', @@ -10446,7 +10454,6 @@ $zh2Hans = array( '湞' => '浈', '湧' => '涌', '湯' => '汤', -'湻' => '淳', '湼' => '涅', '溈' => '沩', '準' => '准', @@ -10550,7 +10557,6 @@ $zh2Hans = array( '灙' => '𣺼', '灝' => '灏', '灠' => '漤', -'灡' => '㳕', '灣' => '湾', '灤' => '滦', '灧' => '滟', @@ -10794,7 +10800,6 @@ $zh2Hans = array( '矚' => '瞩', '矯' => '矫', '砲' => '炮', -'硃' => '朱', '硜' => '硁', '硤' => '硖', '硨' => '砗', @@ -11334,7 +11339,6 @@ $zh2Hans = array( '薟' => '莶', '薦' => '荐', '薩' => '萨', -'薳' => '䓕', '薴' => '苧', '薺' => '荠', '藍' => '蓝', @@ -11426,7 +11430,6 @@ $zh2Hans = array( '衊' => '蔑', '術' => '术', '衕' => '同', -'衖' => '弄', '衚' => '胡', '衛' => '卫', '衝' => '冲', @@ -11489,7 +11492,6 @@ $zh2Hans = array( '覽' => '览', '覿' => '觌', '觀' => '观', -'觔' => '斤', '觝' => '抵', '觴' => '觞', '觶' => '觯', @@ -11920,7 +11922,6 @@ $zh2Hans = array( '適' => '适', '遯' => '遁', '遲' => '迟', -'遶' => '绕', '遷' => '迁', '選' => '选', '遺' => '遗', @@ -12023,6 +12024,7 @@ $zh2Hans = array( '鈿' => '钿', '鉀' => '钾', '鉁' => '𨱅', +'鉄' => '铁', '鉅' => '钜', '鉆' => '钻', '鉈' => '铊', @@ -12380,7 +12382,6 @@ $zh2Hans = array( '靭' => '韧', '靱' => '韧', '鞀' => '鼗', -'鞌' => '鞍', '鞏' => '巩', '鞝' => '绱', '鞦' => '秋', @@ -12528,6 +12529,7 @@ $zh2Hans = array( '餭' => '𫗮', '餱' => '糇', '餳' => '饧', +'餵' => '喂', '餶' => '馉', '餷' => '馇', '餸' => '𩠌', @@ -12582,7 +12584,6 @@ $zh2Hans = array( '駧' => '𩧲', '駩' => '𩧴', '駭' => '骇', -'駮' => '驳', '駰' => '骃', '駱' => '骆', '駶' => '𩧺', @@ -12621,7 +12622,7 @@ $zh2Hans = array( '驁' => '骜', '驂' => '骖', '驃' => '骠', -'驄' => '𩨂', +'驄' => '骢', '驅' => '驱', '驊' => '骅', '驋' => '𩧯', @@ -12649,10 +12650,8 @@ $zh2Hans = array( '體' => '体', '髕' => '髌', '髖' => '髋', -'髣' => '仿', '髥' => '髯', '髮' => '发', -'髴' => '佛', '鬀' => '剃', '鬆' => '松', '鬉' => '鬃', @@ -12665,7 +12664,6 @@ $zh2Hans = array( '鬨' => '哄', '鬩' => '阋', '鬪' => '斗', -'鬭' => '斗', '鬮' => '阄', '鬰' => '郁', '鬱' => '郁', @@ -12944,7 +12942,7 @@ $zh2Hans = array( '鼇' => '鳌', '鼈' => '鳖', '鼉' => '鼍', -'鼕' => '冬', +'鼕' => '咚', '鼴' => '鼹', '齊' => '齐', '齋' => '斋', @@ -12977,43 +12975,30 @@ $zh2Hans = array( '龜' => '龟', '龭' => '𩨎', '龯' => '𨱆', -'𠇮' => '命', -'𠌂' => '伞', '𠌥' => '𠆿', '𠏢' => '𠉗', '𠕂' => '再', '𠕅' => '再', -'𠖇' => '冥', '𠞆' => '𠛆', '𠞰' => '剿', '𠠎' => '𠚳', -'𠪾' => '历', -'𠴟' => '咩', -'𠻳' => '嗽', '𡄔' => '𠴢', '𡄣' => '𠵸', '𡅏' => '𠲥', -'𡐨' => '野', '𡑭' => '𡋗', '𡓾' => '𡋀', '𡚁' => '弊', '𡞵' => '㛟', '𡠹' => '㛿', '𡢃' => '㛠', -'𡨘' => '冤', '𡨥' => '寇', -'𡬶' => '寻', '𡮉' => '𡭜', '𡮣' => '𡭬', '𡻕' => '岁', '𡾱' => '㟜', '𢣚' => '𢘝', '𢣭' => '𢘞', -'𢬸' => '括', -'𢭏' => '捣', -'𢮥' => '操', '𢶫' => '𢫞', -'𢷬' => '捣', '𢷮' => '𢫊', '𢹿' => '𢬦', '𣙎' => '㭣', @@ -13030,20 +13015,15 @@ $zh2Hans = array( '𤨏' => '琐', '𤪺' => '㻘', '𤫩' => '㻏', -'𤰜' => '亩', '𤱈' => '亩', -'𤱊' => '留', '𤳸' => '𤳄', '𤸫' => '𤶧', '𤺥' => '瘩', -'𥄨' => '瞅', '𥌃' => '𥅘', '𥕥' => '𥐰', '𥖅' => '𥐯', '𥢢' => '䅪', -'𥦗' => '窗', '𥨐' => '𥧂', -'𥲻' => '纂', '𥵃' => '𥱔', '𥵊' => '𥭉', '𥸠' => '𥮋', @@ -13051,16 +13031,12 @@ $zh2Hans = array( '𥽖' => '𥺇', '𥿊' => '𦈈', '𦂅' => '𦈒', -'𦂳' => '紧', -'𦃂' => '紧', '𦃄' => '𦈗', -'𦉆' => '碴', '𦊱' => '挂', '𦍑' => '羌', '𦕈' => '眇', '𦢈' => '𣍨', '𦣎' => '𦟗', -'𦪙' => '䑽', '𦪽' => '𦨩', '𦵏' => '葬', '𧔥' => '𧒭', @@ -13182,22 +13158,21 @@ $zh2Hans = array( '』' => '’', '「' => '“', '「' => '“', -'」' => '”', '」' => '”', +'」' => '”', '。陞' => '。升', '《易乾' => '《易乾', '一釐' => '一厘', -'一口鍾' => '一口钟', -'一鍾' => '一钟', '上昇' => '上升', +'不穀' => '不穀', '專著' => '专著', -'世界鍾' => '世界钟', -'喪鍾' => '丧钟', '乾一坛' => '乾一坛', '乾一壇' => '乾一坛', -'乾一组' => '乾一组', '乾一組' => '乾一组', +'乾一组' => '乾一组', '乾上乾下' => '乾上乾下', +'乾东' => '乾东', +'乾東' => '乾东', '乾為天' => '乾为天', '乾為陽' => '乾为阳', '乾九' => '乾九', @@ -13212,27 +13187,27 @@ $zh2Hans = array( '乾光' => '乾光', '乾兴' => '乾兴', '乾興' => '乾兴', -'乾岡' => '乾冈', '乾冈' => '乾冈', +'乾岡' => '乾冈', '乾刘' => '乾刘', '乾劉' => '乾刘', '乾刚' => '乾刚', '乾剛' => '乾刚', -'乾務' => '乾务', '乾务' => '乾务', +'乾務' => '乾务', '乾化' => '乾化', '乾卦' => '乾卦', '乾县' => '乾县', '乾縣' => '乾县', '乾台' => '乾台', '乾吉' => '乾吉', -'乾啟' => '乾启', '乾启' => '乾启', +'乾啟' => '乾启', '乾命' => '乾命', '乾和' => '乾和', '乾嘉' => '乾嘉', -'乾圖' => '乾图', '乾图' => '乾图', +'乾圖' => '乾图', '乾坤' => '乾坤', '乾城' => '乾城', '乾基' => '乾基', @@ -13251,9 +13226,8 @@ $zh2Hans = array( '乾崗' => '乾岗', '乾巛' => '乾巛', '乾州' => '乾州', -'乾式' => '乾式', -'乾錄' => '乾录', '乾录' => '乾录', +'乾錄' => '乾录', '乾律' => '乾律', '乾德' => '乾德', '乾心' => '乾心', @@ -13266,17 +13240,17 @@ $zh2Hans = array( '乾旦' => '乾旦', '乾明' => '乾明', '乾昧' => '乾昧', -'乾暉' => '乾晖', '乾晖' => '乾晖', +'乾暉' => '乾晖', '乾景' => '乾景', '乾晷' => '乾晷', '乾曜' => '乾曜', -'乾構' => '乾构', '乾构' => '乾构', +'乾構' => '乾构', '乾枢' => '乾枢', '乾樞' => '乾枢', -'乾棟' => '乾栋', '乾栋' => '乾栋', +'乾棟' => '乾栋', '乾步' => '乾步', '乾氏' => '乾氏', '乾沓和' => '乾沓和', @@ -13288,6 +13262,7 @@ $zh2Hans = array( '乾潭' => '乾潭', '乾灵' => '乾灵', '乾靈' => '乾灵', +'乾生元' => '乾生元', '乾男' => '乾男', '乾皋' => '乾皋', '乾盛世' => '乾盛世', @@ -13295,8 +13270,8 @@ $zh2Hans = array( '乾祐' => '乾祐', '乾神' => '乾神', '乾穹' => '乾穹', -'乾竇' => '乾窦', '乾窦' => '乾窦', +'乾竇' => '乾窦', '乾竺' => '乾竺', '乾笃' => '乾笃', '乾篤' => '乾笃', @@ -13307,14 +13282,14 @@ $zh2Hans = array( '乾红' => '乾红', '乾綱' => '乾纲', '乾纲' => '乾纲', -'乾纽' => '乾纽', '乾紐' => '乾纽', -'乾络' => '乾络', +'乾纽' => '乾纽', '乾絡' => '乾络', +'乾络' => '乾络', '乾統' => '乾统', '乾统' => '乾统', -'乾维' => '乾维', '乾維' => '乾维', +'乾维' => '乾维', '乾罗' => '乾罗', '乾羅' => '乾罗', '乾花' => '乾花', @@ -13325,18 +13300,18 @@ $zh2Hans = array( '乾西' => '乾西', '乾覆' => '乾覆', '乾象' => '乾象', -'乾象歷' => '乾象历', '乾象历' => '乾象历', +'乾象歷' => '乾象历', '乾貞' => '乾贞', '乾贞' => '乾贞', -'乾贵士' => '乾贵士', '乾貴士' => '乾贵士', +'乾贵士' => '乾贵士', '乾貺' => '乾贶', '乾贶' => '乾贶', '乾車' => '乾车', '乾车' => '乾车', -'乾轴' => '乾轴', '乾軸' => '乾轴', +'乾轴' => '乾轴', '乾通' => '乾通', '乾造' => '乾造', '乾道' => '乾道', @@ -13355,25 +13330,27 @@ $zh2Hans = array( '乾風' => '乾风', '乾风' => '乾风', '乾首' => '乾首', -'乾马' => '乾马', '乾馬' => '乾马', +'乾马' => '乾马', '乾鵠' => '乾鹄', '乾鹄' => '乾鹄', '乾鵲' => '乾鹊', '乾鹊' => '乾鹊', -'乾龙' => '乾龙', '乾龍' => '乾龙', +'乾龙' => '乾龙', '乾,健也' => '乾,健也', '乾,天也' => '乾,天也', '五箇山' => '五箇山', +'什么' => '什么', '仇讎' => '仇雠', '以微知著' => '以微知著', -'以莛叩鍾' => '以莛叩钟', '仰屋著書' => '仰屋著书', '彷彿' => '仿佛', '夥計' => '伙计', '佛頭著糞' => '佛头著粪', '偵蒐' => '侦搜', +'倖一郎' => '倖一郎', +'倖田' => '倖田', '候覆' => '候复', '藉助' => '借助', '藉口' => '借口', @@ -13384,19 +13361,18 @@ $zh2Hans = array( '藉由' => '借由', '藉端' => '借端', '藉詞' => '借词', +'傒倖' => '傒倖', '先名後姓' => '先名后姓', +'兒寬' => '兒宽', '六么' => '六幺', '蘭質薰心' => '兰质薰心', '內聯陞' => '内联升', '憑藉' => '凭借', -'分鍾' => '分钟', '初昇' => '初升', '利欲薰心' => '利欲薰心', -'刻鍾' => '刻钟', '剋了' => '剋了', '剋架' => '剋架', '剖釐' => '剖厘', -'千鍾' => '千钟', '陞為' => '升为', '陞了' => '升了', '昇仙' => '升仙', @@ -13412,7 +13388,6 @@ $zh2Hans = array( '昇降' => '升降', '卓著' => '卓著', '博和託' => '博和讬', -'卷舌' => '卷舌', '歷陞' => '历升', '釐改' => '厘改', '釐整' => '厘整', @@ -13421,14 +13396,11 @@ $zh2Hans = array( '釐清' => '厘清', '釐訂' => '厘订', '釐革' => '厘革', -'原子鍾' => '原子钟', '原著' => '原著', '又陞' => '又升', '反反覆覆' => '反反复复', '反覆' => '反复', -'古鍾' => '古钟', '可穿著' => '可穿著', -'台鍾' => '台钟', '吃衣著飯' => '吃衣著饭', '合著' => '合著', '同陞和' => '同升和', @@ -13439,41 +13411,40 @@ $zh2Hans = array( '回覆' => '回复', '土著' => '土著', '坤乾' => '坤乾', -'塔鍾' => '塔钟', '墨瀋' => '墨渖', -'壁鍾' => '壁钟', '覆查' => '复查', '覆核' => '复核', '覆检' => '复检', '復甦' => '复苏', -'多鍾' => '多钟', +'多么' => '多么', '大麴' => '大曲', -'大鍾' => '大钟', -'天道為乾' => '天道为乾', '天道为乾' => '天道为乾', +'天道為乾' => '天道为乾', '奧區' => '奧区', '如瀋' => '如渖', '姓么' => '姓幺', '子餘' => '子馀', '字乾生' => '字乾生', -'孫乾' => '孙乾', '孙乾' => '孙乾', -'宋鍾國' => '宋钟国', +'孫乾' => '孙乾', '宏碁' => '宏碁', '官陞' => '官升', '將軍抽俥' => '将军抽俥', '將軍抽車' => '将军抽車', '爾冬陞' => '尔冬升', '尼乾陀' => '尼乾陀', +'侷促' => '局促', '跼促' => '局促', +'侷限' => '局限', '跼限' => '局限', -'山崩鍾應' => '山崩钟应', -'崔秀鍾' => '崔秀钟', +'山崎闇齋' => '山崎闇斋', +'岳託' => '岳讬', '巨著' => '巨著', '乾乾淨淨' => '干干净净', '乾乾脆脆' => '干干脆脆', '乾泉水' => '干泉水', '年陞' => '年升', +'么九' => '幺九', '么二三' => '幺二三', '么元' => '幺元', '么鳳' => '幺凤', @@ -13482,15 +13453,18 @@ $zh2Hans = array( '么廝' => '幺厮', '幺厮' => '幺厮', '么叔' => '幺叔', +'么女' => '幺女', '么媽' => '幺妈', '么妹' => '幺妹', '么姓' => '幺姓', '么姨' => '幺姨', '么娘' => '幺娘', -'幺孃' => '幺娘', '么孃' => '幺娘', +'幺孃' => '幺娘', +'么子' => '幺子', '么小' => '幺小', '么弟' => '幺弟', +'么正' => '幺正', '么氏' => '幺氏', '么爸' => '幺爸', '么爹' => '幺爹', @@ -13498,22 +13472,20 @@ $zh2Hans = array( '么舅' => '幺舅', '么蛾子' => '幺蛾子', '么謙' => '幺谦', -'么麽' => '幺麽', '么麼' => '幺麽', +'么麽' => '幺麽', '么麽小丑' => '幺麽小丑', '慶餘' => '庆馀', -'座鍾' => '座钟', '康乾' => '康乾', -'張法乾' => '张法乾', '张法乾' => '张法乾', -'張鍾英' => '张钟英', +'張法乾' => '张法乾', '彰明較著' => '彰明较著', '待覆' => '待复', '後姓' => '後姓', '慫慂' => '怂恿', +'怎么' => '怎么', '恩威並著' => '恩威并著', '噁心' => '恶心', -'懸鍾' => '悬钟', '情蒐' => '情搜', '情鍾' => '情钟', '惏悷' => '惏悷', @@ -13526,6 +13498,7 @@ $zh2Hans = array( '扞格' => '扞格', '執著' => '执著', '批覆' => '批复', +'承乾' => '承乾', '拉鍊' => '拉链', '拙著' => '拙著', '拚命' => '拚命', @@ -13533,9 +13506,7 @@ $zh2Hans = array( '拚死' => '拚死', '拾瀋' => '拾渖', '拿破崙' => '拿破仑', -'掛鍾' => '挂钟', '挨剋' => '挨剋', -'掩耳盜鍾' => '掩耳盗钟', '提昇' => '提升', '蒐錄' => '搜录', '蒐索' => '搜索', @@ -13544,17 +13515,15 @@ $zh2Hans = array( '蒐證' => '搜证', '蒐購' => '搜购', '蒐輯' => '搜辑', -'蒐采' => '搜采', '蒐採' => '搜采', +'蒐采' => '搜采', '蒐集' => '搜集', '搥打' => '搥打', '搥胸頓足' => '搥胸顿足', -'擺鍾' => '摆钟', -'撞鍾' => '撞钟', '撰著' => '撰著', '效果顯著' => '效果显著', -'敲鍾' => '敲钟', '文徵明' => '文徵明', +'觔斗' => '斤斗', '新著' => '新著', '於世成' => '於世成', '於之瑩' => '於之莹', @@ -13572,10 +13541,10 @@ $zh2Hans = array( '於志賀' => '於志贺', '於志贺' => '於志贺', '於戲' => '於戏', -'於梨華' => '於梨华', '於梨华' => '於梨华', +'於梨華' => '於梨华', '於氏' => '於氏', -'於潜县' => '於潜县', +'於潜' => '於潜', '於潛縣' => '於潜县', '於祥玉' => '於祥玉', '於菟' => '於菟', @@ -13583,7 +13552,6 @@ $zh2Hans = array( '於除鞬' => '於除鞬', '旋乾轉坤' => '旋乾转坤', '旋乾转坤' => '旋乾转坤', -'時鍾' => '时钟', '曠若發矇' => '旷若发矇', '崑崙' => '昆仑', '崑劇' => '昆剧', @@ -13593,10 +13561,10 @@ $zh2Hans = array( '崑蘇' => '昆苏', '崑調' => '昆调', '易·乾' => '易·乾', -'易经·乾' => '易经·乾', '易經·乾' => '易经·乾', -'易经乾' => '易经乾', +'易经·乾' => '易经·乾', '易經乾' => '易经乾', +'易经乾' => '易经乾', '昭著' => '昭著', '顯著' => '显著', '顯著地' => '显著地', @@ -13606,17 +13574,17 @@ $zh2Hans = array( '顯著效果' => '显著效果', '顯著特點' => '显著特点', '晉陞' => '晋升', -'晚鍾' => '晚钟', -'晨鍾' => '晨钟', '暗闇' => '暗闇', '麴黴' => '曲霉', -'曾運乾' => '曾运乾', '曾运乾' => '曾运乾', +'曾運乾' => '曾运乾', '月陞' => '月升', '朝乾夕惕' => '朝乾夕惕', -'朝鍾暮鼓' => '朝钟暮鼓', '朱有燉' => '朱有燉', '朱淛' => '朱淛', +'硃砂' => '朱砂', +'硃紅' => '朱红', +'硃色' => '朱色', '朴於宇同' => '朴於宇同', '李乾德' => '李乾德', '李乾順' => '李乾顺', @@ -13624,18 +13592,14 @@ $zh2Hans = array( '李澤鉅' => '李泽钜', '李祕' => '李祕', '李譔' => '李譔', -'李鍾原' => '李钟原', -'林鍾' => '林钟', -'柳诒徵' => '柳诒徵', '柳詒徵' => '柳诒徵', +'柳诒徵' => '柳诒徵', '校讎' => '校雠', '楈枒' => '楈枒', '樊於期' => '樊於期', -'橡椀' => '橡椀', -'此鍾' => '此钟', '殘瀋' => '残渖', -'慇懃' => '殷勤', '慇勤' => '殷勤', +'慇懃' => '殷勤', '比較顯著' => '比较显著', '毫釐' => '毫厘', '氆氌' => '氆氌', @@ -13647,9 +13611,7 @@ $zh2Hans = array( '沈默' => '沉默', '氾濫' => '泛滥', '洗鍊' => '洗练', -'洪鍾' => '洪钟', '瀋液' => '渖液', -'點鍾' => '点钟', '薰習' => '熏习', '薰心' => '熏心', '薰沐' => '熏沐', @@ -13660,36 +13622,34 @@ $zh2Hans = array( '王道乾' => '王道乾', '王餘魚' => '王馀鱼', '甚夥' => '甚夥', -'生物鍾' => '生物钟', -'電鍾' => '电钟', -'男為乾' => '男为乾', '男为乾' => '男为乾', -'男爲乾' => '男为乾', -'男性爲乾' => '男性为乾', -'男性為乾' => '男性为乾', +'男為乾' => '男为乾', '男性为乾' => '男性为乾', +'男性為乾' => '男性为乾', '療效顯著' => '疗效显著', '白瀋' => '白渖', '皁保' => '皁保', '目劄' => '目劄', '直昇' => '直升', '睹微知著' => '睹微知著', -'瞭臺' => '瞭台', '瞭台' => '瞭台', +'瞭臺' => '瞭台', '瞭望' => '瞭望', '矇眬' => '矇眬', '矇矓' => '矇眬', '石碁' => '石碁', '石碁鎮' => '石碁镇', -'石英鍾' => '石英钟', -'石鍾乳' => '石钟乳', +'碩託' => '硕讬', '鹼菜' => '硷菜', -'碁聖' => '碁圣', '碁圣' => '碁圣', +'碁聖' => '碁圣', '碁所' => '碁所', '祕宜' => '祕宜', -'秒鍾' => '秒钟', +'穀旦' => '穀旦', '穀梁' => '穀梁', +'穀水' => '穀水', +'穀阳' => '穀阳', +'穀陽' => '穀阳', '穿著者' => '穿着者', '竹昇' => '竹升', '答覆' => '答复', @@ -13697,10 +13657,11 @@ $zh2Hans = array( '米瀋' => '米渖', '餬口' => '糊口', '繙㠾' => '繙㠾', +'遶境' => '绕境', '線國安' => '缐国安', '線姓' => '缐姓', '編著' => '编著', -'編鍾' => '编钟', +'老么' => '老幺', '肉乾乾' => '肉干干', '肘手鍊足' => '肘手链足', '甦醒' => '苏醒', @@ -13723,14 +13684,19 @@ $zh2Hans = array( '著者' => '著者', '著身' => '著身', '著述' => '著述', -'覆蓋' => '覆蓋', +'蔡孝乾' => '蔡孝乾', +'蔡絛' => '蔡絛', +'行餘' => '行馀', +'覆蓋' => '覆盖', '見微知著' => '见微知著', '見著' => '见著', '視微知著' => '视微知著', '言幾析理' => '言幾析理', '諲譔' => '諲譔', -'警鍾' => '警钟', '譩譆' => '譩譆', +'託庸' => '讬庸', +'託恩多' => '讬恩多', +'託麻' => '讬麻', '論著' => '论著', '譯著' => '译著', '謝肇淛' => '谢肇淛', @@ -13739,12 +13705,11 @@ $zh2Hans = array( '較著' => '较著', '近角聪信' => '近角聪信', '这么' => '这么', -'進化鍾' => '进化钟', '造麴' => '造曲', '遺著' => '遗著', +'那么' => '那么', '那麽' => '那麽', '郭子乾' => '郭子乾', -'郭行餘' => '郭行馀', '酒麴' => '酒曲', '醉瀋' => '醉渖', '醯壶' => '醯壶', @@ -13753,48 +13718,17 @@ $zh2Hans = array( '醯醬' => '醯酱', '醯醋' => '醯醋', '醯醢' => '醯醢', -'醯鸡' => '醯鸡', '醯雞' => '醯鸡', +'醯鸡' => '醯鸡', '重覆' => '重复', -'金尚鍾' => '金尚钟', -'金民鍾' => '金民钟', -'金鍾' => '金钟', '金鍊' => '金链', -'鍾麗緹' => '钟丽缇', -'鍾乳石' => '钟乳石', -'鍾儀奏楚' => '钟仪奏楚', -'鍾關' => '钟关', -'鍾聲' => '钟声', -'鍾頭' => '钟头', -'鍾山' => '钟山', -'鍾差' => '钟差', -'鍾座' => '钟座', '鍾情' => '钟情', '鍾意' => '钟意', -'鍾慧冰' => '钟慧冰', -'鍾擺' => '钟摆', -'鍾架' => '钟架', -'鍾楚紅' => '钟楚红', -'鍾樓' => '钟楼', -'鍾漢良' => '钟汉良', -'鍾汶' => '钟汶', -'鍾淑慧' => '钟淑慧', '鍾靈' => '钟灵', -'鍾點' =&g |