From: jenkins-bot Date: Fri, 4 Dec 2015 21:05:22 +0000 (+0000) Subject: Merge "Fix phpdoc of StatusValue::merge" X-Git-Tag: 1.31.0-rc.0~8806 X-Git-Url: https://git.cyclocoop.org/%27.WWW_URL.%27admin/?a=commitdiff_plain;h=adb0c734befa5aff7362bbbc010785af40e543a2;hp=b796300aa614e93d525c7ecd615a3109ba919601;p=lhc%2Fweb%2Fwiklou.git Merge "Fix phpdoc of StatusValue::merge" --- diff --git a/autoload.php b/autoload.php index 3e5c1b50a5..685849b368 100644 --- a/autoload.php +++ b/autoload.php @@ -164,8 +164,8 @@ $wgAutoloadLocalClasses = array( 'BenchIfSwitch' => __DIR__ . '/maintenance/benchmarks/bench_if_switch.php', 'BenchStrtrStrReplace' => __DIR__ . '/maintenance/benchmarks/bench_strtr_str_replace.php', 'BenchUtf8TitleCheck' => __DIR__ . '/maintenance/benchmarks/bench_utf8_title_check.php', - 'BenchWikimediaBaseConvert' => __DIR__ . '/maintenance/benchmarks/bench_Wikimedia_base_convert.php', 'BenchWfIsWindows' => __DIR__ . '/maintenance/benchmarks/bench_wfIsWindows.php', + 'BenchWikimediaBaseConvert' => __DIR__ . '/maintenance/benchmarks/bench_Wikimedia_base_convert.php', 'BenchmarkDeleteTruncate' => __DIR__ . '/maintenance/benchmarks/bench_delete_truncate.php', 'BenchmarkHooks' => __DIR__ . '/maintenance/benchmarks/benchmarkHooks.php', 'BenchmarkParse' => __DIR__ . '/maintenance/benchmarks/benchmarkParse.php', @@ -307,7 +307,7 @@ $wgAutoloadLocalClasses = array( 'DateFormats' => __DIR__ . '/maintenance/language/date-formats.php', 'DateFormatter' => __DIR__ . '/includes/parser/DateFormatter.php', 'DeadendPagesPage' => __DIR__ . '/includes/specials/SpecialDeadendpages.php', - 'DeferrableUpdate' => __DIR__ . '/includes/deferred/DeferredUpdates.php', + 'DeferrableUpdate' => __DIR__ . '/includes/deferred/DeferrableUpdate.php', 'DeferredStringifier' => __DIR__ . '/includes/libs/DeferredStringifier.php', 'DeferredUpdates' => __DIR__ . '/includes/deferred/DeferredUpdates.php', 'DeleteAction' => __DIR__ . '/includes/actions/DeleteAction.php', @@ -793,6 +793,7 @@ $wgAutoloadLocalClasses = array( 'MergeHistoryPager' => __DIR__ . '/includes/specials/SpecialMergeHistory.php', 'MergeLogFormatter' => __DIR__ . '/includes/logging/MergeLogFormatter.php', 'MergeMessageFileList' => __DIR__ . '/maintenance/mergeMessageFileList.php', + 'MergeableUpdate' => __DIR__ . '/includes/deferred/MergeableUpdate.php', 'Message' => __DIR__ . '/includes/Message.php', 'MessageBlobStore' => __DIR__ . '/includes/cache/MessageBlobStore.php', 'MessageCache' => __DIR__ . '/includes/cache/MessageCache.php', @@ -1089,6 +1090,7 @@ $wgAutoloadLocalClasses = array( 'SearchDump' => __DIR__ . '/maintenance/dumpIterator.php', 'SearchEngine' => __DIR__ . '/includes/search/SearchEngine.php', 'SearchEngineDummy' => __DIR__ . '/includes/search/SearchEngine.php', + 'SearchExactMatchRescorer' => __DIR__ . '/includes/search/SearchExactMatchRescorer.php', 'SearchHighlighter' => __DIR__ . '/includes/search/SearchHighlighter.php', 'SearchMssql' => __DIR__ . '/includes/search/SearchMssql.php', 'SearchMySQL' => __DIR__ . '/includes/search/SearchMySQL.php', diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php index edc54f2722..e76b627cf6 100644 --- a/includes/DefaultSettings.php +++ b/includes/DefaultSettings.php @@ -4365,6 +4365,10 @@ $wgCentralIdLookupProvider = 'local'; * - PasswordCannotMatchUsername - Password cannot match username to * - PasswordCannotMatchBlacklist - Username/password combination cannot * match a specific, hardcoded blacklist. + * - PasswordCannotBePopular - Blacklist passwords which are known to be + * commonly chosen. Set to integer n to ban the top n passwords. + * If you want to ban all common passwords on file, use the + * PHP_INT_MAX constant. * @since 1.26 */ $wgPasswordPolicy = array( @@ -4373,11 +4377,13 @@ $wgPasswordPolicy = array( 'MinimalPasswordLength' => 8, 'MinimumPasswordLengthToLogin' => 1, 'PasswordCannotMatchUsername' => true, + 'PasswordCannotBePopular' => 25, ), 'sysop' => array( 'MinimalPasswordLength' => 8, 'MinimumPasswordLengthToLogin' => 1, 'PasswordCannotMatchUsername' => true, + 'PasswordCannotBePopular' => 25, ), 'bot' => array( 'MinimalPasswordLength' => 8, @@ -4397,6 +4403,7 @@ $wgPasswordPolicy = array( 'PasswordCannotMatchUsername' => 'PasswordPolicyChecks::checkPasswordCannotMatchUsername', 'PasswordCannotMatchBlacklist' => 'PasswordPolicyChecks::checkPasswordCannotMatchBlacklist', 'MaximalPasswordLength' => 'PasswordPolicyChecks::checkMaximalPasswordLength', + 'PasswordCannotBePopular' => 'PasswordPolicyChecks::checkPopularPasswordBlacklist' ), ); @@ -4590,6 +4597,7 @@ $wgDefaultUserOptions = array( 'watchlisthideown' => 0, 'watchlisthidepatrolled' => 0, 'watchlisthidecategorization' => 1, + 'watchlistreloadautomatically' => 0, 'watchmoves' => 0, 'watchrollback' => 0, 'wllimit' => 250, @@ -7790,6 +7798,19 @@ $wgVirtualRestConfig = array( */ $wgSearchRunSuggestedQuery = true; +/** + * Where popular password file is located. + * + * Default in core contains 50,000 most popular. This config + * allows you to change which file, in case you want to generate + * a password file with > 50000 entries in it. + * + * @see maintenance/createCommonPasswordCdb.php + * @since 1.27 + * @var string path to file + */ +$wgPopularPasswordFile = __DIR__ . '/../serialized/commonpasswords.cdb'; + /** * For really cool vim folding this needs to be at the end: * vim: foldmarker=@{,@} foldmethod=marker diff --git a/includes/Hooks.php b/includes/Hooks.php index 980d350c6b..9018581639 100644 --- a/includes/Hooks.php +++ b/includes/Hooks.php @@ -193,34 +193,17 @@ class Hooks { $badhookmsg = null; $hook_args = array_merge( $hook, $args ); - set_error_handler( 'Hooks::hookErrorHandler' ); - // mark hook as deprecated, if deprecation version is specified if ( $deprecatedVersion !== null ) { wfDeprecated( "$event hook (used in $func)", $deprecatedVersion ); } - try { - $retval = call_user_func_array( $callback, $hook_args ); - } catch ( MWHookException $e ) { - $badhookmsg = $e->getMessage(); - } catch ( Exception $e ) { - restore_error_handler(); - throw $e; - } - - restore_error_handler(); + $retval = call_user_func_array( $callback, $hook_args ); // Process the return value. if ( is_string( $retval ) ) { // String returned means error. throw new FatalError( $retval ); - } elseif ( $badhookmsg !== null ) { - // Exception was thrown from Hooks::hookErrorHandler. - throw new MWException( - 'Detected bug in an extension! ' . - "Hook $func has invalid call signature; " . $badhookmsg - ); } elseif ( $retval === false ) { // False was returned. Stop processing, but no error. return false; @@ -229,31 +212,4 @@ class Hooks { return true; } - - /** - * Handle PHP errors issued inside a hook. Catch errors that have to do - * with a function expecting a reference, missing arguments, or wrong argument - * types. Pass all others through to to the default error handler. - * - * This is useful for throwing errors for major callback invocation errors - * (with regard to parameter signature) which PHP just gives warnings for. - * - * @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 - */ - public static function hookErrorHandler( $errno, $errstr ) { - if ( strpos( $errstr, 'expected to be a reference, value given' ) !== false - || strpos( $errstr, 'Missing argument ' ) !== false - || strpos( $errstr, ' expects parameter ' ) !== false - ) { - throw new MWHookException( $errstr, $errno ); - } - - // Delegate unhandled errors to the default handlers - return false; - } } diff --git a/includes/MediaWiki.php b/includes/MediaWiki.php index 38d9e476f4..ecfd8f87aa 100644 --- a/includes/MediaWiki.php +++ b/includes/MediaWiki.php @@ -509,8 +509,10 @@ class MediaWiki { $factory = wfGetLBFactory(); $factory->commitMasterChanges(); $factory->shutdown(); + wfDebug( __METHOD__ . ': all transactions committed' ); - wfDebug( __METHOD__ . ' completed; all transactions committed' ); + DeferredUpdates::doUpdates( 'enqueue', DeferredUpdates::PRESEND ); + wfDebug( __METHOD__ . ': pre-send deferred updates completed' ); // Set a cookie to tell all CDN edge nodes to "stick" the user to the // DC that handles this POST request (e.g. the "master" data center) diff --git a/includes/Preferences.php b/includes/Preferences.php index 096f8e3100..c7ab9cddc9 100644 --- a/includes/Preferences.php +++ b/includes/Preferences.php @@ -1006,6 +1006,11 @@ class Preferences { 'section' => 'watchlist/advancedwatchlist', 'label-message' => 'tog-watchlisthideliu', ); + $defaultPreferences['watchlistreloadautomatically'] = array( + 'type' => 'toggle', + 'section' => 'watchlist/advancedwatchlist', + 'label-message' => 'tog-watchlistreloadautomatically', + ); if ( $config->get( 'RCWatchCategoryMembership' ) ) { $defaultPreferences['watchlisthidecategorization'] = array( diff --git a/includes/PrefixSearch.php b/includes/PrefixSearch.php index e328e9f95a..c6f187d2b7 100644 --- a/includes/PrefixSearch.php +++ b/includes/PrefixSearch.php @@ -164,112 +164,9 @@ abstract class PrefixSearch { return $this->strings( $this->handleResultFromHook( $srchres, $namespaces, $search, $limit ) ); } - /** - * Default search backend does proper prefix searching, but custom backends - * may sort based on other algorythms that may cause the exact title match - * to not be in the results or be lower down the list. - * @param array $srchres results from the hook - * @return array munged results from the hook - */ private function handleResultFromHook( $srchres, $namespaces, $search, $limit ) { - // Pick namespace (based on PrefixSearch::defaultSearchBackend) - $ns = in_array( NS_MAIN, $namespaces ) ? NS_MAIN : $namespaces[0]; - $t = Title::newFromText( $search, $ns ); - if ( !$t || !$t->exists() ) { - // No exact match so just return the search results - return $srchres; - } - $string = $t->getPrefixedText(); - $key = array_search( $string, $srchres ); - if ( $key !== false ) { - // Exact match was in the results so just move it to the front - return $this->pullFront( $key, $srchres ); - } - // Exact match not in the search results so check for some redirect handling cases - if ( $t->isRedirect() ) { - $target = $this->getRedirectTarget( $t ); - $key = array_search( $target, $srchres ); - if ( $key !== false ) { - // Exact match is a redirect to one of the returned matches so pull the - // returned match to the front. This might look odd but the alternative - // is to put the redirect in front and drop the match. The name of the - // found match is often more descriptive/better formed than the name of - // the redirect AND by definition they share a prefix. Hopefully this - // choice is less confusing and more helpful. But it might not be. But - // it is the choice we're going with for now. - return $this->pullFront( $key, $srchres ); - } - $redirectTargetsToRedirect = $this->redirectTargetsToRedirect( $srchres ); - if ( isset( $redirectTargetsToRedirect[$target] ) ) { - // The exact match and something in the results list are both redirects - // to the same thing! In this case we'll pull the returned match to the - // top following the same logic above. Again, it might not be a perfect - // choice but it'll do. - return $this->pullFront( $redirectTargetsToRedirect[$target], $srchres ); - } - } else { - $redirectTargetsToRedirect = $this->redirectTargetsToRedirect( $srchres ); - if ( isset( $redirectTargetsToRedirect[$string] ) ) { - // The exact match is the target of a redirect already in the results list so remove - // the redirect from the results list and push the exact match to the front - array_splice( $srchres, $redirectTargetsToRedirect[$string], 1 ); - array_unshift( $srchres, $string ); - return $srchres; - } - } - - // Exact match is totally unique from the other results so just add it to the front - array_unshift( $srchres, $string ); - // And roll one off the end if the results are too long - if ( count( $srchres ) > $limit ) { - array_pop( $srchres ); - } - return $srchres; - } - - /** - * @param Array(string) $titles as strings - * @return Array(string => int) redirect target prefixedText to index of title in titles - * that is a redirect to it. - */ - private function redirectTargetsToRedirect( $titles ) { - $result = array(); - foreach ( $titles as $key => $titleText ) { - $title = Title::newFromText( $titleText ); - if ( !$title || !$title->isRedirect() ) { - continue; - } - $target = $this->getRedirectTarget( $title ); - if ( !$target ) { - continue; - } - $result[$target] = $key; - } - return $result; - } - - /** - * @param int $key key to pull to the front - * @return array $array with the item at $key pulled to the front - */ - private function pullFront( $key, $array ) { - $cut = array_splice( $array, $key, 1 ); - array_unshift( $array, $cut[0] ); - return $array; - } - - /** - * Get a redirect's destination from a title - * @param Title $title A title to redirect. It may not redirect or even exist - * @return null|string If title exists and redirects, get the destination's prefixed name - */ - private function getRedirectTarget( $title ) { - $page = WikiPage::factory( $title ); - if ( !$page->exists() ) { - return null; - } - $redir = $page->getRedirectTarget(); - return $redir ? $redir->getPrefixedText() : null; + $rescorer = new SearchExactMatchRescorer(); + return $rescorer->rescore( $search, $namespaces, $srchres, $limit ); } /** diff --git a/includes/Title.php b/includes/Title.php index 4b39efd199..46131c1da0 100644 --- a/includes/Title.php +++ b/includes/Title.php @@ -3591,12 +3591,10 @@ class Title { * Purge all applicable Squid URLs */ public function purgeSquid() { - global $wgUseSquid; - if ( $wgUseSquid ) { - $urls = $this->getSquidURLs(); - $u = new SquidUpdate( $urls ); - $u->doUpdate(); - } + DeferredUpdates::addUpdate( + new SquidUpdate( $this->getSquidURLs() ), + DeferredUpdates::PRESEND + ); } /** diff --git a/includes/deferred/DeferrableUpdate.php b/includes/deferred/DeferrableUpdate.php new file mode 100644 index 0000000000..5f4d821060 --- /dev/null +++ b/includes/deferred/DeferrableUpdate.php @@ -0,0 +1,14 @@ +merge( $update ); + } else { + $queue[$class] = $update; + } + } else { + $queue[] = $update; + } + if ( self::$forceDeferral ) { - return; + return; // do not run } // CLI scripts may forget to periodically flush these updates, - // so try to handle that rather than OOMing and losing them. - // Try to run the updates as soon as there is no local transaction. + // so try to handle that rather than OOMing and losing them entirely. + // Try to run the updates as soon as there is no current wiki transaction. static $waitingOnTrx = false; // de-duplicate callback if ( $wgCommandLineMode && !$waitingOnTrx ) { $lb = wfGetLB(); @@ -81,34 +132,12 @@ class DeferredUpdates { } } - /** - * Add a callable update. In a lot of cases, we just need a callback/closure, - * defining a new DeferrableUpdate object is not necessary - * @see MWCallableUpdate::__construct() - * @param callable $callable - */ - public static function addCallableUpdate( $callable ) { - self::addUpdate( new MWCallableUpdate( $callable ) ); - } - - /** - * Do any deferred updates and clear the list - * - * @param string $mode Use "enqueue" to use the job queue when possible [Default: run] - * prevent lock contention - * @param string $oldMode Unused - */ - public static function doUpdates( $mode = 'run', $oldMode = '' ) { - // B/C for ( $commit, $mode ) args - $mode = $oldMode ?: $mode; - if ( $mode === 'commit' ) { - $mode = 'run'; - } - - $updates = self::$updates; + public static function execute( array &$queue, $mode ) { + $updates = $queue; // snapshot of queue + // Keep doing rounds of updates until none get enqueued while ( count( $updates ) ) { - self::clearPendingUpdates(); + $queue = array(); // clear the queue /** @var DataUpdate[] $dataUpdates */ $dataUpdates = array(); /** @var DeferrableUpdate[] $otherUpdates */ @@ -140,7 +169,7 @@ class DeferredUpdates { } } - $updates = self::$updates; + $updates = $queue; // new snapshot of queue (check for new entries) } } @@ -149,7 +178,8 @@ class DeferredUpdates { * want or need to call this. Unit tests need it though. */ public static function clearPendingUpdates() { - self::$updates = array(); + self::$preSendUpdates = array(); + self::$postSendUpdates = array(); } /** diff --git a/includes/deferred/HTMLCacheUpdate.php b/includes/deferred/HTMLCacheUpdate.php index a480aec83a..db3790f7d3 100644 --- a/includes/deferred/HTMLCacheUpdate.php +++ b/includes/deferred/HTMLCacheUpdate.php @@ -43,24 +43,8 @@ class HTMLCacheUpdate implements DeferrableUpdate { } public function doUpdate() { - $job = new HTMLCacheUpdateJob( - $this->mTitle, - array( - 'table' => $this->mTable, - 'recursive' => true - ) + Job::newRootJobParams( // "overall" refresh links job info - "htmlCacheUpdate:{$this->mTable}:{$this->mTitle->getPrefixedText()}" - ) - ); + $job = HTMLCacheUpdateJob::newForBacklinks( $this->mTitle, $this->mTable ); - $count = $this->mTitle->getBacklinkCache()->getNumLinks( $this->mTable, 100 ); - if ( $count >= 100 ) { // many backlinks - JobQueueGroup::singleton()->lazyPush( $job ); - } else { // few backlinks ($count might be off even if 0) - $dbw = wfGetDB( DB_MASTER ); - $dbw->onTransactionIdle( function () use ( $job ) { - $job->run(); // just do the purge query now - } ); - } + JobQueueGroup::singleton()->lazyPush( $job ); } } diff --git a/includes/deferred/MergeableUpdate.php b/includes/deferred/MergeableUpdate.php new file mode 100644 index 0000000000..70760ce49c --- /dev/null +++ b/includes/deferred/MergeableUpdate.php @@ -0,0 +1,16 @@ +urls = array_unique( $urlArr ); + $this->urls = $urlArr; } /** @@ -59,9 +60,7 @@ class SquidUpdate implements DeferrableUpdate { * @deprecated 1.27 */ public static function newSimplePurge( Title $title ) { - $urlArr = $title->getSquidURLs(); - - return new SquidUpdate( $urlArr ); + return new SquidUpdate( $title->getSquidURLs() ); } /** @@ -71,6 +70,13 @@ class SquidUpdate implements DeferrableUpdate { self::purge( $this->urls ); } + public function merge( MergeableUpdate $update ) { + /** @var SquidUpdate $update */ + Assert::parameterType( __CLASS__, $update, '$update' ); + + $this->urls = array_merge( $this->urls, $update->urls ); + } + /** * Purges a list of Squids defined in $wgSquidServers. * $urlArr should contain the full URLs to purge as values @@ -86,6 +92,9 @@ class SquidUpdate implements DeferrableUpdate { return; } + // Remove duplicate URLs from list + $urlArr = array_unique( $urlArr ); + wfDebugLog( 'squid', __METHOD__ . ': ' . implode( ' ', $urlArr ) ); if ( $wgHTCPRouting ) { @@ -93,8 +102,6 @@ class SquidUpdate implements DeferrableUpdate { } if ( $wgSquidServers ) { - // Remove duplicate URLs - $urlArr = array_unique( $urlArr ); // Maximum number of parallel connections per squid $maxSocketsPerSquid = 8; // Number of requests to send per socket @@ -127,7 +134,7 @@ class SquidUpdate implements DeferrableUpdate { * @throws MWException * @param string[] $urlArr Collection of URLs to purge */ - protected static function HTCPPurge( array $urlArr ) { + private static function HTCPPurge( array $urlArr ) { global $wgHTCPRouting, $wgHTCPMulticastTTL; // HTCP CLR operation @@ -158,8 +165,6 @@ class SquidUpdate implements DeferrableUpdate { $wgHTCPMulticastTTL ); } - // Remove duplicate URLs from collection - $urlArr = array_unique( $urlArr ); // Get sequential trx IDs for packet loss counting $ids = UIDGenerator::newSequentialPerNodeIDs( 'squidhtcppurge', 32, count( $urlArr ), UIDGenerator::QUICK_VOLATILE diff --git a/includes/filerepo/file/LocalFile.php b/includes/filerepo/file/LocalFile.php index c2bbb4eae4..dcc87412ec 100644 --- a/includes/filerepo/file/LocalFile.php +++ b/includes/filerepo/file/LocalFile.php @@ -879,7 +879,10 @@ class LocalFile extends File { $this->purgeThumbnails( $options ); // Purge squid cache for this file - SquidUpdate::purge( array( $this->getURL() ) ); + DeferredUpdates::addUpdate( + new SquidUpdate( array( $this->getUrl() ) ), + DeferredUpdates::PRESEND + ); } /** @@ -887,8 +890,6 @@ class LocalFile extends File { * @param string $archiveName Name of the archived file */ function purgeOldThumbnails( $archiveName ) { - global $wgUseSquid; - // Get a list of old thumbnails and URLs $files = $this->getThumbnails( $archiveName ); @@ -899,14 +900,11 @@ class LocalFile extends File { $this->purgeThumbList( $dir, $files ); // Purge the squid - if ( $wgUseSquid ) { - $urls = array(); - foreach ( $files as $file ) { - $urls[] = $this->getArchiveThumbUrl( $archiveName, $file ); - } - SquidUpdate::purge( $urls ); + $urls = array(); + foreach ( $files as $file ) { + $urls[] = $this->getArchiveThumbUrl( $archiveName, $file ); } - + DeferredUpdates::addUpdate( new SquidUpdate( $urls ), DeferredUpdates::PRESEND ); } /** @@ -914,18 +912,14 @@ class LocalFile extends File { * @param array $options */ public function purgeThumbnails( $options = array() ) { - global $wgUseSquid; - // Delete thumbnails $files = $this->getThumbnails(); // Always purge all files from squid regardless of handler filters $urls = array(); - if ( $wgUseSquid ) { - foreach ( $files as $file ) { - $urls[] = $this->getThumbUrl( $file ); - } - array_shift( $urls ); // don't purge directory + foreach ( $files as $file ) { + $urls[] = $this->getThumbUrl( $file ); } + array_shift( $urls ); // don't purge directory // Give media handler a chance to filter the file purge list if ( !empty( $options['forThumbRefresh'] ) ) { @@ -942,10 +936,7 @@ class LocalFile extends File { $this->purgeThumbList( $dir, $files ); // Purge the squid - if ( $wgUseSquid ) { - SquidUpdate::purge( $urls ); - } - + DeferredUpdates::addUpdate( new SquidUpdate( $urls ), DeferredUpdates::PRESEND ); } /** @@ -1444,7 +1435,10 @@ class LocalFile extends File { # Delete old thumbnails $that->purgeThumbnails(); # Remove the old file from the squid cache - SquidUpdate::purge( array( $that->getURL() ) ); + DeferredUpdates::addUpdate( + new SquidUpdate( array( $that->getUrl() ) ), + DeferredUpdates::PRESEND + ); } else { # Update backlink pages pointing to this title if created LinksUpdate::queueRecursiveJobsForTable( $that->getTitle(), 'imagelinks' ); @@ -1631,24 +1625,20 @@ class LocalFile extends File { $that = $this; $this->getRepo()->getMasterDB()->onTransactionIdle( function () use ( $that, $archiveNames ) { - global $wgUseSquid; - $that->purgeEverything(); foreach ( $archiveNames as $archiveName ) { $that->purgeOldThumbnails( $archiveName ); } - - if ( $wgUseSquid ) { - // Purge the squid - $purgeUrls = array(); - foreach ( $archiveNames as $archiveName ) { - $purgeUrls[] = $that->getArchiveUrl( $archiveName ); - } - SquidUpdate::purge( $purgeUrls ); - } } ); + // Purge the squid + $purgeUrls = array(); + foreach ( $archiveNames as $archiveName ) { + $purgeUrls[] = $this->getArchiveUrl( $archiveName ); + } + DeferredUpdates::addUpdate( new SquidUpdate( $purgeUrls ), DeferredUpdates::PRESEND ); + return $status; } @@ -1668,7 +1658,6 @@ class LocalFile extends File { * @return FileRepoStatus */ function deleteOld( $archiveName, $reason, $suppress = false, $user = null ) { - global $wgUseSquid; if ( $this->getRepo()->getReadOnlyReason() !== false ) { return $this->readOnlyFatalStatus(); } @@ -1685,10 +1674,10 @@ class LocalFile extends File { $this->purgeDescription(); } - if ( $wgUseSquid ) { - // Purge the squid - SquidUpdate::purge( array( $this->getArchiveUrl( $archiveName ) ) ); - } + DeferredUpdates::addUpdate( + new SquidUpdate( array( $this->getArchiveUrl( $archiveName ) ) ), + DeferredUpdates::PRESEND + ); return $status; } diff --git a/includes/jobqueue/jobs/CategoryMembershipChangeJob.php b/includes/jobqueue/jobs/CategoryMembershipChangeJob.php index 2b0018d4a2..c9e20a9bea 100644 --- a/includes/jobqueue/jobs/CategoryMembershipChangeJob.php +++ b/includes/jobqueue/jobs/CategoryMembershipChangeJob.php @@ -165,7 +165,7 @@ class CategoryMembershipChangeJob extends Job { $insertCount = 0; foreach ( $categoryInserts as $categoryName ) { - $categoryTitle = Title::newFromText( $categoryName, NS_CATEGORY ); + $categoryTitle = Title::makeTitle( NS_CATEGORY, $categoryName ); $catMembChange->triggerCategoryAddedNotification( $categoryTitle ); if ( $insertCount++ && ( $insertCount % $batchSize ) == 0 ) { $dbw->commit( __METHOD__, 'flush' ); @@ -174,7 +174,7 @@ class CategoryMembershipChangeJob extends Job { } foreach ( $categoryDeletes as $categoryName ) { - $categoryTitle = Title::newFromText( $categoryName, NS_CATEGORY ); + $categoryTitle = Title::makeTitle( NS_CATEGORY, $categoryName ); $catMembChange->triggerCategoryRemovedNotification( $categoryTitle ); if ( $insertCount++ && ( $insertCount++ % $batchSize ) == 0 ) { $dbw->commit( __METHOD__, 'flush' ); diff --git a/includes/jobqueue/jobs/HTMLCacheUpdateJob.php b/includes/jobqueue/jobs/HTMLCacheUpdateJob.php index 6b1a1e3f4b..ae35e30ad0 100644 --- a/includes/jobqueue/jobs/HTMLCacheUpdateJob.php +++ b/includes/jobqueue/jobs/HTMLCacheUpdateJob.php @@ -29,7 +29,7 @@ * - a) Recursive jobs to purge caches for backlink pages for a given title. * These jobs have (recursive:true,table:) set. * - b) Jobs to purge caches for a set of titles (the job title is ignored). - * These jobs have (pages:(:(,),...) set. + * These jobs have (pages:(<page ID>:(<namespace>,<title>),...) set. * * @ingroup JobQueue */ @@ -40,6 +40,23 @@ class HTMLCacheUpdateJob extends Job { $this->removeDuplicates = ( !isset( $params['range'] ) && !isset( $params['pages'] ) ); } + /** + * @param Title $title Title to purge backlink pages from + * @param string $table Backlink table name + * @return HTMLCacheUpdateJob + */ + public static function newForBacklinks( Title $title, $table ) { + return new self( + $title, + array( + 'table' => $table, + 'recursive' => true + ) + Job::newRootJobParams( // "overall" refresh links job info + "htmlCacheUpdate:{$table}:{$title->getPrefixedText()}" + ) + ); + } + function run() { global $wgUpdateRowsPerJob, $wgUpdateRowsPerQuery; @@ -77,7 +94,7 @@ class HTMLCacheUpdateJob extends Job { * @param array $pages Map of (page ID => (namespace, DB key)) entries */ protected function invalidateTitles( array $pages ) { - global $wgUpdateRowsPerQuery, $wgUseFileCache, $wgUseSquid; + global $wgUpdateRowsPerQuery, $wgUseFileCache; // Get all page IDs in this query into an array $pageIds = array_keys( $pages ); @@ -123,10 +140,8 @@ class HTMLCacheUpdateJob extends Job { ) ); // Update squid - if ( $wgUseSquid ) { - $u = SquidUpdate::newFromTitles( $titleArray ); - $u->doUpdate(); - } + $u = SquidUpdate::newFromTitles( $titleArray ); + $u->doUpdate(); // Update file cache if ( $wgUseFileCache ) { diff --git a/includes/jobqueue/utils/BacklinkJobUtils.php b/includes/jobqueue/utils/BacklinkJobUtils.php index c8e5df66f7..1770ac75e4 100644 --- a/includes/jobqueue/utils/BacklinkJobUtils.php +++ b/includes/jobqueue/utils/BacklinkJobUtils.php @@ -25,6 +25,27 @@ /** * Class with Backlink related Job helper methods * + * When an asset changes, a base job can be inserted to update all assets that depend on it. + * The base job splits into per-title "leaf" jobs and a "remnant" job to handle the remaining + * range of backlinks. This recurs until the remnant job's backlink range is small enough that + * only leaf jobs are created from it. + * + * For example, if templates A and B are edited (at the same time) the queue will have: + * (A base, B base) + * When these jobs run, the queue will have per-title and remnant partition jobs: + * (titleX,titleY,titleZ,...,A remnant,titleM,titleN,titleO,...,B remnant) + * + * This works best when the queue is FIFO, for several reasons: + * - a) Since the remnant jobs are enqueued after the leaf jobs, the slower leaf jobs have to + * get popped prior to the fast remnant jobs. This avoids flooding the queue with leaf jobs + * for every single backlink of widely used assets (which can be millions). + * - b) Other jobs going in the queue still get a chance to run after a widely used asset changes. + * This is due to the large remnant job pushing to the end of the queue with each division. + * + * The size of the queues used in this manner depend on the number of assets changes and the + * number of workers. Also, with FIFO-per-partition queues, the queue size can be somewhat larger, + * depending on the number of queue partitions. + * * @ingroup JobQueue * @since 1.23 */ @@ -71,6 +92,7 @@ class BacklinkJobUtils { if ( isset( $params['pages'] ) || empty( $params['recursive'] ) ) { $ranges = array(); // sanity; this is a leaf node + $realBSize = 0; wfWarn( __METHOD__ . " called on {$job->getType()} leaf job (explosive recursion)." ); } elseif ( isset( $params['range'] ) ) { // This is a range job to trigger the insertion of partitioned/title jobs... @@ -88,8 +110,10 @@ class BacklinkJobUtils { // Combine the first range (of size $bSize) backlinks into leaf jobs if ( isset( $ranges[0] ) ) { list( $start, $end ) = $ranges[0]; - $titles = $title->getBacklinkCache()->getLinks( $params['table'], $start, $end ); - foreach ( array_chunk( iterator_to_array( $titles ), $cSize ) as $titleBatch ) { + $iter = $title->getBacklinkCache()->getLinks( $params['table'], $start, $end ); + $titles = iterator_to_array( $iter ); + /** @var Title[] $titleBatch */ + foreach ( array_chunk( $titles, $cSize ) as $titleBatch ) { $pages = array(); foreach ( $titleBatch as $tl ) { $pages[$tl->getArticleId()] = array( $tl->getNamespace(), $tl->getDBKey() ); diff --git a/includes/page/WikiPage.php b/includes/page/WikiPage.php index 5a6511ca8a..4fbb8450fb 100644 --- a/includes/page/WikiPage.php +++ b/includes/page/WikiPage.php @@ -881,7 +881,8 @@ class WikiPage implements Page, IDBAccessObject { if ( $row && !is_null( $row->rd_fragment ) && !is_null( $row->rd_interwiki ) ) { $this->mRedirectTarget = Title::makeTitle( $row->rd_namespace, $row->rd_title, - $row->rd_fragment, $row->rd_interwiki ); + $row->rd_fragment, $row->rd_interwiki + ); return $this->mRedirectTarget; } @@ -891,39 +892,54 @@ class WikiPage implements Page, IDBAccessObject { } /** - * Insert an entry for this page into the redirect table. + * Insert an entry for this page into the redirect table if the content is a redirect + * + * The database update will be deferred via DeferredUpdates * * Don't call this function directly unless you know what you're doing. * @return Title|null Title object or null if not a redirect */ public function insertRedirect() { - // recurse through to only get the final target $content = $this->getContent(); $retval = $content ? $content->getUltimateRedirectTarget() : null; if ( !$retval ) { return null; } - $this->insertRedirectEntry( $retval ); + + // Update the DB post-send if the page has not cached since now + $that = $this; + $latest = $this->getLatest(); + DeferredUpdates::addCallableUpdate( function() use ( $that, $retval, $latest ) { + $that->insertRedirectEntry( $retval, $latest ); + } ); + return $retval; } /** - * Insert or update the redirect table entry for this page to indicate - * it redirects to $rt . + * Insert or update the redirect table entry for this page to indicate it redirects to $rt * @param Title $rt Redirect target + * @param int|null $oldLatest Prior page_latest for check and set */ - public function insertRedirectEntry( $rt ) { + public function insertRedirectEntry( Title $rt, $oldLatest = null ) { $dbw = wfGetDB( DB_MASTER ); - $dbw->replace( 'redirect', array( 'rd_from' ), - array( - 'rd_from' => $this->getId(), - 'rd_namespace' => $rt->getNamespace(), - 'rd_title' => $rt->getDBkey(), - 'rd_fragment' => $rt->getFragment(), - 'rd_interwiki' => $rt->getInterwiki(), - ), - __METHOD__ - ); + $dbw->startAtomic( __METHOD__ ); + + if ( !$oldLatest || $oldLatest == $this->lockAndGetLatest() ) { + $dbw->replace( 'redirect', + array( 'rd_from' ), + array( + 'rd_from' => $this->getId(), + 'rd_namespace' => $rt->getNamespace(), + 'rd_title' => $rt->getDBkey(), + 'rd_fragment' => $rt->getFragment(), + 'rd_interwiki' => $rt->getInterwiki(), + ), + __METHOD__ + ); + } + + $dbw->endAtomic( __METHOD__ ); } /** @@ -1027,55 +1043,6 @@ class WikiPage implements Page, IDBAccessObject { return new UserArrayFromResult( $res ); } - /** - * Get the last N authors - * @param int $num Number of revisions to get - * @param int|string $revLatest The latest rev_id, selected from the master (optional) - * @return array Array of authors, duplicates not removed - */ - public function getLastNAuthors( $num, $revLatest = 0 ) { - // First try the slave - // If that doesn't have the latest revision, try the master - $continue = 2; - $db = wfGetDB( DB_SLAVE ); - - do { - $res = $db->select( array( 'page', 'revision' ), - array( 'rev_id', 'rev_user_text' ), - array( - 'page_namespace' => $this->mTitle->getNamespace(), - 'page_title' => $this->mTitle->getDBkey(), - 'rev_page = page_id' - ), __METHOD__, - array( - 'ORDER BY' => 'rev_timestamp DESC', - 'LIMIT' => $num - ) - ); - - if ( !$res ) { - return array(); - } - - $row = $db->fetchObject( $res ); - - if ( $continue == 2 && $revLatest && $row->rev_id != $revLatest ) { - $db = wfGetDB( DB_MASTER ); - $continue--; - } else { - $continue = 0; - } - } while ( $continue ); - - $authors = array( $row->rev_user_text ); - - foreach ( $res as $row ) { - $authors[] = $row->rev_user_text; - } - - return $authors; - } - /** * Should the parser cache be used? * @@ -1160,16 +1127,16 @@ class WikiPage implements Page, IDBAccessObject { $title = $this->mTitle; wfGetDB( DB_MASTER )->onTransactionIdle( function() use ( $title ) { - global $wgUseSquid; // Invalidate the cache in auto-commit mode $title->invalidateCache(); - if ( $wgUseSquid ) { - // Send purge now that page_touched update was committed above - $update = new SquidUpdate( $title->getSquidURLs() ); - $update->doUpdate(); - } } ); + // Send purge after above page_touched update was committed + DeferredUpdates::addUpdate( + new SquidUpdate( $title->getSquidURLs() ), + DeferredUpdates::PRESEND + ); + if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { // @todo move this logic to MessageCache if ( $this->exists() ) { diff --git a/includes/password/PasswordPolicyChecks.php b/includes/password/PasswordPolicyChecks.php index eb4a9582dc..b1098f5b84 100644 --- a/includes/password/PasswordPolicyChecks.php +++ b/includes/password/PasswordPolicyChecks.php @@ -20,6 +20,8 @@ * @file */ +use \Cdb\Reader as CdbReader; + /** * Functions to check passwords against a policy requirement * @since 1.26 @@ -112,4 +114,50 @@ class PasswordPolicyChecks { return $status; } + /** + * Ensure that password isn't in top X most popular passwords + * + * @param $policyVal int Cut off to use. Will automatically shrink to the max + * supported for error messages if set to more than max number of passwords on file, + * so you can use the PHP_INT_MAX constant here safely. + * @param $user User + * @param $password String + * @since 1.27 + * @return Status + */ + public static function checkPopularPasswordBlacklist( $policyVal, User $user, $password ) { + global $wgPopularPasswordFile, $wgSitename; + $status = Status::newGood(); + if ( $policyVal > 0 ) { + $langEn = Language::factory( 'en' ); + $passwordKey = $langEn->lc( trim( $password ) ); + + // People often use the name of the current site, which won't be + // in the common password file. Also check '' for people who use + // just whitespace. + $sitename = $langEn->lc( trim( $wgSitename ) ); + $hardcodedCommonPasswords = array( '', 'wiki', 'mediawiki', $sitename ); + if ( in_array( $passwordKey, $hardcodedCommonPasswords ) ) { + $status->error( 'passwordtoopopular' ); + return $status; + } + + // This could throw an exception, but there's not a good way + // of failing gracefully, if say the file is missing, so just + // let the exception fall through. + // Format of cdb file is mapping password => popularity rank. + // See maintenance/createCommonPasswordCdb.php + $db = CdbReader::open( $wgPopularPasswordFile ); + + $res = $db->get( $passwordKey ); + if ( $res && (int)$res <= $policyVal ) { + // Note: If you want to find the true number of common + // passwords stored (for reporting the error), you have to take + // the max of the policyVal and $db->get( '_TOTALENTRIES' ). + $status->error( 'passwordtoopopular' ); + } + } + return $status; + } + } diff --git a/includes/revisiondelete/RevDelFileList.php b/includes/revisiondelete/RevDelFileList.php index 2295eaa106..e5f32d22db 100644 --- a/includes/revisiondelete/RevDelFileList.php +++ b/includes/revisiondelete/RevDelFileList.php @@ -108,16 +108,18 @@ class RevDelFileList extends RevDelList { $file = wfLocalFile( $this->title ); $file->purgeCache(); $file->purgeDescription(); + + // Purge full images from cache $purgeUrls = array(); foreach ( $this->ids as $timestamp ) { $archiveName = $timestamp . '!' . $this->title->getDBkey(); $file->purgeOldThumbnails( $archiveName ); $purgeUrls[] = $file->getArchiveUrl( $archiveName ); } - if ( $this->getConfig()->get( 'UseSquid' ) ) { - // purge full images from cache - SquidUpdate::purge( $purgeUrls ); - } + DeferredUpdates::addUpdate( + new SquidUpdate( $purgeUrls ), + DeferredUpdates::PRESEND + ); return Status::newGood(); } diff --git a/includes/search/SearchExactMatchRescorer.php b/includes/search/SearchExactMatchRescorer.php new file mode 100644 index 0000000000..0ff628def0 --- /dev/null +++ b/includes/search/SearchExactMatchRescorer.php @@ -0,0 +1,144 @@ +<?php +/** + * Rescores results from a prefix search/opensearch to make sure the + * exact match is the first result. + * + * 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 + */ + +/** + * An utility class to rescore search results by looking for an exact match + * in the db and add the page found to the first position. + * + * NOTE: extracted from TitlePrefixSearch + * @ingroup Search + */ +class SearchExactMatchRescorer { + /** + * Default search backend does proper prefix searching, but custom backends + * may sort based on other algorithms that may cause the exact title match + * to not be in the results or be lower down the list. + * @param string $search the query + * @param int[] $namespaces the namespaces + * @param int $limit the max number of results to return + * @param string[] $srchres results + * @return string[] munged results + */ + public function rescore( $search, $namespaces, $srchres, $limit ) { + // Pick namespace (based on PrefixSearch::defaultSearchBackend) + $ns = in_array( NS_MAIN, $namespaces ) ? NS_MAIN : $namespaces[0]; + $t = Title::newFromText( $search, $ns ); + if ( !$t || !$t->exists() ) { + // No exact match so just return the search results + return $srchres; + } + $string = $t->getPrefixedText(); + $key = array_search( $string, $srchres ); + if ( $key !== false ) { + // Exact match was in the results so just move it to the front + return $this->pullFront( $key, $srchres ); + } + // Exact match not in the search results so check for some redirect handling cases + if ( $t->isRedirect() ) { + $target = $this->getRedirectTarget( $t ); + $key = array_search( $target, $srchres ); + if ( $key !== false ) { + // Exact match is a redirect to one of the returned matches so pull the + // returned match to the front. This might look odd but the alternative + // is to put the redirect in front and drop the match. The name of the + // found match is often more descriptive/better formed than the name of + // the redirect AND by definition they share a prefix. Hopefully this + // choice is less confusing and more helpful. But it might not be. But + // it is the choice we're going with for now. + return $this->pullFront( $key, $srchres ); + } + $redirectTargetsToRedirect = $this->redirectTargetsToRedirect( $srchres ); + if ( isset( $redirectTargetsToRedirect[$target] ) ) { + // The exact match and something in the results list are both redirects + // to the same thing! In this case we'll pull the returned match to the + // top following the same logic above. Again, it might not be a perfect + // choice but it'll do. + return $this->pullFront( $redirectTargetsToRedirect[$target], $srchres ); + } + } else { + $redirectTargetsToRedirect = $this->redirectTargetsToRedirect( $srchres ); + if ( isset( $redirectTargetsToRedirect[$string] ) ) { + // The exact match is the target of a redirect already in the results list so remove + // the redirect from the results list and push the exact match to the front + array_splice( $srchres, $redirectTargetsToRedirect[$string], 1 ); + array_unshift( $srchres, $string ); + return $srchres; + } + } + + // Exact match is totally unique from the other results so just add it to the front + array_unshift( $srchres, $string ); + // And roll one off the end if the results are too long + if ( count( $srchres ) > $limit ) { + array_pop( $srchres ); + } + return $srchres; + } + + /** + * @param string[] $titles as strings + * @return array redirect target prefixedText to index of title in titles + * that is a redirect to it. + */ + private function redirectTargetsToRedirect( $titles ) { + $result = array(); + foreach ( $titles as $key => $titleText ) { + $title = Title::newFromText( $titleText ); + if ( !$title || !$title->isRedirect() ) { + continue; + } + $target = $this->getRedirectTarget( $title ); + if ( !$target ) { + continue; + } + $result[$target] = $key; + } + return $result; + } + + /** + * Returns an array where the element of $array at index $key becomes + * the first element. + * @param int $key key to pull to the front + * @return array $array with the item at $key pulled to the front + */ + private function pullFront( $key, $array ) { + $cut = array_splice( $array, $key, 1 ); + array_unshift( $array, $cut[0] ); + return $array; + } + + /** + * Get a redirect's destination from a title + * @param Title $title A title to redirect. It may not redirect or even exist + * @return null|string If title exists and redirects, get the destination's prefixed name + */ + private function getRedirectTarget( $title ) { + $page = WikiPage::factory( $title ); + if ( !$page->exists() ) { + return null; + } + $redir = $page->getRedirectTarget(); + return $redir ? $redir->getPrefixedText() : null; + } +} diff --git a/includes/specials/SpecialEmailuser.php b/includes/specials/SpecialEmailuser.php index 6b0d1ec73b..3b31530698 100644 --- a/includes/specials/SpecialEmailuser.php +++ b/includes/specials/SpecialEmailuser.php @@ -260,6 +260,7 @@ class SpecialEmailUser extends UnlistedSpecialPage { * @return string Form asking for user name. */ protected function userForm( $name ) { + $this->getOutput()->addModules( 'mediawiki.userSuggest' ); $string = Xml::openElement( 'form', array( 'method' => 'get', 'action' => wfScript(), 'id' => 'askusername' ) @@ -272,7 +273,11 @@ class SpecialEmailUser extends UnlistedSpecialPage { 'target', 'emailusertarget', 30, - $name + $name, + array( + 'class' => 'mw-autocomplete-user', // used by mediawiki.userSuggest + 'autofocus' => true, + ) ) . ' ' . Xml::submitButton( $this->msg( 'emailusernamesubmit' )->text() ) . diff --git a/includes/specials/SpecialProtectedpages.php b/includes/specials/SpecialProtectedpages.php index 685931052d..0119781c6d 100644 --- a/includes/specials/SpecialProtectedpages.php +++ b/includes/specials/SpecialProtectedpages.php @@ -228,7 +228,7 @@ class SpecialProtectedpages extends SpecialPage { } return '<span class="mw-input-with-label">' . - Xml::label( $this->msg( 'restriction-type' )->text(), $this->IdType ) . ' ' . + Xml::label( $this->msg( 'restriction-type' )->text(), $this->IdType ) . ' ' . Xml::tags( 'select', array( 'id' => $this->IdType, 'name' => $this->IdType ), implode( "\n", $options ) ) . "</span>"; diff --git a/includes/specials/SpecialWatchlist.php b/includes/specials/SpecialWatchlist.php index 32d4552762..b5382a6703 100644 --- a/includes/specials/SpecialWatchlist.php +++ b/includes/specials/SpecialWatchlist.php @@ -403,8 +403,15 @@ class SpecialWatchlist extends ChangesListSpecialPage { */ public function doHeader( $opts, $numRows ) { $user = $this->getUser(); + $out = $this->getOutput(); - $this->getOutput()->addSubtitle( + // if the user wishes, that the watchlist is reloaded, whenever a filter changes, + // add the module for that + if ( $user->getBoolOption( 'watchlistreloadautomatically' ) ) { + $out->addModules( array( 'mediawiki.special.watchlist' ) ); + } + + $out->addSubtitle( $this->msg( 'watchlistfor2', $user->getName() ) ->rawParams( SpecialEditWatchlist::buildTools( null ) ) ); @@ -469,7 +476,7 @@ class SpecialWatchlist extends ChangesListSpecialPage { $form .= $this->msg( 'watchlist-hide' ) . $this->msg( 'colon-separator' )->escaped() . implode( ' ', $links ); - $form .= "\n<hr />\n<p>"; + $form .= "\n<br />\n"; $form .= Html::namespaceSelector( array( 'selected' => $opts['namespace'], @@ -495,7 +502,7 @@ class SpecialWatchlist extends ChangesListSpecialPage { $opts['associated'], array( 'title' => $this->msg( 'tooltip-namespace_association' )->text() ) ) . "</span>\n"; - $form .= Xml::submitButton( $this->msg( 'allpagessubmit' )->text() ) . "</p>\n"; + $form .= Xml::submitButton( $this->msg( 'allpagessubmit' )->text() ) . "\n"; foreach ( $hiddenFields as $key => $value ) { $form .= Html::hidden( $key, $value ) . "\n"; } diff --git a/languages/i18n/en.json b/languages/i18n/en.json index 0092ada6f2..32e9b00685 100644 --- a/languages/i18n/en.json +++ b/languages/i18n/en.json @@ -35,6 +35,7 @@ "tog-watchlisthidebots": "Hide bot edits from the watchlist", "tog-watchlisthideminor": "Hide minor edits from the watchlist", "tog-watchlisthideliu": "Hide edits by logged in users from the watchlist", + "tog-watchlistreloadautomatically": "Reload the watchlist automatically whenever a filter is changed (JavaScript required)", "tog-watchlisthideanons": "Hide edits by anonymous users from the watchlist", "tog-watchlisthidepatrolled": "Hide patrolled edits from the watchlist", "tog-watchlisthidecategorization": "Hide categorization of pages", @@ -470,6 +471,7 @@ "wrongpasswordempty": "Password entered was blank.\nPlease try again.", "passwordtooshort": "Passwords must be at least {{PLURAL:$1|1 character|$1 characters}}.", "passwordtoolong": "Passwords cannot be longer than {{PLURAL:$1|1 character|$1 characters}}.", + "passwordtoopopular": "Commonly chosen passwords cannot be used. Please choose a more unique password.", "password-name-match": "Your password must be different from your username.", "password-login-forbidden": "The use of this username and password has been forbidden.", "mailmypassword": "Reset password", diff --git a/languages/i18n/qqq.json b/languages/i18n/qqq.json index dcf26b3dbc..15538e636b 100644 --- a/languages/i18n/qqq.json +++ b/languages/i18n/qqq.json @@ -209,6 +209,7 @@ "tog-watchlisthidebots": "[[Special:Preferences]], tab 'Watchlist'. Offers user to hide bot edits from watchlist. {{Gender}}\n\n{{Related|Preferences-watchlistrc-toggle}}", "tog-watchlisthideminor": "[[Special:Preferences]], tab 'Watchlist'. Offers user to hide minor edits from watchlist. {{Gender}}\n\n{{Related|Preferences-watchlistrc-toggle}}", "tog-watchlisthideliu": "Option in tab 'Watchlist' of [[Special:Preferences]]. {{Gender}}\n\n{{Related|Preferences-watchlistrc-toggle}}", + "tog-watchlistreloadautomatically": "[[Special:Preferences]], tab 'Watchlist'. Offers user to to automatically refresh the watchlist page, when a filter is changed.", "tog-watchlisthideanons": "Option in tab 'Watchlist' of [[Special:Preferences]]. {{Gender}}\n\n{{Related|Preferences-watchlistrc-toggle}}", "tog-watchlisthidepatrolled": "Option in Watchlist tab of [[Special:Preferences]]. {{Gender}}\n\n{{Related|Preferences-watchlistrc-toggle}}", "tog-watchlisthidecategorization": "Option in Watchlist tab of [[Special:Preferences]]. Offers user to hide/show categorization of pages. Appears next to checkboxes with labels such as {{msg-mw|tog-watchlisthideminor}}.", @@ -644,6 +645,7 @@ "wrongpasswordempty": "Error message displayed when entering a blank password.\n{{Identical|Please try again}}", "passwordtooshort": "This message is shown in [[Special:Preferences]] and [[Special:CreateAccount]].\n\nParameters:\n* $1 - the minimum number of characters in the password", "passwordtoolong": "This message is shown in [[Special:Preferences]], [[Special:CreateAccount]], and [[Special:Userlogin]].\n\nParameters:\n* $1 - the maximum number of characters in the password", + "passwordtoopopular": "Shown if the user chooses a really popular password.", "password-name-match": "Used as error message when password validity check failed.", "password-login-forbidden": "Error message shown when the user has tried to log in using one of the special username/password combinations used for MediaWiki testing. (See [[mwr:75589]], [[mwr:75605]].)", "mailmypassword": "Used as label for Submit button in [[Special:PasswordReset]].\n{{Identical|Reset password}}", diff --git a/maintenance/createCommonPasswordCdb.php b/maintenance/createCommonPasswordCdb.php new file mode 100644 index 0000000000..c6787122b0 --- /dev/null +++ b/maintenance/createCommonPasswordCdb.php @@ -0,0 +1,117 @@ +<?php +/** + * Create serialized/commonpasswords.cdb + * + * 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 + * @ingroup Maintenance + */ + +require_once __DIR__ . '/Maintenance.php'; + +/** + * Maintenance script to create common password cdb database. + * + * Meant to take a file like + * https://github.com/danielmiessler/SecLists/blob/master/Passwords/rockyou.txt?raw=true + * as input. + * @see serialized/commonpasswords.cdb and PasswordPolicyChecks::checkPopularPasswordBlacklist + * @since 1.27 + * @ingroup Maintenance + */ +class GenerateCommonPassword extends Maintenance { + public function __construct() { + global $IP; + parent::__construct(); + $this->mDescription = "Generate CDB file of common passwords"; + $this->addOption( 'limit', "Max number of passwords to write", false, true, 'l' ); + $this->addArg( 'inputfile', 'List of passwords (one per line) to use or - for stdin', true ); + $this->addArg( + 'output', + "Location to write CDB file to (Try $IP/serialized/commonpasswords.cdb)", + true + ); + } + + public function execute() { + $limit = (int)$this->getOption( 'limit', PHP_INT_MAX ); + $langEn = Language::factory( 'en' ); + + $infile = $this->getArg( 0 ); + if ( $infile === '-' ) { + $infile = 'php://stdin'; + } + $outfile = $this->getArg( 1 ); + + if ( !is_readable( $infile ) && $infile !== 'php://stdin' ) { + $this->error( "Cannot open input file $infile for reading", 1 ); + } + + $file = fopen( $infile, 'r' ); + if ( $file === false ) { + $this->error( "Cannot read input file $infile", 1 ); + } + + try { + $db = \Cdb\Writer::open( $outfile ); + + $alreadyWritten = array(); + $skipped = 0; + for ( $i = 0; ( $i - $skipped ) < $limit; $i++ ) { + if ( feof( $file ) ) { + break; + } + $rawLine = fgets( $file ); + + if ( $rawLine === false ) { + $this->error( "Error reading input file" ); + break; + } + if ( substr( $rawLine, -1 ) !== "\n" && !feof( $file ) ) { + // We're assuming that this just won't happen. + $this->error( "fgets did not return whole line at $i??" ); + } + $line = $langEn->lc( trim( $rawLine ) ); + if ( $line === '' ) { + $this->error( "Line number " . ( $i + 1 ) . " is blank?" ); + $skipped++; + continue; + } + if ( isset( $alreadyWritten[$line] ) ) { + $this->output( "Password '$line' already written (line " . ( $i + 1 ) .")\n" ); + $skipped++; + continue; + } + $alreadyWritten[$line] = true; + $db->set( $line, $i + 1 - $skipped ); + } + // All caps, so cannot conflict with potential password + $db->set( '_TOTALENTRIES', $i - $skipped ); + $db->close(); + + $this->output( "Successfully wrote " . ( $i - $skipped ) . + " (out of $i) passwords to $outfile\n" + ); + } catch ( \Cdb\Exception $e ) { + $this->error( "Error writing cdb file: " . $e->getMessage(), 2 ); + } + + } +} + +$maintClass = "GenerateCommonPassword"; +require_once RUN_MAINTENANCE_IF_MAIN; diff --git a/resources/Resources.php b/resources/Resources.php index 0eb5118670..aba5ce8304 100644 --- a/resources/Resources.php +++ b/resources/Resources.php @@ -1834,6 +1834,9 @@ return array( 'mediawiki.util', ), ), + 'mediawiki.special.watchlist' => array( + 'scripts' => 'resources/src/mediawiki.special/mediawiki.special.watchlist.js', + ), 'mediawiki.special.javaScriptTest' => array( 'scripts' => 'resources/src/mediawiki.special/mediawiki.special.javaScriptTest.js', 'messages' => array_merge( Skin::getSkinNameMessages(), array( diff --git a/resources/src/mediawiki.special/mediawiki.special.watchlist.js b/resources/src/mediawiki.special/mediawiki.special.watchlist.js new file mode 100644 index 0000000000..a35f4d1064 --- /dev/null +++ b/resources/src/mediawiki.special/mediawiki.special.watchlist.js @@ -0,0 +1,15 @@ +/*! + * JavaScript for Special:Watchlist + * + * This script is only loaded, if the user opt-in a setting in Special:Preferences, + * that the watchlist should be automatically reloaded, when a filter option is + * changed in the header form. + */ +jQuery( function ( $ ) { + // add a listener on all form elements in the header form + $( '#mw-watchlist-form input, #mw-watchlist-form select' ).on( 'change', function () { + // submit the form, when one of the input fields was changed + $( '#mw-watchlist-form' ).submit(); + } ); + +} ); diff --git a/serialized/commonpasswords.cdb b/serialized/commonpasswords.cdb new file mode 100644 index 0000000000..7b7b043171 Binary files /dev/null and b/serialized/commonpasswords.cdb differ diff --git a/tests/phpunit/includes/deferred/DeferredUpdatesTest.php b/tests/phpunit/includes/deferred/DeferredUpdatesTest.php index df4213abdf..736f6e8909 100644 --- a/tests/phpunit/includes/deferred/DeferredUpdatesTest.php +++ b/tests/phpunit/includes/deferred/DeferredUpdatesTest.php @@ -34,6 +34,31 @@ class DeferredUpdatesTest extends MediaWikiTestCase { $this->expectOutputString( implode( '', $updates ) ); DeferredUpdates::doUpdates(); + + $x = null; + $y = null; + DeferredUpdates::addCallableUpdate( + function () use ( &$x ) { + $x = 'Sherity'; + }, + DeferredUpdates::PRESEND + ); + DeferredUpdates::addCallableUpdate( + function () use ( &$y ) { + $y = 'Marychu'; + }, + DeferredUpdates::POSTSEND + ); + + $this->assertNull( $x, "Update not run yet" ); + $this->assertNull( $y, "Update not run yet" ); + + DeferredUpdates::doUpdates( 'run', DeferredUpdates::PRESEND ); + $this->assertEquals( "Sherity", $x, "PRESEND update ran" ); + $this->assertNull( $y, "POSTSEND update not run yet" ); + + DeferredUpdates::doUpdates( 'run', DeferredUpdates::POSTSEND ); + $this->assertEquals( "Marychu", $y, "POSTSEND update ran" ); } public function testDoUpdatesCLI() { diff --git a/tests/phpunit/includes/deferred/SquidUpdateTest.php b/tests/phpunit/includes/deferred/SquidUpdateTest.php new file mode 100644 index 0000000000..6ceb42c116 --- /dev/null +++ b/tests/phpunit/includes/deferred/SquidUpdateTest.php @@ -0,0 +1,25 @@ +<?php + +class SquidUpdatesTest extends MediaWikiTestCase { + public function testPurgeMergeWeb() { + $this->setMwGlobals( 'wgCommandLineMode', false ); + + $urls1 = array(); + $title = Title::newMainPage(); + $urls1[] = $title->getCanonicalURL( '?x=1' ); + $urls1[] = $title->getCanonicalURL( '?x=2' ); + $urls1[] = $title->getCanonicalURL( '?x=3' ); + $update1 = new SquidUpdate( $urls1 ); + DeferredUpdates::addUpdate( $update1 ); + + $urls2 = array(); + $urls2[] = $title->getCanonicalURL( '?x=2' ); + $urls2[] = $title->getCanonicalURL( '?x=3' ); + $urls2[] = $title->getCanonicalURL( '?x=4' ); + $update2 = new SquidUpdate( $urls2 ); + DeferredUpdates::addUpdate( $update2 ); + + $wrapper = TestingAccessWrapper::newFromObject( $update1 ); + $this->assertEquals( array_merge( $urls1, $urls2 ), $wrapper->urls ); + } +}