'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',
'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',
'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',
'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',
* - 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(
'MinimalPasswordLength' => 8,
'MinimumPasswordLengthToLogin' => 1,
'PasswordCannotMatchUsername' => true,
+ 'PasswordCannotBePopular' => 25,
),
'sysop' => array(
'MinimalPasswordLength' => 8,
'MinimumPasswordLengthToLogin' => 1,
'PasswordCannotMatchUsername' => true,
+ 'PasswordCannotBePopular' => 25,
),
'bot' => array(
'MinimalPasswordLength' => 8,
'PasswordCannotMatchUsername' => 'PasswordPolicyChecks::checkPasswordCannotMatchUsername',
'PasswordCannotMatchBlacklist' => 'PasswordPolicyChecks::checkPasswordCannotMatchBlacklist',
'MaximalPasswordLength' => 'PasswordPolicyChecks::checkMaximalPasswordLength',
+ 'PasswordCannotBePopular' => 'PasswordPolicyChecks::checkPopularPasswordBlacklist'
),
);
'watchlisthideown' => 0,
'watchlisthidepatrolled' => 0,
'watchlisthidecategorization' => 1,
+ 'watchlistreloadautomatically' => 0,
'watchmoves' => 0,
'watchrollback' => 0,
'wllimit' => 250,
*/
$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
$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;
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;
- }
}
$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)
'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(
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 );
}
/**
* 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
+ );
}
/**
--- /dev/null
+<?php
+
+/**
+ * Interface that deferrable updates should implement. Basically required so we
+ * can validate input on DeferredUpdates::addUpdate()
+ *
+ * @since 1.19
+ */
+interface DeferrableUpdate {
+ /**
+ * Perform the actual work
+ */
+ function doUpdate();
+}
* @file
*/
-/**
- * Interface that deferrable updates should implement. Basically required so we
- * can validate input on DeferredUpdates::addUpdate()
- *
- * @since 1.19
- */
-interface DeferrableUpdate {
- /**
- * Perform the actual work
- */
- function doUpdate();
-}
-
/**
* Class for managing the deferred updates
*
- * Deferred updates can be run at the end of the request,
- * after the HTTP response has been sent. In CLI mode, updates
- * are only deferred until there is no local master DB transaction.
- * When updates are deferred, they go into a simple FIFO queue.
+ * In web request mode, deferred updates can be run at the end of the request, either before or
+ * after the HTTP response has been sent. In either case, they run after the DB commit step. If
+ * an update runs after the response is sent, it will not block clients. If sent before, it will
+ * run synchronously. If such an update works via queueing, it will be more likely to complete by
+ * the time the client makes their next request after this one.
+ *
+ * In CLI mode, updates are only deferred until the current wiki has no DB write transaction
+ * active within this request.
+ *
+ * When updates are deferred, they use a FIFO queue (one for pre-send and one for post-send).
*
* @since 1.19
*/
class DeferredUpdates {
- /** @var DeferrableUpdate[] Updates to be deferred until the end of the request */
- private static $updates = array();
+ /** @var DeferrableUpdate[] Updates to be deferred until before request end */
+ private static $preSendUpdates = array();
+ /** @var DeferrableUpdate[] Updates to be deferred until after request end */
+ private static $postSendUpdates = array();
+
/** @var bool Defer updates fully even in CLI mode */
private static $forceDeferral = false;
+ const ALL = 0; // all updates
+ const PRESEND = 1; // for updates that should run before flushing output buffer
+ const POSTSEND = 2; // for updates that should run after flushing output buffer
+
/**
* Add an update to the deferred list
+ *
* @param DeferrableUpdate $update Some object that implements doUpdate()
+ * @param integer $type DeferredUpdates constant (PRESEND or POSTSEND) (since 1.27)
*/
- public static function addUpdate( DeferrableUpdate $update ) {
+ public static function addUpdate( DeferrableUpdate $update, $type = self::POSTSEND ) {
+ if ( $type === self::PRESEND ) {
+ self::push( self::$preSendUpdates, $update );
+ } else {
+ self::push( self::$postSendUpdates, $update );
+ }
+ }
+
+ /**
+ * 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
+ * @param integer $type DeferredUpdates constant (PRESEND or POSTSEND) (since 1.27)
+ */
+ public static function addCallableUpdate( $callable, $type = self::POSTSEND ) {
+ self::addUpdate( new MWCallableUpdate( $callable ), $type );
+ }
+
+ /**
+ * Do any deferred updates and clear the list
+ *
+ * @param string $mode Use "enqueue" to use the job queue when possible [Default: "run"]
+ * @param integer $type DeferredUpdates constant (PRESEND, POSTSEND, or ALL) (since 1.27)
+ */
+ public static function doUpdates( $mode = 'run', $type = self::ALL ) {
+ if ( $type === self::ALL || $type == self::PRESEND ) {
+ self::execute( self::$preSendUpdates, $mode );
+ }
+
+ if ( $type === self::ALL || $type == self::POSTSEND ) {
+ self::execute( self::$postSendUpdates, $mode );
+ }
+ }
+
+ private static function push( array &$queue, DeferrableUpdate $update ) {
global $wgCommandLineMode;
- array_push( self::$updates, $update );
+ if ( $update instanceof MergeableUpdate ) {
+ $class = get_class( $update ); // fully-qualified class
+ if ( isset( $queue[$class] ) ) {
+ /** @var $existingUpdate MergeableUpdate */
+ $existingUpdate = $queue[$class];
+ $existingUpdate->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();
}
}
- /**
- * 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 */
}
}
- $updates = self::$updates;
+ $updates = $queue; // new snapshot of queue (check for new entries)
}
}
* want or need to call this. Unit tests need it though.
*/
public static function clearPendingUpdates() {
- self::$updates = array();
+ self::$preSendUpdates = array();
+ self::$postSendUpdates = array();
}
/**
}
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 );
}
}
--- /dev/null
+<?php
+
+/**
+ * Interface that deferrable updates can implement. DeferredUpdates uses this to merge
+ * all pending updates of PHP class into a single update by calling merge().
+ *
+ * @since 1.27
+ */
+interface MergeableUpdate {
+ /**
+ * Merge this update with $update
+ *
+ * @param MergeableUpdate $update Update of the same class type
+ */
+ function merge( MergeableUpdate $update );
+}
* @ingroup Cache
*/
+use Wikimedia\Assert\Assert;
+
/**
* Handles purging appropriate Squid URLs given a title (or titles)
* @ingroup Cache
*/
-class SquidUpdate implements DeferrableUpdate {
+class SquidUpdate implements DeferrableUpdate, MergeableUpdate {
/** @var string[] Collection of URLs to purge */
protected $urls = array();
* @param string[] $urlArr Collection of URLs to purge
*/
public function __construct( array $urlArr ) {
- // Remove duplicate URLs from list
- $this->urls = array_unique( $urlArr );
+ $this->urls = $urlArr;
}
/**
* @deprecated 1.27
*/
public static function newSimplePurge( Title $title ) {
- $urlArr = $title->getSquidURLs();
-
- return new SquidUpdate( $urlArr );
+ return new SquidUpdate( $title->getSquidURLs() );
}
/**
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
return;
}
+ // Remove duplicate URLs from list
+ $urlArr = array_unique( $urlArr );
+
wfDebugLog( 'squid', __METHOD__ . ': ' . implode( ' ', $urlArr ) );
if ( $wgHTCPRouting ) {
}
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
* @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
$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
$this->purgeThumbnails( $options );
// Purge squid cache for this file
- SquidUpdate::purge( array( $this->getURL() ) );
+ DeferredUpdates::addUpdate(
+ new SquidUpdate( array( $this->getUrl() ) ),
+ DeferredUpdates::PRESEND
+ );
}
/**
* @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 );
$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 );
}
/**
* @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'] ) ) {
$this->purgeThumbList( $dir, $files );
// Purge the squid
- if ( $wgUseSquid ) {
- SquidUpdate::purge( $urls );
- }
-
+ DeferredUpdates::addUpdate( new SquidUpdate( $urls ), DeferredUpdates::PRESEND );
}
/**
# 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' );
$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;
}
* @return FileRepoStatus
*/
function deleteOld( $archiveName, $reason, $suppress = false, $user = null ) {
- global $wgUseSquid;
if ( $this->getRepo()->getReadOnlyReason() !== false ) {
return $this->readOnlyFatalStatus();
}
$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;
}
$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' );
}
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' );
* - a) Recursive jobs to purge caches for backlink pages for a given title.
* These jobs have (recursive:true,table:<table>) set.
* - b) Jobs to purge caches for a set of titles (the job title is ignored).
- * These jobs have (pages:(<page ID>:(<namespace>,<title>),...) set.
+ * These jobs have (pages:(<page ID>:(<namespace>,<title>),...) set.
*
* @ingroup JobQueue
*/
$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;
* @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 );
) );
// Update squid
- if ( $wgUseSquid ) {
- $u = SquidUpdate::newFromTitles( $titleArray );
- $u->doUpdate();
- }
+ $u = SquidUpdate::newFromTitles( $titleArray );
+ $u->doUpdate();
// Update file cache
if ( $wgUseFileCache ) {
/**
* 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
*/
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...
// 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() );
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;
}
}
/**
- * 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__ );
}
/**
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?
*
$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() ) {
* @file
*/
+use \Cdb\Reader as CdbReader;
+
/**
* Functions to check passwords against a policy requirement
* @since 1.26
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;
+ }
+
}
$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();
}
--- /dev/null
+<?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;
+ }
+}
* @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' )
'target',
'emailusertarget',
30,
- $name
+ $name,
+ array(
+ 'class' => 'mw-autocomplete-user', // used by mediawiki.userSuggest
+ 'autofocus' => true,
+ )
) .
' ' .
Xml::submitButton( $this->msg( 'emailusernamesubmit' )->text() ) .
}
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>";
*/
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 ) )
);
$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'],
$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";
}
"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",
"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",
"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}}.",
"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}}",
--- /dev/null
+<?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;
'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(
--- /dev/null
+/*!
+ * 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();
+ } );
+
+} );
$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() {
--- /dev/null
+<?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 );
+ }
+}