Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
4.19% covered (danger)
4.19%
7 / 167
0.00% covered (danger)
0.00%
0 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
MediaUploader
4.19% covered (danger)
4.19%
7 / 167
0.00% covered (danger)
0.00%
0 / 11
1669.10
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 38
0.00% covered (danger)
0.00%
0 / 1
110
 loadConfig
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 tryLoadCampaignConfig
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
56
 displayError
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 addJsVars
0.00% covered (danger)
0.00%
0 / 52
0.00% covered (danger)
0.00%
0 / 1
72
 getMaxUploads
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 isUploadAllowed
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 isUserUploadAllowed
77.78% covered (warning)
77.78%
7 / 9
0.00% covered (danger)
0.00%
0 / 1
4.18
 getWizardHtml
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 getGroupName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
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
12namespace MediaWiki\Extension\MediaUploader\Special;
13
14use BitmapHandler;
15use ChangeTags;
16use DerivativeContext;
17use Html;
18use MediaWiki\Extension\MediaUploader\Campaign\CampaignStore;
19use MediaWiki\Extension\MediaUploader\Campaign\Exception\BaseCampaignException;
20use MediaWiki\Extension\MediaUploader\Config\ConfigFactory;
21use MediaWiki\Extension\MediaUploader\Config\ParsedConfig;
22use MediaWiki\Extension\MediaUploader\Config\RawConfig;
23use MediaWiki\Extension\MediaUploader\Hooks\RegistrationHooks;
24use MediaWiki\User\UserOptionsLookup;
25use MediaWiki\Widget\SpinnerWidget;
26use PermissionsError;
27use SpecialPage;
28use Title;
29use UploadBase;
30use User;
31use UserBlockedError;
32
33class 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}