Файловый менеджер - Редактировать - /var/www/html/jobs.zip
Ðазад
PK ! �U�S S NullJob.phpnu �Iw�� <?php /** * 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 */ use MediaWiki\MediaWikiServices; /** * No-op job that does nothing. * * This is used for testing purposes, e.g. to measure overall system * performance of the JobQueue system, lock contention, etc. * * This job can optionally recursively re-queue itself a number of times * or spend a fixed amount of time idling in execution time. * * @par Example: * Inserting a null job in the configured job queue: * @code * $ php maintenance/eval.php * > $queue = MediaWikiServices::getInstance()->getJobQueueGroup(); * > $job = new NullJob( [ 'lives' => 10 ] ); * > $queue->push( $job ); * @endcode * * You can confirm the job has been enqueued via maintenance/showJobs.php: * * @code * $ php maintenance/showJobs.php --group * null: 1 queue; 0 claimed (0 active, 0 abandoned) * @endcode * * @ingroup JobQueue */ class NullJob extends Job implements GenericParameterJob { /** * @param array $params Job parameters (lives, usleep) */ public function __construct( array $params ) { parent::__construct( 'null', $params ); if ( !isset( $this->params['lives'] ) ) { $this->params['lives'] = 1; } if ( !isset( $this->params['usleep'] ) ) { $this->params['usleep'] = 0; } $this->removeDuplicates = !empty( $this->params['removeDuplicates'] ); } public function run() { if ( $this->params['usleep'] > 0 ) { usleep( $this->params['usleep'] ); } if ( $this->params['lives'] > 1 ) { $params = $this->params; $params['lives']--; $job = new self( $params ); MediaWikiServices::getInstance()->getJobQueueGroup()->push( $job ); } return true; } } PK ! �s#� DeletePageJob.phpnu �Iw�� <?php use MediaWiki\MediaWikiServices; use MediaWiki\Page\DeletePage; use MediaWiki\Title\Title; /** * @newable * @since 1.32 * @ingroup JobQueue */ class DeletePageJob extends Job implements GenericParameterJob { public function __construct( array $params ) { parent::__construct( 'deletePage', $params ); $this->title = Title::makeTitle( $params['namespace'], $params['title'] ); } public function run() { $services = MediaWikiServices::getInstance(); // Failure to load the page is not job failure. // A parallel deletion operation may have already completed the page deletion. $wikiPage = $services->getWikiPageFactory()->newFromID( $this->params['wikiPageId'] ); if ( $wikiPage ) { $deletePage = $services->getDeletePageFactory()->newDeletePage( $wikiPage, $services->getUserFactory()->newFromId( $this->params['userId'] ) ); $deletePage ->setSuppress( $this->params['suppress'] ) ->setTags( json_decode( $this->params['tags'] ) ) ->setLogSubtype( $this->params['logsubtype'] ) ->setDeletionAttempted() ->deleteInternal( $wikiPage, // Use a fallback for BC with queued jobs. $this->params['pageRole'] ?? DeletePage::PAGE_BASE, $this->params['reason'], $this->getRequestId() ); } return true; } } PK ! mwǔ� � HTMLCacheUpdateJob.phpnu �Iw�� <?php /** * 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 */ use MediaWiki\MainConfigNames; use MediaWiki\MediaWikiServices; use MediaWiki\Page\PageReference; use MediaWiki\Title\Title; /** * Job to purge the HTML/file cache for all pages that link to or use another page or file * * This job comes in a few variants: * - 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. * * @ingroup JobQueue * @ingroup Cache */ class HTMLCacheUpdateJob extends Job { /** @var int Lag safety margin when comparing root job time age to CDN max-age */ private const NORMAL_MAX_LAG = 10; public function __construct( Title $title, array $params ) { parent::__construct( 'htmlCacheUpdate', $title, $params ); // Avoid the overhead of de-duplication when it would be pointless. // Note that these jobs always set page_touched to the current time, // so letting the older existing job "win" is still correct. $this->removeDuplicates = ( // Ranges rarely will line up !isset( $params['range'] ) && // Multiple pages per job make matches unlikely !( isset( $params['pages'] ) && count( $params['pages'] ) != 1 ) ); $this->params += [ 'causeAction' => 'HTMLCacheUpdateJob', 'causeAgent' => 'unknown' ]; } /** * @param PageReference $page Page to purge backlink pages from * @param string $table Backlink table name * @param array $params Additional job parameters * * @return HTMLCacheUpdateJob */ public static function newForBacklinks( PageReference $page, $table, $params = [] ) { $title = Title::newFromPageReference( $page ); return new self( $title, [ 'table' => $table, 'recursive' => true ] + Job::newRootJobParams( // "overall" refresh links job info "htmlCacheUpdate:{$table}:{$title->getPrefixedText()}" ) + $params ); } public function run() { $updateRowsPerJob = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::UpdateRowsPerJob ); $updateRowsPerQuery = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::UpdateRowsPerQuery ); if ( isset( $this->params['table'] ) && !isset( $this->params['pages'] ) ) { $this->params['recursive'] = true; // b/c; base job } // Job to purge all (or a range of) backlink pages for a page if ( !empty( $this->params['recursive'] ) ) { // Carry over information for de-duplication $extraParams = $this->getRootJobParams(); // Carry over cause information for logging $extraParams['causeAction'] = $this->params['causeAction']; $extraParams['causeAgent'] = $this->params['causeAgent']; // Convert this into no more than $wgUpdateRowsPerJob HTMLCacheUpdateJob per-title // jobs and possibly a recursive HTMLCacheUpdateJob job for the rest of the backlinks $jobs = BacklinkJobUtils::partitionBacklinkJob( $this, $updateRowsPerJob, $updateRowsPerQuery, // jobs-per-title // Carry over information for de-duplication [ 'params' => $extraParams ] ); MediaWikiServices::getInstance()->getJobQueueGroup()->push( $jobs ); // Job to purge pages for a set of titles } elseif ( isset( $this->params['pages'] ) ) { $this->invalidateTitles( $this->params['pages'] ); // Job to update a single title } else { $t = $this->title; $this->invalidateTitles( [ $t->getArticleID() => [ $t->getNamespace(), $t->getDBkey() ] ] ); } return true; } /** * @param array $pages Map of (page ID => (namespace, DB key)) entries */ protected function invalidateTitles( array $pages ) { // Get all page IDs in this query into an array $pageIds = array_keys( $pages ); if ( !$pageIds ) { return; } $rootTsUnix = wfTimestampOrNull( TS_UNIX, $this->params['rootJobTimestamp'] ?? null ); // Bump page_touched to the current timestamp. This previously used the root job timestamp // (e.g. template/file edit time), which is a bit more efficient when template edits are // rare and don't effect the same pages much. However, this way better de-duplicates jobs, // which is much more useful for wikis with high edit rates. Note that RefreshLinksJob, // enqueued alongside HTMLCacheUpdateJob, saves the parser output since it has to parse // anyway. We assume that vast majority of the cache jobs finish before the link jobs, // so using the current timestamp instead of the root timestamp is not expected to // invalidate these cache entries too often. $newTouchedUnix = time(); // Timestamp used to bypass pages already invalided since the triggering event $casTsUnix = $rootTsUnix ?? $newTouchedUnix; $services = MediaWikiServices::getInstance(); $config = $services->getMainConfig(); $dbProvider = $services->getConnectionProvider(); $dbw = $dbProvider->getPrimaryDatabase(); $ticket = $dbProvider->getEmptyTransactionTicket( __METHOD__ ); // Update page_touched (skipping pages already touched since the root job). // Check $wgUpdateRowsPerQuery; batch jobs are sized by that already. $batches = array_chunk( $pageIds, $config->get( MainConfigNames::UpdateRowsPerQuery ) ); foreach ( $batches as $batch ) { $dbw->newUpdateQueryBuilder() ->update( 'page' ) ->set( [ 'page_touched' => $dbw->timestamp( $newTouchedUnix ) ] ) ->where( [ 'page_id' => $batch ] ) ->andWhere( $dbw->expr( 'page_touched', '<', $dbw->timestamp( $casTsUnix ) ) ) ->caller( __METHOD__ )->execute(); if ( count( $batches ) > 1 ) { $dbProvider->commitAndWaitForReplication( __METHOD__, $ticket ); } } // Get the list of affected pages (races only mean something else did the purge) $queryBuilder = $dbw->newSelectQueryBuilder() ->select( [ 'page_namespace', 'page_title' ] ) ->from( 'page' ) ->where( [ 'page_id' => $pageIds, 'page_touched' => $dbw->timestamp( $newTouchedUnix ) ] ); if ( $config->get( MainConfigNames::PageLanguageUseDB ) ) { $queryBuilder->field( 'page_lang' ); } $titleArray = $services->getTitleFactory()->newTitleArrayFromResult( $queryBuilder->caller( __METHOD__ )->fetchResultSet() ); // Update CDN and file caches $htmlCache = $services->getHtmlCacheUpdater(); $htmlCache->purgeTitleUrls( $titleArray, $htmlCache::PURGE_NAIVE | $htmlCache::PURGE_URLS_LINKSUPDATE_ONLY, [ $htmlCache::UNLESS_CACHE_MTIME_AFTER => $casTsUnix + self::NORMAL_MAX_LAG ] ); } public function getDeduplicationInfo() { $info = parent::getDeduplicationInfo(); if ( is_array( $info['params'] ) ) { // For per-pages jobs, the job title is that of the template that changed // (or similar), so remove that since it ruins duplicate detection if ( isset( $info['params']['pages'] ) ) { unset( $info['namespace'] ); unset( $info['title'] ); } } return $info; } public function workItemCount() { if ( !empty( $this->params['recursive'] ) ) { return 0; // nothing actually purged } elseif ( isset( $this->params['pages'] ) ) { return count( $this->params['pages'] ); } return 1; // one title } } PK ! ��Bʴ � RevertedTagUpdateJob.phpnu �Iw�� <?php /** * 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 */ use MediaWiki\Config\ServiceOptions; use MediaWiki\Logger\LoggerFactory; use MediaWiki\MediaWikiServices; use MediaWiki\Storage\EditResult; use MediaWiki\Storage\RevertedTagUpdate; /** * Job for deferring the execution of RevertedTagUpdate. * * @internal For use by \MediaWiki\Storage\RevertedTagUpdate * @since 1.36 * @ingroup JobQueue * @author Ostrzyciel */ class RevertedTagUpdateJob extends Job implements GenericParameterJob { /** * Returns a JobSpecification for this job. * * @param int $revertRevisionId * @param EditResult $editResult * * @return JobSpecification */ public static function newSpec( int $revertRevisionId, EditResult $editResult ): JobSpecification { return new JobSpecification( 'revertedTagUpdate', [ 'revertId' => $revertRevisionId, 'editResult' => $editResult->jsonSerialize() ] ); } /** * @param array $params * @phan-param array{revertId:int,editResult:array} $params */ public function __construct( array $params ) { parent::__construct( 'revertedTagUpdate', $params ); } /** * Unpacks the job arguments and runs the update. * * @return bool */ public function run() { $services = MediaWikiServices::getInstance(); $editResult = EditResult::newFromArray( $this->params['editResult'] ); $update = new RevertedTagUpdate( $services->getRevisionStore(), LoggerFactory::getInstance( 'RevertedTagUpdate' ), $services->getChangeTagsStore(), $services->getConnectionProvider(), new ServiceOptions( RevertedTagUpdate::CONSTRUCTOR_OPTIONS, $services->getMainConfig() ), $this->params['revertId'], $editResult ); $update->doUpdate(); return true; } } PK ! ���! �! UploadJobTrait.phpnu �Iw�� <?php /** * 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 * @defgroup JobQueue JobQueue */ use MediaWiki\Api\ApiUpload; use MediaWiki\Context\RequestContext; use MediaWiki\Logger\LoggerFactory; use MediaWiki\Status\Status; use MediaWiki\User\User; use Wikimedia\ScopedCallback; /** * Common functionality for async uploads * * @ingroup Upload * @ingroup JobQueue */ trait UploadJobTrait { /** @var User|null */ private $user; /** @var string */ private $cacheKey; /** @var UploadBase */ private $upload; /** @var array The job parameters */ public $params; /** * Set up the job * * @param string $cacheKey * @return void */ protected function initialiseUploadJob( $cacheKey ): void { $this->cacheKey = $cacheKey; $this->user = null; } /** * Do not allow retries on jobs by default. * * @return bool */ public function allowRetries(): bool { return false; } /** * Run the job * * @return bool */ public function run(): bool { $this->user = $this->getUserFromSession(); if ( $this->user === null ) { return false; } try { // Check the initial status of the upload $startingStatus = UploadBase::getSessionStatus( $this->user, $this->cacheKey ); // Warn if in wrong stage, but still continue. User may be able to trigger // this by retrying after failure. if ( !$startingStatus || ( $startingStatus['result'] ?? '' ) !== 'Poll' || ( $startingStatus['stage'] ?? '' ) !== 'queued' ) { $logger = LoggerFactory::getInstance( 'upload' ); $logger->warning( "Tried to publish upload that is in stage {stage}/{result}", $this->logJobParams( $startingStatus ) ); } // Fetch the file if needed if ( !$this->fetchFile() ) { return false; } // Verify the upload is valid if ( !$this->verifyUpload() ) { return false; } // Actually upload the file if ( !$this->performUpload() ) { return false; } // All done $this->setStatusDone(); // Cleanup any temporary local file $this->getUpload()->cleanupTempFile(); } catch ( Exception $e ) { $this->setStatus( 'publish', 'Failure', Status::newFatal( 'api-error-publishfailed' ) ); $this->setLastError( get_class( $e ) . ": " . $e->getMessage() ); // To prevent potential database referential integrity issues. // See T34551. MWExceptionHandler::rollbackPrimaryChangesAndLog( $e ); return false; } return true; } /** * Get the cache key used to store status * * @return string */ public function getCacheKey() { return $this->cacheKey; } /** * Get user data from the session key * * @return User|null */ private function getUserFromSession() { $scope = RequestContext::importScopedSession( $this->params['session'] ); $this->addTeardownCallback( static function () use ( &$scope ) { ScopedCallback::consume( $scope ); // T126450 } ); $context = RequestContext::getMain(); $user = $context->getUser(); if ( !$user->isRegistered() ) { $this->setLastError( "Could not load the author user from session." ); return null; } return $user; } /** * Set the upload status * * @param string $stage * @param string $result * @param Status|null $status * @param array $additionalInfo * */ private function setStatus( $stage, $result = 'Poll', $status = null, $additionalInfo = [] ) { // We're most probably not running in a job. // @todo maybe throw an exception? if ( $this->user === null ) { return; } $status ??= Status::newGood(); $info = [ 'result' => $result, 'stage' => $stage, 'status' => $status ]; $info += $additionalInfo; UploadBase::setSessionStatus( $this->user, $this->cacheKey, $info ); } /** * Ensure we have the file available. A noop here. * * @return bool */ protected function fetchFile(): bool { $this->setStatus( 'fetching' ); // make sure the upload file is here. This is a noop in most cases. $status = $this->getUpload()->fetchFile(); if ( !$status->isGood() ) { $this->setStatus( 'fetching', 'Failure', $status ); $this->setLastError( "Error while fetching the image." ); return false; } $this->setStatus( 'publish' ); // We really don't care as this is, as mentioned, generally a noop. // When that's not the case, classes will need to override this method anyways. return true; } /** * Verify the upload is ok * * @return bool */ private function verifyUpload(): bool { // Check if the local file checks out (this is generally a no-op) $verification = $this->getUpload()->verifyUpload(); if ( $verification['status'] !== UploadBase::OK ) { $status = Status::newFatal( 'verification-error' ); $status->value = [ 'verification' => $verification ]; $this->setStatus( 'publish', 'Failure', $status ); $this->setLastError( "Could not verify upload." ); return false; } // Verify title permissions for this user $titleVerification = $this->getUpload()->verifyTitlePermissions( $this->user ); if ( $titleVerification !== true ) { $this->setStatus( 'publish', 'Failure', null, $titleVerification ); $this->setLastError( "Could not verify title permissions." ); return false; } // Verify if any upload warnings are present $ignoreWarnings = $this->params['ignorewarnings'] ?? false; $isReupload = $this->params['reupload'] ?? false; if ( $ignoreWarnings ) { // If we're ignoring warnings, we don't need to check them return true; } $warnings = $this->getUpload()->checkWarnings( $this->user ); if ( $warnings ) { // If the file exists and we're reuploading, ignore the warning // and continue with the upload if ( count( $warnings ) === 1 && isset( $warnings['exists'] ) && $isReupload ) { return true; } // Make the array serializable $serializableWarnings = UploadBase::makeWarningsSerializable( $warnings ); $this->setStatus( 'publish', 'Warning', null, [ 'warnings' => $serializableWarnings ] ); $this->setLastError( "Upload warnings present." ); return false; } return true; } /** * Upload the stashed file to a permanent location * * @return bool */ private function performUpload(): bool { if ( $this->user === null ) { return false; } $status = $this->getUpload()->performUpload( $this->params['comment'], $this->params['text'], $this->params['watch'], $this->user, $this->params['tags'] ?? [], $this->params['watchlistexpiry'] ?? null ); if ( !$status->isGood() ) { $this->setStatus( 'publish', 'Failure', $status ); $this->setLastError( $status->getWikiText( false, false, 'en' ) ); return false; } return true; } /** * Set the status at the end or processing * */ private function setStatusDone() { // Build the image info array while we have the local reference handy $imageInfo = ApiUpload::getDummyInstance()->getUploadImageInfo( $this->getUpload() ); // Cache the info so the user doesn't have to wait forever to get the final info $this->setStatus( 'publish', 'Success', Status::newGood(), [ 'filename' => $this->getUpload()->getLocalFile()->getName(), 'imageinfo' => $imageInfo ] ); } /** * Getter for the upload. Needs to be implemented by the job class * * @return UploadBase */ abstract protected function getUpload(): UploadBase; /** * Get the job parameters for logging. Needs to be implemented by the job class. * * @param Status[] $status * @return array */ abstract protected function logJobParams( $status ): array; /** * This is actually implemented in the Job class * * @param mixed $error * @return void */ abstract protected function setLastError( $error ); /** * This is actually implemented in the Job class * * @param callable $callback * @return void */ abstract protected function addTeardownCallback( $callback ); } PK ! �{J ParsoidCachePrewarmJob.phpnu �Iw�� <?php /** * 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 */ use MediaWiki\Logger\LoggerFactory; use MediaWiki\Page\PageLookup; use MediaWiki\Page\PageRecord; use MediaWiki\Page\ParserOutputAccess; use MediaWiki\Parser\ParserOptions; use MediaWiki\Parser\Parsoid\Config\SiteConfig as ParsoidSiteConfig; use MediaWiki\Revision\RevisionLookup; use MediaWiki\Revision\SlotRecord; use Psr\Log\LoggerInterface; /** * @ingroup JobQueue * @internal * @since 1.40 */ class ParsoidCachePrewarmJob extends Job { private LoggerInterface $logger; private ParserOutputAccess $parserOutputAccess; private PageLookup $pageLookup; private RevisionLookup $revisionLookup; private ParsoidSiteConfig $parsoidSiteConfig; /** * @param array $params * @param ParserOutputAccess $parserOutputAccess * @param PageLookup $pageLookup * @param RevisionLookup $revisionLookup * @param ParsoidSiteConfig $parsoidSiteConfig */ public function __construct( array $params, ParserOutputAccess $parserOutputAccess, PageLookup $pageLookup, RevisionLookup $revisionLookup, ParsoidSiteConfig $parsoidSiteConfig ) { parent::__construct( 'parsoidCachePrewarm', $params ); // TODO: find a way to inject the logger $this->logger = LoggerFactory::getInstance( 'ParsoidCachePrewarmJob' ); $this->parserOutputAccess = $parserOutputAccess; $this->pageLookup = $pageLookup; $this->revisionLookup = $revisionLookup; $this->parsoidSiteConfig = $parsoidSiteConfig; } /** * @param int $revisionId * @param PageRecord $page * @param array $params Additional options for the job. Known keys: * - causeAction: Indicate what action caused the job to be scheduled. Used for monitoring. * - options: Flags to be passed to ParserOutputAccess:getParserOutput. * May be set to ParserOutputAccess::OPT_FORCE_PARSE to force a parsing even if there * already is cached output. * * @return JobSpecification */ public static function newSpec( int $revisionId, PageRecord $page, array $params = [] ): JobSpecification { $pageId = $page->getId(); $pageTouched = $page->getTouched(); $params += [ 'options' => 0 ]; $params += self::newRootJobParams( "parsoidCachePrewarm:$pageId:$revisionId:$pageTouched:{$params['options']}" ); $opts = [ 'removeDuplicates' => true ]; return new JobSpecification( 'parsoidCachePrewarm', [ 'revId' => $revisionId, 'pageId' => $pageId, 'page_touched' => $pageTouched, ] + $params, $opts ); } private function doParsoidCacheUpdate() { $page = $this->pageLookup->getPageById( $this->params['pageId'] ); $revId = $this->params['revId']; if ( $page === null ) { // This happens when the page got deleted in the meantime. $this->logger->info( "Page with ID {$this->params['pageId']} not found" ); return; } if ( $page->getLatest() !== $revId ) { $this->logger->info( 'ParsoidCachePrewarmJob: The ID of the new revision does not match the page\'s current revision ID' ); return; } $rev = $this->revisionLookup->getRevisionById( $revId ); if ( !$rev ) { return; } $parserOpts = ParserOptions::newFromAnon(); $parserOpts->setUseParsoid(); $renderReason = $this->params['causeAction'] ?? $this->command; $parserOpts->setRenderReason( $renderReason ); $mainSlot = $rev->getSlot( SlotRecord::MAIN ); if ( !$this->parsoidSiteConfig->supportsContentModel( $mainSlot->getModel() ) ) { $this->logger->debug( __METHOD__ . ': Parsoid does not support content model ' . $mainSlot->getModel() ); return; } $this->logger->debug( __METHOD__ . ': generating Parsoid output' ); // We may get the OPT_FORCE_PARSE flag this way $options = $this->params['options'] ?? 0; // getParserOutput() will write to ParserCache. $status = $this->parserOutputAccess->getParserOutput( $page, $parserOpts, $rev, $options ); if ( !$status->isOK() ) { $this->logger->error( __METHOD__ . ': Parsoid error', [ 'errors' => $status->getErrors(), 'page' => $page->getDBkey(), 'rev' => $rev->getId(), ] ); } } public function run() { $this->doParsoidCacheUpdate(); return true; } } PK ! 3�\�-* -* CategoryMembershipChangeJob.phpnu �Iw�� <?php /** * 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 */ use MediaWiki\MainConfigNames; use MediaWiki\MediaWikiServices; use MediaWiki\Page\PageIdentity; use MediaWiki\Revision\RevisionRecord; use MediaWiki\Revision\RevisionStoreRecord; use MediaWiki\Title\Title; use Wikimedia\Rdbms\IDBAccessObject; use Wikimedia\Rdbms\LBFactory; use Wikimedia\Rdbms\RawSQLExpression; use Wikimedia\Rdbms\SelectQueryBuilder; /** * Job to add recent change entries mentioning category membership changes * * This allows users to easily scan categories for recent page membership changes * * Parameters include: * - pageId : page ID * - revTimestamp : timestamp of the triggering revision * * Category changes will be mentioned for revisions at/after the timestamp for this page * * @since 1.27 * @ingroup JobQueue */ class CategoryMembershipChangeJob extends Job { /** @var int|null */ private $ticket; private const ENQUEUE_FUDGE_SEC = 60; /** * @param PageIdentity $page the page for which to update category membership. * @param string $revisionTimestamp The timestamp of the new revision that triggered the job. * @param bool $forImport Whether the new revision that triggered the import was imported * @return JobSpecification */ public static function newSpec( PageIdentity $page, $revisionTimestamp, bool $forImport ) { return new JobSpecification( 'categoryMembershipChange', [ 'pageId' => $page->getId(), 'revTimestamp' => $revisionTimestamp, 'forImport' => $forImport, ], [ 'removeDuplicates' => true, 'removeDuplicatesIgnoreParams' => [ 'revTimestamp' ] ], $page ); } /** * Constructor for use by the Job Queue infrastructure. * @note Don't call this when queueing a new instance, use newSpec() instead. * @param PageIdentity $page the categorized page. * @param array $params Such latest revision instance of the categorized page. */ public function __construct( PageIdentity $page, array $params ) { parent::__construct( 'categoryMembershipChange', $page, $params ); // Only need one job per page. Note that ENQUEUE_FUDGE_SEC handles races where an // older revision job gets inserted while the newer revision job is de-duplicated. $this->removeDuplicates = true; } public function run() { $services = MediaWikiServices::getInstance(); $lbFactory = $services->getDBLoadBalancerFactory(); $lb = $lbFactory->getMainLB(); $dbw = $lb->getConnection( DB_PRIMARY ); $this->ticket = $lbFactory->getEmptyTransactionTicket( __METHOD__ ); $page = $services->getWikiPageFactory()->newFromID( $this->params['pageId'], IDBAccessObject::READ_LATEST ); if ( !$page ) { $this->setLastError( "Could not find page #{$this->params['pageId']}" ); return false; // deleted? } // Cut down on the time spent in waitForPrimaryPos() in the critical section $dbr = $lb->getConnection( DB_REPLICA ); if ( !$lb->waitForPrimaryPos( $dbr ) ) { $this->setLastError( "Timed out while pre-waiting for replica DB to catch up" ); return false; } // Use a named lock so that jobs for this page see each others' changes $lockKey = "{$dbw->getDomainID()}:CategoryMembershipChange:{$page->getId()}"; // per-wiki $scopedLock = $dbw->getScopedLockAndFlush( $lockKey, __METHOD__, 3 ); if ( !$scopedLock ) { $this->setLastError( "Could not acquire lock '$lockKey'" ); return false; } // Wait till replica DB is caught up so that jobs for this page see each others' changes if ( !$lb->waitForPrimaryPos( $dbr ) ) { $this->setLastError( "Timed out while waiting for replica DB to catch up" ); return false; } // Clear any stale REPEATABLE-READ snapshot $dbr->flushSnapshot( __METHOD__ ); $cutoffUnix = wfTimestamp( TS_UNIX, $this->params['revTimestamp'] ); // Using ENQUEUE_FUDGE_SEC handles jobs inserted out of revision order due to the delay // between COMMIT and actual enqueueing of the CategoryMembershipChangeJob job. $cutoffUnix -= self::ENQUEUE_FUDGE_SEC; // Get the newest page revision that has a SRC_CATEGORIZE row. // Assume that category changes before it were already handled. $subQuery = $dbr->newSelectQueryBuilder() ->select( '1' ) ->from( 'recentchanges' ) ->where( 'rc_this_oldid = rev_id' ) ->andWhere( [ 'rc_source' => RecentChange::SRC_CATEGORIZE ] ); $row = $dbr->newSelectQueryBuilder() ->select( [ 'rev_timestamp', 'rev_id' ] ) ->from( 'revision' ) ->where( [ 'rev_page' => $page->getId() ] ) ->andWhere( $dbr->expr( 'rev_timestamp', '>=', $dbr->timestamp( $cutoffUnix ) ) ) ->andWhere( new RawSQLExpression( 'EXISTS (' . $subQuery->getSQL() . ')' ) ) ->orderBy( [ 'rev_timestamp', 'rev_id' ], SelectQueryBuilder::SORT_DESC ) ->caller( __METHOD__ )->fetchRow(); // Only consider revisions newer than any such revision if ( $row ) { $cutoffUnix = wfTimestamp( TS_UNIX, $row->rev_timestamp ); $lastRevId = (int)$row->rev_id; } else { $lastRevId = 0; } // Find revisions to this page made around and after this revision which lack category // notifications in recent changes. This lets jobs pick up were the last one left off. $revisionStore = $services->getRevisionStore(); $res = $revisionStore->newSelectQueryBuilder( $dbr ) ->joinComment() ->where( [ 'rev_page' => $page->getId(), $dbr->buildComparison( '>', [ 'rev_timestamp' => $dbr->timestamp( $cutoffUnix ), 'rev_id' => $lastRevId, ] ) ] ) ->orderBy( [ 'rev_timestamp', 'rev_id' ], SelectQueryBuilder::SORT_ASC ) ->caller( __METHOD__ )->fetchResultSet(); // Apply all category updates in revision timestamp order foreach ( $res as $row ) { $this->notifyUpdatesForRevision( $lbFactory, $page, $revisionStore->newRevisionFromRow( $row ) ); } return true; } /** * @param LBFactory $lbFactory * @param WikiPage $page * @param RevisionRecord $newRev */ protected function notifyUpdatesForRevision( LBFactory $lbFactory, WikiPage $page, RevisionRecord $newRev ) { $title = $page->getTitle(); // Get the new revision if ( $newRev->isDeleted( RevisionRecord::DELETED_TEXT ) ) { return; } $services = MediaWikiServices::getInstance(); // Get the prior revision (the same for null edits) if ( $newRev->getParentId() ) { $oldRev = $services->getRevisionLookup() ->getRevisionById( $newRev->getParentId(), IDBAccessObject::READ_LATEST ); if ( !$oldRev || $oldRev->isDeleted( RevisionRecord::DELETED_TEXT ) ) { return; } } else { $oldRev = null; } // Parse the new revision and get the categories $categoryChanges = $this->getExplicitCategoriesChanges( $page, $newRev, $oldRev ); [ $categoryInserts, $categoryDeletes ] = $categoryChanges; if ( !$categoryInserts && !$categoryDeletes ) { return; // nothing to do } $blc = $services->getBacklinkCacheFactory()->getBacklinkCache( $title ); $catMembChange = new CategoryMembershipChange( $title, $blc, $newRev, $this->params['forImport'] ?? false ); $catMembChange->checkTemplateLinks(); $batchSize = $services->getMainConfig()->get( MainConfigNames::UpdateRowsPerQuery ); $insertCount = 0; foreach ( $categoryInserts as $categoryName ) { $categoryTitle = Title::makeTitle( NS_CATEGORY, $categoryName ); $catMembChange->triggerCategoryAddedNotification( $categoryTitle ); if ( $insertCount++ && ( $insertCount % $batchSize ) == 0 ) { $lbFactory->commitAndWaitForReplication( __METHOD__, $this->ticket ); } } foreach ( $categoryDeletes as $categoryName ) { $categoryTitle = Title::makeTitle( NS_CATEGORY, $categoryName ); $catMembChange->triggerCategoryRemovedNotification( $categoryTitle ); if ( $insertCount++ && ( $insertCount++ % $batchSize ) == 0 ) { $lbFactory->commitAndWaitForReplication( __METHOD__, $this->ticket ); } } } private function getExplicitCategoriesChanges( WikiPage $page, RevisionRecord $newRev, ?RevisionRecord $oldRev = null ) { // Inject the same timestamp for both revision parses to avoid seeing category changes // due to time-based parser functions. Inject the same page title for the parses too. // Note that REPEATABLE-READ makes template/file pages appear unchanged between parses. $parseTimestamp = $newRev->getTimestamp(); // Parse the old rev and get the categories. Do not use link tables as that // assumes these updates are perfectly FIFO and that link tables are always // up to date, neither of which are true. $oldCategories = $oldRev ? $this->getCategoriesAtRev( $page, $oldRev, $parseTimestamp ) : []; // Parse the new revision and get the categories $newCategories = $this->getCategoriesAtRev( $page, $newRev, $parseTimestamp ); $categoryInserts = array_values( array_diff( $newCategories, $oldCategories ) ); $categoryDeletes = array_values( array_diff( $oldCategories, $newCategories ) ); return [ $categoryInserts, $categoryDeletes ]; } /** * @param WikiPage $page * @param RevisionRecord $rev * @param string $parseTimestamp TS_MW * * @return string[] category names */ private function getCategoriesAtRev( WikiPage $page, RevisionRecord $rev, $parseTimestamp ) { $services = MediaWikiServices::getInstance(); $options = $page->makeParserOptions( 'canonical' ); $options->setTimestamp( $parseTimestamp ); $options->setRenderReason( 'CategoryMembershipChangeJob' ); $output = $rev instanceof RevisionStoreRecord && $rev->isCurrent() ? $services->getParserCache()->get( $page, $options ) : null; if ( !$output || $output->getCacheRevisionId() !== $rev->getId() ) { $output = $services->getRevisionRenderer()->getRenderedRevision( $rev, $options ) ->getRevisionParserOutput(); } // array keys will cast numeric category names to ints; // ::getCategoryNames() is careful to cast them back to strings // to avoid breaking things! return $output->getCategoryNames(); } public function getDeduplicationInfo() { $info = parent::getDeduplicationInfo(); unset( $info['params']['revTimestamp'] ); // first job wins return $info; } } PK ! ��>- CdnPurgeJob.phpnu �Iw�� <?php /** * 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 */ use MediaWiki\Deferred\CdnCacheUpdate; /** * Job to purge a set of URLs from CDN * * @since 1.27 * @ingroup JobQueue */ class CdnPurgeJob extends Job implements GenericParameterJob { public function __construct( array $params ) { parent::__construct( 'cdnPurge', $params ); $this->removeDuplicates = false; // delay semantics are critical } public function run() { // Use purge() directly to avoid infinite recursion CdnCacheUpdate::purge( $this->params['urls'] ); return true; } } PK ! ���� � ThumbnailRenderJob.phpnu �Iw�� <?php /** * 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 */ use MediaWiki\MainConfigNames; use MediaWiki\MediaWikiServices; use MediaWiki\Status\Status; use MediaWiki\Title\Title; use Wikimedia\Rdbms\IDBAccessObject; /** * Job for asynchronous rendering of thumbnails, e.g. after new uploads. * * @ingroup JobQueue */ class ThumbnailRenderJob extends Job { public function __construct( Title $title, array $params ) { parent::__construct( 'ThumbnailRender', $title, $params ); } public function run() { $uploadThumbnailRenderMethod = MediaWikiServices::getInstance() ->getMainConfig()->get( MainConfigNames::UploadThumbnailRenderMethod ); $transformParams = $this->params['transformParams']; $file = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo() ->newFile( $this->title ); $file->load( IDBAccessObject::READ_LATEST ); if ( $file && $file->exists() ) { if ( $uploadThumbnailRenderMethod === 'jobqueue' ) { $thumb = $file->transform( $transformParams, File::RENDER_NOW ); if ( !$thumb || $thumb->isError() ) { if ( $thumb instanceof MediaTransformError ) { $this->setLastError( __METHOD__ . ': thumbnail couldn\'t be generated:' . $thumb->toText() ); } else { $this->setLastError( __METHOD__ . ': thumbnail couldn\'t be generated' ); } return false; } $this->maybeEnqueueNextPage( $transformParams ); return true; } elseif ( $uploadThumbnailRenderMethod === 'http' ) { $res = $this->hitThumbUrl( $file, $transformParams ); $this->maybeEnqueueNextPage( $transformParams ); return $res; } else { $this->setLastError( __METHOD__ . ': unknown thumbnail render method ' . $uploadThumbnailRenderMethod ); return false; } } else { $this->setLastError( __METHOD__ . ': file doesn\'t exist' ); return false; } } /** * @param LocalFile $file * @param array $transformParams * @return bool Success status (error will be set via setLastError() when false) */ protected function hitThumbUrl( LocalFile $file, $transformParams ) { $config = MediaWikiServices::getInstance()->getMainConfig(); $uploadThumbnailRenderHttpCustomHost = $config->get( MainConfigNames::UploadThumbnailRenderHttpCustomHost ); $uploadThumbnailRenderHttpCustomDomain = $config->get( MainConfigNames::UploadThumbnailRenderHttpCustomDomain ); $handler = $file->getHandler(); if ( !$handler ) { $this->setLastError( __METHOD__ . ': could not get handler' ); return false; } elseif ( !$handler->normaliseParams( $file, $transformParams ) ) { $this->setLastError( __METHOD__ . ': failed to normalize' ); return false; } $thumbName = $file->thumbName( $transformParams ); $thumbUrl = $file->getThumbUrl( $thumbName ); if ( $thumbUrl === null ) { $this->setLastError( __METHOD__ . ': could not get thumb URL' ); return false; } if ( $uploadThumbnailRenderHttpCustomDomain ) { $parsedUrl = wfGetUrlUtils()->parse( $thumbUrl ); if ( !isset( $parsedUrl['path'] ) || $parsedUrl['path'] === '' ) { $this->setLastError( __METHOD__ . ": invalid thumb URL: $thumbUrl" ); return false; } $thumbUrl = '//' . $uploadThumbnailRenderHttpCustomDomain . $parsedUrl['path']; } wfDebug( __METHOD__ . ": hitting url {$thumbUrl}" ); // T203135 We don't wait for the request to complete, as this is mostly fire & forget. // Looking at the HTTP status of requests that take less than 1s is a double check. $request = MediaWikiServices::getInstance()->getHttpRequestFactory()->create( $thumbUrl, [ 'method' => 'HEAD', 'followRedirects' => true, 'timeout' => 1 ], __METHOD__ ); if ( $uploadThumbnailRenderHttpCustomHost ) { $request->setHeader( 'Host', $uploadThumbnailRenderHttpCustomHost ); } $status = $request->execute(); $statusCode = $request->getStatus(); wfDebug( __METHOD__ . ": received status {$statusCode}" ); // 400 happens when requesting a size greater or equal than the original // TODO use proper error signaling. 400 could mean a number of other things. if ( $statusCode === 200 || $statusCode === 301 || $statusCode === 302 || $statusCode === 400 ) { return true; } elseif ( $statusCode ) { $this->setLastError( __METHOD__ . ": incorrect HTTP status $statusCode when hitting $thumbUrl" ); } elseif ( $status->hasMessage( 'http-timed-out' ) ) { // T203135 we ignore timeouts, as it would be inefficient for this job to wait for // minutes for the slower thumbnails to complete. return true; } else { $this->setLastError( __METHOD__ . ': HTTP request failure: ' . Status::wrap( $status )->getWikiText( false, false, 'en' ) ); } return false; } private function maybeEnqueueNextPage( $transformParams ) { if ( ( $this->params['enqueueNextPage'] ?? false ) && ( $transformParams['page'] ?? 0 ) < ( $this->params['pageLimit'] ?? 0 ) ) { $transformParams['page'] += 1; $job = new ThumbnailRenderJob( $this->getTitle(), [ 'transformParams' => $transformParams, 'enqueueNextPage' => true, 'pageLimit' => $this->params['pageLimit'] ] ); MediaWikiServices::getInstance()->getJobQueueGroup()->lazyPush( [ $job ] ); } } /** * Whether to retry the job. * @return bool */ public function allowRetries() { // ThumbnailRenderJob is a warmup for the thumbnails cache, // so loosing it is not a problem. Most times the job fails // for non-renderable or missing images which will not be fixed // by a retry, but will create additional load on the renderer. return false; } } PK ! m��% % DoubleRedirectJob.phpnu �Iw�� <?php /** * 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 */ use MediaWiki\Cache\CacheKeyHelper; use MediaWiki\Linker\LinkTarget; use MediaWiki\MediaWikiServices; use MediaWiki\Page\PageReference; use MediaWiki\Page\WikiPageFactory; use MediaWiki\Parser\MagicWordFactory; use MediaWiki\Revision\RevisionLookup; use MediaWiki\Revision\SlotRecord; use MediaWiki\Title\Title; use MediaWiki\User\User; use Wikimedia\Rdbms\IDBAccessObject; /** * Fix any double redirects after moving a page. * * @ingroup JobQueue */ class DoubleRedirectJob extends Job { /** * @var int Max number of double redirect jobs counter. * This is meant to avoid excessive memory usage. This is * also used in fixDoubleRedirects.php script. */ public const MAX_DR_JOBS_COUNTER = 10000; /** @var Title The title which has changed, redirects pointing to this * title are fixed */ private $redirTitle; /** @var User */ private static $user; /** @var RevisionLookup */ private $revisionLookup; /** @var MagicWordFactory */ private $magicWordFactory; /** @var WikiPageFactory */ private $wikiPageFactory; /** * @param PageReference $page * @param array $params Expected to contain these elements: * - 'redirTitle' => string The title that changed and should be fixed. * - 'reason' => string Reason for the change, can be "move" or "maintenance". Used as a suffix * for the message keys "double-redirect-fixed-move" and * "double-redirect-fixed-maintenance". * ] * @param RevisionLookup $revisionLookup * @param MagicWordFactory $magicWordFactory * @param WikiPageFactory $wikiPageFactory */ public function __construct( PageReference $page, array $params, RevisionLookup $revisionLookup, MagicWordFactory $magicWordFactory, WikiPageFactory $wikiPageFactory ) { parent::__construct( 'fixDoubleRedirect', $page, $params ); $this->redirTitle = Title::newFromText( $params['redirTitle'] ); $this->revisionLookup = $revisionLookup; $this->magicWordFactory = $magicWordFactory; $this->wikiPageFactory = $wikiPageFactory; } /** * Insert jobs into the job queue to fix redirects to the given title * @param string $reason The reason for the fix, see message * "double-redirect-fixed-<reason>" * @param LinkTarget $redirTitle The title which has changed, redirects * pointing to this title are fixed */ public static function fixRedirects( $reason, $redirTitle ) { # Need to use the primary DB to get the redirect table updated in the same transaction $services = MediaWikiServices::getInstance(); $dbw = $services->getConnectionProvider()->getPrimaryDatabase(); $res = $dbw->newSelectQueryBuilder() ->select( [ 'page_namespace', 'page_title' ] ) ->from( 'redirect' ) ->join( 'page', null, 'page_id = rd_from' ) ->where( [ 'rd_namespace' => $redirTitle->getNamespace(), 'rd_title' => $redirTitle->getDBkey() ] ) ->andWhere( [ 'rd_interwiki' => '' ] ) ->caller( __METHOD__ )->fetchResultSet(); if ( !$res->numRows() ) { return; } $jobs = []; $jobQueueGroup = $services->getJobQueueGroup(); foreach ( $res as $row ) { $title = Title::makeTitleSafe( $row->page_namespace, $row->page_title ); if ( !$title || !$title->canExist() ) { continue; } $jobs[] = new self( $title, [ 'reason' => $reason, 'redirTitle' => $services->getTitleFormatter() ->getPrefixedDBkey( $redirTitle ) ], $services->getRevisionLookup(), $services->getMagicWordFactory(), $services->getWikiPageFactory() ); # Avoid excessive memory usage if ( count( $jobs ) > self::MAX_DR_JOBS_COUNTER ) { $jobQueueGroup->push( $jobs ); $jobs = []; } } $jobQueueGroup->push( $jobs ); } /** * @return bool */ public function run() { if ( !$this->redirTitle ) { $this->setLastError( 'Invalid title' ); return false; } if ( !$this->title->canExist() ) { // Needs a proper title for WikiPageFactory::newFromTitle and RevisionStore::getRevisionByTitle $this->setLastError( 'Cannot edit title' ); return false; } $targetRev = $this->revisionLookup ->getRevisionByTitle( $this->title, 0, IDBAccessObject::READ_LATEST ); if ( !$targetRev ) { wfDebug( __METHOD__ . ": target redirect already deleted, ignoring" ); return true; } $content = $targetRev->getContent( SlotRecord::MAIN ); $currentDest = $content ? $content->getRedirectTarget() : null; if ( !$currentDest || !$currentDest->equals( $this->redirTitle ) ) { wfDebug( __METHOD__ . ": Redirect has changed since the job was queued" ); return true; } // Check for a suppression tag (used e.g. in periodically archived discussions) $mw = $this->magicWordFactory->get( 'staticredirect' ); if ( $content->matchMagicWord( $mw ) ) { wfDebug( __METHOD__ . ": skipping: suppressed with __STATICREDIRECT__" ); return true; } // Find the current final destination $newTitle = self::getFinalDestination( $this->redirTitle ); if ( !$newTitle ) { wfDebug( __METHOD__ . ": skipping: single redirect, circular redirect or invalid redirect destination" ); return true; } if ( $newTitle->equals( $this->redirTitle ) ) { // The redirect is already right, no need to change it // This can happen if the page was moved back (say after vandalism) wfDebug( __METHOD__ . " : skipping, already good" ); } // Preserve fragment (T16904) $newTitle = Title::makeTitle( $newTitle->getNamespace(), $newTitle->getDBkey(), $currentDest->getFragment(), $newTitle->getInterwiki() ); // Fix the text $newContent = $content->updateRedirect( $newTitle ); if ( $newContent->equals( $content ) ) { $this->setLastError( 'Content unchanged???' ); return false; } $user = $this->getUser(); if ( !$user ) { $this->setLastError( 'Invalid user' ); return false; } // Save it // phpcs:ignore MediaWiki.Usage.DeprecatedGlobalVariables.Deprecated$wgUser global $wgUser; $oldUser = $wgUser; $wgUser = $user; $article = $this->wikiPageFactory->newFromTitle( $this->title ); // Messages: double-redirect-fixed-move, double-redirect-fixed-maintenance $reason = wfMessage( 'double-redirect-fixed-' . $this->params['reason'], $this->redirTitle->getPrefixedText(), $newTitle->getPrefixedText() )->inContentLanguage()->text(); // Avoid RC flood, and use minor to avoid email notifs $flags = EDIT_UPDATE | EDIT_SUPPRESS_RC | EDIT_INTERNAL | EDIT_MINOR; $article->doUserEditContent( $newContent, $user, $reason, $flags ); $wgUser = $oldUser; return true; } /** * Get the final destination of a redirect * * @param LinkTarget $title * * @return Title|false The final Title after following all redirects, or false if * the page is not a redirect or the redirect loops. */ public static function getFinalDestination( $title ) { $dbw = MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase(); // Circular redirect check $seenTitles = []; $dest = false; while ( true ) { $titleText = CacheKeyHelper::getKeyForPage( $title ); if ( isset( $seenTitles[$titleText] ) ) { wfDebug( __METHOD__, "Circular redirect detected, aborting" ); return false; } $seenTitles[$titleText] = true; if ( $title->isExternal() ) { // If the target is interwiki, we have to break early (T42352). // Otherwise it will look up a row in the local page table // with the namespace/page of the interwiki target which can cause // unexpected results (e.g. X -> foo:Bar -> Bar -> .. ) break; } $row = $dbw->newSelectQueryBuilder() ->select( [ 'rd_namespace', 'rd_title', 'rd_interwiki' ] ) ->from( 'redirect' ) ->join( 'page', null, 'page_id = rd_from' ) ->where( [ 'page_namespace' => $title->getNamespace() ] ) ->andWhere( [ 'page_title' => $title->getDBkey() ] ) ->caller( __METHOD__ )->fetchRow(); if ( !$row ) { # No redirect from here, chain terminates break; } else { $dest = $title = Title::makeTitle( $row->rd_namespace, $row->rd_title, '', $row->rd_interwiki ); } } return $dest; } /** * Get a user object for doing edits, from a request-lifetime cache * False will be returned if the user name specified in the * 'double-redirect-fixer' message is invalid. * * @return User|false */ private function getUser() { if ( !self::$user ) { $username = wfMessage( 'double-redirect-fixer' )->inContentLanguage()->text(); self::$user = User::newFromName( $username ); # User::newFromName() can return false on a badly configured wiki. if ( self::$user && !self::$user->isRegistered() ) { self::$user->addToDatabase(); } } return self::$user; } } PK ! �5� UserOptionsUpdateJob.phpnu �Iw�� <?php /** * 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 3 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 */ use MediaWiki\MediaWikiServices; use MediaWiki\User\User; use Wikimedia\Rdbms\IDBAccessObject; /** * Job that updates a user's preferences. * * The following job parameters are required: * - userId: the user ID * - options: a map of (option => value) * * @since 1.33 * @ingroup JobQueue */ class UserOptionsUpdateJob extends Job implements GenericParameterJob { public function __construct( array $params ) { parent::__construct( 'userOptionsUpdate', $params ); $this->removeDuplicates = true; } public function run() { if ( !$this->params['options'] ) { return true; // nothing to do } $user = User::newFromId( $this->params['userId'] ); $user->load( IDBAccessObject::READ_EXCLUSIVE ); if ( !$user->isNamed() ) { return true; } $userOptionsManager = MediaWikiServices::getInstance() ->getUserOptionsManager(); foreach ( $this->params['options'] as $name => $value ) { $userOptionsManager->setOption( $user, $name, $value ); } $user->saveSettings(); return true; } } PK ! 'n5a 5a RefreshLinksJob.phpnu �Iw�� <?php /** * 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 */ use MediaWiki\Deferred\LinksUpdate\LinksUpdate; use MediaWiki\Deferred\RefreshSecondaryDataUpdate; use MediaWiki\Logger\LoggerFactory; use MediaWiki\MainConfigNames; use MediaWiki\MediaWikiServices; use MediaWiki\Page\PageAssertionException; use MediaWiki\Page\PageIdentity; use MediaWiki\Parser\ParserCache; use MediaWiki\Parser\ParserOutput; use MediaWiki\Revision\RevisionRecord; use MediaWiki\Revision\RevisionRenderer; use MediaWiki\Revision\SlotRecord; use MediaWiki\Title\Title; use MediaWiki\User\User; use MediaWiki\WikiMap\WikiMap; use Wikimedia\Rdbms\IDBAccessObject; use Wikimedia\Stats\StatsFactory; /** * Job to update link tables for rerendered wiki pages. * * This job comes in a few variants: * * - a) Recursive jobs to update links for backlink pages for a given title. * Scheduled by {@see LinksUpdate::queueRecursiveJobsForTable()}; used to * refresh pages which link/transclude a given title. * These jobs have (recursive:true,table:<table>) set. They just look up * which pages link to the job title and schedule them as a set of non-recursive * RefreshLinksJob jobs (and possible one new recursive job as a way of * continuation). * - b) Jobs to update links for a set of pages (the job title is ignored). * These jobs have (pages:(<page ID>:(<namespace>,<title>),...) set. * - c) Jobs to update links for a single page (the job title). * These jobs need no extra fields set. * * Job parameters for all jobs: * - recursive (bool): When false, updates the current page. When true, updates * the pages which link/transclude the current page. * - triggeringRevisionId (int): The revision of the edit which caused the link * refresh. For manually triggered updates, the last revision of the page (at the * time of scheduling). * - triggeringUser (array): The user who triggered the refresh, in the form of a * [ 'userId' => int, 'userName' => string ] array. This is not necessarily the user * who created the revision. * - triggeredRecursive (bool): Set on all jobs which were partitioned from another, * recursive job. For debugging. * - Standard deduplication params (see {@see JobQueue::deduplicateRootJob()}). * For recursive jobs: * - table (string): Which table to use (imagelinks or templatelinks) when searching for * affected pages. * - range (array): Used for recursive jobs when some pages have already been partitioned * into separate jobs. Contains the list of ranges that still need to be partitioned. * See {@see BacklinkJobUtils::partitionBacklinkJob()}. * - division: Number of times the job was partitioned already (for debugging). * For non-recursive jobs: * - pages (array): Associative array of [ <page ID> => [ <namespace>, <dbkey> ] ]. * Might be omitted, then the job title will be used. * - isOpportunistic (bool): Set for opportunistic single-page updates. These are "free" * updates that are queued when most of the work needed to be performed anyway for * non-linkrefresh-related reasons, and can be more easily discarded if they don't seem * useful. See {@see WikiPage::triggerOpportunisticLinksUpdate()}. * - useRecursiveLinksUpdate (bool): When true, triggers recursive jobs for each page. * * Metrics: * - `refreshlinks_superseded_updates_total`: The number of times the job was cancelled * because the target page had already been refreshed by a different edit or job. * The job is considered to have succeeded in this case. * * - `refreshlinks_warnings_total`: The number of times the job failed due to a recoverable issue. * Possible `reason` label values include: * - `lag_wait_failed`: The job timed out while waiting for replication. * * - `refreshlinks_failures_total`: The number of times the job failed. * The `reason` label may be: * - `page_not_found`: The target page did not exist. * - `rev_not_current`: The target revision was no longer the latest revision for the target page. * - `rev_not_found`: The target revision was not found. * - `lock_failure`: The job failed to acquire an exclusive lock to refresh the target page. * * - `refreshlinks_parsercache_operations_total`: The number of times the job attempted * to fetch parser output from the parser cache. * Possible `status` label values include: * - `cache_hit`: The parser output was found in the cache. * - `cache_miss`: The parser output was not found in the cache. * * @ingroup JobQueue * @see RefreshSecondaryDataUpdate * @see WikiPage::doSecondaryDataUpdates() */ class RefreshLinksJob extends Job { /** @var int Lag safety margin when comparing root job times to last-refresh times */ private const NORMAL_MAX_LAG = 10; /** @var int How many seconds to wait for replica DBs to catch up */ private const LAG_WAIT_TIMEOUT = 15; public function __construct( PageIdentity $page, array $params ) { if ( empty( $params['pages'] ) && !$page->canExist() ) { // BC with the Title class throw new PageAssertionException( 'The given PageIdentity {pageIdentity} does not represent a proper page', [ 'pageIdentity' => $page ] ); } parent::__construct( 'refreshLinks', $page, $params ); // Avoid the overhead of de-duplication when it would be pointless $this->removeDuplicates = ( // Ranges rarely will line up !isset( $params['range'] ) && // Multiple pages per job make matches unlikely !( isset( $params['pages'] ) && count( $params['pages'] ) != 1 ) ); $this->params += [ 'causeAction' => 'RefreshLinksJob', 'causeAgent' => 'unknown' ]; // Tell JobRunner to not automatically wrap run() in a transaction round. // Each runForTitle() call will manage its own rounds in order to run DataUpdates // and to avoid contention as well. $this->executionFlags |= self::JOB_NO_EXPLICIT_TRX_ROUND; } /** * @param PageIdentity $page * @param array $params * @return RefreshLinksJob */ public static function newPrioritized( PageIdentity $page, array $params ) { $job = new self( $page, $params ); $job->command = 'refreshLinksPrioritized'; return $job; } /** * @param PageIdentity $page * @param array $params * @return RefreshLinksJob */ public static function newDynamic( PageIdentity $page, array $params ) { $job = new self( $page, $params ); $job->command = 'refreshLinksDynamic'; return $job; } public function run() { $ok = true; if ( !empty( $this->params['recursive'] ) ) { // Job to update all (or a range of) backlink pages for a page // When the base job branches, wait for the replica DBs to catch up to the primary. // From then on, we know that any template changes at the time the base job was // enqueued will be reflected in backlink page parses when the leaf jobs run. $services = MediaWikiServices::getInstance(); if ( !isset( $this->params['range'] ) ) { $lbFactory = $services->getDBLoadBalancerFactory(); if ( !$lbFactory->waitForReplication( [ 'timeout' => self::LAG_WAIT_TIMEOUT ] ) ) { // only try so hard, keep going with what we have $stats = $services->getStatsFactory(); $stats->getCounter( 'refreshlinks_warnings_total' ) ->setLabel( 'reason', 'lag_wait_failed' ) ->copyToStatsdAt( 'refreshlinks_warning.lag_wait_failed' ) ->increment(); } } // Carry over information for de-duplication $extraParams = $this->getRootJobParams(); $extraParams['triggeredRecursive'] = true; // Carry over cause information for logging $extraParams['causeAction'] = $this->params['causeAction']; $extraParams['causeAgent'] = $this->params['causeAgent']; // Convert this into no more than $wgUpdateRowsPerJob RefreshLinks per-title // jobs and possibly a recursive RefreshLinks job for the rest of the backlinks $jobs = BacklinkJobUtils::partitionBacklinkJob( $this, $services->getMainConfig()->get( MainConfigNames::UpdateRowsPerJob ), 1, // job-per-title [ 'params' => $extraParams ] ); $services->getJobQueueGroup()->push( $jobs ); } elseif ( isset( $this->params['pages'] ) ) { // Job to update link tables for a set of titles foreach ( $this->params['pages'] as [ $ns, $dbKey ] ) { $title = Title::makeTitleSafe( $ns, $dbKey ); if ( $title && $title->canExist() ) { $ok = $this->runForTitle( $title ) && $ok; } else { $ok = false; $this->setLastError( "Invalid title ($ns,$dbKey)." ); } } } else { // Job to update link tables for a given title $ok = $this->runForTitle( $this->title ); } return $ok; } /** * @param PageIdentity $pageIdentity * @return bool */ protected function runForTitle( PageIdentity $pageIdentity ) { $services = MediaWikiServices::getInstance(); $stats = $services->getStatsFactory(); $renderer = $services->getRevisionRenderer(); $parserCache = $services->getParserCache(); $lbFactory = $services->getDBLoadBalancerFactory(); $ticket = $lbFactory->getEmptyTransactionTicket( __METHOD__ ); // Load the page from the primary DB $page = $services->getWikiPageFactory()->newFromTitle( $pageIdentity ); $page->loadPageData( IDBAccessObject::READ_LATEST ); if ( !$page->exists() ) { // Probably due to concurrent deletion or renaming of the page $logger = LoggerFactory::getInstance( 'RefreshLinksJob' ); $logger->warning( 'The page does not exist. Perhaps it was deleted?', [ 'page_title' => $this->title->getPrefixedDBkey(), 'job_params' => $this->getParams(), 'job_metadata' => $this->getMetadata() ] ); $this->incrementFailureCounter( $stats, 'page_not_found' ); // retry later to handle unlucky race condition return false; } // Serialize link update job by page ID so they see each others' changes. // The page ID and latest revision ID will be queried again after the lock // is acquired to bail if they are changed from that of loadPageData() above. // Serialize links updates by page ID so they see each others' changes $dbw = $lbFactory->getPrimaryDatabase(); /** @noinspection PhpUnusedLocalVariableInspection */ $scopedLock = LinksUpdate::acquirePageLock( $dbw, $page->getId(), 'job' ); if ( $scopedLock === null ) { // Another job is already updating the page, likely for a prior revision (T170596) $this->setLastError( 'LinksUpdate already running for this page, try again later.' ); $this->incrementFailureCounter( $stats, 'lock_failure' ); // retry later when overlapping job for previous rev is done return false; } if ( $this->isAlreadyRefreshed( $page ) ) { // this job has been superseded, e.g. by overlapping recursive job // for a different template edit, or by direct edit or purge. $stats->getCounter( 'refreshlinks_superseded_updates_total' ) ->copyToStatsdAt( 'refreshlinks_outcome.good_update_superseded' ) ->increment(); // treat as success return true; } // Parse during a fresh transaction round for better read consistency $lbFactory->beginPrimaryChanges( __METHOD__ ); $output = $this->getParserOutput( $renderer, $parserCache, $page, $stats ); $options = $this->getDataUpdateOptions(); $lbFactory->commitPrimaryChanges( __METHOD__ ); if ( !$output ) { // probably raced out. // Specific refreshlinks_outcome metric sent by getCurrentRevisionIfUnchanged(). // Don't retry job. return true; } // Tell DerivedPageDataUpdater to use this parser output $options['known-revision-output'] = $output; // Execute corresponding DataUpdates immediately $page->doSecondaryDataUpdates( $options ); InfoAction::invalidateCache( $page ); // NOTE: Since 2019 (f588586e) this no longer saves the new ParserOutput to the ParserCache! // This means the page will have to be rendered on-the-fly when it is next viewed. // This is to avoid spending limited ParserCache capacity on rarely visited pages. // TODO: Save the ParserOutput to ParserCache by calling WikiPage::updateParserCache() // for pages that are likely to benefit (T327162). // Commit any writes here in case this method is called in a loop. // In that case, the scoped lock will fail to be acquired. $lbFactory->commitAndWaitForReplication( __METHOD__, $ticket ); return true; } /** * @return string|null Minimum lag-safe TS_MW timestamp with regard to root job creation */ private function getLagAwareRootTimestamp() { // Get the timestamp of the change that triggered this job $rootTimestamp = $this->params['rootJobTimestamp'] ?? null; if ( $rootTimestamp === null ) { return null; } if ( !empty( $this->params['isOpportunistic'] ) ) { // Neither clock skew nor DB snapshot/replica DB lag matter much for // such updates; focus on reusing the (often recently updated) cache $lagAwareTimestamp = $rootTimestamp; } else { // For transclusion updates, the template changes must be reflected $lagAwareTimestamp = wfTimestamp( TS_MW, (int)wfTimestamp( TS_UNIX, $rootTimestamp ) + self::NORMAL_MAX_LAG ); } return $lagAwareTimestamp; } /** * @param WikiPage $page * @return bool Whether something updated the backlinks with data newer than this job */ private function isAlreadyRefreshed( WikiPage $page ) { $lagAwareTimestamp = $this->getLagAwareRootTimestamp(); return ( $lagAwareTimestamp !== null && $page->getLinksTimestamp() > $lagAwareTimestamp ); } /** * @see DerivedPageDataUpdater::shouldGenerateHTMLOnEdit * @return bool true if at least one of slots require rendering HTML on edit, false otherwise. * This is needed for example in populating ParserCache. */ private function shouldGenerateHTMLOnEdit( RevisionRecord $revision ): bool { $services = MediaWikiServices::getInstance(); foreach ( $revision->getSlots()->getSlotRoles() as $role ) { $slot = $revision->getSlots()->getSlot( $role ); $contentHandler = $services->getContentHandlerFactory()->getContentHandler( $slot->getModel() ); if ( $contentHandler->generateHTMLOnEdit() ) { return true; } } return false; } /** * Get the parser output if the page is unchanged from what was loaded in $page * * @param RevisionRenderer $renderer * @param ParserCache $parserCache * @param WikiPage $page Page already loaded with READ_LATEST * @param StatsFactory $stats * @return ParserOutput|null Combined output for all slots; might only contain metadata */ private function getParserOutput( RevisionRenderer $renderer, ParserCache $parserCache, WikiPage $page, StatsFactory $stats ) { $revision = $this->getCurrentRevisionIfUnchanged( $page, $stats ); if ( !$revision ) { // race condition? return null; } $cachedOutput = $this->getParserOutputFromCache( $parserCache, $page, $revision, $stats ); $statsCounter = $stats->getCounter( 'refreshlinks_parsercache_operations_total' ); if ( $cachedOutput && $this->canUseParserOutputFromCache( $cachedOutput, $revision ) ) { $statsCounter ->setLabel( 'status', 'cache_hit' ) ->setLabel( 'html_changed', 'n/a' ) ->copyToStatsdAt( 'refreshlinks.parser_cached' ) ->increment(); return $cachedOutput; } $causeAction = $this->params['causeAction'] ?? 'RefreshLinksJob'; $parserOptions = $page->makeParserOptions( 'canonical' ); // T371713: Temporary statistics collection code to determine // feasibility of Parsoid selective update $sampleRate = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::ParsoidSelectiveUpdateSampleRate ); $doSample = $sampleRate && mt_rand( 1, $sampleRate ) === 1; if ( $doSample && $cachedOutput === null ) { // In order to collect accurate statistics, check for // a dirty copy in the cache even if we wouldn't have // to otherwise. $cachedOutput = $parserCache->getDirty( $page, $parserOptions ) ?: null; } $renderedRevision = $renderer->getRenderedRevision( $revision, $parserOptions, null, [ 'audience' => $revision::RAW, 'causeAction' => $causeAction, // Providing a previous parse potentially allows for // selective updates 'previous-output' => $cachedOutput, ] ); $parseTimestamp = wfTimestampNow(); // timestamp that parsing started $output = $renderedRevision->getRevisionParserOutput( [ // To avoid duplicate parses, this must match DerivedPageDataUpdater::shouldGenerateHTMLOnEdit() (T301309) 'generate-html' => $this->shouldGenerateHTMLOnEdit( $revision ) ] ); $output->setCacheTime( $parseTimestamp ); // notify LinksUpdate::doUpdate() // T371713: Temporary statistics collection code to determine // feasibility of Parsoid selective update if ( $doSample ) { $content = $revision->getContent( SlotRecord::MAIN ); $labels = [ 'source' => 'RefreshLinksJob', 'type' => $cachedOutput === null ? 'full' : 'selective', 'reason' => $causeAction, 'parser' => $parserOptions->getUseParsoid() ? 'parsoid' : 'legacy', 'opportunistic' => empty( $this->params['isOpportunistic'] ) ? 'false' : 'true', 'wiki' => WikiMap::getCurrentWikiId(), 'model' => $content ? $content->getModel() : 'unknown', ]; $stats ->getCounter( 'ParserCache_selective_total' ) ->setLabels( $labels ) ->increment(); $stats ->getCounter( 'ParserCache_selective_cpu_seconds' ) ->setLabels( $labels ) ->incrementBy( $output->getTimeProfile( 'cpu' ) ); } // Collect stats on parses that don't actually change the page content. // In that case, we could abort here, and perhaps we could also avoid // triggering CDN purges (T369898). if ( !$cachedOutput ) { // There was no cached output $htmlChanged = 'unknown'; } elseif ( $cachedOutput->getRawText() === $output->getRawText() ) { // We have cached output, but we couldn't be sure that it was still good. // So we parsed again, but the result turned out to be the same HTML as // before. $htmlChanged = 'no'; } else { // Re-parsing yielded HTML different from the cached output. $htmlChanged = 'yes'; } $statsCounter ->setLabel( 'status', 'cache_miss' ) ->setLabel( 'html_changed', $htmlChanged ) ->copyToStatsdAt( 'refreshlinks.parser_uncached' ) ->increment(); return $output; } /** * Get the current revision record if it is unchanged from what was loaded in $page * * @param WikiPage $page Page already loaded with READ_LATEST * @param StatsFactory $stats * @return RevisionRecord|null The same instance that $page->getRevisionRecord() uses */ private function getCurrentRevisionIfUnchanged( WikiPage $page, StatsFactory $stats ) { $title = $page->getTitle(); // Get the latest ID since acquirePageLock() in runForTitle() flushed the transaction. // This is used to detect edits/moves after loadPageData() but before the scope lock. // The works around the chicken/egg problem of determining the scope lock key name $latest = $title->getLatestRevID( IDBAccessObject::READ_LATEST ); $triggeringRevisionId = $this->params['triggeringRevisionId'] ?? null; if ( $triggeringRevisionId && $triggeringRevisionId !== $latest ) { // This job is obsolete and one for the latest revision will handle updates $this->incrementFailureCounter( $stats, 'rev_not_current' ); $this->setLastError( "Revision $triggeringRevisionId is not current" ); return null; } // Load the current revision. Note that $page should have loaded with READ_LATEST. // This instance will be reused in WikiPage::doSecondaryDataUpdates() later on. $revision = $page->getRevisionRecord(); if ( !$revision ) { // revision just got deleted? $this->incrementFailureCounter( $stats, 'rev_not_found' ); $this->setLastError( "Revision not found for {$title->getPrefixedDBkey()}" ); return null; } elseif ( $revision->getId() !== $latest || $revision->getPageId() !== $page->getId() ) { // Do not clobber over newer updates with older ones. If all jobs where FIFO and // serialized, it would be OK to update links based on older revisions since it // would eventually get to the latest. Since that is not the case (by design), // only update the link tables to a state matching the current revision's output. $this->incrementFailureCounter( $stats, 'rev_not_current' ); $this->setLastError( "Revision {$revision->getId()} is not current" ); return null; } return $revision; } /** * Get the parser output from cache if it reflects the change that triggered this job * * @param ParserCache $parserCache * @param WikiPage $page * @param RevisionRecord $currentRevision * @param StatsFactory $stats * @return ParserOutput|null */ private function getParserOutputFromCache( ParserCache $parserCache, WikiPage $page, RevisionRecord $currentRevision, StatsFactory $stats ): ?ParserOutput { // Parsoid can do selective updates, so it is always worth the I/O // to check for a previous parse. $parserOptions = $page->makeParserOptions( 'canonical' ); if ( $parserOptions->getUseParsoid() ) { return $parserCache->getDirty( $page, $parserOptions ) ?: null; } // If page_touched changed after this root job, then it is likely that // any views of the pages already resulted in re-parses which are now in // cache. The cache can be reused to avoid expensive parsing in some cases. $rootTimestamp = $this->params['rootJobTimestamp'] ?? null; if ( $rootTimestamp !== null ) { $opportunistic = !empty( $this->params['isOpportunistic'] ); if ( $page->getTouched() >= $rootTimestamp || $opportunistic ) { // Cache is suspected to be up-to-date so it's worth the I/O of checking. // We call canUseParserOutputFromCache() later to check if it's usable. return $parserCache->getDirty( $page, $parserOptions ) ?: null; } } return null; } private function canUseParserOutputFromCache( ParserOutput $cachedOutput, RevisionRecord $currentRevision ) { // As long as the cache rev ID matches the current rev ID and it reflects // the job's triggering change, then it is usable. return $cachedOutput->getCacheRevisionId() == $currentRevision->getId() && $cachedOutput->getCacheTime() >= $this->getLagAwareRootTimestamp(); } /** * Increment the RefreshLinks failure counter metric with the given reason. * * @param StatsFactory $stats * @param string $reason Well-known failure reason string * @return void */ private function incrementFailureCounter( StatsFactory $stats, $reason ): void { $stats->getCounter( 'refreshlinks_failures_total' ) ->setLabel( 'reason', $reason ) ->copyToStatsdAt( "refreshlinks_outcome.bad_$reason" ) ->increment(); } /** * @return array */ private function getDataUpdateOptions() { $options = [ 'recursive' => !empty( $this->params['useRecursiveLinksUpdate'] ), // Carry over cause so the update can do extra logging 'causeAction' => $this->params['causeAction'], 'causeAgent' => $this->params['causeAgent'] ]; if ( !empty( $this->params['triggeringUser'] ) ) { $userInfo = $this->params['triggeringUser']; if ( $userInfo['userId'] ) { $options['triggeringUser'] = User::newFromId( $userInfo['userId'] ); } else { // Anonymous, use the username $options['triggeringUser'] = User::newFromName( $userInfo['userName'], false ); } } return $options; } public function getDeduplicationInfo() { $info = parent::getDeduplicationInfo(); unset( $info['causeAction'] ); unset( $info['causeAgent'] ); if ( is_array( $info['params'] ) ) { // For per-pages jobs, the job title is that of the template that changed // (or similar), so remove that since it ruins duplicate detection if ( isset( $info['params']['pages'] ) ) { unset( $info['namespace'] ); unset( $info['title'] ); } } return $info; } public function workItemCount() { if ( !empty( $this->params['recursive'] ) ) { return 0; // nothing actually refreshed } elseif ( isset( $this->params['pages'] ) ) { return count( $this->params['pages'] ); } return 1; // one title } } PK ! *�+� � UploadFromUrlJob.phpnu �Iw�� <?php /** * 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 */ use MediaWiki\Status\Status; /** * Upload a file by URL, via the jobqueue. * */ class UploadFromUrlJob extends Job implements GenericParameterJob { use UploadJobTrait; public function __construct( array $params ) { // @TODO: fix the invokation of Job::__construct in the parent class parent::__construct( 'UploadFromUrl', $params ); $this->removeDuplicates = true; $this->user = null; $this->cacheKey = UploadFromUrl::getCacheKey( $this->params ); } /** * Deduplicate on title, url alone. * * Please note that this could cause some * edge case failure, when the image at the * same remote url is changed before the first upload * is ran. * * @return array */ public function getDeduplicationInfo() { $info = parent::getDeduplicationInfo(); if ( is_array( $info['params'] ) ) { $info['params'] = [ 'url' => $info['params']['url'], 'title' => $info['params']['filename'] ]; } return $info; } /** * getter for the upload * * @return UploadBase */ protected function getUpload(): UploadBase { if ( $this->upload === null ) { $this->upload = new UploadFromUrl; $this->upload->initialize( $this->params['filename'], $this->params['url'] ); } return $this->upload; } /** * Get the parameters for job logging * * @param Status[] $status * @return array */ public function logJobParams( $status ): array { return [ 'stage' => $status['stage'] ?? '-', 'result' => $status['result'] ?? '-', 'status' => (string)( $status['status'] ?? '-' ), 'url' => $this->params['url'], 'filename' => $this->params['filename'], 'user' => $this->user->getName(), ]; } } PK ! ��� � # Hook/RecentChangesPurgeRowsHook.phpnu �Iw�� <?php namespace MediaWiki\Hook; /** * This is a hook handler interface, see docs/Hooks.md. * Use the hook name "RecentChangesPurgeRows" to register handlers implementing this interface. * * @stable to implement * @ingroup Hooks */ interface RecentChangesPurgeRowsHook { /** * This hook is called by RecentChangesUpdateJob when expired recentchanges rows are deleted, after * deleting those rows but within the same database transaction. * * @since 1.35 * @param \stdClass[] $rows Deleted rows as an array of recentchanges row objects (with up to * $wgUpdateRowsPerQuery items) * @return void This hook must not abort, it must return no value */ public function onRecentChangesPurgeRows( $rows ): void; } PK ! T�E�� � PublishStashedFileJob.phpnu �Iw�� <?php /** * 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 * @defgroup JobQueue JobQueue */ use MediaWiki\Status\Status; /** * Upload a file from the upload stash into the local file repo. * * @ingroup Upload * @ingroup JobQueue */ class PublishStashedFileJob extends Job implements GenericParameterJob { use UploadJobTrait; public function __construct( array $params ) { parent::__construct( 'PublishStashedFile', $params ); $this->removeDuplicates = true; $this->initialiseUploadJob( $this->params['filekey'] ); } public function getDeduplicationInfo() { $info = parent::getDeduplicationInfo(); if ( is_array( $info['params'] ) ) { $info['params'] = [ 'filekey' => $info['params']['filekey'] ]; } return $info; } /** * Get the parameters for job logging * * @param Status[] $status * @return array */ public function logJobParams( $status ): array { return [ 'stage' => $status['stage'] ?? '-', 'result' => $status['result'] ?? '-', 'status' => (string)( $status['status'] ?? '-' ), 'filekey' => $this->params['filekey'], 'filename' => $this->params['filename'], 'user' => $this->user->getName(), ]; } /** * getter for the upload * * @return UploadFromStash */ protected function getUpload(): UploadBase { if ( $this->upload === null ) { $this->upload = new UploadFromStash( $this->user ); // @todo initialize() causes a GET, ideally we could frontload the antivirus // checks and anything else to the stash stage (which includes concatenation and // the local file is thus already there). That way, instead of GET+PUT, there could // just be a COPY operation from the stash to the public zone. $this->upload->initialize( $this->params['filekey'], $this->params['filename'] ); } return $this->upload; } } PK ! Y?7� � AssembleUploadChunksJob.phpnu �Iw�� <?php /** * 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 */ use MediaWiki\Api\ApiUpload; use MediaWiki\Context\RequestContext; use MediaWiki\Logger\LoggerFactory; use MediaWiki\Request\WebRequestUpload; use MediaWiki\Status\Status; use Wikimedia\ScopedCallback; /** * Assemble the segments of a chunked upload. * * @ingroup Upload * @ingroup JobQueue */ class AssembleUploadChunksJob extends Job implements GenericParameterJob { public function __construct( array $params ) { parent::__construct( 'AssembleUploadChunks', $params ); $this->removeDuplicates = true; } public function run() { $scope = RequestContext::importScopedSession( $this->params['session'] ); $this->addTeardownCallback( static function () use ( &$scope ) { ScopedCallback::consume( $scope ); // T126450 } ); $logger = LoggerFactory::getInstance( 'upload' ); $context = RequestContext::getMain(); $user = $context->getUser(); try { if ( !$user->isRegistered() ) { $this->setLastError( "Could not load the author user from session." ); return false; } // TODO add some sort of proper locking maybe $startingStatus = UploadBase::getSessionStatus( $user, $this->params['filekey'] ); if ( !$startingStatus || ( $startingStatus['result'] ?? '' ) !== 'Poll' || ( $startingStatus['stage'] ?? '' ) !== 'queued' ) { $logger->warning( "Tried to assemble upload that is in stage {stage}/{result}", [ 'stage' => $startingStatus['stage'] ?? '-', 'result' => $startingStatus['result'] ?? '-', 'status' => (string)( $startingStatus['status'] ?? '-' ), 'filekey' => $this->params['filekey'], 'filename' => $this->params['filename'], 'user' => $user->getName(), ] ); // If it is marked as currently in progress, abort. Otherwise // assume it is some sort of replag issue or maybe a retry even // though retries are impossible and just warn. if ( $startingStatus && $startingStatus['stage'] === 'assembling' && $startingStatus['result'] !== 'Failure' ) { $this->setLastError( __METHOD__ . " already in progress" ); return false; } } UploadBase::setSessionStatus( $user, $this->params['filekey'], [ 'result' => 'Poll', 'stage' => 'assembling', 'status' => Status::newGood() ] ); $upload = new UploadFromChunks( $user ); $upload->continueChunks( $this->params['filename'], $this->params['filekey'], new WebRequestUpload( $context->getRequest(), 'null' ) ); if ( isset( $this->params['filesize'] ) && $this->params['filesize'] !== (int)$upload->getOffset() ) { // Check to make sure we are not executing prior to the API's // transaction being committed. (T350917) throw new UnexpectedValueException( "UploadStash file size does not match job's. Potential mis-nested transaction?" ); } // Combine all of the chunks into a local file and upload that to a new stash file $status = $upload->concatenateChunks(); if ( !$status->isGood() ) { UploadBase::setSessionStatus( $user, $this->params['filekey'], [ 'result' => 'Failure', 'stage' => 'assembling', 'status' => $status ] ); $logger->info( "Chunked upload assembly job failed for {filekey} because {status}", [ 'filekey' => $this->params['filekey'], 'filename' => $this->params['filename'], 'user' => $user->getName(), 'status' => (string)$status ] ); // the chunks did not get assembled, but this should not be considered a job // failure - they simply didn't pass verification for some reason, and that // reason is stored in above session to inform the clients return true; } // We can only get warnings like 'duplicate' after concatenating the chunks $status = Status::newGood(); $status->value = [ 'warnings' => UploadBase::makeWarningsSerializable( $upload->checkWarnings( $user ) ) ]; // We have a new filekey for the fully concatenated file $newFileKey = $upload->getStashFile()->getFileKey(); // Remove the old stash file row and first chunk file // Note: This does not delete the chunks, only the stash file // which is same as first chunk but with a different name. $upload->stash->removeFileNoAuth( $this->params['filekey'] ); // Build the image info array while we have the local reference handy $apiUpload = ApiUpload::getDummyInstance(); $imageInfo = $apiUpload->getUploadImageInfo( $upload ); // Cleanup any temporary local file $upload->cleanupTempFile(); // Cache the info so the user doesn't have to wait forever to get the final info UploadBase::setSessionStatus( $user, $this->params['filekey'], [ 'result' => 'Success', 'stage' => 'assembling', 'filekey' => $newFileKey, 'imageinfo' => $imageInfo, 'status' => $status ] ); $logger->info( "{filekey} successfully assembled into {newkey}", [ 'filekey' => $this->params['filekey'], 'newkey' => $newFileKey, 'filename' => $this->params['filename'], 'user' => $user->getName(), 'status' => (string)$status ] ); } catch ( Exception $e ) { UploadBase::setSessionStatus( $user, $this->params['filekey'], [ 'result' => 'Failure', 'stage' => 'assembling', 'status' => Status::newFatal( 'api-error-stashfailed' ) ] ); $this->setLastError( get_class( $e ) . ": " . $e->getMessage() ); // To be extra robust. MWExceptionHandler::rollbackPrimaryChangesAndLog( $e ); return false; } return true; } public function getDeduplicationInfo() { $info = parent::getDeduplicationInfo(); if ( is_array( $info['params'] ) ) { $info['params'] = [ 'filekey' => $info['params']['filekey'] ]; } return $info; } public function allowRetries() { return false; } } PK ! 0R� � � DuplicateJob.phpnu �Iw�� <?php /** * 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 */ /** * No-op job that does nothing. * * This is used by JobQueue::pop to temporarily represent duplicates. * * @internal * @ingroup JobQueue */ final class DuplicateJob extends Job implements GenericParameterJob { /** * Callers should use DuplicateJob::newFromJob() instead * * @param array $params Job parameters */ public function __construct( array $params ) { parent::__construct( 'duplicate', $params ); } /** * Get a duplicate no-op version of a job * * @param RunnableJob $job * @return Job */ public static function newFromJob( RunnableJob $job ) { $djob = new self( $job->getParams() ); $djob->command = $job->getType(); $djob->params = is_array( $djob->params ) ? $djob->params : []; $djob->params = [ 'isDuplicate' => true ] + $djob->params; $djob->metadata = $job->getMetadata(); return $djob; } public function run() { return true; } } PK ! �r( ( DeleteLinksJob.phpnu �Iw�� <?php /** * 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 */ use MediaWiki\Deferred\LinksUpdate\LinksDeletionUpdate; use MediaWiki\Deferred\LinksUpdate\LinksUpdate; use MediaWiki\MediaWikiServices; use MediaWiki\Title\Title; use Wikimedia\Rdbms\IDBAccessObject; /** * Job to prune link tables for pages that were deleted * * @internal For use by core in LinksDeletionUpdate only. * @since 1.27 * @ingroup JobQueue */ class DeleteLinksJob extends Job { public function __construct( Title $title, array $params ) { parent::__construct( 'deleteLinks', $title, $params ); $this->removeDuplicates = true; } public function run() { if ( $this->title === null ) { $this->setLastError( "deleteLinks: Invalid title" ); return false; } $pageId = $this->params['pageId']; // Serialize links updates by page ID so they see each others' changes $dbw = MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase(); $scopedLock = LinksUpdate::acquirePageLock( $dbw, $pageId, 'job' ); if ( $scopedLock === null ) { $this->setLastError( 'LinksUpdate already running for this page, try again later.' ); return false; } $services = MediaWikiServices::getInstance(); $wikiPageFactory = $services->getWikiPageFactory(); if ( $wikiPageFactory->newFromID( $pageId, IDBAccessObject::READ_LATEST ) ) { // The page was restored somehow or something went wrong $this->setLastError( "deleteLinks: Page #$pageId exists" ); return false; } $dbProvider = $services->getConnectionProvider(); $timestamp = $this->params['timestamp'] ?? null; $page = $wikiPageFactory->newFromTitle( $this->title ); // title when deleted $update = new LinksDeletionUpdate( $page, $pageId, $timestamp ); $update->setTransactionTicket( $dbProvider->getEmptyTransactionTicket( __METHOD__ ) ); $update->doUpdate(); return true; } } PK ! �U�S S NullJob.phpnu �Iw�� PK ! �s#� � DeletePageJob.phpnu �Iw�� PK ! mwǔ� � � HTMLCacheUpdateJob.phpnu �Iw�� PK ! ��Bʴ � #. RevertedTagUpdateJob.phpnu �Iw�� PK ! ���! �! 8 UploadJobTrait.phpnu �Iw�� PK ! �{J �Y ParsoidCachePrewarmJob.phpnu �Iw�� PK ! 3�\�-* -* _m CategoryMembershipChangeJob.phpnu �Iw�� PK ! ��>- ۗ CdnPurgeJob.phpnu �Iw�� PK ! ���� � "� ThumbnailRenderJob.phpnu �Iw�� PK ! m��% % � DoubleRedirectJob.phpnu �Iw�� PK ! �5� c� UserOptionsUpdateJob.phpnu �Iw�� PK ! 'n5a 5a �� RefreshLinksJob.phpnu �Iw�� PK ! *�+� � 7D UploadFromUrlJob.phpnu �Iw�� PK ! ��� � # N Hook/RecentChangesPurgeRowsHook.phpnu �Iw�� PK ! T�E�� � 2Q PublishStashedFileJob.phpnu �Iw�� PK ! Y?7� � `[ AssembleUploadChunksJob.phpnu �Iw�� PK ! 0R� � � |u DuplicateJob.phpnu �Iw�� PK ! �r( ( P| DeleteLinksJob.phpnu �Iw�� PK ��
| ver. 1.1 | |
.
| PHP 8.4.18 | Ð“ÐµÐ½ÐµÑ€Ð°Ñ†Ð¸Ñ Ñтраницы: 0.01 |
proxy
|
phpinfo
|
ÐаÑтройка