Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
26.80% covered (danger)
26.80%
26 / 97
14.29% covered (danger)
14.29%
2 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
WikiFilePage
26.80% covered (danger)
26.80%
26 / 97
14.29% covered (danger)
14.29%
2 / 14
544.24
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 setFile
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 loadFile
75.00% covered (warning)
75.00%
9 / 12
0.00% covered (danger)
0.00%
0 / 1
4.25
 getRedirectTarget
33.33% covered (danger)
33.33%
3 / 9
0.00% covered (danger)
0.00%
0 / 1
8.74
 followRedirect
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 isRedirect
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 isLocal
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getFile
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getDuplicates
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
42
 doPurge
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
20
 getForeignCategories
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
6
 getWikiDisplayName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSourceURL
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getActionOverrides
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2/**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 */
20
21use MediaWiki\Actions\FileDeleteAction;
22use MediaWiki\MediaWikiServices;
23use MediaWiki\Title\Title;
24use MediaWiki\Title\TitleArrayFromResult;
25use Wikimedia\Rdbms\FakeResultWrapper;
26
27/**
28 * Special handling for representing file pages.
29 *
30 * @ingroup Media
31 */
32class WikiFilePage extends WikiPage {
33    /** @var File|false */
34    protected $mFile = false;
35    /** @var LocalRepo|null */
36    protected $mRepo = null;
37    /** @var bool */
38    protected $mFileLoaded = false;
39    /** @var array|null */
40    protected $mDupes = null;
41
42    /**
43     * @param Title $title
44     */
45    public function __construct( $title ) {
46        parent::__construct( $title );
47        $this->mDupes = null;
48        $this->mRepo = null;
49    }
50
51    /**
52     * @param File $file
53     */
54    public function setFile( File $file ) {
55        $this->mFile = $file;
56        $this->mFileLoaded = true;
57    }
58
59    /**
60     * @return bool
61     */
62    protected function loadFile() {
63        $services = MediaWikiServices::getInstance();
64        if ( $this->mFileLoaded ) {
65            return true;
66        }
67
68        $this->mFile = $services->getRepoGroup()->findFile( $this->mTitle );
69        if ( !$this->mFile ) {
70            $this->mFile = $services->getRepoGroup()->getLocalRepo()
71                ->newFile( $this->mTitle );
72        }
73
74        if ( !$this->mFile instanceof File ) {
75            throw new RuntimeException( 'Expected to find file. See T250767' );
76        }
77
78        $this->mRepo = $this->mFile->getRepo();
79        $this->mFileLoaded = true;
80        return true;
81    }
82
83    /**
84     * @return mixed|null|Title
85     */
86    public function getRedirectTarget() {
87        $this->loadFile();
88        if ( $this->mFile->isLocal() ) {
89            return parent::getRedirectTarget();
90        }
91        // Foreign image page
92        $from = $this->mFile->getRedirected();
93        $to = $this->mFile->getName();
94        if ( $from === null || $from === $to ) {
95            return null;
96        }
97        $this->mRedirectTarget = Title::makeTitle( NS_FILE, $to );
98        return $this->mRedirectTarget;
99    }
100
101    /**
102     * @return bool|Title|string False, Title of in-wiki target, or string with URL
103     */
104    public function followRedirect() {
105        $this->loadFile();
106        if ( $this->mFile->isLocal() ) {
107            return parent::followRedirect();
108        }
109        $from = $this->mFile->getRedirected();
110        $to = $this->mFile->getName();
111        if ( $from === null || $from === $to ) {
112            return false;
113        }
114        return Title::makeTitle( NS_FILE, $to );
115    }
116
117    /**
118     * @return bool
119     */
120    public function isRedirect() {
121        $this->loadFile();
122        if ( $this->mFile->isLocal() ) {
123            return parent::isRedirect();
124        }
125
126        return $this->mFile->getRedirected() !== null;
127    }
128
129    /**
130     * @return bool
131     */
132    public function isLocal() {
133        $this->loadFile();
134        return $this->mFile->isLocal();
135    }
136
137    /**
138     * @return File
139     */
140    public function getFile(): File {
141        $this->loadFile();
142        return $this->mFile;
143    }
144
145    /**
146     * @return File[]|null
147     */
148    public function getDuplicates() {
149        $this->loadFile();
150        if ( $this->mDupes !== null ) {
151            return $this->mDupes;
152        }
153        $hash = $this->mFile->getSha1();
154        if ( !( $hash ) ) {
155            $this->mDupes = [];
156            return $this->mDupes;
157        }
158        $dupes = MediaWikiServices::getInstance()->getRepoGroup()->findBySha1( $hash );
159        // Remove duplicates with self and non matching file sizes
160        $self = $this->mFile->getRepoName() . ':' . $this->mFile->getName();
161        $size = $this->mFile->getSize();
162
163        /**
164         * @var File $file
165         */
166        foreach ( $dupes as $index => $file ) {
167            $key = $file->getRepoName() . ':' . $file->getName();
168            if ( $key === $self || $file->getSize() != $size ) {
169                unset( $dupes[$index] );
170            }
171        }
172        $this->mDupes = $dupes;
173        return $this->mDupes;
174    }
175
176    /**
177     * Override handling of action=purge
178     * @return bool
179     */
180    public function doPurge() {
181        $this->loadFile();
182
183        if ( $this->mFile->exists() ) {
184            wfDebug( 'ImagePage::doPurge purging ' . $this->mFile->getName() );
185            $job = HTMLCacheUpdateJob::newForBacklinks(
186                $this->mTitle,
187                'imagelinks',
188                [ 'causeAction' => 'file-purge' ]
189            );
190            MediaWikiServices::getInstance()->getJobQueueGroup()->lazyPush( $job );
191        } else {
192            wfDebug( 'ImagePage::doPurge no image for '
193                . $this->mFile->getName() . "; limiting purge to cache only" );
194        }
195
196        // even if the file supposedly doesn't exist, force any cached information
197        // to be updated (in case the cached information is wrong)
198
199        // Purge current version and its thumbnails
200        $this->mFile->purgeCache( [ 'forThumbRefresh' => true ] );
201
202        // Purge the old versions and their thumbnails
203        foreach ( $this->mFile->getHistory() as $oldFile ) {
204            $oldFile->purgeCache( [ 'forThumbRefresh' => true ] );
205        }
206
207        if ( $this->mRepo ) {
208            // Purge redirect cache
209            $this->mRepo->invalidateImageRedirect( $this->mTitle );
210        }
211
212        return parent::doPurge();
213    }
214
215    /**
216     * Get the categories this file is a member of on the wiki where it was uploaded.
217     * For local files, this is the same as getCategories().
218     * For foreign API files (InstantCommons), this is not supported currently.
219     * Results will include hidden categories.
220     *
221     * @return TitleArrayFromResult
222     * @since 1.23
223     */
224    public function getForeignCategories() {
225        $this->loadFile();
226        $title = $this->mTitle;
227        $file = $this->mFile;
228        $titleFactory = MediaWikiServices::getInstance()->getTitleFactory();
229
230        if ( !$file instanceof LocalFile ) {
231            wfDebug( __METHOD__ . " is not supported for this file" );
232            return $titleFactory->newTitleArrayFromResult( new FakeResultWrapper( [] ) );
233        }
234
235        /** @var LocalRepo $repo */
236        $repo = $file->getRepo();
237        $dbr = $repo->getReplicaDB();
238
239        $res = $dbr->newSelectQueryBuilder()
240            ->select( [ 'page_title' => 'cl_to', 'page_namespace' => (string)NS_CATEGORY ] )
241            ->from( 'page' )
242            ->join( 'categorylinks', null, 'page_id = cl_from' )
243            ->where( [ 'page_namespace' => $title->getNamespace(), 'page_title' => $title->getDBkey(), ] )
244            ->caller( __METHOD__ )->fetchResultSet();
245
246        return $titleFactory->newTitleArrayFromResult( $res );
247    }
248
249    /**
250     * @since 1.28
251     * @return string
252     */
253    public function getWikiDisplayName() {
254        return $this->getFile()->getRepo()->getDisplayName();
255    }
256
257    /**
258     * @since 1.28
259     * @return string
260     */
261    public function getSourceURL() {
262        return $this->getFile()->getDescriptionUrl();
263    }
264
265    /**
266     * @inheritDoc
267     */
268    public function getActionOverrides() {
269        $file = $this->getFile();
270        if ( $file->exists() && $file->isLocal() && !$file->getRedirected() ) {
271            // Would be an actual file deletion
272            return [ 'delete' => FileDeleteAction::class ] + parent::getActionOverrides();
273        }
274        // It should use the normal article deletion interface
275        return parent::getActionOverrides();
276    }
277}