Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
9.80% covered (danger)
9.80%
10 / 102
CRAP
18.78% covered (danger)
18.78%
392 / 2087
EditPage
0.00% covered (danger)
0.00%
0 / 1
9.80% covered (danger)
9.80%
10 / 102
251326.07
18.78% covered (danger)
18.78%
392 / 2087
 __construct
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
20 / 20
 getArticle
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 getContext
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 getTitle
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 setContextTitle
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 getContextTitle
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 3
 isSupportedContentModel
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
2 / 2
 setApiEditOverride
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 edit
0.00% covered (danger)
0.00%
0 / 1
992
0.00% covered (danger)
0.00%
0 / 76
 getEditPermissionErrors
0.00% covered (danger)
0.00%
0 / 1
72
0.00% covered (danger)
0.00%
0 / 25
 displayPermissionsError
0.00% covered (danger)
0.00%
0 / 1
56
0.00% covered (danger)
0.00%
0 / 13
 displayViewSourcePage
0.00% covered (danger)
0.00%
0 / 1
30
0.00% covered (danger)
0.00%
0 / 28
 previewOnOpen
0.00% covered (danger)
0.00%
0 / 1
132
0.00% covered (danger)
0.00%
0 / 19
 isWrongCaseUserConfigPage
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 9
 isSectionEditSupported
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
6 / 6
 importFormData
0.00% covered (danger)
0.00%
0 / 1
152.04
55.26% covered (warning)
55.26%
63 / 114
 importContentFormData
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 initialiseForm
0.00% covered (danger)
0.00%
0 / 1
240
0.00% covered (danger)
0.00%
0 / 41
 getContentObject
0.00% covered (danger)
0.00%
0 / 1
1722
0.00% covered (danger)
0.00%
0 / 129
 getUndoContent
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
15 / 15
 getOriginalContent
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 8
 getParentRevId
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 3
 getCurrentContent
0.00% covered (danger)
0.00%
0 / 1
72
0.00% covered (danger)
0.00%
0 / 30
 setPreloadedContent
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 getPreloadedContent
0.00% covered (danger)
0.00%
0 / 1
90
0.00% covered (danger)
0.00%
0 / 28
 isPageExistingAndViewable
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 1
 tokenOk
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 5
 setPostEditCookie
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 10
 attemptSave
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 5
 incrementResolvedConflicts
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 4
 handleStatus
0.00% covered (danger)
0.00%
0 / 1
1482
0.00% covered (danger)
0.00%
0 / 84
 runPostMergeFilters
0.00% covered (danger)
0.00%
0 / 1
26.60
26.32% covered (danger)
26.32%
5 / 19
 formatStatusErrors
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 5
 newSectionSummary
0.00% covered (danger)
0.00%
0 / 1
6.00
50.00% covered (danger)
50.00%
7 / 14
 internalAttemptSave
0.00% covered (danger)
0.00%
0 / 1
925.74
54.17% covered (warning)
54.17%
156 / 288
 isUndoClean
0.00% covered (danger)
0.00%
0 / 1
8.06
90.00% covered (success)
90.00%
18 / 20
 addContentModelChangeLogEntry
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 11
 updateWatchlist
0.00% covered (danger)
0.00%
0 / 1
3.05
81.82% covered (warning)
81.82%
9 / 11
 mergeChangesIntoContent
0.00% covered (danger)
0.00%
0 / 1
6.02
91.30% covered (success)
91.30%
21 / 23
 getBaseRevision
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 5
 getExpectedParentRevision
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
12 / 12
 matchSpamRegex
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 matchSummarySpamRegex
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 setHeaders
0.00% covered (danger)
0.00%
0 / 1
210
0.00% covered (danger)
0.00%
0 / 35
 showIntro
0.00% covered (danger)
0.00%
0 / 1
650
0.00% covered (danger)
0.00%
0 / 62
 showCustomIntro
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 9
 toEditText
0.00% covered (danger)
0.00%
0 / 1
6.60
60.00% covered (warning)
60.00%
3 / 5
 toEditContent
0.00% covered (danger)
0.00%
0 / 1
4.05
85.71% covered (warning)
85.71%
6 / 7
 showEditForm
0.00% covered (danger)
0.00%
0 / 1
1190
0.00% covered (danger)
0.00%
0 / 135
 makeTemplatesOnThisPageList
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 9
 extractSectionTitle
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
4 / 4
 showHeader
0.00% covered (danger)
0.00%
0 / 1
1482
0.00% covered (danger)
0.00%
0 / 107
 getSummaryInputAttributes
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 7
 getSummaryInputWidget
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 14
 showSummaryInput
0.00% covered (danger)
0.00%
0 / 1
42
0.00% covered (danger)
0.00%
0 / 12
 getSummaryPreview
0.00% covered (danger)
0.00%
0 / 1
42
0.00% covered (danger)
0.00%
0 / 12
 showFormBeforeText
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 7
 showFormAfterText
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 4
 showContentForm
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 showTextbox1
0.00% covered (danger)
0.00%
0 / 1
30
0.00% covered (danger)
0.00%
0 / 16
 showTextbox2
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 showTextbox
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 9
 displayPreviewArea
0.00% covered (danger)
0.00%
0 / 1
42
0.00% covered (danger)
0.00%
0 / 24
 showPreview
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 8
 showDiff
0.00% covered (danger)
0.00%
0 / 1
156
0.00% covered (danger)
0.00%
0 / 34
 showHeaderCopyrightWarning
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 5
 showTosSummary
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 8
 showEditTools
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 4
 getCopywarn
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 getCopyrightWarning
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 12
 getPreviewLimitReport
0.00% covered (danger)
0.00%
0 / 1
72
0.00% covered (danger)
0.00%
0 / 24
 showStandardInputs
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 29
 showConflict
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 6
 incrementConflictStats
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 getCancelLink
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 11
 getActionURL
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 wasDeletedSinceLastEdit
0.00% covered (danger)
0.00%
0 / 1
6.29
80.00% covered (warning)
80.00%
8 / 10
 getLastDelete
0.00% covered (danger)
0.00%
0 / 1
4.05
85.71% covered (warning)
85.71%
18 / 21
 getPreviewText
0.00% covered (danger)
0.00%
0 / 1
930
0.00% covered (danger)
0.00%
0 / 90
 incrementEditFailureStats
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 3
 getPreviewParserOptions
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 5
 doPreviewParse
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 9
 getTemplates
0.00% covered (danger)
0.00%
0 / 1
42
0.00% covered (danger)
0.00%
0 / 9
 getEditToolbar
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 5
 getCheckboxesDefinition
0.00% covered (danger)
0.00%
0 / 1
5.34
56.25% covered (warning)
56.25%
9 / 16
 getCheckboxesDefinitionForWatchlist
0.00% covered (danger)
0.00%
0 / 1
30
0.00% covered (danger)
0.00%
0 / 24
 getCheckboxesWidget
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 28
 getSubmitButtonLabel
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 6
 getEditButtons
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 33
 noSuchSectionPage
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 7
 spamPageWithContent
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 16
 addEditNotices
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 10
 addTalkPageText
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 3
 addLongPageWarningHeader
0.00% covered (danger)
0.00%
0 / 1
30
0.00% covered (danger)
0.00%
0 / 16
 addPageProtectionWarningHeaders
0.00% covered (danger)
0.00%
0 / 1
90
0.00% covered (danger)
0.00%
0 / 23
 addExplainConflictHeader
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 3
 buildTextboxAttribs
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 addNewLineAtEnd
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 guessSectionName
0.00% covered (danger)
0.00%
0 / 1
3.04
83.33% covered (warning)
83.33%
5 / 6
 setEditConflictHelperFactory
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 3
 getEditConflictHelper
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 5
 newTextConflictHelper
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 6
<?php
/**
 * User interface for page editing.
 *
 * 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\Block\DatabaseBlock;
use MediaWiki\Content\IContentHandlerFactory;
use MediaWiki\EditPage\IEditObject;
use MediaWiki\EditPage\TextboxBuilder;
use MediaWiki\EditPage\TextConflictHelper;
use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
use MediaWiki\Logger\LoggerFactory;
use MediaWiki\MediaWikiServices;
use MediaWiki\Permissions\PermissionManager;
use MediaWiki\Revision\RevisionRecord;
use MediaWiki\Revision\RevisionStore;
use MediaWiki\Revision\RevisionStoreRecord;
use MediaWiki\Revision\SlotRecord;
use OOUI\CheckboxInputWidget;
use OOUI\DropdownInputWidget;
use OOUI\FieldLayout;
use Wikimedia\ParamValidator\TypeDef\ExpiryDef;
use Wikimedia\ScopedCallback;
/**
 * The edit page/HTML interface (split from Article)
 * The actual database and text munging is still in Article,
 * but it should get easier to call those from alternate
 * interfaces.
 *
 * EditPage cares about two distinct titles:
 * $this->mContextTitle is the page that forms submit to, links point to,
 * redirects go to, etc. $this->mTitle (as well as $mArticle) is the
 * page in the database that is actually being edited. These are
 * usually the same, but they are now allowed to be different.
 *
 * Surgeon General's Warning: prolonged exposure to this class is known to cause
 * headaches, which may be fatal.
 *
 * @newable
 * @note marked as newable in 1.35 for lack of a better alternative,
 *       but should be split up into service objects and command objects
 *       in the future (T157658).
 */
class EditPage implements IEditObject {
    use DeprecationHelper;
    use ProtectedHookAccessorTrait;
    /**
     * Used for Unicode support checks
     */
    public const UNICODE_CHECK = 'ℳ𝒲β™₯π“Šπ“ƒπ’Ύπ’Έβ„΄π’Ήβ„―';
    /**
     * HTML id and name for the beginning of the edit form.
     */
    public const EDITFORM_ID = 'editform';
    /**
     * Prefix of key for cookie used to pass post-edit state.
     * The revision id edited is added after this
     */
    public const POST_EDIT_COOKIE_KEY_PREFIX = 'PostEditRevision';
    /**
     * Duration of PostEdit cookie, in seconds.
     * The cookie will be removed instantly if the JavaScript runs.
     *
     * Otherwise, though, we don't want the cookies to accumulate.
     * RFC 2109 ( https://www.ietf.org/rfc/rfc2109.txt ) specifies a possible
     * limit of only 20 cookies per domain. This still applies at least to some
     * versions of IE without full updates:
     * https://blogs.msdn.com/b/ieinternals/archive/2009/08/20/wininet-ie-cookie-internals-faq.aspx
     *
     * A value of 20 minutes should be enough to take into account slow loads and minor
     * clock skew while still avoiding cookie accumulation when JavaScript is turned off.
     */
    public const POST_EDIT_COOKIE_DURATION = 1200;
    /**
     * @deprecated for public usage since 1.30 use EditPage::getArticle()
     * @var Article
     */
    public $mArticle;
    /** @var WikiPage */
    private $page;
    /**
     * @deprecated for public usage since 1.30 use EditPage::getTitle()
     * @var Title
     */
    public $mTitle;
    /** @var null|Title */
    private $mContextTitle = null;
    /** @var string */
    public $action = 'submit';
    /** @var bool Whether an edit conflict needs to be resolved. Detected based on whether
     * $editRevId is different than the latest revision. When a conflict has successfully
     * been resolved by a 3-way-merge, this field is set to false.
     */
    public $isConflict = false;
    /** @var bool New page or new section */
    public $isNew = false;
    /**
     * @var bool
     * @internal
     */
    public $deletedSinceEdit;
    /** @var string */
    public $formtype;
    /** @var bool
     * True the first time the edit form is rendered, false after re-rendering
     * with diff, save prompts, etc.
     */
    public $firsttime;
    /**
     * @var bool|stdClass
     * @internal
     */
    public $lastDelete;
    /**
     * @var bool
     * @internal
     */
    public $mTokenOk = false;
    /**
     * @var bool
     * @internal
     */
    public $mTokenOkExceptSuffix = false;
    /**
     * @var bool
     * @internal
     */
    public $mTriedSave = false;
    /**
     * @var bool
     * @internal
     */
    public $incompleteForm = false;
    /**
     * @var bool
     * @internal
     */
    public $tooBig = false;
    /**
     * @var bool
     * @internal
     */
    public $missingComment = false;
    /**
     * @var bool
     * @internal
     */
    public $missingSummary = false;
    /**
     * @var bool
     * @internal
     */
    public $allowBlankSummary = false;
    /** @var bool */
    protected $blankArticle = false;
    /** @var bool */
    protected $allowBlankArticle = false;
    /** @var bool */
    protected $selfRedirect = false;
    /** @var bool */
    protected $allowSelfRedirect = false;
    /**
     * @var string
     * @internal
     */
    public $autoSumm = '';
    /** @var string */
    private $hookError = '';
    /**
     * @var ParserOutput
     * @internal
     */
    public $mParserOutput;
    /**
     * @var bool Has a summary been preset using GET parameter &summary= ?
     * @internal
     */
    public $hasPresetSummary = false;
    /**
     * @var Revision|bool|null
     *
     * A revision object corresponding to $this->editRevId.
     * Formerly public as part of using Revision objects
     *
     * @deprecated since 1.35
     */
    protected $mBaseRevision = false;
    /**
     * @var RevisionRecord|bool|null
     *
     * A RevisionRecord corresponding to $this->editRevId or $this->edittime
     * Replaced $mBaseRevision
     */
    private $mExpectedParentRevision = false;
    /** @var bool */
    public $mShowSummaryField = true;
    # Form values
    /** @var bool */
    public $save = false;
    /** @var bool */
    public $preview = false;
    /** @var bool */
    public $diff = false;
    /**
     * @var bool
     * @internal
     */
    public $minoredit = false;
    /**
     * @var bool
     * @internal
     */
    public $watchthis = false;
    /** @var bool Corresponds to $wgWatchlistExpiry */
    private $watchlistExpiryEnabled = false;
    /** @var WatchedItemStoreInterface */
    private $watchedItemStore;
    /** @var string|null The expiry time of the watch item, or null if it is not watched temporarily. */
    private $watchlistExpiry;
    /**
     * @var bool
     * @internal
     */
    public $recreate = false;
    /** @var string
     * Page content input field.
     */
    public $textbox1 = '';
    /** @var string */
    public $textbox2 = '';
    /** @var string */
    public $summary = '';
    /**
     * @var bool
     * @internal
     * If true, hide the summary field.
     */
    public $nosummary = false;
    /** @var string
     * Timestamp of the latest revision of the page when editing was initiated
     * on the client.
     */
    public $edittime = '';
    /** @var int Revision ID of the latest revision of the page when editing
     * was initiated on the client.  This is used to detect and resolve edit
     * conflicts.
     *
     * @note 0 if the page did not exist at that time.
     * @note When starting an edit from an old revision, this still records the current
     * revision at the time, not the one the edit is based on.
     *
     * @see $oldid
     * @see getExpectedParentRevision()
     */
    private $editRevId = null;
    /** @var string */
    public $section = '';
    /** @var string */
    public $sectiontitle = '';
    /** @var string
     * Timestamp from the first time the edit form was rendered.
     */
    public $starttime = '';
    /** @var int Revision ID the edit is based on, or 0 if it's the current revision.
     * FIXME: This isn't used in conflict resolution--provide a better
     * justification or merge with parentRevId.
     * @see $editRevId
     */
    public $oldid = 0;
    /**
     * @var int Revision ID the edit is based on, adjusted when an edit conflict is resolved.
     * @internal
     * @see $editRevId
     * @see $oldid
     * @see getparentRevId()
     */
    public $parentRevId = 0;
    /**
     * @var string
     * @internal
     */
    public $editintro = '';
    /**
     * @var int|null
     * @internal
     */
    public $scrolltop = null;
    /**
     * @var bool
     * @internal
     */
    public $markAsBot = true;
    /** @var string */
    public $contentModel;
    /** @var null|string */
    public $contentFormat = null;
    /** @var null|array */
    private $changeTags = null;
    # Placeholders for text injection by hooks (must be HTML)
    # extensions should take care to _append_ to the present value
    /** @var string Before even the preview */
    public $editFormPageTop = '';
    public $editFormTextTop = '';
    public $editFormTextBeforeContent = '';
    public $editFormTextAfterWarn = '';
    public $editFormTextAfterTools = '';
    public $editFormTextBottom = '';
    public $editFormTextAfterContent = '';
    public $previewTextAfterContent = '';
    public $mPreloadContent = null;
    /* $didSave should be set to true whenever an article was successfully altered. */
    public $didSave = false;
    public $undidRev = 0;
    public $undoAfter = 0;
    public $suppressIntro = false;
    /** @var bool */
    protected $edit;
    /** @var bool|int */
    protected $contentLength = false;
    /**
     * @var bool Set in ApiEditPage, based on ContentHandler::allowsDirectApiEditing
     */
    private $enableApiEditOverride = false;
    /**
     * @var IContextSource
     */
    protected $context;
    /**
     * @var bool Whether an old revision is edited
     */
    private $isOldRev = false;
    /**
     * @var string|null What the user submitted in the 'wpUnicodeCheck' field
     */
    private $unicodeCheck;
    /**
     * Factory function to create an edit conflict helper
     *
     * @var callable
     */
    private $editConflictHelperFactory;
    /**
     * @var TextConflictHelper|null
     */
    private $editConflictHelper;
    /**
     * @var IContentHandlerFactory
     */
    private $contentHandlerFactory;
    /**
     * @var PermissionManager
     */
    private $permManager;
    /**
     * @var RevisionStore
     */
    private $revisionStore;
    /**
     * @stable to call
     * @param Article $article
     */
    public function __construct( Article $article ) {
        $this->mArticle = $article;
        $this->page = $article->getPage(); // model object
        $this->mTitle = $article->getTitle();
        // Make sure the local context is in sync with other member variables.
        // Particularly make sure everything is using the same WikiPage instance.
        // This should probably be the case in Article as well, but it's
        // particularly important for EditPage, to make use of the in-place caching
        // facility in WikiPage::prepareContentForEdit.
        $this->context = new DerivativeContext( $article->getContext() );
        $this->context->setWikiPage( $this->page );
        $this->context->setTitle( $this->mTitle );
        $this->contentModel = $this->mTitle->getContentModel();
        $services = MediaWikiServices::getInstance();
        $this->contentHandlerFactory = $services->getContentHandlerFactory();
        $this->contentFormat = $this->contentHandlerFactory
            ->getContentHandler( $this->contentModel )
            ->getDefaultFormat();
        $this->editConflictHelperFactory = [ $this, 'newTextConflictHelper' ];
        $this->permManager = $services->getPermissionManager();
        $this->revisionStore = $services->getRevisionStore();
        $this->watchlistExpiryEnabled = $this->getContext()->getConfig() instanceof Config
            && $this->getContext()->getConfig()->get( 'WatchlistExpiry' );
        $this->watchedItemStore = $services->getWatchedItemStore();
        $this->deprecatePublicProperty( 'mBaseRevision', '1.35', __CLASS__ );
    }
    /**
     * @return Article
     */
    public function getArticle() {
        return $this->mArticle;
    }
    /**
     * @since 1.28
     * @return IContextSource
     */
    public function getContext() {
        return $this->context;
    }
    /**
     * @since 1.19
     * @return Title
     */
    public function getTitle() {
        return $this->mTitle;
    }
    /**
     * Set the context Title object
     *
     * @param Title|null $title Title object or null
     */
    public function setContextTitle( $title ) {
        $this->mContextTitle = $title;
    }
    /**
     * Get the context title object.
     *
     * @throws RuntimeException if no context title was set
     * @return Title
     */
    public function getContextTitle() {
        if ( $this->mContextTitle === null ) {
            throw new RuntimeException( "EditPage does not have a context title set" );
        } else {
            return $this->mContextTitle;
        }
    }
    /**
     * Returns if the given content model is editable.
     *
     * @param string $modelId The ID of the content model to test. Use CONTENT_MODEL_XXX constants.
     * @return bool
     * @throws MWException If $modelId has no known handler
     */
    public function isSupportedContentModel( $modelId ) {
        return $this->enableApiEditOverride === true ||
            $this->contentHandlerFactory->getContentHandler( $modelId )->supportsDirectEditing();
    }
    /**
     * Allow editing of content that supports API direct editing, but not general
     * direct editing. Set to false by default.
     *
     * @param bool $enableOverride
     */
    public function setApiEditOverride( $enableOverride ) {
        $this->enableApiEditOverride = $enableOverride;
    }
    /**
     * This is the function that gets called for "action=edit". It
     * sets up various member variables, then passes execution to
     * another function, usually showEditForm()
     *
     * The edit form is self-submitting, so that when things like
     * preview and edit conflicts occur, we get the same form back
     * with the extra stuff added.  Only when the final submission
     * is made and all is well do we actually save and redirect to
     * the newly-edited page.
     */
    public function edit() {
        // Allow extensions to modify/prevent this form or submission
        if ( !$this->getHookRunner()->onAlternateEdit( $this ) ) {
            return;
        }
        wfDebug( __METHOD__ . ": enter" );
        $request = $this->context->getRequest();
        // If they used redlink=1 and the page exists, redirect to the main article
        if ( $request->getBool( 'redlink' ) && $this->mTitle->exists() ) {
            $this->context->getOutput()->redirect( $this->mTitle->getFullURL() );
            return;
        }
        $this->importFormData( $request );
        $this->firsttime = false;
        if ( wfReadOnly() && $this->save ) {
            // Force preview
            $this->save = false;
            $this->preview = true;
        }
        if ( $this->save ) {
            $this->formtype = 'save';
        } elseif ( $this->preview ) {
            $this->formtype = 'preview';
        } elseif ( $this->diff ) {
            $this->formtype = 'diff';
        } else { # First time through
            $this->firsttime = true;
            if ( $this->previewOnOpen() ) {
                $this->formtype = 'preview';
            } else {
                $this->formtype = 'initial';
            }
        }
        $permErrors = $this->getEditPermissionErrors(
            $this->save ? PermissionManager::RIGOR_SECURE : PermissionManager::RIGOR_FULL
        );
        if ( $permErrors ) {
            wfDebug( __METHOD__ . ": User can't edit" );
            if ( $this->context->getUser()->getBlock() ) {
                // Auto-block user's IP if the account was "hard" blocked
                if ( !wfReadOnly() ) {
                    DeferredUpdates::addCallableUpdate( function () {
                        $this->context->getUser()->spreadAnyEditBlock();
                    } );
                }
            }
            $this->displayPermissionsError( $permErrors );
            return;
        }
        $revRecord = $this->mArticle->fetchRevisionRecord();
        // Disallow editing revisions with content models different from the current one
        // Undo edits being an exception in order to allow reverting content model changes.
        $revContentModel = $revRecord ?
            $revRecord->getSlot( SlotRecord::MAIN, RevisionRecord::RAW )->getModel() :
            false;
        if ( $revContentModel && $revContentModel !== $this->contentModel ) {
            $prevRevRecord = null;
            $prevContentModel = false;
            if ( $this->undidRev ) {
                $undidRevRecord = $this->revisionStore
                    ->getRevisionById( $this->undidRev );
                $prevRevRecord = $undidRevRecord ?
                    $this->revisionStore->getPreviousRevision( $undidRevRecord ) :
                    null;
                $prevContentModel = $prevRevRecord ?
                    $prevRevRecord
                        ->getSlot( SlotRecord::MAIN, RevisionRecord::RAW )
                        ->getModel() :
                    '';
            }
            if ( !$this->undidRev
                || !$prevRevRecord
                || $prevContentModel !== $this->contentModel
            ) {
                $this->displayViewSourcePage(
                    $this->getContentObject(),
                    $this->context->msg(
                        'contentmodelediterror',
                        $revContentModel,
                        $this->contentModel
                    )->plain()
                );
                return;
            }
        }
        $this->isConflict = false;
        # Show applicable editing introductions
        if ( $this->formtype == 'initial' || $this->firsttime ) {
            $this->showIntro();
        }
        # Attempt submission here.  This will check for edit conflicts,
        # and redundantly check for locked database, blocked IPs, etc.
        # that edit() already checked just in case someone tries to sneak
        # in the back door with a hand-edited submission URL.
        if ( $this->formtype == 'save' ) {
            $resultDetails = null;
            $status = $this->attemptSave( $resultDetails );
            if ( !$this->handleStatus( $status, $resultDetails ) ) {
                return;
            }
        }
        # First time through: get contents, set time for conflict
        # checking, etc.
        if ( $this->formtype == 'initial' || $this->firsttime ) {
            if ( $this->initialiseForm() === false ) {
                return;
            }
            if ( !$this->mTitle->getArticleID() ) {
                $this->getHookRunner()->onEditFormPreloadText( $this->textbox1, $this->mTitle );
            } else {
                $this->getHookRunner()->onEditFormInitialText( $this );
            }
        }
        $this->showEditForm();
    }
    /**
     * @param string $rigor PermissionManager::RIGOR_ constant
     * @return array
     */
    protected function getEditPermissionErrors( $rigor = PermissionManager::RIGOR_SECURE ) {
        $user = $this->context->getUser();
        $permErrors = $this->permManager->getPermissionErrors(
            'edit',
            $user,
            $this->mTitle,
            $rigor
        );
        # Can this title be created?
        if ( !$this->mTitle->exists() ) {
            $permErrors = array_merge(
                $permErrors,
                wfArrayDiff2(
                    $this->permManager->getPermissionErrors(
                        'create',
                        $user,
                        $this->mTitle,
                        $rigor
                    ),
                    $permErrors
                )
            );
        }
        # Ignore some permissions errors when a user is just previewing/viewing diffs
        $remove = [];
        foreach ( $permErrors as $error ) {
            if ( ( $this->preview || $this->diff )
                && (
                    $error[0] == 'blockedtext' ||
                    $error[0] == 'autoblockedtext' ||
                    $error[0] == 'systemblockedtext'
                )
            ) {
                $remove[] = $error;
            }
        }
        $permErrors = wfArrayDiff2( $permErrors, $remove );
        return $permErrors;
    }
    /**
     * Display a permissions error page, like OutputPage::showPermissionsErrorPage(),
     * but with the following differences:
     * - If redlink=1, the user will be redirected to the page
     * - If there is content to display or the error occurs while either saving,
     *   previewing or showing the difference, it will be a
     *   "View source for ..." page displaying the source code after the error message.
     *
     * @since 1.19
     * @param array $permErrors Array of permissions errors
     * @throws PermissionsError
     */
    protected function displayPermissionsError( array $permErrors ) {
        $out = $this->context->getOutput();
        if ( $this->context->getRequest()->getBool( 'redlink' ) ) {
            // The edit page was reached via a red link.
            // Redirect to the article page and let them click the edit tab if
            // they really want a permission error.
            $out->redirect( $this->mTitle->getFullURL() );
            return;
        }
        $content = $this->getContentObject();
        # Use the normal message if there's nothing to display
        if ( $this->firsttime && ( !$content || $content->isEmpty() ) ) {
            $action = $this->mTitle->exists() ? 'edit' :
                ( $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage' );
            throw new PermissionsError( $action, $permErrors );
        }
        $this->displayViewSourcePage(
            $content,
            $out->formatPermissionsErrorMessage( $permErrors, 'edit' )
        );
    }
    /**
     * Display a read-only View Source page
     * @param Content $content
     * @param string $errorMessage additional wikitext error message to display
     */
    protected function displayViewSourcePage( Content $content, $errorMessage = '' ) {
        $out = $this->context->getOutput();
        $this->getHookRunner()->onEditPage__showReadOnlyForm_initial( $this, $out );
        $out->setRobotPolicy( 'noindex,nofollow' );
        $out->setPageTitle( $this->context->msg(
            'viewsource-title',
            $this->getContextTitle()->getPrefixedText()
        ) );
        $out->addBacklinkSubtitle( $this->getContextTitle() );
        $out->addHTML( $this->editFormPageTop );
        $out->addHTML( $this->editFormTextTop );
        if ( $errorMessage !== '' ) {
            $out->addWikiTextAsInterface( $errorMessage );
            $out->addHTML( "<hr />\n" );
        }
        # If the user made changes, preserve them when showing the markup
        # (This happens when a user is blocked during edit, for instance)
        if ( !$this->firsttime ) {
            $text = $this->textbox1;
            $out->addWikiMsg( 'viewyourtext' );
        } else {
            try {
                $text = $this->toEditText( $content );
            } catch ( MWException $e ) {
                # Serialize using the default format if the content model is not supported
                # (e.g. for an old revision with a different model)
                $text = $content->serialize();
            }
            $out->addWikiMsg( 'viewsourcetext' );
        }
        $out->addHTML( $this->editFormTextBeforeContent );
        $this->showTextbox( $text, 'wpTextbox1', [ 'readonly' ] );
        $out->addHTML( $this->editFormTextAfterContent );
        $out->addHTML( $this->makeTemplatesOnThisPageList( $this->getTemplates() ) );
        $out->addModules( 'mediawiki.action.edit.collapsibleFooter' );
        $out->addHTML( $this->editFormTextBottom );
        if ( $this->mTitle->exists() ) {
            $out->returnToMain( null, $this->mTitle );
        }
    }
    /**
     * Should we show a preview when the edit form is first shown?
     *
     * @return bool
     */
    protected function previewOnOpen() {
        $config = $this->context->getConfig();
        $previewOnOpenNamespaces = $config->get( 'PreviewOnOpenNamespaces' );
        $request = $this->context->getRequest();
        if ( $config->get( 'RawHtml' ) ) {
            // If raw HTML is enabled, disable preview on open
            // since it has to be posted with a token for
            // security reasons
            return false;
        }
        if ( $request->getVal( 'preview' ) == 'yes' ) {
            // Explicit override from request
            return true;
        } elseif ( $request->getVal( 'preview' ) == 'no' ) {
            // Explicit override from request
            return false;
        } elseif ( $this->section == 'new' ) {
            // Nothing *to* preview for new sections
            return false;
        } elseif ( ( $request->getCheck( 'preload' ) || $this->mTitle->exists() )
            && $this->context->getUser()->getOption( 'previewonfirst' )
        ) {
            // Standard preference behavior
            return true;
        } elseif ( !$this->mTitle->exists()
            && isset( $previewOnOpenNamespaces[$this->mTitle->getNamespace()] )
            && $previewOnOpenNamespaces[$this->mTitle->getNamespace()]
        ) {
            // Categories are special
            return true;
        } else {
            return false;
        }
    }
    /**
     * Checks whether the user entered a skin name in uppercase,
     * e.g. "User:Example/Monobook.css" instead of "monobook.css"
     *
     * @return bool
     */
    protected function isWrongCaseUserConfigPage() {
        if ( $this->mTitle->isUserConfigPage() ) {
            $name = $this->mTitle->getSkinFromConfigSubpage();
            $skinFactory = MediaWikiServices::getInstance()->getSkinFactory();
            $skins = array_merge(
                array_keys( $skinFactory->getSkinNames() ),
                [ 'common' ]
            );
            return !in_array( $name, $skins )
                && in_array( strtolower( $name ), $skins );
        } else {
            return false;
        }
    }
    /**
     * Section editing is supported when the page content model allows
     * section edit and we are editing current revision.
     *
     * @return bool True if this edit page supports sections, false otherwise.
     */
    protected function isSectionEditSupported() {
        $currentRev = $this->page->getRevisionRecord();
        // $currentRev is null for non-existing pages, use the page default content model.
        $revContentModel = $currentRev
            ? $currentRev->getSlot( SlotRecord::MAIN, RevisionRecord::RAW )->getModel()
            : $this->page->getContentModel();
        return (
            ( $this->mArticle->getRevIdFetched() === $this->page->getLatest() ) &&
            $this->contentHandlerFactory->getContentHandler( $revContentModel )->supportsSections()
        );
    }
    /**
     * This function collects the form data and uses it to populate various member variables.
     * @param WebRequest &$request
     * @throws ErrorPageError
     */
    public function importFormData( &$request ) {
        # Section edit can come from either the form or a link
        $this->section = $request->getVal( 'wpSection', $request->getVal( 'section' ) );
        if ( $this->section !== null && $this->section !== '' && !$this->isSectionEditSupported() ) {
            throw new ErrorPageError( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
        }
        $this->isNew = !$this->mTitle->exists() || $this->section == 'new';
        if ( $request->wasPosted() ) {
            # These fields need to be checked for encoding.
            # Also remove trailing whitespace, but don't remove _initial_
            # whitespace from the text boxes. This may be significant formatting.
            $this->textbox1 = rtrim( $request->getText( 'wpTextbox1' ) );
            if ( !$request->getCheck( 'wpTextbox2' ) ) {
                // Skip this if wpTextbox2 has input, it indicates that we came
                // from a conflict page with raw page text, not a custom form
                // modified by subclasses
                $textbox1 = $this->importContentFormData( $request );
                if ( $textbox1 !== null ) {
                    $this->textbox1 = $textbox1;
                }
            }
            $this->unicodeCheck = $request->getText( 'wpUnicodeCheck' );
            $this->summary = $request->getText( 'wpSummary' );
            # If the summary consists of a heading, e.g. '==Foobar==', extract the title from the
            # header syntax, e.g. 'Foobar'. This is mainly an issue when we are using wpSummary for
            # section titles.
            $this->summary = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->summary );
            # Treat sectiontitle the same way as summary.
            # Note that wpSectionTitle is not yet a part of the actual edit form, as wpSummary is
            # currently doing double duty as both edit summary and section title. Right now this
            # is just to allow API edits to work around this limitation, but this should be
            # incorporated into the actual edit form when EditPage is rewritten (T20654, T28312).
            $this->sectiontitle = $request->getText( 'wpSectionTitle' );
            $this->sectiontitle = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->sectiontitle );
            $this->edittime = $request->getVal( 'wpEdittime' );
            $this->editRevId = $request->getIntOrNull( 'editRevId' );
            $this->starttime = $request->getVal( 'wpStarttime' );
            $undidRev = $request->getInt( 'wpUndidRevision' );
            if ( $undidRev ) {
                $this->undidRev = $undidRev;
            }
            $undoAfter = $request->getInt( 'wpUndoAfter' );
            if ( $undoAfter ) {
                $this->undoAfter = $undoAfter;
            }
            $this->scrolltop = $request->getIntOrNull( 'wpScrolltop' );
            if ( $this->textbox1 === '' && !$request->getCheck( 'wpTextbox1' ) ) {
                // wpTextbox1 field is missing, possibly due to being "too big"
                // according to some filter rules such as Suhosin's setting for
                // suhosin.request.max_value_length (d'oh)
                $this->incompleteForm = true;
            } else {
                // If we receive the last parameter of the request, we can fairly
                // claim the POST request has not been truncated.
                $this->incompleteForm = !$request->getVal( 'wpUltimateParam' );
            }
            if ( $this->incompleteForm ) {
                # If the form is incomplete, force to preview.
                wfDebug( __METHOD__ . ": Form data appears to be incomplete" );
                wfDebug( "POST DATA: " . var_export( $request->getPostValues(), true ) );
                $this->preview = true;
            } else {
                $this->preview = $request->getCheck( 'wpPreview' );
                $this->diff = $request->getCheck( 'wpDiff' );
                // Remember whether a save was requested, so we can indicate
                // if we forced preview due to session failure.
                $this->mTriedSave = !$this->preview;
                if ( $this->tokenOk( $request ) ) {
                    # Some browsers will not report any submit button
                    # if the user hits enter in the comment box.
                    # The unmarked state will be assumed to be a save,
                    # if the form seems otherwise complete.
                    wfDebug( __METHOD__ . ": Passed token check." );
                } elseif ( $this->diff ) {
                    # Failed token check, but only requested "Show Changes".
                    wfDebug( __METHOD__ . ": Failed token check; Show Changes requested." );
                } else {
                    # Page might be a hack attempt posted from
                    # an external site. Preview instead of saving.
                    wfDebug( __METHOD__ . ": Failed token check; forcing preview" );
                    $this->preview = true;
                }
            }
            $this->save = !$this->preview && !$this->diff;
            if ( !preg_match( '/^\d{14}$/', $this->edittime ) ) {
                $this->edittime = null;
            }
            if ( !preg_match( '/^\d{14}$/', $this->starttime ) ) {
                $this->starttime = null;
            }
            $this->recreate = $request->getCheck( 'wpRecreate' );
            $user = $this->getContext()->getUser();
            $this->minoredit = $request->getCheck( 'wpMinoredit' );
            $this->watchthis = $request->getCheck( 'wpWatchthis' );
            if ( $this->watchlistExpiryEnabled ) {
                // This parsing of the user-posted expiry is done for both preview and saving. This
                // is necessary because ApiEditPage uses preview when it saves (yuck!). Note that it
                // only works because the unnormalized value is retrieved again below in
                // getCheckboxesDefinitionForWatchlist().
                $expiry = ExpiryDef::normalizeExpiry( $request->getText( 'wpWatchlistExpiry' ) );
                if ( $expiry !== false ) {
                    $this->watchlistExpiry = $expiry;
                }
            }
            # Don't force edit summaries when a user is editing their own user or talk page
            if ( ( $this->mTitle->mNamespace === NS_USER || $this->mTitle->mNamespace === NS_USER_TALK )
                && $this->mTitle->getText() == $user->getName()
            ) {
                $this->allowBlankSummary = true;
            } else {
                $this->allowBlankSummary = $request->getBool( 'wpIgnoreBlankSummary' )
                    || !$user->getOption( 'forceeditsummary' );
            }
            $this->autoSumm = $request->getText( 'wpAutoSummary' );
            $this->allowBlankArticle = $request->getBool( 'wpIgnoreBlankArticle' );
            $this->allowSelfRedirect = $request->getBool( 'wpIgnoreSelfRedirect' );
            $changeTags = $request->getVal( 'wpChangeTags' );
            if ( $changeTags === null || $changeTags === '' ) {
                $this->changeTags = [];
            } else {
                $this->changeTags = array_filter( array_map( 'trim', explode( ',',
                    $changeTags ) ) );
            }
        } else {
            # Not a posted form? Start with nothing.
            wfDebug( __METHOD__ . ": Not a posted form." );
            $this->textbox1 = '';
            $this->summary = '';
            $this->sectiontitle = '';
            $this->edittime = '';
            $this->editRevId = null;
            $this->starttime = wfTimestampNow();
            $this->edit = false;
            $this->preview = false;
            $this->save = false;
            $this->diff = false;
            $this->minoredit = false;
            // Watch may be overridden by request parameters
            $this->watchthis = $request->getBool( 'watchthis', false );
            if ( $this->watchlistExpiryEnabled ) {
                $this->watchlistExpiry = null;
            }
            $this->recreate = false;
            // When creating a new section, we can preload a section title by passing it as the
            // preloadtitle parameter in the URL (T15100)
            if ( $this->section == 'new' && $request->getVal( 'preloadtitle' ) ) {
                $this->sectiontitle = $request->getVal( 'preloadtitle' );
                // Once wpSummary isn't being use for setting section titles, we should delete this.
                $this->summary = $request->getVal( 'preloadtitle' );
            } elseif ( $this->section != 'new' && $request->getVal( 'summary' ) !== '' ) {
                $this->summary = $request->getText( 'summary' );
                if ( $this->summary !== '' ) {
                    $this->hasPresetSummary = true;
                }
            }
            if ( $request->getVal( 'minor' ) ) {
                $this->minoredit = true;
            }
        }
        $this->oldid = $request->getInt( 'oldid' );
        $this->parentRevId = $request->getInt( 'parentRevId' );
        $this->markAsBot = $request->getBool( 'bot', true );
        $this->nosummary = $request->getBool( 'nosummary' );
        // May be overridden by revision.
        $this->contentModel = $request->getText( 'model', $this->contentModel );
        // May be overridden by revision.
        $this->contentFormat = $request->getText( 'format', $this->contentFormat );
        try {
            $handler = $this->contentHandlerFactory->getContentHandler( $this->contentModel );
        } catch ( MWUnknownContentModelException $e ) {
            throw new ErrorPageError(
                'editpage-invalidcontentmodel-title',
                'editpage-invalidcontentmodel-text',
                [ wfEscapeWikiText( $this->contentModel ) ]
            );
        }
        if ( !$handler->isSupportedFormat( $this->contentFormat ) ) {
            throw new ErrorPageError(
                'editpage-notsupportedcontentformat-title',
                'editpage-notsupportedcontentformat-text',
                [
                    wfEscapeWikiText( $this->contentFormat ),
                    wfEscapeWikiText( ContentHandler::getLocalizedName( $this->contentModel ) )
                ]
            );
        }
        /**
         * @todo Check if the desired model is allowed in this namespace, and if
         *   a transition from the page's current model to the new model is
         *   allowed.
         */
        $this->editintro = $request->getText( 'editintro',
            // Custom edit intro for new sections
            $this->section === 'new' ? 'MediaWiki:addsection-editintro' : '' );
        // Allow extensions to modify form data
        $this->getHookRunner()->onEditPage__importFormData( $this, $request );
    }
    /**
     * Subpage overridable method for extracting the page content data from the
     * posted form to be placed in $this->textbox1, if using customized input
     * this method should be overridden and return the page text that will be used
     * for saving, preview parsing and so on...
     *
     * @param WebRequest &$request
     * @return string|null
     */
    protected function importContentFormData( &$request ) {