Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 71
0.00% covered (danger)
0.00%
0 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
Hooks
0.00% covered (danger)
0.00%
0 / 71
0.00% covered (danger)
0.00%
0 / 9
506
0.00% covered (danger)
0.00%
0 / 1
 onLinksUpdateComplete
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
20
 onPageMoveComplete
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
20
 onArticleDeleteComplete
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 onFileDeleteComplete
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 onFileUndeleteComplete
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 onUploadComplete
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 fileUpdatesCreatePurgeJobs
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 getGlobalUsage
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 onWgQueryPages
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2/**
3 * GlobalUsage hooks for updating globalimagelinks table.
4 *
5 * UI hooks in SpecialGlobalUsage.
6 */
7
8namespace MediaWiki\Extension\GlobalUsage;
9
10use FileRepo;
11use LocalFile;
12use ManualLogEntry;
13use MediaWiki\Content\Content;
14use MediaWiki\Deferred\LinksUpdate\LinksUpdate;
15use MediaWiki\Hook\FileDeleteCompleteHook;
16use MediaWiki\Hook\FileUndeleteCompleteHook;
17use MediaWiki\Hook\LinksUpdateCompleteHook;
18use MediaWiki\Hook\PageMoveCompleteHook;
19use MediaWiki\Hook\UploadCompleteHook;
20use MediaWiki\Linker\LinkTarget;
21use MediaWiki\MediaWikiServices;
22use MediaWiki\Page\Hook\ArticleDeleteCompleteHook;
23use MediaWiki\Revision\RevisionRecord;
24use MediaWiki\SpecialPage\Hook\WgQueryPagesHook;
25use MediaWiki\Title\Title;
26use MediaWiki\User\User;
27use MediaWiki\User\UserIdentity;
28use MediaWiki\WikiMap\WikiMap;
29use UploadBase;
30use WikiFilePage;
31use Wikimedia\Rdbms\IDBAccessObject;
32use WikiPage;
33
34class Hooks implements
35    LinksUpdateCompleteHook,
36    ArticleDeleteCompleteHook,
37    FileDeleteCompleteHook,
38    FileUndeleteCompleteHook,
39    UploadCompleteHook,
40    PageMoveCompleteHook,
41    WgQueryPagesHook
42{
43    /**
44     * Hook to LinksUpdateComplete
45     * Deletes old links from usage table and insert new ones.
46     * @param LinksUpdate $linksUpdater
47     * @param int|null $ticket
48     */
49    public function onLinksUpdateComplete( $linksUpdater, $ticket ) {
50        $title = $linksUpdater->getTitle();
51
52        // Create a list of locally existing images (DB keys)
53        $images = array_keys( $linksUpdater->getImages() );
54
55        $localFiles = [];
56        $repo = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo();
57        $imagesInfo = $repo->findFiles( $images, FileRepo::NAME_AND_TIME_ONLY );
58        foreach ( $imagesInfo as $dbKey => $info ) {
59            '@phan-var array $info';
60            $localFiles[] = $dbKey;
61            if ( $dbKey !== $info['title'] ) { // redirect
62                $localFiles[] = $info['title'];
63            }
64        }
65        $localFiles = array_values( array_unique( $localFiles ) );
66
67        $missingFiles = array_diff( $images, $localFiles );
68
69        $gu = self::getGlobalUsage();
70        $articleId = $title->getArticleID( IDBAccessObject::READ_NORMAL );
71        $existing = $gu->getLinksFromPage( $articleId );
72
73        // Calculate changes
74        $added = array_diff( $missingFiles, $existing );
75        $removed = array_diff( $existing, $missingFiles );
76
77        // Add new usages and delete removed
78        $gu->insertLinks( $title, $added, IDBAccessObject::READ_LATEST, $ticket );
79        if ( $removed ) {
80            $gu->deleteLinksFromPage( $articleId, $removed, $ticket );
81        }
82    }
83
84    /**
85     * Hook to PageMoveComplete
86     * Sets the page title in usage table to the new name.
87     * For shared file moves, purges all pages in the wiki farm that use the files.
88     * @param LinkTarget $ot
89     * @param LinkTarget $nt
90     * @param UserIdentity $user
91     * @param int $pageid
92     * @param int $redirid
93     * @param string $reason
94     * @param RevisionRecord $revisionRecord
95     */
96    public function onPageMoveComplete(
97        $ot,
98        $nt,
99        $user,
100        $pageid,
101        $redirid,
102        $reason,
103        $revisionRecord
104    ) {
105        $ot = Title::newFromLinkTarget( $ot );
106        $nt = Title::newFromLinkTarget( $nt );
107
108        $gu = self::getGlobalUsage();
109        $gu->moveTo( $pageid, $nt );
110
111        if ( self::fileUpdatesCreatePurgeJobs() ) {
112            $jobs = [];
113            if ( $ot->inNamespace( NS_FILE ) ) {
114                $jobs[] = new GlobalUsageCachePurgeJob( $ot, [] );
115            }
116            if ( $nt->inNamespace( NS_FILE ) ) {
117                $jobs[] = new GlobalUsageCachePurgeJob( $nt, [] );
118            }
119            // Push the jobs after DB commit but cancel on rollback
120            MediaWikiServices::getInstance()
121                ->getConnectionProvider()
122                ->getPrimaryDatabase()
123                ->onTransactionCommitOrIdle( static function () use ( $jobs ) {
124                    MediaWikiServices::getInstance()->getJobQueueGroupFactory()->makeJobQueueGroup()->lazyPush( $jobs );
125                }, __METHOD__ );
126        }
127    }
128
129    /**
130     * Hook to ArticleDeleteComplete
131     * Deletes entries from usage table.
132     * @param WikiPage $article
133     * @param User $user
134     * @param string $reason
135     * @param int $id
136     * @param Content|null $content
137     * @param ManualLogEntry $logEntry
138     * @param int $archivedRevisionCount
139     */
140    public function onArticleDeleteComplete( $article, $user, $reason, $id,
141        $content, $logEntry, $archivedRevisionCount
142    ) {
143        $gu = self::getGlobalUsage();
144        // @FIXME: avoid making DB replication lag
145        $gu->deleteLinksFromPage( $id );
146    }
147
148    /**
149     * Hook to FileDeleteComplete
150     * Copies the local link table to the global.
151     * Purges all pages in the wiki farm that use the file if it is a shared repo file.
152     * @param LocalFile $file
153     * @param string|null $oldimage
154     * @param WikiFilePage|null $article
155     * @param User $user
156     * @param string $reason
157     */
158    public function onFileDeleteComplete( $file, $oldimage, $article, $user, $reason ) {
159        if ( !$oldimage ) {
160            if ( !GlobalUsage::onSharedRepo() ) {
161                $gu = self::getGlobalUsage();
162                $gu->copyLocalImagelinks(
163                    $file->getTitle(),
164                    MediaWikiServices::getInstance()
165                        ->getConnectionProvider()
166                        ->getPrimaryDatabase()
167                );
168            }
169
170            if ( self::fileUpdatesCreatePurgeJobs() ) {
171                $job = new GlobalUsageCachePurgeJob( $file->getTitle(), [] );
172                MediaWikiServices::getInstance()->getJobQueueGroupFactory()->makeJobQueueGroup()->push( $job );
173            }
174        }
175    }
176
177    /**
178     * Hook to FileUndeleteComplete
179     * Deletes the file from the global link table.
180     * Purges all pages in the wiki farm that use the file if it is a shared repo file.
181     * @param Title $title
182     * @param array $versions
183     * @param User $user
184     * @param string $reason
185     */
186    public function onFileUndeleteComplete( $title, $versions, $user, $reason ) {
187        $gu = self::getGlobalUsage();
188        $gu->deleteLinksToFile( $title );
189
190        if ( self::fileUpdatesCreatePurgeJobs() ) {
191            $job = new GlobalUsageCachePurgeJob( $title, [] );
192            MediaWikiServices::getInstance()->getJobQueueGroupFactory()->makeJobQueueGroup()->push( $job );
193        }
194    }
195
196    /**
197     * Hook to UploadComplete
198     * Deletes the file from the global link table.
199     * Purges all pages in the wiki farm that use the file if it is a shared repo file.
200     * @param UploadBase $upload
201     */
202    public function onUploadComplete( $upload ) {
203        $gu = self::getGlobalUsage();
204        $gu->deleteLinksToFile( $upload->getTitle() );
205
206        if ( self::fileUpdatesCreatePurgeJobs() ) {
207            $job = new GlobalUsageCachePurgeJob( $upload->getTitle(), [] );
208            MediaWikiServices::getInstance()->getJobQueueGroupFactory()->makeJobQueueGroup()->push( $job );
209        }
210    }
211
212    /**
213     *
214     * Check if file updates on this wiki should cause backlink page purge jobs
215     *
216     * @return bool
217     */
218    private static function fileUpdatesCreatePurgeJobs() {
219        global $wgGlobalUsageSharedRepoWiki, $wgGlobalUsagePurgeBacklinks;
220
221        return ( $wgGlobalUsagePurgeBacklinks && WikiMap::getCurrentWikiId() === $wgGlobalUsageSharedRepoWiki );
222    }
223
224    /**
225     * Initializes a GlobalUsage object for the current wiki.
226     *
227     * @return GlobalUsage
228     */
229    private static function getGlobalUsage() {
230        return new GlobalUsage(
231            WikiMap::getCurrentWikiId(),
232            GlobalUsage::getGlobalDB( DB_PRIMARY ),
233            GlobalUsage::getGlobalDB( DB_REPLICA )
234        );
235    }
236
237    public function onWgQueryPages( &$queryPages ) {
238        $queryPages[] = [ 'SpecialMostGloballyLinkedFiles', 'MostGloballyLinkedFiles' ];
239        $queryPages[] = [ 'SpecialGloballyWantedFiles', 'GloballyWantedFiles' ];
240        if ( GlobalUsage::onSharedRepo() ) {
241            $queryPages[] = [ 'SpecialGloballyUnusedFiles', 'GloballyUnusedFiles' ];
242        }
243    }
244}