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 / 106 |
|
0.00% |
0 / 10 |
992 | |
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 / 12 |
|
0.00% |
0 / 1 |
12 | |||
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 | use MediaWiki\Context\RequestContext; |
23 | use MediaWiki\Logger\LoggerFactory; |
24 | use MediaWiki\Status\Status; |
25 | use MediaWiki\User\User; |
26 | use Wikimedia\ScopedCallback; |
27 | |
28 | /** |
29 | * Common functionality for async uploads |
30 | * |
31 | * @ingroup Upload |
32 | * @ingroup JobQueue |
33 | */ |
34 | trait 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 | } |