Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 106 |
|
0.00% |
0 / 10 |
CRAP | |
0.00% |
0 / 1 |
UploadJobTrait | |
0.00% |
0 / 105 |
|
0.00% |
0 / 10 |
930 | |
0.00% |
0 / 1 |
initialiseUploadJob | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
allowRetries | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
run | |
0.00% |
0 / 25 |
|
0.00% |
0 / 1 |
90 | |||
getCacheKey | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getUserFromSession | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
6 | |||
setStatus | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
6 | |||
fetchFile | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
6 | |||
verifyUpload | |
0.00% |
0 / 25 |
|
0.00% |
0 / 1 |
72 | |||
performUpload | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
12 | |||
setStatusDone | |
0.00% |
0 / 7 |
|
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 | |
22 | namespace MediaWiki\JobQueue\Jobs; |
23 | |
24 | use Exception; |
25 | use MediaWiki\Api\ApiUpload; |
26 | use MediaWiki\Context\RequestContext; |
27 | use MediaWiki\Exception\MWExceptionHandler; |
28 | use MediaWiki\Logger\LoggerFactory; |
29 | use MediaWiki\Status\Status; |
30 | use MediaWiki\User\User; |
31 | use UploadBase; |
32 | use Wikimedia\ScopedCallback; |
33 | |
34 | /** |
35 | * Common functionality for async uploads |
36 | * |
37 | * @ingroup Upload |
38 | * @ingroup JobQueue |
39 | */ |
40 | trait 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 */ |
321 | class_alias( UploadJobTrait::class, 'UploadJobTrait' ); |