Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 106
0.00% covered (danger)
0.00%
0 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
UploadJobTrait
0.00% covered (danger)
0.00%
0 / 105
0.00% covered (danger)
0.00%
0 / 10
930
0.00% covered (danger)
0.00%
0 / 1
 initialiseUploadJob
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 allowRetries
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 run
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
90
 getCacheKey
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getUserFromSession
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 setStatus
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
 fetchFile
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 verifyUpload
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
72
 performUpload
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
12
 setStatusDone
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 getUpload
n/a
0 / 0
n/a
0 / 0
0
 logJobParams
n/a
0 / 0
n/a
0 / 0
0
 setLastError
n/a
0 / 0
n/a
0 / 0
0
 addTeardownCallback
n/a
0 / 0
n/a
0 / 0
0
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 * @defgroup JobQueue JobQueue
20 */
21
22namespace MediaWiki\JobQueue\Jobs;
23
24use Exception;
25use MediaWiki\Api\ApiUpload;
26use MediaWiki\Context\RequestContext;
27use MediaWiki\Exception\MWExceptionHandler;
28use MediaWiki\Logger\LoggerFactory;
29use MediaWiki\Status\Status;
30use MediaWiki\User\User;
31use UploadBase;
32use Wikimedia\ScopedCallback;
33
34/**
35 * Common functionality for async uploads
36 *
37 * @ingroup Upload
38 * @ingroup JobQueue
39 */
40trait UploadJobTrait {
41    /** @var User|null */
42    private $user;
43
44    /** @var string */
45    private $cacheKey;
46
47    /** @var UploadBase */
48    private $upload;
49
50    /** @var array The job parameters */
51    public $params;
52
53    /**
54     * Set up the job
55     *
56     * @param string $cacheKey
57     * @return void
58     */
59    protected function initialiseUploadJob( $cacheKey ): void {
60        $this->cacheKey = $cacheKey;
61        $this->user = null;
62    }
63
64    /**
65     * Do not allow retries on jobs by default.
66     */
67    public function allowRetries(): bool {
68        return false;
69    }
70
71    /**
72     * Run the job
73     */
74    public function run(): bool {
75        $this->user = $this->getUserFromSession();
76        if ( $this->user === null ) {
77            return false;
78        }
79
80        try {
81            // Check the initial status of the upload
82            $startingStatus = UploadBase::getSessionStatus( $this->user, $this->cacheKey );
83            // Warn if in wrong stage, but still continue. User may be able to trigger
84            // this by retrying after failure.
85            if (
86                !$startingStatus ||
87                ( $startingStatus['result'] ?? '' ) !== 'Poll' ||
88                ( $startingStatus['stage'] ?? '' ) !== 'queued'
89            ) {
90                $logger = LoggerFactory::getInstance( 'upload' );
91                $logger->warning( "Tried to publish upload that is in stage {stage}/{result}",
92                    $this->logJobParams( $startingStatus )
93                );
94            }
95
96            // Fetch the file if needed
97            if ( !$this->fetchFile() ) {
98                return false;
99            }
100
101            // Verify the upload is valid
102            if ( !$this->verifyUpload() ) {
103                return false;
104            }
105
106            // Actually upload the file
107            if ( !$this->performUpload() ) {
108                return false;
109            }
110
111            // All done
112            $this->setStatusDone();
113
114            // Cleanup any temporary local file
115            $this->getUpload()->cleanupTempFile();
116
117        } catch ( Exception $e ) {
118            $this->setStatus( 'publish', 'Failure', Status::newFatal( 'api-error-publishfailed' ) );
119            $this->setLastError( get_class( $e ) . ": " . $e->getMessage() );
120            // To prevent potential database referential integrity issues.
121            // See T34551.
122            MWExceptionHandler::rollbackPrimaryChangesAndLog( $e );
123            return false;
124        }
125
126        return true;
127    }
128
129    /**
130     * Get the cache key used to store status
131     *
132     * @return string
133     */
134    public function getCacheKey() {
135        return $this->cacheKey;
136    }
137
138    /**
139     * Get user data from the session key
140     *
141     * @return User|null
142     */
143    private function getUserFromSession() {
144        $scope = RequestContext::importScopedSession( $this->params['session'] );
145        $this->addTeardownCallback( static function () use ( &$scope ) {
146            ScopedCallback::consume( $scope ); // T126450
147        } );
148
149        $context = RequestContext::getMain();
150        $user = $context->getUser();
151        if ( !$user->isRegistered() ) {
152            $this->setLastError( "Could not load the author user from session." );
153
154            return null;
155        }
156        return $user;
157    }
158
159    /**
160     * Set the upload status
161     *
162     * @param string $stage
163     * @param string $result
164     * @param Status|null $status
165     * @param array $additionalInfo
166     *
167     */
168    private function setStatus( $stage, $result = 'Poll', $status = null, $additionalInfo = [] ) {
169        // We're most probably not running in a job.
170        // @todo maybe throw an exception?
171        if ( $this->user === null ) {
172            return;
173        }
174        $status ??= Status::newGood();
175        $info = [ 'result' => $result, 'stage' => $stage, 'status' => $status ];
176        $info += $additionalInfo;
177        UploadBase::setSessionStatus(
178            $this->user,
179            $this->cacheKey,
180            $info
181        );
182    }
183
184    /**
185     * Ensure we have the file available. A noop here.
186     */
187    protected function fetchFile(): bool {
188        $this->setStatus( 'fetching' );
189        // make sure the upload file is here. This is a noop in most cases.
190        $status = $this->getUpload()->fetchFile();
191        if ( !$status->isGood() ) {
192            $this->setStatus( 'fetching', 'Failure', $status );
193            $this->setLastError( "Error while fetching the image." );
194            return false;
195        }
196        $this->setStatus( 'publish' );
197        // We really don't care as this is, as mentioned, generally a noop.
198        // When that's not the case, classes will need to override this method anyways.
199        return true;
200    }
201
202    /**
203     * Verify the upload is ok
204     */
205    private function verifyUpload(): bool {
206        // Check if the local file checks out (this is generally a no-op)
207        $verification = $this->getUpload()->verifyUpload();
208        if ( $verification['status'] !== UploadBase::OK ) {
209            $status = Status::newFatal( 'verification-error' );
210            $status->value = [ 'verification' => $verification ];
211            $this->setStatus( 'publish', 'Failure', $status );
212            $this->setLastError( "Could not verify upload." );
213            return false;
214        }
215        // Verify title permissions for this user
216        $status = $this->getUpload()->authorizeUpload( $this->user );
217        if ( !$status->isGood() ) {
218            $this->setStatus( 'publish', 'Failure', Status::wrap( $status ) );
219            $this->setLastError( "Could not verify title permissions." );
220            return false;
221        }
222
223        // Verify if any upload warnings are present
224        $ignoreWarnings = $this->params['ignorewarnings'] ?? false;
225        $isReupload = $this->params['reupload'] ?? false;
226        if ( $ignoreWarnings ) {
227            // If we're ignoring warnings, we don't need to check them
228            return true;
229        }
230        $warnings = $this->getUpload()->checkWarnings( $this->user );
231        if ( $warnings ) {
232            // If the file exists and we're reuploading, ignore the warning
233            // and continue with the upload
234            if ( count( $warnings ) === 1 && isset( $warnings['exists'] ) && $isReupload ) {
235                return true;
236            }
237            // Make the array serializable
238            $serializableWarnings = UploadBase::makeWarningsSerializable( $warnings );
239            $this->setStatus( 'publish', 'Warning', null, [ 'warnings' => $serializableWarnings ] );
240            $this->setLastError( "Upload warnings present." );
241            return false;
242        }
243
244        return true;
245    }
246
247    /**
248     * Upload the stashed file to a permanent location
249     */
250    private function performUpload(): bool {
251        if ( $this->user === null ) {
252            return false;
253        }
254        $status = $this->getUpload()->performUpload(
255            $this->params['comment'],
256            $this->params['text'],
257            $this->params['watch'],
258            $this->user,
259            $this->params['tags'] ?? [],
260            $this->params['watchlistexpiry'] ?? null
261        );
262        if ( !$status->isGood() ) {
263            $this->setStatus( 'publish', 'Failure', $status );
264            $this->setLastError( $status->getWikiText( false, false, 'en' ) );
265            return false;
266        }
267        return true;
268    }
269
270    /**
271     * Set the status at the end or processing
272     *
273     */
274    private function setStatusDone() {
275        // Build the image info array while we have the local reference handy
276        $imageInfo = ApiUpload::getDummyInstance()->getUploadImageInfo( $this->getUpload() );
277
278        // Cache the info so the user doesn't have to wait forever to get the final info
279        $this->setStatus(
280            'publish',
281            'Success',
282            Status::newGood(),
283            [ 'filename' => $this->getUpload()->getLocalFile()->getName(), 'imageinfo' => $imageInfo ]
284        );
285    }
286
287    /**
288     * Getter for the upload. Needs to be implemented by the job class
289     *
290     * @return UploadBase
291     */
292    abstract protected function getUpload(): UploadBase;
293
294    /**
295     * Get the job parameters for logging. Needs to be implemented by the job class.
296     *
297     * @param Status[] $status
298     * @return array
299     */
300    abstract protected function logJobParams( $status ): array;
301
302    /**
303     * This is actually implemented in the Job class
304     *
305     * @param mixed $error
306     * @return void
307     */
308    abstract protected function setLastError( $error );
309
310    /**
311     * This is actually implemented in the Job class
312     *
313     * @param callable $callback
314     * @return void
315     */
316    abstract protected function addTeardownCallback( $callback );
317
318}
319
320/** @deprecated class alias since 1.44 */
321class_alias( UploadJobTrait::class, 'UploadJobTrait' );