Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
13.54% covered (danger)
13.54%
13 / 96
CRAP
25.04% covered (danger)
25.04%
508 / 2029
EditPage
0.00% covered (danger)
0.00%
0 / 1
13.54% covered (danger)
13.54%
13 / 96
174809.03
25.04% covered (danger)
25.04%
508 / 2029
 __construct
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
41 / 41
 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
1332
0.00% covered (danger)
0.00%
0 / 95
 getEditPermissionErrors
0.00% covered (danger)
0.00%
0 / 1
56
0.00% covered (danger)
0.00%
0 / 13
 displayPermissionsError
0.00% covered (danger)
0.00%
0 / 1
56
0.00% covered (danger)
0.00%
0 / 12
 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
160.32
55.17% covered (warning)
55.17%
64 / 116
 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
1260
0.00% covered (danger)
0.00%
0 / 103
 getUndoContent
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
12 / 12
 getOriginalContent
0.00% covered (danger)
0.00%
0 / 1
4.12
50.00% covered (danger)
50.00%
4 / 8
 getParentRevId
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 3
 getCurrentContent
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
10 / 10
 getPreloadedContent
0.00% covered (danger)
0.00%
0 / 1
90
0.00% covered (danger)
0.00%
0 / 29
 isPageExistingAndViewable
0.00% covered (danger)
0.00%
0 / 1
20
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 / 4
 setPostEditCookie
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 10
 attemptSave
100.00% covered (success)
100.00%
1 / 1
4
100.00% covered (success)
100.00%
7 / 7
 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
1560
0.00% covered (danger)
0.00%
0 / 81
 newSectionSummary
0.00% covered (danger)
0.00%
0 / 1
4.34
72.22% covered (warning)
72.22%
13 / 18
 internalAttemptSave
0.00% covered (danger)
0.00%
0 / 1
68.07
83.71% covered (warning)
83.71%
221 / 264
 handleFailedConstraint
0.00% covered (danger)
0.00%
0 / 1
16.98
58.82% covered (warning)
58.82%
10 / 17
 isUndoClean
0.00% covered (danger)
0.00%
0 / 1
8.05
90.91% covered (success)
90.91%
20 / 22
 addContentModelChangeLogEntry
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 11
 updateWatchlist
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
9 / 9
 mergeChangesIntoContent
0.00% covered (danger)
0.00%
0 / 1
6
95.24% covered (success)
95.24%
20 / 21
 getExpectedParentRevision
100.00% covered (success)
100.00%
1 / 1
4
100.00% covered (success)
100.00%
13 / 13
 setHeaders
0.00% covered (danger)
0.00%
0 / 1
156
0.00% covered (danger)
0.00%
0 / 34
 showIntro
0.00% covered (danger)
0.00%
0 / 1
812
0.00% covered (danger)
0.00%
0 / 68
 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
1260
0.00% covered (danger)
0.00%
0 / 139
 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 / 106
 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 / 13
 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 / 15
 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 / 7
 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 / 36
 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 / 11
 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 / 28
 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.06
84.21% covered (warning)
84.21%
16 / 19
 getPreviewText
0.00% covered (danger)
0.00%
0 / 1
870
0.00% covered (danger)
0.00%
0 / 87
 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 / 4
 doPreviewParse
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 12
 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
42
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
56
0.00% covered (danger)
0.00%
0 / 18
 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
 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 / 5
<?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\Constraint\AccidentalRecreationConstraint;
use MediaWiki\EditPage\Constraint\AutoSummaryMissingSummaryConstraint;
use MediaWiki\EditPage\Constraint\ChangeTagsConstraint;
use MediaWiki\EditPage\Constraint\ContentModelChangeConstraint;
use MediaWiki\EditPage\Constraint\CreationPermissionConstraint;
use MediaWiki\EditPage\Constraint\DefaultTextConstraint;
use MediaWiki\EditPage\Constraint\EditConstraintRunner;
use MediaWiki\EditPage\Constraint\EditFilterMergedContentHookConstraint;
use MediaWiki\EditPage\Constraint\EditRightConstraint;
use MediaWiki\EditPage\Constraint\IEditConstraint;
use MediaWiki\EditPage\Constraint\ImageRedirectConstraint;
use MediaWiki\EditPage\Constraint\MissingCommentConstraint;
use MediaWiki\EditPage\Constraint\NewSectionMissingSummaryConstraint;
use MediaWiki\EditPage\Constraint\PageSizeConstraint;
use MediaWiki\EditPage\Constraint\SelfRedirectConstraint;
use MediaWiki\EditPage\Constraint\SpamRegexConstraint;
use MediaWiki\EditPage\Constraint\UnicodeConstraint;
use MediaWiki\EditPage\Constraint\UserBlockConstraint;
use MediaWiki\EditPage\Constraint\UserRateLimitConstraint;
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\Page\PageIdentity;
use MediaWiki\Page\WikiPageFactory;
use MediaWiki\Permissions\Authority;
use MediaWiki\Permissions\PermissionManager;
use MediaWiki\Revision\RevisionRecord;
use MediaWiki\Revision\RevisionStore;
use MediaWiki\Revision\RevisionStoreRecord;
use MediaWiki\Revision\SlotRecord;
use MediaWiki\Storage\EditResult;
use MediaWiki\User\UserIdentity;
use MediaWiki\User\UserNameUtils;
use MediaWiki\Watchlist\WatchlistManager;
use OOUI\CheckboxInputWidget;
use OOUI\DropdownInputWidget;
use OOUI\FieldLayout;
use Wikimedia\Message\MessageValue;
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 = UnicodeConstraint::VALID_UNICODE;
    /**
     * 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 */
    private $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 */
    private $lastDelete;
    /** @var bool */
    private $mTokenOk = false;
    /** @var bool */
    private $mTriedSave = false;
    /** @var bool */
    private $incompleteForm = false;
    /** @var bool */
    private $tooBig = false;
    /** @var bool */
    private $missingComment = false;
    /** @var bool */
    private $missingSummary = false;
    /** @var bool */
    private $allowBlankSummary = false;
    /** @var bool */
    protected $blankArticle = false;
    /** @var bool */
    protected $allowBlankArticle = false;
    /** @var bool */
    protected $selfRedirect = false;
    /** @var bool */
    protected $allowSelfRedirect = false;
    /** @var string */
    private $autoSumm = '';
    /** @var string */
    private $hookError = '';
    /** @var ParserOutput */
    private $mParserOutput;
    /**
     * @var bool Has a summary been preset using GET parameter &summary= ?
     */
    private $hasPresetSummary = false;
    /**
     * @var RevisionRecord|bool|null
     *
     * A RevisionRecord corresponding to $this->editRevId or $this->edittime
     */
    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 */
    private $minoredit = false;
    /** @var bool */
    private $watchthis = false;
    /** @var bool Corresponds to $wgWatchlistExpiry */
    private $watchlistExpiryEnabled;
    /** @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 */
    private $recreate = false;
    /** @var string
     * Page content input field.
     */
    public $textbox1 = '';
    /** @var string */
    public $textbox2 = '';
    /** @var string */
    public $summary = '';
    /**
     * @var bool
     * If true, hide the summary field.
     */
    private $nosummary = false;
    /** @var string|null
     * Timestamp of the latest revision of the page when editing was initiated
     * on the client.
     */
    public $edittime = '';
    /** @var int|null 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.
     * @see $editRevId
     * @see $oldid
     * @see getparentRevId()
     */
    private $parentRevId = 0;
    /** @var string */
    private $editintro = '';
    /** @var int|null */
    private $scrolltop = null;
    /** @var bool */
    private $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;
    /**
     * @var WikiPageFactory
     */
    private $wikiPageFactory;
    /**
     * @var WatchlistManager
     */
    private $watchlistManager;
    /**
     * @var UserNameUtils
     */
    private $userNameUtils;
    /**
     * @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->wikiPageFactory = $services->getWikiPageFactory();
        $this->watchlistManager = $services->getWatchlistManager();
        $this->userNameUtils = $services->getUserNameUtils();
        $this->deprecatePublicProperty( 'deletedSinceEdit', '1.35', __CLASS__ );
        $this->deprecatePublicProperty( 'lastDelete', '1.35', __CLASS__ );
        $this->deprecatePublicProperty( 'mTokenOk', '1.35', __CLASS__ );
        $this->deprecatePublicProperty( 'mTriedSave', '1.35', __CLASS__ );
        $this->deprecatePublicProperty( 'incompleteForm', '1.35', __CLASS__ );
        $this->deprecatePublicProperty( 'tooBig', '1.35', __CLASS__ );
        $this->deprecatePublicProperty( 'missingComment', '1.35', __CLASS__ );
        $this->deprecatePublicProperty( 'missingSummary', '1.35', __CLASS__ );
        $this->deprecatePublicProperty( 'allowBlankSummary', '1.35', __CLASS__ );
        $this->deprecatePublicProperty( 'autoSumm', '1.35', __CLASS__ );
        $this->deprecatePublicProperty( 'mParserOutput', '1.35', __CLASS__ );
        $this->deprecatePublicProperty( 'hasPresetSummary', '1.35', __CLASS__ );
        $this->deprecatePublicProperty( 'minoredit', '1.35', __CLASS__ );
        $this->deprecatePublicProperty( 'watchthis', '1.35', __CLASS__ );
        $this->deprecatePublicProperty( 'recreate', '1.35', __CLASS__ );
        $this->deprecatePublicProperty( 'nosummaryparentRevId', '1.35', __CLASS__ );
        $this->deprecatePublicProperty( 'editintro', '1.35', __CLASS__ );
        $this->deprecatePublicProperty( 'scrolltop', '1.35', __CLASS__ );
        $this->deprecatePublicProperty( 'markAsBot', '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;
    }
    /**
     * @param Title|null $title
     */
    public function setContextTitle( $title ) {
        $this->mContextTitle = $title;
    }
    /**
     * @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 ( $this->save && wfReadOnly() ) {
            // 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 );
            }
        }
        // If we're displaying an old revision, and there are differences between it and the
        // current revision outside the main slot, then we can't allow the old revision to be
        // editable, as what would happen to the non-main-slot data if someone saves the old
        // revision is undefined.
        // When this is the case, display a read-only version of the page instead, with a link
        // to a diff page from which the old revision can be restored
        $curRevisionRecord = $this->page->getRevisionRecord();
        if ( $curRevisionRecord
            && $revRecord
            && $curRevisionRecord->getId() !== $revRecord->getId()
            && ( WikiPage::hasDifferencesOutsideMainSlot(
                    $revRecord,
                    $curRevisionRecord
                ) || !$this->isSupportedContentModel(
                    $revRecord->getSlot(
                        SlotRecord::MAIN,
                        RevisionRecord::RAW
                    )->getModel()
                ) )
        ) {
            $restoreLink = $this->mTitle->getFullURL(
                [
                    'action' => 'mcrrestore',
                    'restore' => $revRecord->getId(),
                ]
            );
            $this->displayViewSourcePage(
                $this->getContentObject(),
                $this->context->msg(
                    'nonmain-slot-differences-therefore-readonly',
                    $restoreLink
                )->plain()
            );
            return;
        }
        $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
        );
        # Ignore some permissions errors when a user is just previewing/viewing diffs
        if ( $this->preview || $this->diff ) {
            $remove = [];
            foreach ( $permErrors as $error ) {
                if ( $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
        // We used to only do this if $this->firsttime was truthy, and there was no content
        // or the content was empty, but sometimes there was no content even if it not the
        // first time, we can't use displayViewSourcePage if there is no content (T281400)
        if ( !$content || ( $this->firsttime && $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->getInstalledSkins() ),
                [ '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' );
            $expiry = $request->getText( 'wpWatchlistExpiry' );
            if ( $this->watchlistExpiryEnabled && $expiry !== '' ) {
                // 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( $expiry, TS_ISO_8601 );
                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->getNamespace() === NS_USER || $this->mTitle->getNamespace() === NS_USER_TALK )
                && $this->mTitle->getText() == $user->getName()
            ) {
                $this->allowBlankSummary = true;
            } else {
                $this->allowBlankSummary = $request->getBool( 'wpIgnoreBlankSummary' )