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