Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
46.97% covered (danger)
46.97%
31 / 66
33.33% covered (danger)
33.33%
2 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
FileRevisionFromRemoteUrl
46.97% covered (danger)
46.97%
31 / 66
33.33% covered (danger)
33.33%
2 / 6
48.55
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 prepare
80.00% covered (warning)
80.00%
16 / 20
0.00% covered (danger)
0.00%
0 / 1
4.13
 validate
52.94% covered (warning)
52.94%
9 / 17
0.00% covered (danger)
0.00%
0 / 1
5.67
 commit
44.44% covered (danger)
44.44%
4 / 9
0.00% covered (danger)
0.00%
0 / 1
4.54
 createUploadLog
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
6
 getWikiRevision
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace FileImporter\Operations;
4
5use FileImporter\Data\FileRevision;
6use FileImporter\Data\TextRevision;
7use FileImporter\Exceptions\HttpRequestException;
8use FileImporter\Exceptions\ImportException;
9use FileImporter\Exceptions\LocalizedImportException;
10use FileImporter\Interfaces\ImportOperation;
11use FileImporter\Services\Http\HttpRequestExecutor;
12use FileImporter\Services\UploadBase\UploadBaseFactory;
13use FileImporter\Services\UploadBase\ValidatingUploadBase;
14use FileImporter\Services\WikiRevisionFactory;
15use MediaWiki\Http\MWHttpRequest;
16use MediaWiki\Import\UploadRevisionImporter;
17use MediaWiki\Import\WikiRevision;
18use MediaWiki\Logging\ManualLogEntry;
19use MediaWiki\Permissions\RestrictionStore;
20use MediaWiki\Status\Status;
21use MediaWiki\Title\Title;
22use MediaWiki\Upload\UploadBase;
23use MediaWiki\User\User;
24use MediaWiki\User\UserIdentityLookup;
25use Psr\Log\LoggerInterface;
26use Psr\Log\NullLogger;
27use StatusValue;
28use UnexpectedValueException;
29use Wikimedia\FileBackend\FSFile\TempFSFileFactory;
30
31/**
32 * @license GPL-2.0-or-later
33 * @author Addshore
34 */
35class FileRevisionFromRemoteUrl implements ImportOperation {
36
37    /** @var WikiRevision|null */
38    private $wikiRevision = null;
39    /** @var ValidatingUploadBase|null */
40    private $uploadBase = null;
41
42    public function __construct(
43        private readonly Title $plannedTitle,
44        private readonly User $user,
45        private readonly FileRevision $fileRevision,
46        private readonly ?TextRevision $textRevision,
47        private readonly UserIdentityLookup $userLookup,
48        private readonly HttpRequestExecutor $httpRequestExecutor,
49        private readonly WikiRevisionFactory $wikiRevisionFactory,
50        private readonly UploadBaseFactory $uploadBaseFactory,
51        private readonly UploadRevisionImporter $importer,
52        private readonly RestrictionStore $restrictionStore,
53        private readonly LoggerInterface $logger = new NullLogger(),
54    ) {
55    }
56
57    /**
58     * Method to prepare an operation. This will not commit anything to any persistent storage.
59     * @return StatusValue isOK on success
60     */
61    public function prepare(): StatusValue {
62        $fileUrl = $this->fileRevision->getField( 'url' );
63        if ( !MWHttpRequest::isValidURI( $fileUrl ) ) {
64            // invalid URL detected
65            return StatusValue::newFatal( 'fileimporter-cantparseurl', $fileUrl );
66        }
67
68        $tmpFile = ( new TempFSFileFactory( wfTempDir() ) )->newTempFSFile( 'fileimporter_' );
69        $tmpFile->bind( $this );
70
71        try {
72            $this->httpRequestExecutor->executeAndSave( $fileUrl, $tmpFile->getPath() );
73        } catch ( HttpRequestException $ex ) {
74            if ( $ex->getCode() === 404 ) {
75                throw new LocalizedImportException( 'fileimporter-filemissinginrevision', $ex );
76            }
77            throw $ex;
78        }
79
80        $this->wikiRevision = $this->wikiRevisionFactory->newFromFileRevision(
81            $this->fileRevision,
82            $tmpFile->getPath()
83        );
84
85        $this->wikiRevision->setTitle( $this->plannedTitle );
86
87        $this->uploadBase = $this->uploadBaseFactory->newValidatingUploadBase(
88            $this->plannedTitle,
89            $this->wikiRevision->getFileSrc()
90        );
91
92        return StatusValue::newGood();
93    }
94
95    /**
96     * Method to validate prepared data that should be committed.
97     *
98     * @return StatusValue isOK on success
99     * @throws ImportException when critical validations fail
100     */
101    public function validate(): StatusValue {
102        $errorCode = $this->uploadBase->validateTitle();
103        if ( $errorCode !== UploadBase::OK ) {
104            $this->logger->error(
105                __METHOD__ . " failed to validate title, error code {$errorCode}",
106                [ 'fileRevision-getFields' => $this->fileRevision->getFields() ]
107            );
108            return StatusValue::newFatal( 'fileimporter-filenameerror-illegal' );
109        }
110
111        // Even administrators should not (accidentially) move a file to a protected file name
112        if ( $this->restrictionStore->isProtected( $this->plannedTitle ) ) {
113            return StatusValue::newFatal( 'fileimporter-filenameerror-protected' );
114        }
115
116        $fileValidationStatus = $this->uploadBase->validateFile();
117        if ( !$fileValidationStatus->isOK() ) {
118            $message = Status::wrap( $fileValidationStatus )->getMessage();
119            return StatusValue::newFatal( 'fileimporter-cantimportfileinvalid', $message );
120        }
121
122        return $this->uploadBase->validateUpload(
123            $this->user,
124            $this->textRevision
125        );
126    }
127
128    /**
129     * Commit this operation to persistent storage.
130     * @return StatusValue isOK on success
131     */
132    public function commit(): StatusValue {
133        $status = $this->importer->import( $this->wikiRevision );
134
135        if ( !$status->isGood() ) {
136            $this->logger->error( __METHOD__ . ' failed to commit.', [
137                'fileRevision-getFields' => $this->fileRevision->getFields(),
138                'status' => $status->__toString(),
139            ] );
140        }
141
142        /**
143         * Core only creates log entries for the latest revision. This results in a complete upload
144         * log only when the revisions are uploaded in chronological order, and all using the same
145         * file name.
146         *
147         * Here we are not only working in reverse chronological order, but also with archive file
148         * names that are all different. Core can't know if it needs to create historical log
149         * entries for these.
150         *
151         * According to {@see \LocalFile::publishTo} the {@see \StatusValue::$value} contains the
152         * archive file name.
153         */
154        if ( $status->value !== '' ) {
155            $this->createUploadLog();
156        }
157
158        return $status;
159    }
160
161    /**
162     * @see \LocalFile::recordUpload2
163     */
164    private function createUploadLog(): void {
165        $username = $this->wikiRevision->getUser();
166        $performer = $this->userLookup->getUserIdentityByName( $username );
167        if ( !$performer ) {
168            throw new UnexpectedValueException( "Unexpected non-normalized username \"$username\"" );
169        }
170
171        $logEntry = new ManualLogEntry( 'upload', 'upload' );
172        $logEntry->setTimestamp( $this->wikiRevision->getTimestamp() );
173        $logEntry->setPerformer( $performer );
174        $logEntry->setComment( $this->wikiRevision->getComment() );
175        $logEntry->setAssociatedRevId( $this->wikiRevision->getID() );
176        $logEntry->setTarget( $this->wikiRevision->getTitle() );
177        $logEntry->setParameters(
178            [
179                'img_sha1' => $this->wikiRevision->getSha1(),
180                'img_timestamp' => $this->wikiRevision->getTimestamp()
181            ]
182        );
183
184        $logId = $logEntry->insert();
185        $logEntry->publish( $logId );
186    }
187
188    /**
189     * @return WikiRevision|null
190     */
191    public function getWikiRevision() {
192        return $this->wikiRevision;
193    }
194
195}