wikimedia/mediawiki-core

View on GitHub
includes/specials/forms/UploadForm.php

Summary

Maintainability
D
2 days
Test Coverage
<?php
/**
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 * http://www.gnu.org/copyleft/gpl.html
 *
 * @file
 */

use MediaWiki\Context\IContextSource;
use MediaWiki\HookContainer\HookContainer;
use MediaWiki\HookContainer\HookRunner;
use MediaWiki\Html\Html;
use MediaWiki\HTMLForm\HTMLForm;
use MediaWiki\Linker\LinkRenderer;
use MediaWiki\MainConfigNames;
use MediaWiki\MediaWikiServices;
use MediaWiki\Specials\SpecialUpload;
use MediaWiki\Status\Status;
use MediaWiki\Title\NamespaceInfo;
use Wikimedia\RequestTimeout\TimeoutException;

/**
 * Sub class of HTMLForm that provides the form section of SpecialUpload
 */
class UploadForm extends HTMLForm {
    /** @var bool */
    protected $mWatch;
    /** @var bool */
    protected $mForReUpload;
    /** @var string */
    protected $mSessionKey;
    /** @var bool */
    protected $mHideIgnoreWarning;
    /** @var bool */
    protected $mDestWarningAck;
    /** @var string */
    protected $mDestFile;

    /** @var string */
    protected $mComment;
    /** @var string raw html */
    protected $mTextTop;
    /** @var string raw html */
    protected $mTextAfterSummary;

    /** @var string[] */
    protected $mSourceIds;

    /** @var array */
    protected $mMaxUploadSize = [];

    private LocalRepo $localRepo;
    private Language $contentLanguage;
    private NamespaceInfo $nsInfo;
    private HookRunner $hookRunner;

    /**
     * @param array $options
     * @param IContextSource|null $context
     * @param LinkRenderer|null $linkRenderer
     * @param LocalRepo|null $localRepo
     * @param Language|null $contentLanguage
     * @param NamespaceInfo|null $nsInfo
     * @param HookContainer|null $hookContainer
     */
    public function __construct(
        array $options = [],
        IContextSource $context = null,
        LinkRenderer $linkRenderer = null,
        LocalRepo $localRepo = null,
        Language $contentLanguage = null,
        NamespaceInfo $nsInfo = null,
        HookContainer $hookContainer = null
    ) {
        if ( $context instanceof IContextSource ) {
            $this->setContext( $context );
        }

        $services = MediaWikiServices::getInstance();
        if ( !$linkRenderer ) {
            $linkRenderer = $services->getLinkRenderer();
        }
        if ( !$localRepo ) {
            $localRepo = $services->getRepoGroup()->getLocalRepo();
        }
        if ( !$contentLanguage ) {
            $contentLanguage = $services->getContentLanguage();
        }
        if ( !$nsInfo ) {
            $nsInfo = $services->getNamespaceInfo();
        }
        $this->localRepo = $localRepo;
        $this->contentLanguage = $contentLanguage;
        $this->nsInfo = $nsInfo;
        $this->hookRunner = new HookRunner( $hookContainer ?? $services->getHookContainer() );

        $this->mWatch = !empty( $options['watch'] );
        $this->mForReUpload = !empty( $options['forreupload'] );
        $this->mSessionKey = $options['sessionkey'] ?? '';
        $this->mHideIgnoreWarning = !empty( $options['hideignorewarning'] );
        $this->mDestWarningAck = !empty( $options['destwarningack'] );
        $this->mDestFile = $options['destfile'] ?? '';

        $this->mComment = $options['description'] ?? '';

        $this->mTextTop = $options['texttop'] ?? '';

        $this->mTextAfterSummary = $options['textaftersummary'] ?? '';

        $sourceDescriptor = $this->getSourceSection();
        $descriptor = $sourceDescriptor
            + $this->getDescriptionSection()
            + $this->getOptionsSection();

        $this->hookRunner->onUploadFormInitDescriptor( $descriptor );
        parent::__construct( $descriptor, $this->getContext(), 'upload' );

        # Add a link to edit MediaWiki:Licenses
        if ( $this->getAuthority()->isAllowed( 'editinterface' ) ) {
            $this->getOutput()->addModuleStyles( 'mediawiki.special' );
            $licensesLink = $linkRenderer->makeKnownLink(
                $this->msg( 'licenses' )->inContentLanguage()->getTitle(),
                $this->msg( 'licenses-edit' )->text(),
                [],
                [ 'action' => 'edit' ]
            );
            $editLicenses = '<p class="mw-upload-editlicenses">' . $licensesLink . '</p>';
            $this->addFooterHtml( $editLicenses, 'description' );
        }

        # Set some form properties
        $this->setSubmitTextMsg( 'uploadbtn' );
        $this->setSubmitName( 'wpUpload' );
        # Used message keys: 'accesskey-upload', 'tooltip-upload'
        $this->setSubmitTooltip( 'upload' );
        $this->setId( 'mw-upload-form' );

        # Build a list of IDs for javascript insertion
        $this->mSourceIds = [];
        foreach ( $sourceDescriptor as $field ) {
            if ( !empty( $field['id'] ) ) {
                $this->mSourceIds[] = $field['id'];
            }
        }
    }

    /**
     * Get the descriptor of the fieldset that contains the file source
     * selection. The section is 'source'
     *
     * @return array Descriptor array
     */
    protected function getSourceSection() {
        if ( $this->mSessionKey ) {
            return [
                'SessionKey' => [
                    'type' => 'hidden',
                    'default' => $this->mSessionKey,
                ],
                'SourceType' => [
                    'type' => 'hidden',
                    'default' => 'Stash',
                ],
            ];
        }

        $canUploadByUrl = UploadFromUrl::isEnabled()
            && ( UploadFromUrl::isAllowed( $this->getAuthority() ) === true )
            && $this->getConfig()->get( MainConfigNames::CopyUploadsFromSpecialUpload );
        $radio = $canUploadByUrl;
        $selectedSourceType = strtolower( $this->getRequest()->getText( 'wpSourceType', 'File' ) );

        $descriptor = [];
        if ( $this->mTextTop ) {
            $descriptor['UploadFormTextTop'] = [
                'type' => 'info',
                'section' => 'source',
                'default' => $this->mTextTop,
                'raw' => true,
            ];
        }

        $this->mMaxUploadSize['file'] = min(
            UploadBase::getMaxUploadSize( 'file' ),
            UploadBase::getMaxPhpUploadSize()
        );

        $help = $this->msg( 'upload-maxfilesize' )->sizeParams( $this->mMaxUploadSize['file'] )->parse();

        // If the user can also upload by URL, there are 2 different file size limits.
        // This extra message helps stress which limit corresponds to what.
        if ( $canUploadByUrl ) {
            $help .= $this->msg( 'word-separator' )->escaped();
            $help .= $this->msg( 'upload_source_file' )->parse();
        }

        $descriptor['UploadFile'] = [
            'class' => UploadSourceField::class,
            'section' => 'source',
            'type' => 'file',
            'id' => 'wpUploadFile',
            'radio-id' => 'wpSourceTypeFile',
            'label-message' => 'sourcefilename',
            'upload-type' => 'File',
            'radio' => &$radio,
            'help' => $help,
            'checked' => $selectedSourceType == 'file',
        ];

        if ( $canUploadByUrl ) {
            $this->mMaxUploadSize['url'] = UploadBase::getMaxUploadSize( 'url' );
            $descriptor['UploadFileURL'] = [
                'class' => UploadSourceField::class,
                'section' => 'source',
                'id' => 'wpUploadFileURL',
                'radio-id' => 'wpSourceTypeurl',
                'label-message' => 'sourceurl',
                'upload-type' => 'url',
                'radio' => &$radio,
                'help' => $this->msg( 'upload-maxfilesize' )->sizeParams( $this->mMaxUploadSize['url'] )->parse() .
                    $this->msg( 'word-separator' )->escaped() .
                    $this->msg( 'upload_source_url' )->parse(),
                'checked' => $selectedSourceType == 'url',
            ];
        }
        $this->hookRunner->onUploadFormSourceDescriptors(
            $descriptor, $radio, $selectedSourceType );

        $descriptor['Extensions'] = [
            'type' => 'info',
            'section' => 'source',
            'default' => $this->getExtensionsMessage(),
            'raw' => true,
        ];

        return $descriptor;
    }

    /**
     * Get the messages indicating which extensions are preferred and prohibited.
     *
     * @return string HTML string containing the message
     */
    protected function getExtensionsMessage() {
        # Print a list of allowed file extensions, if so configured.  We ignore
        # MIME type here, it's incomprehensible to most people and too long.
        $config = $this->getConfig();

        if ( $config->get( MainConfigNames::CheckFileExtensions ) ) {
            $fileExtensions = array_unique( $config->get( MainConfigNames::FileExtensions ) );
            if ( $config->get( MainConfigNames::StrictFileExtensions ) ) {
                # Everything not permitted is banned
                $extensionsList =
                    '<div id="mw-upload-permitted">' .
                    $this->msg( 'upload-permitted' )
                        ->params( $this->getLanguage()->commaList( $fileExtensions ) )
                        ->numParams( count( $fileExtensions ) )
                        ->parseAsBlock() .
                    "</div>\n";
            } else {
                # We have to list both preferred and prohibited
                $prohibitedExtensions =
                    array_unique( $config->get( MainConfigNames::ProhibitedFileExtensions ) );
                $extensionsList =
                    '<div id="mw-upload-preferred">' .
                        $this->msg( 'upload-preferred' )
                            ->params( $this->getLanguage()->commaList( $fileExtensions ) )
                            ->numParams( count( $fileExtensions ) )
                            ->parseAsBlock() .
                    "</div>\n" .
                    '<div id="mw-upload-prohibited">' .
                        $this->msg( 'upload-prohibited' )
                            ->params( $this->getLanguage()->commaList( $prohibitedExtensions ) )
                            ->numParams( count( $prohibitedExtensions ) )
                            ->parseAsBlock() .
                    "</div>\n";
            }
        } else {
            # Everything is permitted.
            $extensionsList = '';
        }

        return $extensionsList;
    }

    /**
     * Get the descriptor of the fieldset that contains the file description
     * input. The section is 'description'
     *
     * @return array Descriptor array
     */
    protected function getDescriptionSection() {
        $config = $this->getConfig();
        if ( $this->mSessionKey ) {
            $stash = $this->localRepo->getUploadStash( $this->getUser() );
            try {
                $file = $stash->getFile( $this->mSessionKey );
            } catch ( TimeoutException $e ) {
                throw $e;
            } catch ( Exception $e ) {
                $file = null;
            }
            if ( $file ) {
                $mto = $file->transform( [ 'width' => 120 ] );
                if ( $mto ) {
                    $this->addHeaderHtml(
                        '<div class="thumb t' .
                        $this->contentLanguage->alignEnd() . '">' .
                        Html::element( 'img', [
                            'src' => $mto->getUrl(),
                            'class' => 'thumbimage',
                        ] ) . '</div>', 'description' );
                }
            }
        }

        $descriptor = [
            'DestFile' => [
                'type' => 'text',
                'section' => 'description',
                'id' => 'wpDestFile',
                'label-message' => 'destfilename',
                'size' => 60,
                'default' => $this->mDestFile,
                # @todo FIXME: Hack to work around poor handling of the 'default' option in HTMLForm
                'nodata' => strval( $this->mDestFile ) !== '',
            ],
            'UploadDescription' => [
                'type' => $this->mForReUpload
                    ? 'text'
                    : 'textarea',
                'section' => 'description',
                'id' => 'wpUploadDescription',
                'label-message' => $this->mForReUpload
                    ? 'filereuploadsummary'
                    : 'fileuploadsummary',
                'default' => $this->mComment,
            ]
        ];
        if ( $this->mTextAfterSummary ) {
            $descriptor['UploadFormTextAfterSummary'] = [
                'type' => 'info',
                'section' => 'description',
                'default' => $this->mTextAfterSummary,
                'raw' => true,
            ];
        }

        $descriptor += [
            'EditTools' => [
                'type' => 'edittools',
                'section' => 'description',
                'message' => 'edittools-upload',
            ]
        ];

        if ( $this->mForReUpload ) {
            $descriptor['DestFile']['readonly'] = true;
            $descriptor['UploadDescription']['size'] = 60;
        } else {
            $descriptor['License'] = [
                'type' => 'select',
                'class' => Licenses::class,
                'section' => 'description',
                'id' => 'wpLicense',
                'label-message' => 'license',
            ];
            $descriptor['UploadDescription']['rows'] = 8;
        }

        if ( $config->get( MainConfigNames::UseCopyrightUpload ) ) {
            $descriptor['UploadCopyStatus'] = [
                'type' => 'text',
                'section' => 'description',
                'id' => 'wpUploadCopyStatus',
                'label-message' => 'filestatus',
            ];
            $descriptor['UploadSource'] = [
                'type' => 'text',
                'section' => 'description',
                'id' => 'wpUploadSource',
                'label-message' => 'filesource',
            ];
        }

        return $descriptor;
    }

    /**
     * Get the descriptor of the fieldset that contains the upload options,
     * such as "watch this file". The section is 'options'
     *
     * @return array Descriptor array
     */
    protected function getOptionsSection() {
        $user = $this->getUser();
        if ( $user->isRegistered() ) {
            $descriptor = [
                'Watchthis' => [
                    'type' => 'check',
                    'id' => 'wpWatchthis',
                    'label-message' => 'watchthisupload',
                    'section' => 'options',
                    'default' => $this->mWatch,
                ]
            ];
        }
        if ( !$this->mHideIgnoreWarning ) {
            $descriptor['IgnoreWarning'] = [
                'type' => 'check',
                'id' => 'wpIgnoreWarning',
                'label-message' => 'ignorewarnings',
                'section' => 'options',
            ];
        }

        $descriptor['DestFileWarningAck'] = [
            'type' => 'hidden',
            'id' => 'wpDestFileWarningAck',
            'default' => $this->mDestWarningAck ? '1' : '',
        ];

        if ( $this->mForReUpload ) {
            $descriptor['ForReUpload'] = [
                'type' => 'hidden',
                'id' => 'wpForReUpload',
                'default' => '1',
            ];
        }

        return $descriptor;
    }

    /**
     * Add the upload JS and show the form.
     * @return bool|Status
     */
    public function show() {
        $this->addUploadJS();
        return parent::show();
    }

    /**
     * Add upload JS to the OutputPage
     */
    protected function addUploadJS() {
        $config = $this->getConfig();

        $this->mMaxUploadSize['*'] = UploadBase::getMaxUploadSize();

        $scriptVars = [
            'wgAjaxLicensePreview' => $config->get( MainConfigNames::AjaxLicensePreview ),
            'wgUploadAutoFill' => !$this->mForReUpload &&
                // If we received mDestFile from the request, don't autofill
                // the wpDestFile textbox
                $this->mDestFile === '',
            'wgUploadSourceIds' => $this->mSourceIds,
            'wgCheckFileExtensions' => $config->get( MainConfigNames::CheckFileExtensions ),
            'wgStrictFileExtensions' => $config->get( MainConfigNames::StrictFileExtensions ),
            'wgFileExtensions' =>
                array_values( array_unique( $config->get( MainConfigNames::FileExtensions ) ) ),
            'wgMaxUploadSize' => $this->mMaxUploadSize,
            'wgFileCanRotate' => SpecialUpload::rotationEnabled(),
        ];

        $out = $this->getOutput();
        $out->addJsConfigVars( $scriptVars );

        $out->addModules( [
            'mediawiki.special.upload', // Extras for thumbnail and license preview.
        ] );
    }

    /**
     * Empty function; submission is handled elsewhere.
     *
     * @return bool False
     */
    public function trySubmit() {
        return false;
    }
}