Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
4.19% |
7 / 167 |
|
0.00% |
0 / 11 |
CRAP | |
0.00% |
0 / 1 |
MediaUploader | |
4.19% |
7 / 167 |
|
0.00% |
0 / 11 |
1669.10 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
0.00% |
0 / 38 |
|
0.00% |
0 / 1 |
110 | |||
loadConfig | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
tryLoadCampaignConfig | |
0.00% |
0 / 29 |
|
0.00% |
0 / 1 |
56 | |||
displayError | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
addJsVars | |
0.00% |
0 / 52 |
|
0.00% |
0 / 1 |
72 | |||
getMaxUploads | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
20 | |||
isUploadAllowed | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
isUserUploadAllowed | |
77.78% |
7 / 9 |
|
0.00% |
0 / 1 |
4.18 | |||
getWizardHtml | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
12 | |||
getGroupName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | /** |
3 | * Special:MediaUploader |
4 | * |
5 | * Easy to use multi-file upload page. |
6 | * |
7 | * @file |
8 | * @ingroup SpecialPage |
9 | * @ingroup Upload |
10 | */ |
11 | |
12 | namespace MediaWiki\Extension\MediaUploader\Special; |
13 | |
14 | use BitmapHandler; |
15 | use ChangeTags; |
16 | use DerivativeContext; |
17 | use Html; |
18 | use MediaWiki\Extension\MediaUploader\Campaign\CampaignStore; |
19 | use MediaWiki\Extension\MediaUploader\Campaign\Exception\BaseCampaignException; |
20 | use MediaWiki\Extension\MediaUploader\Config\ConfigFactory; |
21 | use MediaWiki\Extension\MediaUploader\Config\ParsedConfig; |
22 | use MediaWiki\Extension\MediaUploader\Config\RawConfig; |
23 | use MediaWiki\Extension\MediaUploader\Hooks\RegistrationHooks; |
24 | use MediaWiki\User\UserOptionsLookup; |
25 | use MediaWiki\Widget\SpinnerWidget; |
26 | use PermissionsError; |
27 | use SpecialPage; |
28 | use Title; |
29 | use UploadBase; |
30 | use User; |
31 | use UserBlockedError; |
32 | |
33 | class MediaUploader extends SpecialPage { |
34 | /** |
35 | * The name of the upload wizard campaign, or null when none is specified. |
36 | * |
37 | * @var string|null |
38 | */ |
39 | private $campaign = null; |
40 | |
41 | /** @var ParsedConfig|null */ |
42 | private $loadedConfig = null; |
43 | |
44 | /** @var ConfigFactory */ |
45 | private $configFactory; |
46 | |
47 | /** @var RawConfig */ |
48 | private $rawConfig; |
49 | |
50 | /** @var CampaignStore */ |
51 | private $campaignStore; |
52 | |
53 | /** @var UserOptionsLookup */ |
54 | private $userOptionsLookup; |
55 | |
56 | public function __construct( |
57 | RawConfig $rawConfig, |
58 | ConfigFactory $configFactory, |
59 | CampaignStore $campaignStore, |
60 | UserOptionsLookup $userOptionsLookup |
61 | ) { |
62 | parent::__construct( 'MediaUploader', 'upload' ); |
63 | |
64 | $this->configFactory = $configFactory; |
65 | $this->rawConfig = $rawConfig; |
66 | $this->campaignStore = $campaignStore; |
67 | $this->userOptionsLookup = $userOptionsLookup; |
68 | } |
69 | |
70 | /** |
71 | * Replaces default execute method |
72 | * Checks whether uploading enabled, user permissions okay, |
73 | * @param string|null $subPage subpage, e.g. the "foo" in Special:MediaUploader/foo. |
74 | */ |
75 | public function execute( $subPage ) { |
76 | // side effects: if we can't upload, will print error page to wgOut |
77 | // and return false |
78 | if ( !( $this->isUploadAllowed() && $this->isUserUploadAllowed( $this->getUser() ) ) ) { |
79 | return; |
80 | } |
81 | |
82 | $this->setHeaders(); |
83 | $this->outputHeader(); |
84 | |
85 | $req = $this->getRequest(); |
86 | |
87 | $urlOverrides = []; |
88 | $urlArgs = [ 'description', 'lat', 'lon', 'alt' ]; |
89 | |
90 | foreach ( $urlArgs as $arg ) { |
91 | $value = $req->getText( $arg ); |
92 | if ( $value ) { |
93 | $urlOverrides['defaults'][$arg] = $value; |
94 | } |
95 | } |
96 | |
97 | $categories = $req->getText( 'categories' ); |
98 | if ( $categories ) { |
99 | $urlOverrides['defaults']['categories'] = explode( '|', $categories ); |
100 | } |
101 | |
102 | $fields = $req->getArray( 'fields' ); |
103 | |
104 | # Support id and id2 for field0 and field1 |
105 | # Legacy support for old URL structure. They override fields[] |
106 | if ( $req->getText( 'id' ) ) { |
107 | $fields[0] = $req->getText( 'id' ); |
108 | } |
109 | |
110 | if ( $req->getText( 'id2' ) ) { |
111 | $fields[1] = $req->getText( 'id2' ); |
112 | } |
113 | |
114 | if ( $fields ) { |
115 | foreach ( $fields as $index => $value ) { |
116 | $urlOverrides['fields'][$index]['initialValue'] = $value; |
117 | } |
118 | } |
119 | |
120 | $this->loadConfig( $urlOverrides ); |
121 | |
122 | $out = $this->getOutput(); |
123 | |
124 | // fallback for non-JS |
125 | $out->addHTML( '<div class="mediauploader-unavailable">' ); |
126 | $out->addHTML( '<p class="errorbox">' . $this->msg( 'mediauploader-unavailable' )->parse() . '</p>' ); |
127 | // create a simple form for non-JS fallback, which targets the old Special:Upload page. |
128 | // at some point, if we completely subsume its functionality, change that to point here again, |
129 | // but then we'll need to process non-JS uploads in the same way Special:Upload does. |
130 | $derivativeContext = new DerivativeContext( $this->getContext() ); |
131 | $derivativeContext->setTitle( SpecialPage::getTitleFor( 'Upload' ) ); |
132 | $simpleForm = new MediaUploaderSimpleForm( [], $derivativeContext, $this->getLinkRenderer() ); |
133 | $simpleForm->show(); |
134 | $out->addHTML( '</div>' ); |
135 | |
136 | // global javascript variables |
137 | $this->addJsVars( $subPage ); |
138 | |
139 | // dependencies (css, js) |
140 | $out->addModules( [ 'ext.uploadWizard.page' ] ); |
141 | $out->addModuleStyles( [ |
142 | 'ext.uploadWizard.page.styles', |
143 | // load spinner styles early |
144 | 'jquery.spinner.styles' |
145 | ] ); |
146 | |
147 | // where the uploader will go |
148 | // TODO import more from MediaUploader's createInterface call. |
149 | $out->addHTML( $this->getWizardHtml() ); |
150 | } |
151 | |
152 | /** |
153 | * Loads the appropriate config. |
154 | * |
155 | * @param array $urlOverrides |
156 | */ |
157 | protected function loadConfig( array $urlOverrides ): void { |
158 | $this->tryLoadCampaignConfig( $urlOverrides ); |
159 | |
160 | // This is not a campaign or the campaign failed to load |
161 | // Either way, we fall back to the global config |
162 | if ( $this->loadedConfig === null ) { |
163 | |
164 | $this->loadedConfig = $this->configFactory->newGlobalConfig( |
165 | $this->getOutput()->parserOptions(), |
166 | $urlOverrides |
167 | ); |
168 | } |
169 | } |
170 | |
171 | /** |
172 | * Attempts to load a campaign config. |
173 | * Sets $this->loadedConfig if successful. |
174 | * |
175 | * @param array $urlOverrides |
176 | */ |
177 | private function tryLoadCampaignConfig( array $urlOverrides ): void { |
178 | // Establish the name of the campaign to load |
179 | $campaignName = $this->getRequest()->getVal( 'campaign' ); |
180 | if ( $campaignName === null ) { |
181 | $campaignName = $this->rawConfig->getSetting( 'defaultCampaign' ); |
182 | } |
183 | |
184 | if ( $campaignName === null || $campaignName === '' ) { |
185 | return; |
186 | } |
187 | |
188 | // Load it |
189 | $campaignTitle = Title::newFromText( $campaignName, NS_CAMPAIGN ); |
190 | $record = $this->campaignStore->getCampaignByDBkey( |
191 | $campaignTitle->getDBkey(), |
192 | CampaignStore::SELECT_TITLE | CampaignStore::SELECT_CONTENT |
193 | ); |
194 | |
195 | // Handle all possible cases where we should reject this campaign |
196 | if ( $record === null ) { |
197 | $this->displayError( |
198 | $this->msg( 'mediauploader-error-nosuchcampaign', $campaignName )->text() |
199 | ); |
200 | return; |
201 | } |
202 | |
203 | if ( !$record->isEnabled() ) { |
204 | $this->displayError( |
205 | $this->msg( 'mediauploader-error-campaigndisabled', $campaignName )->text() |
206 | ); |
207 | return; |
208 | } |
209 | |
210 | // Load the config |
211 | try { |
212 | $this->loadedConfig = $this->configFactory->newCampaignConfig( |
213 | $this->getOutput()->parserOptions(), |
214 | $record, |
215 | $campaignTitle, |
216 | $urlOverrides |
217 | ); |
218 | $this->campaign = $campaignName; |
219 | } catch ( BaseCampaignException $e ) { |
220 | $this->displayError( $e->getMessage() ); |
221 | } |
222 | } |
223 | |
224 | /** |
225 | * Display an error message. |
226 | * |
227 | * @since 1.2 |
228 | * |
229 | * @param string $message |
230 | */ |
231 | protected function displayError( $message ) { |
232 | $this->getOutput()->addHTML( Html::element( |
233 | 'span', |
234 | [ 'class' => 'errorbox' ], |
235 | $message |
236 | ) . '<br /><br /><br />' ); |
237 | } |
238 | |
239 | /** |
240 | * Adds some global variables for our use, as well as initializes the MediaUploader |
241 | * |
242 | * TODO This should be factored out somewhere so that MediaUploader can be included |
243 | * dynamically. |
244 | * |
245 | * @param string $subPage subpage, e.g. the "foo" in Special:MediaUploader/foo |
246 | */ |
247 | public function addJsVars( $subPage ) { |
248 | $config = $this->loadedConfig->getConfigArray(); |
249 | |
250 | // TODO: use CampaignRecord::getTrackingCategoryName |
251 | if ( array_key_exists( 'trackingCategory', $config ) ) { |
252 | if ( array_key_exists( 'campaign', $config['trackingCategory'] ) ) { |
253 | if ( $this->campaign !== null ) { |
254 | $config['trackingCategory']['campaign'] = str_replace( |
255 | '$1', |
256 | $this->campaign, |
257 | $config['trackingCategory']['campaign'] |
258 | ); |
259 | } else { |
260 | unset( $config['trackingCategory']['campaign'] ); |
261 | } |
262 | } |
263 | } |
264 | |
265 | // Get the user's default license. This will usually be 'default', but |
266 | // can be a specific license like 'ownwork-cc-zero'. |
267 | $userDefaultLicense = $this->userOptionsLookup->getOption( $this->getUser(), 'upwiz_deflicense' ); |
268 | |
269 | if ( $userDefaultLicense !== 'default' ) { |
270 | $licenseParts = explode( '-', $userDefaultLicense, 2 ); |
271 | $userLicenseType = $licenseParts[0]; |
272 | $userDefaultLicense = $licenseParts[1]; |
273 | |
274 | // Determine if the user's default license is valid for this campaign |
275 | $defaultInAllowedLicenses = in_array( |
276 | $userLicenseType, |
277 | $config['licensing']['showTypes'] |
278 | ) && in_array( |
279 | $userDefaultLicense, |
280 | $this->loadedConfig->getAvailableLicenses( $userLicenseType ) |
281 | ); |
282 | |
283 | if ( $defaultInAllowedLicenses ) { |
284 | $config['licensing'][$userLicenseType]['defaults'] = [ $userDefaultLicense ]; |
285 | $config['licensing']['defaultType'] = $userLicenseType; |
286 | |
287 | if ( $userDefaultLicense === 'custom' ) { |
288 | $config['licenses']['custom']['defaultText'] = |
289 | $this->userOptionsLookup->getOption( $this->getUser(), 'upwiz_deflicense_custom' ); |
290 | } |
291 | } |
292 | } |
293 | |
294 | // add an 'uploadwizard' tag, but only if it'll be allowed |
295 | $status = ChangeTags::canAddTagsAccompanyingChange( |
296 | RegistrationHooks::CHANGE_TAGS, |
297 | $this->getUser() |
298 | ); |
299 | $config['CanAddTags'] = $status->isOK(); |
300 | |
301 | // Upload comment should be localized with respect to the wiki's language |
302 | $config['uploadComment'] = [ |
303 | 'ownWork' => $this->msg( 'mediauploader-upload-comment-own-work' ) |
304 | ->inContentLanguage()->plain(), |
305 | 'thirdParty' => $this->msg( 'mediauploader-upload-comment-third-party' ) |
306 | ->inContentLanguage()->plain() |
307 | ]; |
308 | |
309 | // maxUploads depends on the user's rights |
310 | $canMassUpload = $this->getUser()->isAllowed( 'mass-upload' ); |
311 | $config['maxUploads'] = $this->getMaxUploads( |
312 | $config['maxUploads'], |
313 | $canMassUpload, |
314 | 50 |
315 | ); |
316 | |
317 | $bitmapHandler = new BitmapHandler(); |
318 | $this->getOutput()->addJsConfigVars( |
319 | [ |
320 | 'MediaUploaderConfig' => $config, |
321 | 'wgFileCanRotate' => $bitmapHandler->canRotate(), |
322 | ] |
323 | ); |
324 | } |
325 | |
326 | /** |
327 | * Returns the value for the maxUploads setting, based on |
328 | * whether the user has the mass-upload user right. |
329 | * |
330 | * @param mixed $setting |
331 | * @param bool $canMassUpload |
332 | * @param int $default |
333 | * |
334 | * @return mixed |
335 | */ |
336 | private function getMaxUploads( $setting, bool $canMassUpload, int $default ) { |
337 | if ( is_array( $setting ) ) { |
338 | if ( $canMassUpload && in_array( 'mass_upload', $setting ) ) { |
339 | return $setting['mass-upload']; |
340 | } else { |
341 | return $setting['*'] ?? $default; |
342 | } |
343 | } |
344 | return $setting; |
345 | } |
346 | |
347 | /** |
348 | * Check if anyone can upload (or if other sitewide config prevents this) |
349 | * Side effect: will print error page to wgOut if cannot upload. |
350 | * @return bool -- true if can upload |
351 | */ |
352 | private function isUploadAllowed() { |
353 | // Check uploading enabled |
354 | if ( !UploadBase::isEnabled() ) { |
355 | $this->getOutput()->showErrorPage( 'uploaddisabled', 'uploaddisabledtext' ); |
356 | return false; |
357 | } |
358 | |
359 | // Check whether we actually want to allow changing stuff |
360 | $this->checkReadOnly(); |
361 | |
362 | // we got all the way here, so it must be okay to upload |
363 | return true; |
364 | } |
365 | |
366 | /** |
367 | * Check if the user can upload |
368 | * Side effect: will print error page to wgOut if cannot upload. |
369 | * @param User $user |
370 | * @throws PermissionsError |
371 | * @throws UserBlockedError |
372 | * @return bool -- true if can upload |
373 | */ |
374 | private function isUserUploadAllowed( User $user ) { |
375 | // Check permissions |
376 | $permissionRequired = UploadBase::isAllowed( $user ); |
377 | if ( $permissionRequired !== true ) { |
378 | throw new PermissionsError( $permissionRequired ); |
379 | } |
380 | |
381 | // Check blocks |
382 | if ( $user->isBlockedFromUpload() ) { |
383 | // If the user is blocked from uploading then there is a block |
384 | // @phan-suppress-next-line PhanTypeMismatchArgumentNullable |
385 | throw new UserBlockedError( $user->getBlock() ); |
386 | } |
387 | |
388 | // Global blocks |
389 | $globalBlock = $user->getGlobalBlock(); |
390 | if ( $globalBlock ) { |
391 | throw new UserBlockedError( $globalBlock ); |
392 | } |
393 | |
394 | // we got all the way here, so it must be okay to upload |
395 | return true; |
396 | } |
397 | |
398 | /** |
399 | * Return the basic HTML structure for the entire page |
400 | * Will be enhanced by the javascript to actually do stuff |
401 | * @return string html |
402 | * TODO: check this suppression |
403 | * @suppress SecurityCheck-XSS The documentation of $config['display']['headerLabel'] says, |
404 | * it is wikitext, but all *label are used as html |
405 | */ |
406 | protected function getWizardHtml() { |
407 | $config = $this->loadedConfig->getConfigArray(); |
408 | |
409 | if ( array_key_exists( |
410 | 'display', $config ) && array_key_exists( 'headerLabel', $config['display'] ) |
411 | ) { |
412 | $this->getOutput()->addHTML( $config['display']['headerLabel'] ); |
413 | } |
414 | |
415 | // TODO move this into UploadWizard.js or some other javascript resource so the upload wizard |
416 | // can be dynamically included ( for example the add media wizard ) |
417 | return '<div id="upload-wizard" class="upload-section">' . |
418 | '<div id="mediauploader-tutorial-html" style="display:none;">' . |
419 | $config['tutorial']['html'] . |
420 | '</div>' . |
421 | '<div class="mwe-first-spinner">' . |
422 | new SpinnerWidget( [ 'size' => 'large' ] ) . |
423 | '</div>' . |
424 | '</div>'; |
425 | } |
426 | |
427 | /** |
428 | * @inheritDoc |
429 | */ |
430 | protected function getGroupName() { |
431 | return 'media'; |
432 | } |
433 | } |