MediaWiki  master
EditPage.php
Go to the documentation of this file.
1 <?php
47 use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
63 use OOUI\CheckboxInputWidget;
64 use OOUI\DropdownInputWidget;
65 use OOUI\FieldLayout;
68 use Wikimedia\ScopedCallback;
69 
90 class EditPage implements IEditObject {
92  use ProtectedHookAccessorTrait;
93 
97  public const UNICODE_CHECK = UnicodeConstraint::VALID_UNICODE;
98 
102  public const EDITFORM_ID = 'editform';
103 
108  public const POST_EDIT_COOKIE_KEY_PREFIX = 'PostEditRevision';
109 
123  public const POST_EDIT_COOKIE_DURATION = 1200;
124 
129  public $mArticle;
130 
132  private $page;
133 
138  public $mTitle;
139 
141  private $mContextTitle = null;
142 
144  public $action = 'submit';
145 
150  public $isConflict = false;
151 
153  public $isNew = false;
154 
157 
159  public $formtype;
160 
165  public $firsttime;
166 
168  private $lastDelete;
169 
171  private $mTokenOk = false;
172 
174  private $mTriedSave = false;
175 
177  private $incompleteForm = false;
178 
180  private $tooBig = false;
181 
183  private $missingComment = false;
184 
186  private $missingSummary = false;
187 
189  private $allowBlankSummary = false;
190 
192  protected $blankArticle = false;
193 
195  protected $allowBlankArticle = false;
196 
198  protected $selfRedirect = false;
199 
201  protected $allowSelfRedirect = false;
202 
204  private $autoSumm = '';
205 
207  private $hookError = '';
208 
210  private $mParserOutput;
211 
215  private $hasPresetSummary = false;
216 
222  private $mExpectedParentRevision = false;
223 
225  public $mShowSummaryField = true;
226 
227  # Form values
228 
230  public $save = false;
231 
233  public $preview = false;
234 
236  public $diff = false;
237 
239  private $minoredit = false;
240 
242  private $watchthis = false;
243 
246 
249 
252 
254  private $recreate = false;
255 
259  public $textbox1 = '';
260 
262  public $textbox2 = '';
263 
265  public $summary = '';
266 
271  private $nosummary = false;
272 
277  public $edittime = '';
278 
290  private $editRevId = null;
291 
293  public $section = '';
294 
296  public $sectiontitle = '';
297 
301  public $starttime = '';
302 
308  public $oldid = 0;
309 
316  private $parentRevId = 0;
317 
319  private $editintro = '';
320 
322  private $scrolltop = null;
323 
325  private $markAsBot = true;
326 
329 
331  public $contentFormat = null;
332 
334  private $changeTags = null;
335 
336  # Placeholders for text injection by hooks (must be HTML)
337  # extensions should take care to _append_ to the present value
338 
340  public $editFormPageTop = '';
341  public $editFormTextTop = '';
345  public $editFormTextBottom = '';
348 
349  /* $didSave should be set to true whenever an article was successfully altered. */
350  public $didSave = false;
351  public $undidRev = 0;
352  public $undoAfter = 0;
353 
354  public $suppressIntro = false;
355 
357  protected $edit;
358 
360  protected $contentLength = false;
361 
365  private $enableApiEditOverride = false;
366 
370  protected $context;
371 
375  private $isOldRev = false;
376 
380  private $unicodeCheck;
381 
388 
393 
398 
402  private $permManager;
403 
407  private $revisionStore;
408 
413 
418 
422  private $userNameUtils;
423 
426 
431  public function __construct( Article $article ) {
432  $this->mArticle = $article;
433  $this->page = $article->getPage(); // model object
434  $this->mTitle = $article->getTitle();
435 
436  // Make sure the local context is in sync with other member variables.
437  // Particularly make sure everything is using the same WikiPage instance.
438  // This should probably be the case in Article as well, but it's
439  // particularly important for EditPage, to make use of the in-place caching
440  // facility in WikiPage::prepareContentForEdit.
441  $this->context = new DerivativeContext( $article->getContext() );
442  $this->context->setWikiPage( $this->page );
443  $this->context->setTitle( $this->mTitle );
444 
445  $this->contentModel = $this->mTitle->getContentModel();
446 
447  $services = MediaWikiServices::getInstance();
448  $this->contentHandlerFactory = $services->getContentHandlerFactory();
449  $this->contentFormat = $this->contentHandlerFactory
450  ->getContentHandler( $this->contentModel )
451  ->getDefaultFormat();
452  $this->editConflictHelperFactory = [ $this, 'newTextConflictHelper' ];
453  $this->permManager = $services->getPermissionManager();
454  $this->revisionStore = $services->getRevisionStore();
455  $this->watchlistExpiryEnabled = $this->getContext()->getConfig() instanceof Config
456  && $this->getContext()->getConfig()->get( 'WatchlistExpiry' );
457  $this->watchedItemStore = $services->getWatchedItemStore();
458  $this->wikiPageFactory = $services->getWikiPageFactory();
459  $this->watchlistManager = $services->getWatchlistManager();
460  $this->userNameUtils = $services->getUserNameUtils();
461  $this->redirectLookup = $services->getRedirectLookup();
462 
463  $this->deprecatePublicProperty( 'deletedSinceEdit', '1.35', __CLASS__ );
464  $this->deprecatePublicProperty( 'lastDelete', '1.35', __CLASS__ );
465  $this->deprecatePublicProperty( 'mTokenOk', '1.35', __CLASS__ );
466  $this->deprecatePublicProperty( 'mTriedSave', '1.35', __CLASS__ );
467  $this->deprecatePublicProperty( 'incompleteForm', '1.35', __CLASS__ );
468  $this->deprecatePublicProperty( 'tooBig', '1.35', __CLASS__ );
469  $this->deprecatePublicProperty( 'missingComment', '1.35', __CLASS__ );
470  $this->deprecatePublicProperty( 'missingSummary', '1.35', __CLASS__ );
471  $this->deprecatePublicProperty( 'allowBlankSummary', '1.35', __CLASS__ );
472  $this->deprecatePublicProperty( 'autoSumm', '1.35', __CLASS__ );
473  $this->deprecatePublicProperty( 'mParserOutput', '1.35', __CLASS__ );
474  $this->deprecatePublicProperty( 'hasPresetSummary', '1.35', __CLASS__ );
475  $this->deprecatePublicProperty( 'minoredit', '1.35', __CLASS__ );
476  $this->deprecatePublicProperty( 'watchthis', '1.35', __CLASS__ );
477  $this->deprecatePublicProperty( 'recreate', '1.35', __CLASS__ );
478  $this->deprecatePublicProperty( 'nosummaryparentRevId', '1.35', __CLASS__ );
479  $this->deprecatePublicProperty( 'editintro', '1.35', __CLASS__ );
480  $this->deprecatePublicProperty( 'scrolltop', '1.35', __CLASS__ );
481  $this->deprecatePublicProperty( 'markAsBot', '1.35', __CLASS__ );
482  }
483 
487  public function getArticle() {
488  return $this->mArticle;
489  }
490 
495  public function getContext() {
496  return $this->context;
497  }
498 
503  public function getTitle() {
504  return $this->mTitle;
505  }
506 
510  public function setContextTitle( $title ) {
511  $this->mContextTitle = $title;
512  }
513 
518  public function getContextTitle() {
519  if ( $this->mContextTitle === null ) {
520  throw new RuntimeException( "EditPage does not have a context title set" );
521  } else {
522  return $this->mContextTitle;
523  }
524  }
525 
533  public function isSupportedContentModel( $modelId ) {
534  return $this->enableApiEditOverride === true ||
535  $this->contentHandlerFactory->getContentHandler( $modelId )->supportsDirectEditing();
536  }
537 
544  public function setApiEditOverride( $enableOverride ) {
545  $this->enableApiEditOverride = $enableOverride;
546  }
547 
559  public function edit() {
560  // Allow extensions to modify/prevent this form or submission
561  if ( !$this->getHookRunner()->onAlternateEdit( $this ) ) {
562  return;
563  }
564 
565  wfDebug( __METHOD__ . ": enter" );
566 
567  $request = $this->context->getRequest();
568  // If they used redlink=1 and the page exists, redirect to the main article
569  if ( $request->getBool( 'redlink' ) && $this->mTitle->exists() ) {
570  $this->context->getOutput()->redirect( $this->mTitle->getFullURL() );
571  return;
572  }
573 
574  $this->importFormData( $request );
575  $this->firsttime = false;
576 
577  if ( $this->save && wfReadOnly() ) {
578  // Force preview
579  $this->save = false;
580  $this->preview = true;
581  }
582 
583  if ( $this->save ) {
584  $this->formtype = 'save';
585  } elseif ( $this->preview ) {
586  $this->formtype = 'preview';
587  } elseif ( $this->diff ) {
588  $this->formtype = 'diff';
589  } else { # First time through
590  $this->firsttime = true;
591  if ( $this->previewOnOpen() ) {
592  $this->formtype = 'preview';
593  } else {
594  $this->formtype = 'initial';
595  }
596  }
597 
598  $permErrors = $this->getEditPermissionErrors(
599  $this->save ? PermissionManager::RIGOR_SECURE : PermissionManager::RIGOR_FULL
600  );
601  if ( $permErrors ) {
602  wfDebug( __METHOD__ . ": User can't edit" );
603 
604  if ( $this->context->getUser()->getBlock() ) {
605  // Auto-block user's IP if the account was "hard" blocked
606  if ( !wfReadOnly() ) {
608  $this->context->getUser()->spreadAnyEditBlock();
609  } );
610  }
611  }
612  $this->displayPermissionsError( $permErrors );
613 
614  return;
615  }
616 
617  $revRecord = $this->mArticle->fetchRevisionRecord();
618  // Disallow editing revisions with content models different from the current one
619  // Undo edits being an exception in order to allow reverting content model changes.
620  $revContentModel = $revRecord ?
621  $revRecord->getSlot( SlotRecord::MAIN, RevisionRecord::RAW )->getModel() :
622  false;
623  if ( $revContentModel && $revContentModel !== $this->contentModel ) {
624  $prevRevRecord = null;
625  $prevContentModel = false;
626  if ( $this->undidRev ) {
627  $undidRevRecord = $this->revisionStore
628  ->getRevisionById( $this->undidRev );
629  $prevRevRecord = $undidRevRecord ?
630  $this->revisionStore->getPreviousRevision( $undidRevRecord ) :
631  null;
632 
633  $prevContentModel = $prevRevRecord ?
634  $prevRevRecord
635  ->getSlot( SlotRecord::MAIN, RevisionRecord::RAW )
636  ->getModel() :
637  '';
638  }
639 
640  if ( !$this->undidRev
641  || !$prevRevRecord
642  || $prevContentModel !== $this->contentModel
643  ) {
644  $this->displayViewSourcePage(
645  $this->getContentObject(),
646  $this->context->msg(
647  'contentmodelediterror',
648  $revContentModel,
649  $this->contentModel
650  )->plain()
651  );
652  return;
653  }
654  }
655 
656  $this->isConflict = false;
657 
658  # Show applicable editing introductions
659  if ( $this->formtype == 'initial' || $this->firsttime ) {
660  $this->showIntro();
661  }
662 
663  # Attempt submission here. This will check for edit conflicts,
664  # and redundantly check for locked database, blocked IPs, etc.
665  # that edit() already checked just in case someone tries to sneak
666  # in the back door with a hand-edited submission URL.
667 
668  if ( $this->formtype == 'save' ) {
669  $resultDetails = null;
670  $status = $this->attemptSave( $resultDetails );
671  if ( !$this->handleStatus( $status, $resultDetails ) ) {
672  return;
673  }
674  }
675 
676  # First time through: get contents, set time for conflict
677  # checking, etc.
678  if ( $this->formtype == 'initial' || $this->firsttime ) {
679  if ( $this->initialiseForm() === false ) {
680  return;
681  }
682 
683  if ( !$this->mTitle->getArticleID() ) {
684  $this->getHookRunner()->onEditFormPreloadText( $this->textbox1, $this->mTitle );
685  } else {
686  $this->getHookRunner()->onEditFormInitialText( $this );
687  }
688 
689  }
690 
691  // If we're displaying an old revision, and there are differences between it and the
692  // current revision outside the main slot, then we can't allow the old revision to be
693  // editable, as what would happen to the non-main-slot data if someone saves the old
694  // revision is undefined.
695  // When this is the case, display a read-only version of the page instead, with a link
696  // to a diff page from which the old revision can be restored
697  $curRevisionRecord = $this->page->getRevisionRecord();
698  if ( $curRevisionRecord
699  && $revRecord
700  && $curRevisionRecord->getId() !== $revRecord->getId()
702  $revRecord,
703  $curRevisionRecord
704  ) || !$this->isSupportedContentModel(
705  $revRecord->getSlot(
706  SlotRecord::MAIN,
707  RevisionRecord::RAW
708  )->getModel()
709  ) )
710  ) {
711  $restoreLink = $this->mTitle->getFullURL(
712  [
713  'action' => 'mcrrestore',
714  'restore' => $revRecord->getId(),
715  ]
716  );
717  $this->displayViewSourcePage(
718  $this->getContentObject(),
719  $this->context->msg(
720  'nonmain-slot-differences-therefore-readonly',
721  $restoreLink
722  )->plain()
723  );
724  return;
725  }
726 
727  $this->showEditForm();
728  }
729 
734  protected function getEditPermissionErrors( $rigor = PermissionManager::RIGOR_SECURE ) {
735  $user = $this->context->getUser();
736  $permErrors = $this->permManager->getPermissionErrors(
737  'edit',
738  $user,
739  $this->mTitle,
740  $rigor
741  );
742  # Ignore some permissions errors when a user is just previewing/viewing diffs
743  if ( $this->preview || $this->diff ) {
744  $remove = [];
745  foreach ( $permErrors as $error ) {
746  if ( $error[0] == 'blockedtext' ||
747  $error[0] == 'autoblockedtext' ||
748  $error[0] == 'systemblockedtext'
749  ) {
750  $remove[] = $error;
751  }
752  }
753  $permErrors = wfArrayDiff2( $permErrors, $remove );
754  }
755 
756  return $permErrors;
757  }
758 
771  protected function displayPermissionsError( array $permErrors ) {
772  $out = $this->context->getOutput();
773  if ( $this->context->getRequest()->getBool( 'redlink' ) ) {
774  // The edit page was reached via a red link.
775  // Redirect to the article page and let them click the edit tab if
776  // they really want a permission error.
777  $out->redirect( $this->mTitle->getFullURL() );
778  return;
779  }
780 
781  $content = $this->getContentObject();
782 
783  // Use the normal message if there's nothing to display
784  // We used to only do this if $this->firsttime was truthy, and there was no content
785  // or the content was empty, but sometimes there was no content even if it not the
786  // first time, we can't use displayViewSourcePage if there is no content (T281400)
787  if ( !$content || ( $this->firsttime && $content->isEmpty() ) ) {
788  $action = $this->mTitle->exists() ? 'edit' :
789  ( $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage' );
790  throw new PermissionsError( $action, $permErrors );
791  }
792 
793  $this->displayViewSourcePage(
794  $content,
795  $out->formatPermissionsErrorMessage( $permErrors, 'edit' )
796  );
797  }
798 
804  protected function displayViewSourcePage( Content $content, $errorMessage = '' ) {
805  $out = $this->context->getOutput();
806  $this->getHookRunner()->onEditPage__showReadOnlyForm_initial( $this, $out );
807 
808  $out->setRobotPolicy( 'noindex,nofollow' );
809  $out->setPageTitle( $this->context->msg(
810  'viewsource-title',
811  $this->getContextTitle()->getPrefixedText()
812  ) );
813  $out->addBacklinkSubtitle( $this->getContextTitle() );
814  $out->addHTML( $this->editFormPageTop );
815  $out->addHTML( $this->editFormTextTop );
816 
817  if ( $errorMessage !== '' ) {
818  $out->addWikiTextAsInterface( $errorMessage );
819  $out->addHTML( "<hr />\n" );
820  }
821 
822  # If the user made changes, preserve them when showing the markup
823  # (This happens when a user is blocked during edit, for instance)
824  if ( !$this->firsttime ) {
825  $text = $this->textbox1;
826  $out->addWikiMsg( 'viewyourtext' );
827  } else {
828  try {
829  $text = $this->toEditText( $content );
830  } catch ( MWException $e ) {
831  # Serialize using the default format if the content model is not supported
832  # (e.g. for an old revision with a different model)
833  $text = $content->serialize();
834  }
835  $out->addWikiMsg( 'viewsourcetext' );
836  }
837 
838  $out->addHTML( $this->editFormTextBeforeContent );
839  $this->showTextbox( $text, 'wpTextbox1', [ 'readonly' ] );
840  $out->addHTML( $this->editFormTextAfterContent );
841 
842  $out->addHTML( $this->makeTemplatesOnThisPageList( $this->getTemplates() ) );
843 
844  $out->addModules( 'mediawiki.action.edit.collapsibleFooter' );
845 
846  $out->addHTML( $this->editFormTextBottom );
847  if ( $this->mTitle->exists() ) {
848  $out->returnToMain( null, $this->mTitle );
849  }
850  }
851 
857  protected function previewOnOpen() {
858  $config = $this->context->getConfig();
859  $previewOnOpenNamespaces = $config->get( 'PreviewOnOpenNamespaces' );
860  $request = $this->context->getRequest();
861  if ( $config->get( 'RawHtml' ) ) {
862  // If raw HTML is enabled, disable preview on open
863  // since it has to be posted with a token for
864  // security reasons
865  return false;
866  }
867  if ( $request->getVal( 'preview' ) == 'yes' ) {
868  // Explicit override from request
869  return true;
870  } elseif ( $request->getVal( 'preview' ) == 'no' ) {
871  // Explicit override from request
872  return false;
873  } elseif ( $this->section == 'new' ) {
874  // Nothing *to* preview for new sections
875  return false;
876  } elseif ( ( $request->getCheck( 'preload' ) || $this->mTitle->exists() )
877  && $this->context->getUser()->getOption( 'previewonfirst' )
878  ) {
879  // Standard preference behavior
880  return true;
881  } elseif ( !$this->mTitle->exists()
882  && isset( $previewOnOpenNamespaces[$this->mTitle->getNamespace()] )
883  && $previewOnOpenNamespaces[$this->mTitle->getNamespace()]
884  ) {
885  // Categories are special
886  return true;
887  } else {
888  return false;
889  }
890  }
891 
898  protected function isWrongCaseUserConfigPage() {
899  if ( $this->mTitle->isUserConfigPage() ) {
900  $name = $this->mTitle->getSkinFromConfigSubpage();
901  $skinFactory = MediaWikiServices::getInstance()->getSkinFactory();
902  $skins = array_merge(
903  array_keys( $skinFactory->getInstalledSkins() ),
904  [ 'common' ]
905  );
906  return !in_array( $name, $skins )
907  && in_array( strtolower( $name ), $skins );
908  } else {
909  return false;
910  }
911  }
912 
919  protected function isSectionEditSupported() {
920  $currentRev = $this->page->getRevisionRecord();
921 
922  // $currentRev is null for non-existing pages, use the page default content model.
923  $revContentModel = $currentRev
924  ? $currentRev->getSlot( SlotRecord::MAIN, RevisionRecord::RAW )->getModel()
925  : $this->page->getContentModel();
926 
927  return (
928  ( $this->mArticle->getRevIdFetched() === $this->page->getLatest() ) &&
929  $this->contentHandlerFactory->getContentHandler( $revContentModel )->supportsSections()
930  );
931  }
932 
938  public function importFormData( &$request ) {
939  # Section edit can come from either the form or a link
940  $this->section = $request->getVal( 'wpSection', $request->getVal( 'section' ) );
941 
942  if ( $this->section !== null && $this->section !== '' && !$this->isSectionEditSupported() ) {
943  throw new ErrorPageError( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
944  }
945 
946  $this->isNew = !$this->mTitle->exists() || $this->section == 'new';
947 
948  if ( $request->wasPosted() ) {
949  # These fields need to be checked for encoding.
950  # Also remove trailing whitespace, but don't remove _initial_
951  # whitespace from the text boxes. This may be significant formatting.
952  $this->textbox1 = rtrim( $request->getText( 'wpTextbox1' ) );
953  if ( !$request->getCheck( 'wpTextbox2' ) ) {
954  // Skip this if wpTextbox2 has input, it indicates that we came
955  // from a conflict page with raw page text, not a custom form
956  // modified by subclasses
957  $textbox1 = $this->importContentFormData( $request );
958  if ( $textbox1 !== null ) {
959  $this->textbox1 = $textbox1;
960  }
961  }
962 
963  $this->unicodeCheck = $request->getText( 'wpUnicodeCheck' );
964 
965  $this->summary = $request->getText( 'wpSummary' );
966 
967  # If the summary consists of a heading, e.g. '==Foobar==', extract the title from the
968  # header syntax, e.g. 'Foobar'. This is mainly an issue when we are using wpSummary for
969  # section titles.
970  $this->summary = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->summary );
971 
972  # Treat sectiontitle the same way as summary.
973  # Note that wpSectionTitle is not yet a part of the actual edit form, as wpSummary is
974  # currently doing double duty as both edit summary and section title. Right now this
975  # is just to allow API edits to work around this limitation, but this should be
976  # incorporated into the actual edit form when EditPage is rewritten (T20654, T28312).
977  $this->sectiontitle = $request->getText( 'wpSectionTitle' );
978  $this->sectiontitle = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->sectiontitle );
979 
980  $this->edittime = $request->getVal( 'wpEdittime' );
981  $this->editRevId = $request->getIntOrNull( 'editRevId' );
982  $this->starttime = $request->getVal( 'wpStarttime' );
983 
984  $undidRev = $request->getInt( 'wpUndidRevision' );
985  if ( $undidRev ) {
986  $this->undidRev = $undidRev;
987  }
988  $undoAfter = $request->getInt( 'wpUndoAfter' );
989  if ( $undoAfter ) {
990  $this->undoAfter = $undoAfter;
991  }
992 
993  $this->scrolltop = $request->getIntOrNull( 'wpScrolltop' );
994 
995  if ( $this->textbox1 === '' && !$request->getCheck( 'wpTextbox1' ) ) {
996  // wpTextbox1 field is missing, possibly due to being "too big"
997  // according to some filter rules such as Suhosin's setting for
998  // suhosin.request.max_value_length (d'oh)
999  $this->incompleteForm = true;
1000  } else {
1001  // If we receive the last parameter of the request, we can fairly
1002  // claim the POST request has not been truncated.
1003  $this->incompleteForm = !$request->getVal( 'wpUltimateParam' );
1004  }
1005  if ( $this->incompleteForm ) {
1006  # If the form is incomplete, force to preview.
1007  wfDebug( __METHOD__ . ": Form data appears to be incomplete" );
1008  wfDebug( "POST DATA: " . var_export( $request->getPostValues(), true ) );
1009  $this->preview = true;
1010  } else {
1011  $this->preview = $request->getCheck( 'wpPreview' );
1012  $this->diff = $request->getCheck( 'wpDiff' );
1013 
1014  // Remember whether a save was requested, so we can indicate
1015  // if we forced preview due to session failure.
1016  $this->mTriedSave = !$this->preview;
1017 
1018  if ( $this->tokenOk( $request ) ) {
1019  # Some browsers will not report any submit button
1020  # if the user hits enter in the comment box.
1021  # The unmarked state will be assumed to be a save,
1022  # if the form seems otherwise complete.
1023  wfDebug( __METHOD__ . ": Passed token check." );
1024  } elseif ( $this->diff ) {
1025  # Failed token check, but only requested "Show Changes".
1026  wfDebug( __METHOD__ . ": Failed token check; Show Changes requested." );
1027  } else {
1028  # Page might be a hack attempt posted from
1029  # an external site. Preview instead of saving.
1030  wfDebug( __METHOD__ . ": Failed token check; forcing preview" );
1031  $this->preview = true;
1032  }
1033  }
1034  $this->save = !$this->preview && !$this->diff;
1035  if ( !preg_match( '/^\d{14}$/', $this->edittime ) ) {
1036  $this->edittime = null;
1037  }
1038 
1039  if ( !preg_match( '/^\d{14}$/', $this->starttime ) ) {
1040  $this->starttime = null;
1041  }
1042 
1043  $this->recreate = $request->getCheck( 'wpRecreate' );
1044 
1045  $user = $this->getContext()->getUser();
1046 
1047  $this->minoredit = $request->getCheck( 'wpMinoredit' );
1048  $this->watchthis = $request->getCheck( 'wpWatchthis' );
1049  $expiry = $request->getText( 'wpWatchlistExpiry' );
1050  if ( $this->watchlistExpiryEnabled && $expiry !== '' ) {
1051  // This parsing of the user-posted expiry is done for both preview and saving. This
1052  // is necessary because ApiEditPage uses preview when it saves (yuck!). Note that it
1053  // only works because the unnormalized value is retrieved again below in
1054  // getCheckboxesDefinitionForWatchlist().
1055  $expiry = ExpiryDef::normalizeExpiry( $expiry, TS_ISO_8601 );
1056  if ( $expiry !== false ) {
1057  $this->watchlistExpiry = $expiry;
1058  }
1059  }
1060 
1061  # Don't force edit summaries when a user is editing their own user or talk page
1062  if ( ( $this->mTitle->getNamespace() === NS_USER || $this->mTitle->getNamespace() === NS_USER_TALK )
1063  && $this->mTitle->getText() == $user->getName()
1064  ) {
1065  $this->allowBlankSummary = true;
1066  } else {
1067  $this->allowBlankSummary = $request->getBool( 'wpIgnoreBlankSummary' )
1068  || !$user->getOption( 'forceeditsummary' );
1069  }
1070 
1071  $this->autoSumm = $request->getText( 'wpAutoSummary' );
1072 
1073  $this->allowBlankArticle = $request->getBool( 'wpIgnoreBlankArticle' );
1074  $this->allowSelfRedirect = $request->getBool( 'wpIgnoreSelfRedirect' );
1075 
1076  $changeTags = $request->getVal( 'wpChangeTags' );
1077  if ( $changeTags === null || $changeTags === '' ) {
1078  $this->changeTags = [];
1079  } else {
1080  $this->changeTags = array_filter(
1081  array_map(
1082  'trim',
1083  explode( ',', $changeTags )
1084  )
1085  );
1086  }
1087  } else {
1088  # Not a posted form? Start with nothing.
1089  wfDebug( __METHOD__ . ": Not a posted form." );
1090  $this->textbox1 = '';
1091  $this->summary = '';
1092  $this->sectiontitle = '';
1093  $this->edittime = '';
1094  $this->editRevId = null;
1095  $this->starttime = wfTimestampNow();
1096  $this->edit = false;
1097  $this->preview = false;
1098  $this->save = false;
1099  $this->diff = false;
1100  $this->minoredit = false;
1101  // Watch may be overridden by request parameters
1102  $this->watchthis = $request->getBool( 'watchthis', false );
1103  if ( $this->watchlistExpiryEnabled ) {
1104  $this->watchlistExpiry = null;
1105  }
1106  $this->recreate = false;
1107 
1108  // When creating a new section, we can preload a section title by passing it as the
1109  // preloadtitle parameter in the URL (T15100)
1110  if ( $this->section == 'new' && $request->getVal( 'preloadtitle' ) ) {
1111  $this->sectiontitle = $request->getVal( 'preloadtitle' );
1112  // Once wpSummary isn't being use for setting section titles, we should delete this.
1113  $this->summary = $request->getVal( 'preloadtitle' );
1114  } elseif ( $this->section != 'new' && $request->getVal( 'summary' ) !== '' ) {
1115  $this->summary = $request->getText( 'summary' );
1116  if ( $this->summary !== '' ) {
1117  $this->hasPresetSummary = true;
1118  }
1119  }
1120 
1121  if ( $request->getVal( 'minor' ) ) {
1122  $this->minoredit = true;
1123  }
1124  }
1125 
1126  $this->oldid = $request->getInt( 'oldid' );
1127  $this->parentRevId = $request->getInt( 'parentRevId' );
1128 
1129  $this->markAsBot = $request->getBool( 'bot', true );
1130  $this->nosummary = $request->getBool( 'nosummary' );
1131 
1132  // May be overridden by revision.
1133  $this->contentModel = $request->getText( 'model', $this->contentModel );
1134  // May be overridden by revision.
1135  $this->contentFormat = $request->getText( 'format', $this->contentFormat );
1136 
1137  try {
1138  $handler = $this->contentHandlerFactory->getContentHandler( $this->contentModel );
1139  } catch ( MWUnknownContentModelException $e ) {
1140  throw new ErrorPageError(
1141  'editpage-invalidcontentmodel-title',
1142  'editpage-invalidcontentmodel-text',
1143  [ wfEscapeWikiText( $this->contentModel ) ]
1144  );
1145  }
1146 
1147  if ( !$handler->isSupportedFormat( $this->contentFormat ) ) {
1148  throw new ErrorPageError(
1149  'editpage-notsupportedcontentformat-title',
1150  'editpage-notsupportedcontentformat-text',
1151  [
1152  wfEscapeWikiText( $this->contentFormat ),
1153  wfEscapeWikiText( ContentHandler::getLocalizedName( $this->contentModel ) )
1154  ]
1155  );
1156  }
1157 
1164  $this->editintro = $request->getText( 'editintro',
1165  // Custom edit intro for new sections
1166  $this->section === 'new' ? 'MediaWiki:addsection-editintro' : '' );
1167 
1168  // Allow extensions to modify form data
1169  $this->getHookRunner()->onEditPage__importFormData( $this, $request );
1170  }
1171 
1181  protected function importContentFormData( &$request ) {
1182  return null; // Don't do anything, EditPage already extracted wpTextbox1
1183  }
1184 
1190  private function initialiseForm() {
1191  $this->edittime = $this->page->getTimestamp();
1192  $this->editRevId = $this->page->getLatest();
1193 
1194  $dummy = $this->contentHandlerFactory
1195  ->getContentHandler( $this->contentModel )
1196  ->makeEmptyContent();
1197  $content = $this->getContentObject( $dummy ); # TODO: track content object?!
1198  if ( $content === $dummy ) { // Invalid section
1199  $this->noSuchSectionPage();
1200  return false;
1201  }
1202 
1203  if ( !$content ) {
1204  $out = $this->context->getOutput();
1205  $this->editFormPageTop .= Html::rawElement(
1206  'div', [ 'class' => 'errorbox' ],
1207  $out->parseAsInterface( $this->context->msg( 'missing-revision-content',
1208  $this->oldid,
1209  Message::plaintextParam( $this->mTitle->getPrefixedText() )
1210  ) )
1211  );
1212  } elseif ( !$this->isSupportedContentModel( $content->getModel() ) ) {
1213  $modelMsg = $this->getContext()->msg( 'content-model-' . $content->getModel() );
1214  $modelName = $modelMsg->exists() ? $modelMsg->text() : $content->getModel();
1215 
1216  $out = $this->context->getOutput();
1217  $out->showErrorPage(
1218  'modeleditnotsupported-title',
1219  'modeleditnotsupported-text',
1220  [ $modelName ]
1221  );
1222  return false;
1223  }
1224 
1225  $this->textbox1 = $this->toEditText( $content );
1226 
1227  $user = $this->context->getUser();
1228  // activate checkboxes if user wants them to be always active
1229  # Sort out the "watch" checkbox
1230  if ( $user->getOption( 'watchdefault' ) ) {
1231  # Watch all edits
1232  $this->watchthis = true;
1233  } elseif ( $user->getOption( 'watchcreations' ) && !$this->mTitle->exists() ) {
1234  # Watch creations
1235  $this->watchthis = true;
1236  } elseif ( $this->watchlistManager->isWatched( $user, $this->mTitle ) ) {
1237  # Already watched
1238  $this->watchthis = true;
1239  }
1240  if ( $this->watchthis && $this->watchlistExpiryEnabled ) {
1241  $watchedItem = $this->watchedItemStore->getWatchedItem( $user, $this->getTitle() );
1242  $this->watchlistExpiry = $watchedItem ? $watchedItem->getExpiry() : null;
1243  }
1244  if ( $user->getOption( 'minordefault' ) && !$this->isNew ) {
1245  $this->minoredit = true;
1246  }
1247  if ( $this->textbox1 === false ) {
1248  return false;
1249  }
1250  return true;
1251  }
1252 
1260  protected function getContentObject( $def_content = null ) {
1261  $disableAnonTalk = MediaWikiServices::getInstance()->getMainConfig()->get( 'DisableAnonTalk' );
1262 
1263  $content = false;
1264 
1265  $user = $this->context->getUser();
1266  $request = $this->context->getRequest();
1267  // For message page not locally set, use the i18n message.
1268  // For other non-existent articles, use preload text if any.
1269  if ( !$this->mTitle->exists() || $this->section == 'new' ) {
1270  if ( $this->mTitle->getNamespace() === NS_MEDIAWIKI && $this->section != 'new' ) {
1271  # If this is a system message, get the default text.
1272  $msg = $this->mTitle->getDefaultMessageText();
1273 
1274  $content = $this->toEditContent( $msg );
1275  }
1276  if ( $content === false ) {
1277  // Custom preload text for new sections
1278  $preload = $this->section === 'new' ? 'MediaWiki:addsection-preload' : '';
1279  $params = [];
1280 
1281  // T297725: Don't trick users into making edits to e.g. .js subpages
1282  if ( $this->mTitle->hasContentModel( CONTENT_MODEL_WIKITEXT ) ) {
1283  $preload = $request->getVal( 'preload', $preload );
1284  $params = $request->getArray( 'preloadparams', $params );
1285  }
1286 
1287  $content = $this->getPreloadedContent( $preload, $params );
1288  }
1289  // For existing pages, get text based on "undo" or section parameters.
1290  } elseif ( $this->section != '' ) {
1291  // Get section edit text (returns $def_text for invalid sections)
1292  $orig = $this->getOriginalContent( $user );
1293  $content = $orig ? $orig->getSection( $this->section ) : null;
1294 
1295  if ( !$content ) {
1296  $content = $def_content;
1297  }
1298  } else {
1299  $undoafter = $request->getInt( 'undoafter' );
1300  $undo = $request->getInt( 'undo' );
1301 
1302  if ( $undo > 0 && $undoafter > 0 ) {
1303  // The use of getRevisionByTitle() is intentional, as allowing access to
1304  // arbitrary revisions on arbitrary pages bypass partial visibility restrictions (T297322).
1305  $undorev = $this->revisionStore->getRevisionByTitle( $this->mTitle, $undo );
1306  $oldrev = $this->revisionStore->getRevisionByTitle( $this->mTitle, $undoafter );
1307  $undoMsg = null;
1308 
1309  # Make sure it's the right page,
1310  # the revisions exist and they were not deleted.
1311  # Otherwise, $content will be left as-is.
1312  if ( $undorev !== null && $oldrev !== null &&
1313  !$undorev->isDeleted( RevisionRecord::DELETED_TEXT ) &&
1314  !$oldrev->isDeleted( RevisionRecord::DELETED_TEXT )
1315  ) {
1316  if ( WikiPage::hasDifferencesOutsideMainSlot( $undorev, $oldrev )
1317  || !$this->isSupportedContentModel(
1318  $oldrev->getSlot( SlotRecord::MAIN, RevisionRecord::RAW )->getModel()
1319  )
1320  ) {
1321  // Hack for undo while EditPage can't handle multi-slot editing
1322  $this->context->getOutput()->redirect( $this->mTitle->getFullURL( [
1323  'action' => 'mcrundo',
1324  'undo' => $undo,
1325  'undoafter' => $undoafter,
1326  ] ) );
1327  return false;
1328  } else {
1329  $content = $this->getUndoContent( $undorev, $oldrev );
1330 
1331  if ( $content === false ) {
1332  # Warn the user that something went wrong
1333  $undoMsg = 'failure';
1334  }
1335  }
1336 
1337  if ( $undoMsg === null ) {
1338  $oldContent = $this->page->getContent( RevisionRecord::RAW );
1339  $services = MediaWikiServices::getInstance();
1340  $popts = ParserOptions::newFromUserAndLang( $user, $services->getContentLanguage() );
1341  $contentTransformer = $services->getContentTransformer();
1342  $newContent = $contentTransformer->preSaveTransform( $content, $this->mTitle, $user, $popts );
1343 
1344  if ( $newContent->getModel() !== $oldContent->getModel() ) {
1345  // The undo may change content
1346  // model if its reverting the top
1347  // edit. This can result in
1348  // mismatched content model/format.
1349  $this->contentModel = $newContent->getModel();
1350  $oldMainSlot = $oldrev->getSlot(
1351  SlotRecord::MAIN,
1352  RevisionRecord::RAW
1353  );
1354  $this->contentFormat = $oldMainSlot->getFormat();
1355  if ( $this->contentFormat === null ) {
1356  $this->contentFormat = $this->contentHandlerFactory
1357  ->getContentHandler( $oldMainSlot->getModel() )
1358  ->getDefaultFormat();
1359  }
1360  }
1361 
1362  if ( $newContent->equals( $oldContent ) ) {
1363  # Tell the user that the undo results in no change,
1364  # i.e. the revisions were already undone.
1365  $undoMsg = 'nochange';
1366  $content = false;
1367  } else {
1368  # Inform the user of our success and set an automatic edit summary
1369  $undoMsg = 'success';
1370 
1371  # If we just undid one rev, use an autosummary
1372  $firstrev = $this->revisionStore->getNextRevision( $oldrev );
1373  if ( $firstrev && $firstrev->getId() == $undo ) {
1374  $userText = $undorev->getUser() ?
1375  $undorev->getUser()->getName() :
1376  '';
1377  if ( $userText === '' ) {
1378  $undoSummary = $this->context->msg(
1379  'undo-summary-username-hidden',
1380  $undo
1381  )->inContentLanguage()->text();
1382  // Handle external users (imported revisions)
1383  } elseif ( ExternalUserNames::isExternal( $userText ) ) {
1384  $userLinkTitle = ExternalUserNames::getUserLinkTitle( $userText );
1385  if ( $userLinkTitle ) {
1386  $userLink = $userLinkTitle->getPrefixedText();
1387  $undoSummary = $this->context->msg(
1388  'undo-summary-import',
1389  $undo,
1390  $userLink,
1391  $userText
1392  )->inContentLanguage()->text();
1393  } else {
1394  $undoSummary = $this->context->msg(
1395  'undo-summary-import2',
1396  $undo,
1397  $userText
1398  )->inContentLanguage()->text();
1399  }
1400  } else {
1401  $undoIsAnon = $undorev->getUser() ?
1402  !$undorev->getUser()->isRegistered() :
1403  true;
1404  $undoMessage = ( $undoIsAnon && $disableAnonTalk ) ?
1405  'undo-summary-anon' :
1406  'undo-summary';
1407  $undoSummary = $this->context->msg(
1408  $undoMessage,
1409  $undo,
1410  $userText
1411  )->inContentLanguage()->text();
1412  }
1413  if ( $this->summary === '' ) {
1414  $this->summary = $undoSummary;
1415  } else {
1416  $this->summary = $undoSummary . $this->context->msg( 'colon-separator' )
1417  ->inContentLanguage()->text() . $this->summary;
1418  }
1419  }
1420  $this->undidRev = $undo;
1421  $this->undoAfter = $undoafter;
1422  $this->formtype = 'diff';
1423  }
1424  }
1425  } else {
1426  // Failed basic checks.
1427  // Older revisions may have been removed since the link
1428  // was created, or we may simply have got bogus input.
1429  $undoMsg = 'norev';
1430  }
1431 
1432  $out = $this->context->getOutput();
1433  // Messages: undo-success, undo-failure, undo-main-slot-only, undo-norev,
1434  // undo-nochange.
1435  $class = ( $undoMsg == 'success' ? '' : 'error ' ) . "mw-undo-{$undoMsg}";
1436  $this->editFormPageTop .= Html::rawElement(
1437  'div',
1438  [ 'class' => $class ],
1439  $out->parseAsInterface(
1440  $this->context->msg( 'undo-' . $undoMsg )->plain()
1441  )
1442  );
1443  }
1444 
1445  if ( $content === false ) {
1446  $content = $this->getOriginalContent( $user );
1447  }
1448  }
1449 
1450  return $content;
1451  }
1452 
1463  private function getUndoContent( RevisionRecord $undoRev, RevisionRecord $oldRev ) {
1464  $handler = $this->contentHandlerFactory
1465  ->getContentHandler( $undoRev->getSlot(
1466  SlotRecord::MAIN,
1467  RevisionRecord::RAW
1468  )->getModel() );
1469  $currentContent = $this->page->getRevisionRecord()
1470  ->getContent( SlotRecord::MAIN );
1471  $undoContent = $undoRev->getContent( SlotRecord::MAIN );
1472  $undoAfterContent = $oldRev->getContent( SlotRecord::MAIN );
1473  $undoIsLatest = $this->page->getRevisionRecord()->getId() === $undoRev->getId();
1474 
1475  return $handler->getUndoContent(
1476  $currentContent,
1477  $undoContent,
1478  $undoAfterContent,
1479  $undoIsLatest
1480  );
1481  }
1482 
1498  private function getOriginalContent( Authority $performer ) {
1499  if ( $this->section == 'new' ) {
1500  return $this->getCurrentContent();
1501  }
1502  $revRecord = $this->mArticle->fetchRevisionRecord();
1503  if ( $revRecord === null ) {
1504  return $this->contentHandlerFactory
1505  ->getContentHandler( $this->contentModel )
1506  ->makeEmptyContent();
1507  }
1508  return $revRecord->getContent( SlotRecord::MAIN, RevisionRecord::FOR_THIS_USER, $performer );
1509  }
1510 
1523  public function getParentRevId() {
1524  if ( $this->parentRevId ) {
1525  return $this->parentRevId;
1526  } else {
1527  return $this->mArticle->getRevIdFetched();
1528  }
1529  }
1530 
1539  protected function getCurrentContent() {
1540  $revRecord = $this->page->getRevisionRecord();
1541  $content = $revRecord ? $revRecord->getContent(
1542  SlotRecord::MAIN,
1543  RevisionRecord::RAW
1544  ) : null;
1545 
1546  if ( $content === null ) {
1547  return $this->contentHandlerFactory
1548  ->getContentHandler( $this->contentModel )
1549  ->makeEmptyContent();
1550  }
1551 
1552  return $content;
1553  }
1554 
1566  protected function getPreloadedContent( $preload, $params = [] ) {
1567  $handler = $this->contentHandlerFactory->getContentHandler( $this->contentModel );
1568 
1569  if ( $preload === '' ) {
1570  return $handler->makeEmptyContent();
1571  }
1572 
1573  $title = Title::newFromText( $preload );
1574 
1575  # Check for existence to avoid getting MediaWiki:Noarticletext
1576  if ( !$this->isPageExistingAndViewable( $title, $this->getContext()->getAuthority() ) ) {
1577  // TODO: somehow show a warning to the user!
1578  return $handler->makeEmptyContent();
1579  }
1580 
1581  $page = $this->wikiPageFactory->newFromTitle( $title );
1582  if ( $page->isRedirect() ) {
1583  $redirTarget = $this->redirectLookup->getRedirectTarget( $title );
1584  $redirTarget = Title::castFromLinkTarget( $redirTarget );
1585  # Same as before
1586  if ( !$this->isPageExistingAndViewable( $redirTarget, $this->getContext()->getAuthority() ) ) {
1587  // TODO: somehow show a warning to the user!
1588  return $handler->makeEmptyContent();
1589  }
1590  $page = $this->wikiPageFactory->newFromTitle( $redirTarget );
1591  }
1592 
1593  $content = $page->getContent( RevisionRecord::RAW );
1594 
1595  if ( !$content ) {
1596  // TODO: somehow show a warning to the user!
1597  return $handler->makeEmptyContent();
1598  }
1599 
1600  if ( $content->getModel() !== $handler->getModelID() ) {
1601  $converted = $content->convert( $handler->getModelID() );
1602 
1603  if ( !$converted ) {
1604  // TODO: somehow show a warning to the user!
1605  wfDebug( "Attempt to preload incompatible content: " .
1606  "can't convert " . $content->getModel() .
1607  " to " . $handler->getModelID() );
1608 
1609  return $handler->makeEmptyContent();
1610  }
1611 
1612  $content = $converted;
1613  }
1614 
1615  $parserOptions = ParserOptions::newFromUser( $this->context->getUser() );
1616  $contentTransformer = MediaWikiServices::getInstance()->getContentTransformer();
1617  return $contentTransformer->preloadTransform(
1618  $content,
1619  $title,
1620  $parserOptions,
1621  $params
1622  );
1623  }
1624 
1634  private function isPageExistingAndViewable( ?PageIdentity $page, Authority $performer ) {
1635  return $page && $page->exists() && $performer->authorizeRead( 'read', $page );
1636  }
1637 
1645  public function tokenOk( &$request ) {
1646  $token = $request->getVal( 'wpEditToken' );
1647  $user = $this->context->getUser();
1648  $this->mTokenOk = $user->matchEditToken( $token );
1649  return $this->mTokenOk;
1650  }
1651 
1666  protected function setPostEditCookie( $statusValue ) {
1667  $revisionId = $this->page->getLatest();
1668  $postEditKey = self::POST_EDIT_COOKIE_KEY_PREFIX . $revisionId;
1669 
1670  $val = 'saved';
1671  if ( $statusValue == self::AS_SUCCESS_NEW_ARTICLE ) {
1672  $val = 'created';
1673  } elseif ( $this->oldid ) {
1674  $val = 'restored';
1675  }
1676 
1677  $response = $this->context->getRequest()->response();
1678  $response->setCookie( $postEditKey, $val, time() + self::POST_EDIT_COOKIE_DURATION );
1679  }
1680 
1687  public function attemptSave( &$resultDetails = false ) {
1688  // Allow bots to exempt some edits from bot flagging
1689  $markAsBot = $this->markAsBot
1690  && $this->context->getAuthority()->isAllowed( 'bot' );
1691 
1692  // Allow trusted users to mark some edits as minor
1693  $markAsMinor = $this->minoredit && !$this->isNew
1694  && $this->context->getAuthority()->isAllowed( 'minoredit' );
1695 
1696  $status = $this->internalAttemptSave( $resultDetails, $markAsBot, $markAsMinor );
1697 
1698  $this->getHookRunner()->onEditPage__attemptSave_after( $this, $status, $resultDetails );
1699 
1700  return $status;
1701  }
1702 
1706  private function incrementResolvedConflicts() {
1707  if ( $this->context->getRequest()->getText( 'mode' ) !== 'conflict' ) {
1708  return;
1709  }
1710 
1711  $this->getEditConflictHelper()->incrementResolvedStats( $this->context->getUser() );
1712  }
1713 
1723  private function handleStatus( Status $status, $resultDetails ) {
1724  $statusValue = is_int( $status->value ) ? $status->value : 0;
1725 
1730  if ( $statusValue == self::AS_SUCCESS_UPDATE
1731  || $statusValue == self::AS_SUCCESS_NEW_ARTICLE
1732  ) {
1733  $this->incrementResolvedConflicts();
1734 
1735  $this->didSave = true;
1736  if ( !$resultDetails['nullEdit'] ) {
1737  $this->setPostEditCookie( $statusValue );
1738  }
1739  }
1740 
1741  $out = $this->context->getOutput();
1742 
1743  // "wpExtraQueryRedirect" is a hidden input to modify
1744  // after save URL and is not used by actual edit form
1745  $request = $this->context->getRequest();
1746  $extraQueryRedirect = $request->getVal( 'wpExtraQueryRedirect' );
1747 
1748  switch ( $statusValue ) {
1756  case self::AS_END:
1759  return true;
1760 
1761  case self::AS_HOOK_ERROR:
1762  return false;
1763 
1765  wfDeprecated(
1766  __METHOD__ . ' with $statusValue == AS_CANNOT_USE_CUSTOM_MODEL',
1767  '1.35'
1768  );
1769  // ...and fall through to next case
1770  case self::AS_PARSE_ERROR:
1772  $out->wrapWikiTextAsInterface( 'error',
1773  $status->getWikiText( false, false, $this->context->getLanguage() )
1774  );
1775  return true;
1776 
1778  $query = $resultDetails['redirect'] ? 'redirect=no' : '';
1779  if ( $extraQueryRedirect ) {
1780  if ( $query !== '' ) {
1781  $query .= '&';
1782  }
1783  $query .= $extraQueryRedirect;
1784  }
1785  $anchor = $resultDetails['sectionanchor'] ?? '';
1786  $out->redirect( $this->mTitle->getFullURL( $query ) . $anchor );
1787  return false;
1788 
1790  $extraQuery = '';
1791  $sectionanchor = $resultDetails['sectionanchor'];
1792 
1793  // Give extensions a chance to modify URL query on update
1794  $this->getHookRunner()->onArticleUpdateBeforeRedirect( $this->mArticle,
1795  $sectionanchor, $extraQuery );
1796 
1797  if ( $resultDetails['redirect'] ) {
1798  if ( $extraQuery !== '' ) {
1799  $extraQuery = '&' . $extraQuery;
1800  }
1801  $extraQuery = 'redirect=no' . $extraQuery;
1802  }
1803  if ( $extraQueryRedirect ) {
1804  if ( $extraQuery !== '' ) {
1805  $extraQuery .= '&';
1806  }
1807  $extraQuery .= $extraQueryRedirect;
1808  }
1809 
1810  $out->redirect( $this->mTitle->getFullURL( $extraQuery ) . $sectionanchor );
1811  return false;
1812 
1813  case self::AS_SPAM_ERROR:
1814  $this->spamPageWithContent( $resultDetails['spam'] ?? false );
1815  return false;
1816 
1818  throw new UserBlockedError(
1819  $this->context->getUser()->getBlock(),
1820  $this->context->getUser(),
1821  $this->context->getLanguage(),
1822  $request->getIP()
1823  );
1824 
1827  throw new PermissionsError( 'upload' );
1828 
1831  throw new PermissionsError( 'edit' );
1832 
1834  throw new ReadOnlyError;
1835 
1836  case self::AS_RATE_LIMITED:
1837  throw new ThrottledError();
1838 
1840  $permission = $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage';
1841  throw new PermissionsError( $permission );
1842 
1844  throw new PermissionsError( 'editcontentmodel' );
1845 
1846  default:
1847  // We don't recognize $statusValue. The only way that can happen
1848  // is if an extension hook aborted from inside ArticleSave.
1849  // Render the status object into $this->hookError
1850  // FIXME this sucks, we should just use the Status object throughout
1851  $this->hookError = Html::errorBox(
1852  "\n" . $status->getWikiText( false, false, $this->context->getLanguage() )
1853  );
1854  return true;
1855  }
1856  }
1857 
1863  private function newSectionSummary(): array {
1864  $newSectionSummary = $this->summary;
1865  $newSectionAnchor = '';
1866  $services = MediaWikiServices::getInstance();
1867  $parser = $services->getParser();
1868  $textFormatter = $services->getMessageFormatterFactory()->getTextFormatter(
1869  $services->getContentLanguage()->getCode()
1870  );
1871 
1872  if ( $this->sectiontitle !== '' ) {
1873  $newSectionAnchor = $this->guessSectionName( $this->sectiontitle );
1874  // If no edit summary was specified, create one automatically from the section
1875  // title and have it link to the new section. Otherwise, respect the summary as
1876  // passed.
1877  if ( $this->summary === '' ) {
1878  $messageValue = MessageValue::new( 'newsectionsummary' )
1879  ->plaintextParams( $parser->stripSectionName( $this->sectiontitle ) );
1880  $newSectionSummary = $textFormatter->format( $messageValue );
1881  }
1882  } elseif ( $this->summary !== '' ) {
1883  $newSectionAnchor = $this->guessSectionName( $this->summary );
1884  // This is a new section, so create a link to the new section
1885  // in the revision summary.
1886  $messageValue = MessageValue::new( 'newsectionsummary' )
1887  ->plaintextParams( $parser->stripSectionName( $this->summary ) );
1888  $newSectionSummary = $textFormatter->format( $messageValue );
1889  }
1890  return [ $newSectionSummary, $newSectionAnchor ];
1891  }
1892 
1919  public function internalAttemptSave( &$result, $markAsBot = false, $markAsMinor = false ) {
1920  $useNPPatrol = MediaWikiServices::getInstance()->getMainConfig()->get( 'UseNPPatrol' );
1921  $useRCPatrol = MediaWikiServices::getInstance()->getMainConfig()->get( 'UseRCPatrol' );
1922  if ( !$this->getHookRunner()->onEditPage__attemptSave( $this ) ) {
1923  wfDebug( "Hook 'EditPage::attemptSave' aborted article saving" );
1924  $status = Status::newFatal( 'hookaborted' );
1925  $status->value = self::AS_HOOK_ERROR;
1926  return $status;
1927  }
1928 
1929  if ( !$this->getHookRunner()->onEditFilter( $this, $this->textbox1, $this->section,
1930  $this->hookError, $this->summary )
1931  ) {
1932  # Error messages etc. could be handled within the hook...
1933  $status = Status::newFatal( 'hookaborted' );
1934  $status->value = self::AS_HOOK_ERROR;
1935  return $status;
1936  } elseif ( $this->hookError != '' ) {
1937  # ...or the hook could be expecting us to produce an error
1938  $status = Status::newFatal( 'hookaborted' );
1939  $status->value = self::AS_HOOK_ERROR_EXPECTED;
1940  return $status;
1941  }
1942 
1943  try {
1944  # Construct Content object
1945  $textbox_content = $this->toEditContent( $this->textbox1 );
1946  } catch ( MWContentSerializationException $ex ) {
1947  $status = Status::newFatal(
1948  'content-failed-to-parse',
1949  $this->contentModel,
1950  $this->contentFormat,
1951  $ex->getMessage()
1952  );
1953  $status->value = self::AS_PARSE_ERROR;
1954  return $status;
1955  }
1956 
1957  $this->contentLength = strlen( $this->textbox1 );
1958  $user = $this->context->getUser();
1959 
1960  $changingContentModel = false;
1961  if ( $this->contentModel !== $this->mTitle->getContentModel() ) {
1962  $changingContentModel = true;
1963  $oldContentModel = $this->mTitle->getContentModel();
1964  }
1965 
1966  // BEGINNING OF MIGRATION TO EDITCONSTRAINT SYSTEM (see T157658)
1967  $constraintFactory = MediaWikiServices::getInstance()->getService( '_EditConstraintFactory' );
1968  $constraintRunner = new EditConstraintRunner();
1969 
1970  // UnicodeConstraint: ensure that `$this->unicodeCheck` is the correct unicode
1971  $constraintRunner->addConstraint(
1972  new UnicodeConstraint( $this->unicodeCheck )
1973  );
1974 
1975  // SimpleAntiSpamConstraint: ensure that the context request does not have
1976  // `wpAntispam` set
1977  $constraintRunner->addConstraint(
1978  $constraintFactory->newSimpleAntiSpamConstraint(
1979  $this->context->getRequest()->getText( 'wpAntispam' ),
1980  $user,
1981  $this->mTitle
1982  )
1983  );
1984 
1985  // SpamRegexConstraint: ensure that the summary and text don't match the spam regex
1986  // FIXME $this->section is documented to always be a string, but it can be null
1987  // since importFormData does not provide a default when getting the section from
1988  // WebRequest, and the default default is null.
1989  $constraintRunner->addConstraint(
1990  $constraintFactory->newSpamRegexConstraint(
1991  $this->summary,
1992  $this->section ?? '',
1993  $this->sectiontitle,
1994  $this->textbox1,
1995  $this->context->getRequest()->getIP(),
1996  $this->mTitle
1997  )
1998  );
1999  $constraintRunner->addConstraint(
2000  new EditRightConstraint( $user )
2001  );
2002  $constraintRunner->addConstraint(
2004  $textbox_content,
2005  $this->mTitle,
2006  $user
2007  )
2008  );
2009  $constraintRunner->addConstraint(
2010  $constraintFactory->newUserBlockConstraint( $this->mTitle, $user )
2011  );
2012  $constraintRunner->addConstraint(
2014  $user,
2015  $this->mTitle,
2016  $this->contentModel
2017  )
2018  );
2019 
2020  $constraintRunner->addConstraint(
2021  $constraintFactory->newReadOnlyConstraint()
2022  );
2023  $constraintRunner->addConstraint(
2024  new UserRateLimitConstraint( $user, $this->mTitle, $this->contentModel )
2025  );
2026  $constraintRunner->addConstraint(
2027  // Same constraint is used to check size before and after merging the
2028  // edits, which use different failure codes
2029  $constraintFactory->newPageSizeConstraint(
2030  $this->contentLength,
2031  PageSizeConstraint::BEFORE_MERGE
2032  )
2033  );
2034  $constraintRunner->addConstraint(
2035  new ChangeTagsConstraint( $user, $this->changeTags )
2036  );
2037 
2038  // If the article has been deleted while editing, don't save it without
2039  // confirmation
2040  $constraintRunner->addConstraint(
2042  $this->wasDeletedSinceLastEdit(),
2043  $this->recreate
2044  )
2045  );
2046 
2047  // Check the constraints
2048  if ( !$constraintRunner->checkConstraints() ) {
2049  $failed = $constraintRunner->getFailedConstraint();
2050 
2051  // Need to check SpamRegexConstraint here, to avoid needing to pass
2052  // $result by reference again
2053  if ( $failed instanceof SpamRegexConstraint ) {
2054  $result['spam'] = $failed->getMatch();
2055  } else {
2056  $this->handleFailedConstraint( $failed );
2057  }
2058 
2059  return Status::wrap( $failed->getLegacyStatus() );
2060  }
2061  // END OF MIGRATION TO EDITCONSTRAINT SYSTEM (continued below)
2062 
2063  # Load the page data from the primary DB. If anything changes in the meantime,
2064  # we detect it by using page_latest like a token in a 1 try compare-and-swap.
2065  $this->page->loadPageData( WikiPage::READ_LATEST );
2066  $new = !$this->page->exists();
2067 
2068  $flags = EDIT_AUTOSUMMARY |
2069  ( $new ? EDIT_NEW : EDIT_UPDATE ) |
2070  ( $markAsMinor ? EDIT_MINOR : 0 ) |
2071  ( $markAsBot ? EDIT_FORCE_BOT : 0 );
2072 
2073  if ( $new ) {
2074  $content = $textbox_content;
2075 
2076  $result['sectionanchor'] = '';
2077  if ( $this->section == 'new' ) {
2078  if ( $this->sectiontitle !== '' ) {
2079  // Insert the section title above the content.
2080  $content = $content->addSectionHeader( $this->sectiontitle );
2081  } elseif ( $this->summary !== '' ) {
2082  // Insert the section title above the content.
2083  $content = $content->addSectionHeader( $this->summary );
2084  }
2085 
2086  list( $newSectionSummary, $anchor ) = $this->newSectionSummary();
2087  $this->summary = $newSectionSummary;
2088  $result['sectionanchor'] = $anchor;
2089  }
2090 
2091  $pageUpdater = $this->page->newPageUpdater( $user )
2092  ->setContent( SlotRecord::MAIN, $content );
2093  $pageUpdater->prepareUpdate( $flags );
2094 
2095  // BEGINNING OF MIGRATION TO EDITCONSTRAINT SYSTEM (see T157658)
2096  // Create a new runner to avoid rechecking the prior constraints, use the same factory
2097  $constraintRunner = new EditConstraintRunner();
2098  // Late check for create permission, just in case *PARANOIA*
2099  $constraintRunner->addConstraint(
2100  new CreationPermissionConstraint( $user, $this->mTitle )
2101  );
2102 
2103  // Don't save a new page if it's blank or if it's a MediaWiki:
2104  // message with content equivalent to default (allow empty pages
2105  // in this case to disable messages, see T52124)
2106  $constraintRunner->addConstraint(
2108  $this->mTitle,
2109  $this->allowBlankArticle,
2110  $this->textbox1
2111  )
2112  );
2113 
2114  $constraintRunner->addConstraint(
2115  $constraintFactory->newEditFilterMergedContentHookConstraint(
2116  $content,
2117  $this->context,
2118  $this->summary,
2119  $markAsMinor
2120  )
2121  );
2122 
2123  // Check the constraints
2124  if ( !$constraintRunner->checkConstraints() ) {
2125  $failed = $constraintRunner->getFailedConstraint();
2126  $this->handleFailedConstraint( $failed );
2127  return Status::wrap( $failed->getLegacyStatus() );
2128  }
2129  // END OF MIGRATION TO EDITCONSTRAINT SYSTEM (continued below)
2130  } else { # not $new
2131 
2132  # Article exists. Check for edit conflict.
2133 
2134  $timestamp = $this->page->getTimestamp();
2135  $latest = $this->page->getLatest();
2136 
2137  wfDebug( "timestamp: {$timestamp}, edittime: {$this->edittime}" );
2138  wfDebug( "revision: {$latest}, editRevId: {$this->editRevId}" );
2139 
2140  $editConflictLogger = LoggerFactory::getInstance( 'EditConflict' );
2141  // An edit conflict is detected if the current revision is different from the
2142  // revision that was current when editing was initiated on the client.
2143  // This is checked based on the timestamp and revision ID.
2144  // TODO: the timestamp based check can probably go away now.
2145  if ( ( $this->edittime !== null && $this->edittime != $timestamp )
2146  || ( $this->editRevId !== null && $this->editRevId != $latest )
2147  ) {
2148  $this->isConflict = true;
2149  list( $newSectionSummary, $newSectionAnchor ) = $this->newSectionSummary();
2150  if ( $this->section == 'new' ) {
2151  if ( $this->page->getUserText() == $user->getName() &&
2152  $this->page->getComment() == $newSectionSummary
2153  ) {
2154  // Probably a duplicate submission of a new comment.
2155  // This can happen when CDN resends a request after
2156  // a timeout but the first one actually went through.
2157  $editConflictLogger->debug(
2158  'Duplicate new section submission; trigger edit conflict!'
2159  );
2160  } else {
2161  // New comment; suppress conflict.
2162  $this->isConflict = false;
2163  $editConflictLogger->debug( 'Conflict suppressed; new section' );
2164  }
2165  } elseif ( $this->section == ''
2166  && $this->edittime
2167  && $this->revisionStore->userWasLastToEdit(
2168  wfGetDB( DB_PRIMARY ),
2169  $this->mTitle->getArticleID(),
2170  $user->getId(),
2171  $this->edittime
2172  )
2173  ) {
2174  # Suppress edit conflict with self, except for section edits where merging is required.
2175  $editConflictLogger->debug( 'Suppressing edit conflict, same user.' );
2176  $this->isConflict = false;
2177  }
2178  }
2179 
2180  // If sectiontitle is set, use it, otherwise use the summary as the section title.
2181  if ( $this->sectiontitle !== '' ) {
2182  $sectionTitle = $this->sectiontitle;
2183  } else {
2184  $sectionTitle = $this->summary;
2185  }
2186 
2187  $content = null;
2188 
2189  if ( $this->isConflict ) {
2190  $editConflictLogger->debug(
2191  'Conflict! Getting section {section} for time {editTime}'
2192  . ' (id {editRevId}, article time {timestamp})',
2193  [
2194  'section' => $this->section,
2195  'editTime' => $this->edittime,
2196  'editRevId' => $this->editRevId,
2197  'timestamp' => $timestamp,
2198  ]
2199  );
2200  // @TODO: replaceSectionAtRev() with base ID (not prior current) for ?oldid=X case
2201  // ...or disable section editing for non-current revisions (not exposed anyway).
2202  if ( $this->editRevId !== null ) {
2203  $content = $this->page->replaceSectionAtRev(
2204  $this->section,
2205  $textbox_content,
2206  $sectionTitle,
2207  $this->editRevId
2208  );
2209  } else {
2210  $content = $this->page->replaceSectionContent(
2211  $this->section,
2212  $textbox_content,
2213  $sectionTitle,
2214  $this->edittime
2215  );
2216  }
2217  } else {
2218  $editConflictLogger->debug(
2219  'Getting section {section}',
2220  [ 'section' => $this->section ]
2221  );
2222  $content = $this->page->replaceSectionContent(
2223  $this->section,
2224  $textbox_content,
2225  $sectionTitle
2226  );
2227  }
2228 
2229  if ( $content === null ) {
2230  $editConflictLogger->debug( 'Activating conflict; section replace failed.' );
2231  $this->isConflict = true;
2232  $content = $textbox_content; // do not try to merge here!
2233  } elseif ( $this->isConflict ) {
2234  // Attempt merge
2235  $mergedChange = $this->mergeChangesIntoContent( $content );
2236  if ( $mergedChange !== false ) {
2237  // Successful merge! Maybe we should tell the user the good news?
2238  $content = $mergedChange[0];
2239  $this->parentRevId = $mergedChange[1];
2240  $this->isConflict = false;
2241  $editConflictLogger->debug( 'Suppressing edit conflict, successful merge.' );
2242  } else {
2243  $this->section = '';
2244  $this->textbox1 = ( $content instanceof TextContent ) ? $content->getText() : '';
2245  $editConflictLogger->debug( 'Keeping edit conflict, failed merge.' );
2246  }
2247  }
2248 
2249  if ( $this->isConflict ) {
2250  return Status::newGood( self::AS_CONFLICT_DETECTED )->setOK( false );
2251  }
2252 
2253  $pageUpdater = $this->page->newPageUpdater( $user )
2254  ->setContent( SlotRecord::MAIN, $content );
2255  $pageUpdater->prepareUpdate( $flags );
2256 
2257  // BEGINNING OF MIGRATION TO EDITCONSTRAINT SYSTEM (see T157658)
2258  // Create a new runner to avoid rechecking the prior constraints, use the same factory
2259  $constraintRunner = new EditConstraintRunner();
2260  $constraintRunner->addConstraint(
2261  $constraintFactory->newEditFilterMergedContentHookConstraint(
2262  $content,
2263  $this->context,
2264  $this->summary,
2265  $markAsMinor
2266  )
2267  );
2268 
2269  if ( $this->section == 'new' ) {
2270  $constraintRunner->addConstraint(
2272  $this->summary,
2273  $this->allowBlankSummary
2274  )
2275  );
2276  $constraintRunner->addConstraint(
2277  new MissingCommentConstraint( $this->textbox1 )
2278  );
2279  } else {
2280  $constraintRunner->addConstraint(
2282  $this->summary,
2283  $this->autoSumm,
2284  $this->allowBlankSummary,
2285  $content,
2286  $this->getOriginalContent( $user )
2287  )
2288  );
2289  }
2290  // Check the constraints
2291  if ( !$constraintRunner->checkConstraints() ) {
2292  $failed = $constraintRunner->getFailedConstraint();
2293  $this->handleFailedConstraint( $failed );
2294  return Status::wrap( $failed->getLegacyStatus() );
2295  }
2296  // END OF MIGRATION TO EDITCONSTRAINT SYSTEM (continued below)
2297 
2298  # All's well
2299  $sectionAnchor = '';
2300  if ( $this->section == 'new' ) {
2301  list( $newSectionSummary, $anchor ) = $this->newSectionSummary();
2302  $this->summary = $newSectionSummary;
2303  $sectionAnchor = $anchor;
2304  } elseif ( $this->section != '' ) {
2305  # Try to get a section anchor from the section source, redirect
2306  # to edited section if header found.
2307  # XXX: Might be better to integrate this into WikiPage::replaceSectionAtRev
2308  # for duplicate heading checking and maybe parsing.
2309  $hasmatch = preg_match( "/^ *([=]{1,6})(.*?)(\\1) *\\n/i", $this->textbox1, $matches );
2310  # We can't deal with anchors, includes, html etc in the header for now,
2311  # headline would need to be parsed to improve this.
2312  if ( $hasmatch && strlen( $matches[2] ) > 0 ) {
2313  $sectionAnchor = $this->guessSectionName( $matches[2] );
2314  }
2315  }
2316  $result['sectionanchor'] = $sectionAnchor;
2317 
2318  // Save errors may fall down to the edit form, but we've now
2319  // merged the section into full text. Clear the section field
2320  // so that later submission of conflict forms won't try to
2321  // replace that into a duplicated mess.
2322  $this->textbox1 = $this->toEditText( $content );
2323  $this->section = '';
2324  }
2325 
2326  // Check for length errors again now that the section is merged in
2327  $this->contentLength = strlen( $this->toEditText( $content ) );
2328 
2329  // BEGINNING OF MIGRATION TO EDITCONSTRAINT SYSTEM (see T157658)
2330  // Create a new runner to avoid rechecking the prior constraints, use the same factory
2331  $constraintRunner = new EditConstraintRunner();
2332  $constraintRunner->addConstraint(
2334  $this->allowSelfRedirect,
2335  $content,
2336  $this->getCurrentContent(),
2337  $this->getTitle()
2338  )
2339  );
2340  $constraintRunner->addConstraint(
2341  // Same constraint is used to check size before and after merging the
2342  // edits, which use different failure codes
2343  $constraintFactory->newPageSizeConstraint(
2344  $this->contentLength,
2345  PageSizeConstraint::AFTER_MERGE
2346  )
2347  );
2348  // Check the constraints
2349  if ( !$constraintRunner->checkConstraints() ) {
2350  $failed = $constraintRunner->getFailedConstraint();
2351  $this->handleFailedConstraint( $failed );
2352  return Status::wrap( $failed->getLegacyStatus() );
2353  }
2354  // END OF MIGRATION TO EDITCONSTRAINT SYSTEM
2355 
2356  if ( $this->undidRev && $this->isUndoClean( $content ) ) {
2357  // As the user can change the edit's content before saving, we only mark
2358  // "clean" undos as reverts. This is to avoid abuse by marking irrelevant
2359  // edits as undos.
2360  $pageUpdater
2361  ->setOriginalRevisionId( $this->undoAfter ?: false )
2362  ->markAsRevert(
2363  EditResult::REVERT_UNDO,
2364  $this->undidRev,
2365  $this->undoAfter ?: null
2366  );
2367  }
2368 
2369  $needsPatrol = $useRCPatrol || ( $useNPPatrol && !$this->page->exists() );
2370  if ( $needsPatrol && $this->context->getAuthority()
2371  ->authorizeWrite( 'autopatrol', $this->getTitle() )
2372  ) {
2373  $pageUpdater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED );
2374  }
2375 
2376  $pageUpdater
2377  ->addTags( $this->changeTags )
2378  ->saveRevision(
2379  CommentStoreComment::newUnsavedComment( trim( $this->summary ) ),
2380  $flags
2381  );
2382  $doEditStatus = $pageUpdater->getStatus();
2383 
2384  if ( !$doEditStatus->isOK() ) {
2385  // Failure from doEdit()
2386  // Show the edit conflict page for certain recognized errors from doEdit(),
2387  // but don't show it for errors from extension hooks
2388  $errors = $doEditStatus->getErrorsArray();
2389  if ( in_array( $errors[0][0],
2390  [ 'edit-gone-missing', 'edit-conflict', 'edit-already-exists' ] )
2391  ) {
2392  $this->isConflict = true;
2393  // Destroys data doEdit() put in $status->value but who cares
2394  $doEditStatus->value = self::AS_END;
2395  }
2396  return $doEditStatus;
2397  }
2398 
2399  $result['nullEdit'] = $doEditStatus->hasMessage( 'edit-no-change' );
2400  if ( $result['nullEdit'] ) {
2401  // We don't know if it was a null edit until now, so increment here
2402  $user->pingLimiter( 'linkpurge' );
2403  }
2404  $result['redirect'] = $content->isRedirect();
2405 
2406  $this->updateWatchlist();
2407 
2408  // If the content model changed, add a log entry
2409  if ( $changingContentModel ) {
2411  $user,
2412  $new ? false : $oldContentModel,
2413  $this->contentModel,
2414  $this->summary
2415  );
2416  }
2417 
2418  // Instead of carrying the same status object throughout, it is created right
2419  // when it is returned, either at an earlier point due to an error or here
2420  // due to a successful edit.
2421  $statusCode = ( $new ? self::AS_SUCCESS_NEW_ARTICLE : self::AS_SUCCESS_UPDATE );
2422  $status = Status::newGood( $statusCode );
2423  return $status;
2424  }
2425 
2434  private function handleFailedConstraint( IEditConstraint $failed ) {
2435  if ( $failed instanceof PageSizeConstraint ) {
2436  // Error will be displayed by showEditForm()
2437  $this->tooBig = true;
2438  } elseif ( $failed instanceof UserBlockConstraint ) {
2439  // Auto-block user's IP if the account was "hard" blocked
2440  if ( !wfReadOnly() ) {
2441  $this->context->getUser()->spreadAnyEditBlock();
2442  }
2443  } elseif ( $failed instanceof DefaultTextConstraint ) {
2444  $this->blankArticle = true;
2445  } elseif ( $failed instanceof EditFilterMergedContentHookConstraint ) {
2446  $this->hookError = $failed->getHookError();
2447  } elseif (
2448  $failed instanceof AutoSummaryMissingSummaryConstraint ||
2449  $failed instanceof NewSectionMissingSummaryConstraint
2450  ) {
2451  $this->missingSummary = true;
2452  } elseif ( $failed instanceof MissingCommentConstraint ) {
2453  $this->missingComment = true;
2454  } elseif ( $failed instanceof SelfRedirectConstraint ) {
2455  $this->selfRedirect = true;
2456  }
2457  }
2458 
2469  private function isUndoClean( Content $content ): bool {
2470  // Check whether the undo was "clean", that is the user has not modified
2471  // the automatically generated content.
2472  $undoRev = $this->revisionStore->getRevisionById( $this->undidRev );
2473  if ( $undoRev === null ) {
2474  return false;
2475  }
2476 
2477  if ( $this->undoAfter ) {
2478  $oldRev = $this->revisionStore->getRevisionById( $this->undoAfter );
2479  } else {
2480  $oldRev = $this->revisionStore->getPreviousRevision( $undoRev );
2481  }
2482 
2483  if ( $oldRev === null ||
2484  $undoRev->isDeleted( RevisionRecord::DELETED_TEXT ) ||
2485  $oldRev->isDeleted( RevisionRecord::DELETED_TEXT )
2486  ) {
2487  return false;
2488  }
2489 
2490  $undoContent = $this->getUndoContent( $undoRev, $oldRev );
2491  if ( !$undoContent ) {
2492  return false;
2493  }
2494 
2495  // Do a pre-save transform on the retrieved undo content
2496  $services = MediaWikiServices::getInstance();
2497  $contentLanguage = $services->getContentLanguage();
2498  $user = $this->context->getUser();
2499  $parserOptions = ParserOptions::newFromUserAndLang( $user, $contentLanguage );
2500  $contentTransformer = $services->getContentTransformer();
2501  $undoContent = $contentTransformer->preSaveTransform( $undoContent, $this->mTitle, $user, $parserOptions );
2502 
2503  if ( $undoContent->equals( $content ) ) {
2504  return true;
2505  }
2506  return false;
2507  }
2508 
2515  protected function addContentModelChangeLogEntry( UserIdentity $user, $oldModel, $newModel, $reason ) {
2516  $new = $oldModel === false;
2517  $log = new ManualLogEntry( 'contentmodel', $new ? 'new' : 'change' );
2518  $log->setPerformer( $user );
2519  $log->setTarget( $this->mTitle );
2520  $log->setComment( $reason );
2521  $log->setParameters( [
2522  '4::oldmodel' => $oldModel,
2523  '5::newmodel' => $newModel
2524  ] );
2525  $logid = $log->insert();
2526  $log->publish( $logid );
2527  }
2528 
2532  protected function updateWatchlist() {
2533  $performer = $this->context->getAuthority();
2534  if ( !$performer->getUser()->isRegistered() ) {
2535  return;
2536  }
2537 
2539  $watch = $this->watchthis;
2541 
2542  // This can't run as a DeferredUpdate due to a possible race condition
2543  // when the post-edit redirect happens if the pendingUpdates queue is
2544  // too large to finish in time (T259564)
2545  $this->watchlistManager->setWatch( $watch, $performer, $title, $watchlistExpiry );
2546 
2547  $this->watchedItemStore->maybeEnqueueWatchlistExpiryJob();
2548  }
2549 
2562  private function mergeChangesIntoContent( $editContent ) {
2563  // This is the revision that was current at the time editing was initiated on the client,
2564  // even if the edit was based on an old revision.
2565  $baseRevRecord = $this->getExpectedParentRevision();
2566  $baseContent = $baseRevRecord ?
2567  $baseRevRecord->getContent( SlotRecord::MAIN ) :
2568  null;
2569 
2570  if ( $baseContent === null ) {
2571  return false;
2572  }
2573 
2574  // The current state, we want to merge updates into it
2575  $currentRevisionRecord = $this->revisionStore->getRevisionByTitle(
2576  $this->mTitle,
2577  0,
2578  RevisionStore::READ_LATEST
2579  );
2580  $currentContent = $currentRevisionRecord
2581  ? $currentRevisionRecord->getContent( SlotRecord::MAIN )
2582  : null;
2583 
2584  if ( $currentContent === null ) {
2585  return false;
2586  }
2587 
2588  $mergedContent = $this->contentHandlerFactory
2589  ->getContentHandler( $baseContent->getModel() )
2590  ->merge3( $baseContent, $editContent, $currentContent );
2591 
2592  if ( $mergedContent ) {
2593  // Also need to update parentRevId to what we just merged.
2594  return [ $mergedContent, $currentRevisionRecord->getId() ];
2595  }
2596 
2597  return false;
2598  }
2599 
2607  public function getExpectedParentRevision() {
2608  if ( $this->mExpectedParentRevision === false ) {
2609  $revRecord = null;
2610  if ( $this->editRevId ) {
2611  $revRecord = $this->revisionStore->getRevisionById(
2612  $this->editRevId,
2613  RevisionStore::READ_LATEST
2614  );
2615  } elseif ( $this->edittime ) {
2616  $revRecord = $this->revisionStore->getRevisionByTimestamp(
2617  $this->getTitle(),
2618  $this->edittime,
2619  RevisionStore::READ_LATEST
2620  );
2621  }
2622  $this->mExpectedParentRevision = $revRecord;
2623  }
2625  }
2626 
2627  public function setHeaders() {
2628  $out = $this->context->getOutput();
2629 
2630  $out->addModules( 'mediawiki.action.edit' );
2631  $out->addModuleStyles( 'mediawiki.action.edit.styles' );
2632  $out->addModuleStyles( 'mediawiki.editfont.styles' );
2633  $out->addModuleStyles( 'mediawiki.interface.helpers.styles' );
2634 
2635  $user = $this->context->getUser();
2636 
2637  if ( $user->getOption( 'uselivepreview' ) ) {
2638  $out->addModules( 'mediawiki.action.edit.preview' );
2639  }
2640 
2641  if ( $user->getOption( 'useeditwarning' ) ) {
2642  $out->addModules( 'mediawiki.action.edit.editWarning' );
2643  }
2644 
2645  # Enabled article-related sidebar, toplinks, etc.
2646  $out->setArticleRelated( true );
2647 
2648  $contextTitle = $this->getContextTitle();
2649  if ( $this->isConflict ) {
2650  $msg = 'editconflict';
2651  } elseif ( $contextTitle->exists() && $this->section != '' ) {
2652  $msg = $this->section == 'new' ? 'editingcomment' : 'editingsection';
2653  } else {
2654  $msg = $contextTitle->exists()
2655  || ( $contextTitle->getNamespace() === NS_MEDIAWIKI
2656  && $contextTitle->getDefaultMessageText() !== false
2657  )
2658  ? 'editing'
2659  : 'creating';
2660  }
2661 
2662  # Use the title defined by DISPLAYTITLE magic word when present
2663  # NOTE: getDisplayTitle() returns HTML while getPrefixedText() returns plain text.
2664  # setPageTitle() treats the input as wikitext, which should be safe in either case.
2665  $displayTitle = isset( $this->mParserOutput ) ? $this->mParserOutput->getDisplayTitle() : false;
2666  if ( $displayTitle === false ) {
2667  $displayTitle = $contextTitle->getPrefixedText();
2668  } else {
2669  $out->setDisplayTitle( $displayTitle );
2670  }
2671 
2672  // Enclose the title with an element. This is used on live preview to update the
2673  // preview of the display title.
2674  $displayTitle = Html::rawElement( 'span', [ 'id' => 'firstHeadingTitle' ], $displayTitle );
2675 
2676  $out->setPageTitle( $this->context->msg( $msg, $displayTitle ) );
2677 
2678  $config = $this->context->getConfig();
2679 
2680  # Transmit the name of the message to JavaScript. This was added for live preview.
2681  # Live preview doesn't use this anymore. The variable is still transmitted because
2682  # other scripts uses this variable.
2683  $out->addJsConfigVars( [
2684  'wgEditMessage' => $msg,
2685  ] );
2686 
2687  // Add whether to use 'save' or 'publish' messages to JavaScript for post-edit, other
2688  // editors, etc.
2689  $out->addJsConfigVars(
2690  'wgEditSubmitButtonLabelPublish',
2691  $config->get( 'EditSubmitButtonLabelPublish' )
2692  );
2693  }
2694 
2698  protected function showIntro() {
2699  if ( $this->suppressIntro ) {
2700  return;
2701  }
2702 
2703  $out = $this->context->getOutput();
2704  $namespace = $this->mTitle->getNamespace();
2705 
2706  if ( $namespace === NS_MEDIAWIKI ) {
2707  # Show a warning if editing an interface message
2708  $out->wrapWikiMsg( "<div class='mw-editinginterface'>\n$1\n</div>", 'editinginterface' );
2709  # If this is a default message (but not css, json, or js),
2710  # show a hint that it is translatable on translatewiki.net
2711  if (
2712  !$this->mTitle->hasContentModel( CONTENT_MODEL_CSS )
2713  && !$this->mTitle->hasContentModel( CONTENT_MODEL_JSON )
2714  && !$this->mTitle->hasContentModel( CONTENT_MODEL_JAVASCRIPT )
2715  ) {
2716  $defaultMessageText = $this->mTitle->getDefaultMessageText();
2717  if ( $defaultMessageText !== false ) {
2718  $out->wrapWikiMsg( "<div class='mw-translateinterface'>\n$1\n</div>",
2719  'translateinterface' );
2720  }
2721  }
2722  } elseif ( $namespace === NS_FILE ) {
2723  # Show a hint to shared repo
2724  $file = MediaWikiServices::getInstance()->getRepoGroup()->findFile( $this->mTitle );
2725  if ( $file && !$file->isLocal() ) {
2726  $descUrl = $file->getDescriptionUrl();
2727  # there must be a description url to show a hint to shared repo
2728  if ( $descUrl ) {
2729  if ( !$this->mTitle->exists() ) {
2730  $out->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-create\">\n$1\n</div>", [
2731  'sharedupload-desc-create', $file->getRepo()->getDisplayName(), $descUrl
2732  ] );
2733  } else {
2734  $out->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-edit\">\n$1\n</div>", [
2735  'sharedupload-desc-edit', $file->getRepo()->getDisplayName(), $descUrl
2736  ] );
2737  }
2738  }
2739  }
2740  }
2741 
2742  # Show a warning message when someone creates/edits a user (talk) page but the user does not exist
2743  # Show log extract when the user is currently blocked
2744  if ( $namespace === NS_USER || $namespace === NS_USER_TALK ) {
2745  $username = explode( '/', $this->mTitle->getText(), 2 )[0];
2746  $user = User::newFromName( $username, false /* allow IP users */ );
2747  $ip = $this->userNameUtils->isIP( $username );
2748  $block = DatabaseBlock::newFromTarget( $user, $user );
2749 
2750  $userExists = ( $user && $user->isRegistered() );
2751  if ( $userExists && $user->isHidden() &&
2752  !$this->permManager->userHasRight( $this->context->getUser(), 'hideuser' )
2753  ) {
2754  // If the user exists, but is hidden, and the viewer cannot see hidden
2755  // users, pretend like they don't exist at all. See T120883
2756  $userExists = false;
2757  }
2758 
2759  if ( !$userExists && !$ip ) { # User does not exist
2760  $out->wrapWikiMsg( "<div class=\"mw-userpage-userdoesnotexist error\">\n$1\n</div>",
2761  [ 'userpage-userdoesnotexist', wfEscapeWikiText( $username ) ] );
2762  } elseif (
2763  $block !== null &&
2764  $block->getType() != DatabaseBlock::TYPE_AUTO &&
2765  (
2766  $block->isSitewide() ||
2767  $user->isBlockedFrom(
2768  $this->mTitle,
2769  true
2770  )
2771  )
2772  ) {
2773  // Show log extract if the user is sitewide blocked or is partially
2774  // blocked and not allowed to edit their user page or user talk page
2776  $out,
2777  'block',
2778  MediaWikiServices::getInstance()->getNamespaceInfo()->
2779  getCanonicalName( NS_USER ) . ':' . $block->getTargetName(),
2780  '',
2781  [
2782  'lim' => 1,
2783  'showIfEmpty' => false,
2784  'msgKey' => [
2785  'blocked-notice-logextract',
2786  $user->getName() # Support GENDER in notice
2787  ]
2788  ]
2789  );
2790  }
2791  }
2792  # Try to add a custom edit intro, or use the standard one if this is not possible.
2793  if ( !$this->showCustomIntro() && !$this->mTitle->exists() ) {
2795  $this->context->msg( 'helppage' )->inContentLanguage()->text()
2796  ) );
2797  if ( $this->context->getUser()->isRegistered() ) {
2798  $out->wrapWikiMsg(
2799  // Suppress the external link icon, consider the help url an internal one
2800  "<div class=\"mw-newarticletext plainlinks\">\n$1\n</div>",
2801  [
2802  'newarticletext',
2803  $helpLink
2804  ]
2805  );
2806  } else {
2807  $out->wrapWikiMsg(
2808  // Suppress the external link icon, consider the help url an internal one
2809  "<div class=\"mw-newarticletextanon plainlinks\">\n$1\n</div>",
2810  [
2811  'newarticletextanon',
2812  $helpLink
2813  ]
2814  );
2815  }
2816  }
2817  # Give a notice if the user is editing a deleted/moved page...
2818  if ( !$this->mTitle->exists() ) {
2819  $dbr = wfGetDB( DB_REPLICA );
2820 
2821  LogEventsList::showLogExtract( $out, [ 'delete', 'move' ], $this->mTitle,
2822  '',
2823  [
2824  'lim' => 10,
2825  'conds' => [ 'log_action != ' . $dbr->addQuotes( 'revision' ) ],
2826  'showIfEmpty' => false,
2827  'msgKey' => [ 'recreate-moveddeleted-warn' ]
2828  ]
2829  );
2830  }
2831  }
2832 
2838  protected function showCustomIntro() {
2839  if ( $this->editintro ) {
2840  $title = Title::newFromText( $this->editintro );
2841  if ( $this->isPageExistingAndViewable( $title, $this->context->getUser() ) ) {
2842  // Added using template syntax, to take <noinclude>'s into account.
2843  $this->context->getOutput()->addWikiTextAsContent(
2844  '<div class="mw-editintro">{{:' . $title->getFullText() . '}}</div>',
2845  /*linestart*/true,
2846  $this->mTitle
2847  );
2848  return true;
2849  }
2850  }
2851  return false;
2852  }
2853 
2872  protected function toEditText( $content ) {
2873  if ( $content === null || $content === false || is_string( $content ) ) {
2874  return $content;
2875  }
2876 
2877  if ( !$this->isSupportedContentModel( $content->getModel() ) ) {
2878  throw new MWException( 'This content model is not supported: ' . $content->getModel() );
2879  }
2880 
2881  return $content->serialize( $this->contentFormat );
2882  }
2883 
2900  protected function toEditContent( $text ) {
2901  if ( $text === false || $text === null ) {
2902  return $text;
2903  }
2904 
2905  $content = ContentHandler::makeContent( $text, $this->getTitle(),
2906  $this->contentModel, $this->contentFormat );
2907 
2908  if ( !$this->isSupportedContentModel( $content->getModel() ) ) {
2909  throw new MWException( 'This content model is not supported: ' . $content->getModel() );
2910  }
2911 
2912  return $content;
2913  }
2914 
2923  public function showEditForm( $formCallback = null ) {
2924  # need to parse the preview early so that we know which templates are used,
2925  # otherwise users with "show preview after edit box" will get a blank list
2926  # we parse this near the beginning so that setHeaders can do the title
2927  # setting work instead of leaving it in getPreviewText
2928  $previewOutput = '';
2929  if ( $this->formtype == 'preview' ) {
2930  $previewOutput = $this->getPreviewText();
2931  }
2932 
2933  $out = $this->context->getOutput();
2934 
2935  $this->getHookRunner()->onEditPage__showEditForm_initial( $this, $out );
2936 
2937  $this->setHeaders();
2938 
2939  $this->addTalkPageText();
2940  $this->addEditNotices();
2941 
2942  if ( !$this->isConflict &&
2943  $this->section != '' &&
2944  !$this->isSectionEditSupported()
2945  ) {
2946  // We use $this->section to much before this and getVal('wgSection') directly in other places
2947  // at this point we can't reset $this->section to '' to fallback to non-section editing.
2948  // Someone is welcome to try refactoring though
2949  $out->showErrorPage( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
2950  return;
2951  }
2952 
2953  $this->showHeader();
2954 
2955  $out->addHTML( $this->editFormPageTop );
2956 
2957  $user = $this->context->getUser();
2958  if ( $user->getOption( 'previewontop' ) ) {
2959  $this->displayPreviewArea( $previewOutput, true );
2960  }
2961 
2962  $out->addHTML( $this->editFormTextTop );
2963 
2964  if ( $this->wasDeletedSinceLastEdit() && $this->formtype !== 'save' ) {
2965  $out->addHTML( Html::errorBox(
2966  $out->msg( 'deletedwhileediting' )->parse(),
2967  '',
2968  'mw-deleted-while-editing'
2969  ) );
2970  }
2971 
2972  // @todo add EditForm plugin interface and use it here!
2973  // search for textarea1 and textarea2, and allow EditForm to override all uses.
2974  $out->addHTML( Html::openElement(
2975  'form',
2976  [
2977  'class' => 'mw-editform',
2978  'id' => self::EDITFORM_ID,
2979  'name' => self::EDITFORM_ID,
2980  'method' => 'post',
2981  'action' => $this->getActionURL( $this->getContextTitle() ),
2982  'enctype' => 'multipart/form-data'
2983  ]
2984  ) );
2985 
2986  if ( is_callable( $formCallback ) ) {
2987  // TODO go through deprecation process
2988  wfWarn( 'The $formCallback parameter to ' . __METHOD__ . ' is deprecated' );
2989  call_user_func_array( $formCallback, [ &$out ] );
2990  }
2991 
2992  // Add a check for Unicode support
2993  $out->addHTML( Html::hidden( 'wpUnicodeCheck', self::UNICODE_CHECK ) );
2994 
2995  // Add an empty field to trip up spambots
2996  $out->addHTML(
2997  Xml::openElement( 'div', [ 'id' => 'antispam-container', 'style' => 'display: none;' ] )
2998  . Html::rawElement(
2999  'label',
3000  [ 'for' => 'wpAntispam' ],
3001  $this->context->msg( 'simpleantispam-label' )->parse()
3002  )
3003  . Xml::element(
3004  'input',
3005  [
3006  'type' => 'text',
3007  'name' => 'wpAntispam',
3008  'id' => 'wpAntispam',
3009  'value' => ''
3010  ]
3011  )
3012  . Xml::closeElement( 'div' )
3013  );
3014 
3015  $this->getHookRunner()->onEditPage__showEditForm_fields( $this, $out );
3016 
3017  // Put these up at the top to ensure they aren't lost on early form submission
3018  $this->showFormBeforeText();
3019 
3020  if ( $this->wasDeletedSinceLastEdit() && $this->formtype == 'save' ) {
3021  $username = $this->lastDelete->actor_name;
3022  $comment = CommentStore::getStore()
3023  ->getComment( 'log_comment', $this->lastDelete )->text;
3024 
3025  // It is better to not parse the comment at all than to have templates expanded in the middle
3026  // TODO: can the checkLabel be moved outside of the div so that wrapWikiMsg could be used?
3027  $key = $comment === ''
3028  ? 'confirmrecreate-noreason'
3029  : 'confirmrecreate';
3030  $out->addHTML(
3031  '<div class="mw-confirm-recreate">' .
3032  $this->context->msg( $key, $username, "<nowiki>$comment</nowiki>" )->parse() .
3033  Xml::checkLabel( $this->context->msg( 'recreate' )->text(), 'wpRecreate', 'wpRecreate', false,
3034  [ 'title' => Linker::titleAttrib( 'recreate' ), 'tabindex' => 1, 'id' => 'wpRecreate' ]
3035  ) .
3036  '</div>'
3037  );
3038  }
3039 
3040  # When the summary is hidden, also hide them on preview/show changes
3041  if ( $this->nosummary ) {
3042  $out->addHTML( Html::hidden( 'nosummary', true ) );
3043  }
3044 
3045  # If a blank edit summary was previously provided, and the appropriate
3046  # user preference is active, pass a hidden tag as wpIgnoreBlankSummary. This will stop the
3047  # user being bounced back more than once in the event that a summary
3048  # is not required.
3049  # ####
3050  # For a bit more sophisticated detection of blank summaries, hash the
3051  # automatic one and pass that in the hidden field wpAutoSummary.
3052  if (
3053  $this->missingSummary ||
3054  ( $this->section == 'new' && $this->nosummary ) ||
3055  $this->allowBlankSummary
3056  ) {
3057  $out->addHTML( Html::hidden( 'wpIgnoreBlankSummary', true ) );
3058  }
3059 
3060  if ( $this->undidRev ) {
3061  $out->addHTML( Html::hidden( 'wpUndidRevision', $this->undidRev ) );
3062  }
3063  if ( $this->undoAfter ) {
3064  $out->addHTML( Html::hidden( 'wpUndoAfter', $this->undoAfter ) );
3065  }
3066 
3067  if ( $this->selfRedirect ) {
3068  $out->addHTML( Html::hidden( 'wpIgnoreSelfRedirect', true ) );
3069  }
3070 
3071  if ( $this->hasPresetSummary ) {
3072  // If a summary has been preset using &summary= we don't want to prompt for
3073  // a different summary. Only prompt for a summary if the summary is blanked.
3074  // (T19416)
3075  $this->autoSumm = md5( '' );
3076  }
3077 
3078  $autosumm = $this->autoSumm !== '' ? $this->autoSumm : md5( $this->summary );
3079  $out->addHTML( Html::hidden( 'wpAutoSummary', $autosumm ) );
3080 
3081  $out->addHTML( Html::hidden( 'oldid', $this->oldid ) );
3082  $out->addHTML( Html::hidden( 'parentRevId', $this->getParentRevId() ) );
3083 
3084  $out->addHTML( Html::hidden( 'format', $this->contentFormat ) );
3085  $out->addHTML( Html::hidden( 'model', $this->contentModel ) );
3086 
3087  $out->enableOOUI();
3088 
3089  if ( $this->section == 'new' ) {
3090  $this->showSummaryInput( true, $this->summary );
3091  $out->addHTML( $this->getSummaryPreview( true, $this->summary ) );
3092  }
3093 
3094  $out->addHTML( $this->editFormTextBeforeContent );
3095  if ( $this->isConflict ) {
3096  // In an edit conflict, we turn textbox2 into the user's text,
3097  // and textbox1 into the stored version
3098  $this->textbox2 = $this->textbox1;
3099 
3100  $content = $this->getCurrentContent();
3101  $this->textbox1 = $this->toEditText( $content );
3102 
3104  $editConflictHelper->setTextboxes( $this->textbox2, $this->textbox1 );
3105  $editConflictHelper->setContentModel( $this->contentModel );
3106  $editConflictHelper->setContentFormat( $this->contentFormat );
3108  }
3109 
3110  if ( !$this->mTitle->isUserConfigPage() ) {
3111  $out->addHTML( self::getEditToolbar() );
3112  }
3113 
3114  if ( $this->blankArticle ) {
3115  $out->addHTML( Html::hidden( 'wpIgnoreBlankArticle', true ) );
3116  }
3117 
3118  if ( $this->isConflict ) {
3119  // In an edit conflict bypass the overridable content form method
3120  // and fallback to the raw wpTextbox1 since editconflicts can't be
3121  // resolved between page source edits and custom ui edits using the
3122  // custom edit ui.
3123  $conflictTextBoxAttribs = [];
3124  if ( $this->wasDeletedSinceLastEdit() ) {
3125  $conflictTextBoxAttribs['style'] = 'display:none;';
3126  } elseif ( $this->isOldRev ) {
3127  $conflictTextBoxAttribs['class'] = 'mw-textarea-oldrev';
3128  }
3129 
3130  $out->addHTML( $editConflictHelper->getEditConflictMainTextBox( $conflictTextBoxAttribs ) );
3132  } else {
3133  $this->showContentForm();
3134  }
3135 
3136  $out->addHTML( $this->editFormTextAfterContent );
3137 
3138  $this->showStandardInputs();
3139 
3140  $this->showFormAfterText();
3141 
3142  $this->showTosSummary();
3143 
3144  $this->showEditTools();
3145 
3146  $out->addHTML( $this->editFormTextAfterTools . "\n" );
3147 
3148  $out->addHTML( $this->makeTemplatesOnThisPageList( $this->getTemplates() ) );
3149 
3150  $out->addHTML( Html::rawElement( 'div', [ 'class' => 'hiddencats' ],
3151  Linker::formatHiddenCategories( $this->page->getHiddenCategories() ) ) );
3152 
3153  $out->addHTML( Html::rawElement( 'div', [ 'class' => 'limitreport' ],
3154  self::getPreviewLimitReport( $this->mParserOutput ) ) );
3155 
3156  $out->addModules( 'mediawiki.action.edit.collapsibleFooter' );
3157 
3158  if ( $this->isConflict ) {
3159  try {
3160  $this->showConflict();
3161  } catch ( MWContentSerializationException $ex ) {
3162  // this can't really happen, but be nice if it does.
3163  $msg = $this->context->msg(
3164  'content-failed-to-parse',
3165  $this->contentModel,
3166  $this->contentFormat,
3167  $ex->getMessage()
3168  );
3169  $out->wrapWikiTextAsInterface( 'error', $msg->plain() );
3170  }
3171  }
3172 
3173  // Set a hidden field so JS knows what edit form mode we are in
3174  if ( $this->isConflict ) {
3175  $mode = 'conflict';
3176  } elseif ( $this->preview ) {
3177  $mode = 'preview';
3178  } elseif ( $this->diff ) {
3179  $mode = 'diff';
3180  } else {
3181  $mode = 'text';
3182  }
3183  $out->addHTML( Html::hidden( 'mode', $mode, [ 'id' => 'mw-edit-mode' ] ) );
3184 
3185  // Marker for detecting truncated form data. This must be the last
3186  // parameter sent in order to be of use, so do not move me.
3187  $out->addHTML( Html::hidden( 'wpUltimateParam', true ) );
3188  $out->addHTML( $this->editFormTextBottom . "\n</form>\n" );
3189 
3190  if ( !$user->getOption( 'previewontop' ) ) {
3191  $this->displayPreviewArea( $previewOutput, false );
3192  }
3193  }
3194 
3202  public function makeTemplatesOnThisPageList( array $templates ) {
3203  $templateListFormatter = new TemplatesOnThisPageFormatter(
3204  $this->context, MediaWikiServices::getInstance()->getLinkRenderer()
3205  );
3206 
3207  // preview if preview, else section if section, else false
3208  $type = false;
3209  if ( $this->preview ) {
3210  $type = 'preview';
3211  } elseif ( $this->section != '' ) {
3212  $type = 'section';
3213  }
3214 
3215  return Html::rawElement( 'div', [ 'class' => 'templatesUsed' ],
3216  $templateListFormatter->format( $templates, $type )
3217  );
3218  }
3219 
3226  private static function extractSectionTitle( $text ) {
3227  if ( preg_match( "/^(=+)(.+)\\1\\s*(\n|$)/i", $text, $matches ) ) {
3228  return MediaWikiServices::getInstance()->getParser()
3229  ->stripSectionName( trim( $matches[2] ) );
3230  } else {
3231  return false;
3232  }
3233  }
3234 
3235  protected function showHeader() {
3236  $out = $this->context->getOutput();
3237  $user = $this->context->getUser();
3238  if ( $this->isConflict ) {
3239  $this->addExplainConflictHeader( $out );
3240  $this->editRevId = $this->page->getLatest();
3241  } else {
3242  if ( $this->section != '' && $this->section != 'new' && !$this->summary &&
3243  !$this->preview && !$this->diff
3244  ) {
3245  $sectionTitle = self::extractSectionTitle( $this->textbox1 ); // FIXME: use Content object
3246  if ( $sectionTitle !== false ) {
3247  $this->summary = "/* $sectionTitle */ ";
3248  }
3249  }
3250 
3251  $buttonLabel = $this->context->msg( $this->getSubmitButtonLabel() )->text();
3252 
3253  if ( $this->missingComment ) {
3254  $out->wrapWikiMsg( "<div id='mw-missingcommenttext'>\n$1\n</div>", 'missingcommenttext' );
3255  }
3256 
3257  if ( $this->missingSummary && $this->section != 'new' ) {
3258  $out->wrapWikiMsg(
3259  "<div id='mw-missingsummary'>\n$1\n</div>",
3260  [ 'missingsummary', $buttonLabel ]
3261  );
3262  }
3263 
3264  if ( $this->missingSummary && $this->section == 'new' ) {
3265  $out->wrapWikiMsg(
3266  "<div id='mw-missingcommentheader'>\n$1\n</div>",
3267  [ 'missingcommentheader', $buttonLabel ]
3268  );
3269  }
3270 
3271  if ( $this->blankArticle ) {
3272  $out->wrapWikiMsg(
3273  "<div id='mw-blankarticle'>\n$1\n</div>",
3274  [ 'blankarticle', $buttonLabel ]
3275  );
3276  }
3277 
3278  if ( $this->selfRedirect ) {
3279  $out->wrapWikiMsg(
3280  "<div id='mw-selfredirect'>\n$1\n</div>",
3281  [ 'selfredirect', $buttonLabel ]
3282  );
3283  }
3284 
3285  if ( $this->hookError !== '' ) {
3286  $out->addWikiTextAsInterface( $this->hookError );
3287  }
3288 
3289  if ( $this->section != 'new' ) {
3290  $revRecord = $this->mArticle->fetchRevisionRecord();
3291  if ( $revRecord && $revRecord instanceof RevisionStoreRecord ) {
3292  // Let sysop know that this will make private content public if saved
3293 
3294  if ( !$revRecord->userCan( RevisionRecord::DELETED_TEXT, $user ) ) {
3295  $out->addHtml(
3297  $out->msg( 'rev-deleted-text-permission', $this->mTitle->getPrefixedDBkey() )->parse(),
3298  'plainlinks'
3299  )
3300  );
3301  } elseif ( $revRecord->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
3302  $out->addHtml(
3304  // title used in wikilinks, should not contain whitespaces
3305  $out->msg( 'rev-deleted-text-view', $this->mTitle->getPrefixedDBkey() )->parse(),
3306  'plainlinks'
3307  )
3308  );
3309  }
3310 
3311  if ( !$revRecord->isCurrent() ) {
3312  $this->mArticle->setOldSubtitle( $revRecord->getId() );
3313  $out->wrapWikiMsg(
3314  Html::warningBox( "\n$1\n" ),
3315  'editingold'
3316  );
3317  $this->isOldRev = true;
3318  }
3319  } elseif ( $this->mTitle->exists() ) {
3320  // Something went wrong
3321 
3322  $out->wrapWikiMsg( "<div class='errorbox'>\n$1\n</div>\n",
3323  [ 'missing-revision', $this->oldid ] );
3324  }
3325  }
3326  }
3327 
3328  if ( wfReadOnly() ) {
3329  $out->wrapWikiMsg(
3330  "<div id=\"mw-read-only-warning\">\n$1\n</div>",
3331  [ 'readonlywarning', wfReadOnlyReason() ]
3332  );
3333  } elseif ( $user->isAnon() ) {
3334  if ( $this->formtype != 'preview' ) {
3335  $returntoquery = array_diff_key(
3336  $this->context->getRequest()->getValues(),
3337  [ 'title' => true, 'returnto' => true, 'returntoquery' => true ]
3338  );
3339  $out->wrapWikiMsg(
3340  "<div id='mw-anon-edit-warning' class='warningbox'>\n$1\n</div>",
3341  [ 'anoneditwarning',
3342  // Log-in link
3343  SpecialPage::getTitleFor( 'Userlogin' )->getFullURL( [
3344  'returnto' => $this->getTitle()->getPrefixedDBkey(),
3345  'returntoquery' => wfArrayToCgi( $returntoquery ),
3346  ] ),
3347  // Sign-up link
3348  SpecialPage::getTitleFor( 'CreateAccount' )->getFullURL( [
3349  'returnto' => $this->getTitle()->getPrefixedDBkey(),
3350  'returntoquery' => wfArrayToCgi( $returntoquery ),
3351  ] )
3352  ]
3353  );
3354  } else {
3355  $out->wrapWikiMsg( "<div id=\"mw-anon-preview-warning\" class=\"warningbox\">\n$1</div>",
3356  'anonpreviewwarning'
3357  );
3358  }
3359  } elseif ( $this->mTitle->isUserConfigPage() ) {
3360  # Check the skin exists
3361  if ( $this->isWrongCaseUserConfigPage() ) {
3362  $out->wrapWikiMsg(
3363  "<div class='errorbox' id='mw-userinvalidconfigtitle'>\n$1\n</div>",
3364  [ 'userinvalidconfigtitle', $this->mTitle->getSkinFromConfigSubpage() ]
3365  );
3366  }
3367  if ( $this->getTitle()->isSubpageOf( $user->getUserPage() ) ) {
3368  $isUserCssConfig = $this->mTitle->isUserCssConfigPage();
3369  $isUserJsonConfig = $this->mTitle->isUserJsonConfigPage();
3370  $isUserJsConfig = $this->mTitle->isUserJsConfigPage();
3371 
3372  $warning = $isUserCssConfig
3373  ? 'usercssispublic'
3374  : ( $isUserJsonConfig ? 'userjsonispublic' : 'userjsispublic' );
3375 
3376  $out->wrapWikiMsg( '<div class="mw-userconfigpublic">$1</div>', $warning );
3377 
3378  if ( $isUserJsConfig ) {
3379  $out->wrapWikiMsg( '<div class="mw-userconfigdangerous">$1</div>', 'userjsdangerous' );
3380  }
3381 
3382  if ( $this->formtype !== 'preview' ) {
3383  $config = $this->context->getConfig();
3384  if ( $isUserCssConfig && $config->get( 'AllowUserCss' ) ) {
3385  $out->wrapWikiMsg(
3386  "<div id='mw-usercssyoucanpreview'>\n$1\n</div>",
3387  [ 'usercssyoucanpreview' ]
3388  );
3389  } elseif ( $isUserJsonConfig /* No comparable 'AllowUserJson' */ ) {
3390  $out->wrapWikiMsg(
3391  "<div id='mw-userjsonyoucanpreview'>\n$1\n</div>",
3392  [ 'userjsonyoucanpreview' ]
3393  );
3394  } elseif ( $isUserJsConfig && $config->get( 'AllowUserJs' ) ) {
3395  $out->wrapWikiMsg(
3396  "<div id='mw-userjsyoucanpreview'>\n$1\n</div>",
3397  [ 'userjsyoucanpreview' ]
3398  );
3399  }
3400  }
3401  }
3402  }
3403 
3405 
3406  $this->addLongPageWarningHeader();
3407 
3408  # Add header copyright warning
3409  $this->showHeaderCopyrightWarning();
3410  }
3411 
3419  private function getSummaryInputAttributes( array $inputAttrs = null ) {
3420  // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
3421  // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
3422  // Unicode codepoints.
3423  return ( is_array( $inputAttrs ) ? $inputAttrs : [] ) + [
3424  'id' => 'wpSummary',
3425  'name' => 'wpSummary',
3427  'tabindex' => 1,
3428  'size' => 60,
3429  'spellcheck' => 'true',
3430  ];
3431  }
3432 
3442  private function getSummaryInputWidget( $summary = "", $labelText = null, $inputAttrs = null ) {
3443  $inputAttrs = OOUI\Element::configFromHtmlAttributes(
3444  $this->getSummaryInputAttributes( $inputAttrs )
3445  );
3446  $inputAttrs += [
3447  'title' => Linker::titleAttrib( 'summary' ),
3448  'accessKey' => Linker::accesskey( 'summary' ),
3449  ];
3450 
3451  // For compatibility with old scripts and extensions, we want the legacy 'id' on the `<input>`
3452  $inputAttrs['inputId'] = $inputAttrs['id'];
3453  $inputAttrs['id'] = 'wpSummaryWidget';
3454 
3455  return new OOUI\FieldLayout(
3456  new OOUI\TextInputWidget( [
3457  'value' => $summary,
3458  'infusable' => true,
3459  ] + $inputAttrs ),
3460  [
3461  'label' => new OOUI\HtmlSnippet( $labelText ),
3462  'align' => 'top',
3463  'id' => 'wpSummaryLabel',
3464  'classes' => [ $this->missingSummary ? 'mw-summarymissed' : 'mw-summary' ],
3465  ]
3466  );
3467  }
3468 
3475  protected function showSummaryInput( $isSubjectPreview, $summary = "" ) {
3476  # Add a class if 'missingsummary' is triggered to allow styling of the summary line
3477  $summaryClass = $this->missingSummary ? 'mw-summarymissed' : 'mw-summary';
3478  if ( $isSubjectPreview ) {
3479  if ( $this->nosummary ) {
3480  return;
3481  }
3482  } elseif ( !$this->mShowSummaryField ) {
3483  return;
3484  }
3485 
3486  $labelText = $this->context->msg( $isSubjectPreview ? 'subject' : 'summary' )->parse();
3487  $this->context->getOutput()->addHTML(
3488  $this->getSummaryInputWidget(
3489  $summary,
3490  $labelText,
3491  [ 'class' => $summaryClass ]
3492  )
3493  );
3494  }
3495 
3503  protected function getSummaryPreview( $isSubjectPreview, $summary = "" ) {
3504  // avoid spaces in preview, gets always trimmed on save
3505  $summary = trim( $summary );
3506  if ( $summary === '' || ( !$this->preview && !$this->diff ) ) {
3507  return "";
3508  }
3509 
3510  if ( $isSubjectPreview ) {
3511  $summary = $this->context->msg( 'newsectionsummary' )
3512  ->rawParams( MediaWikiServices::getInstance()->getParser()
3513  ->stripSectionName( $summary ) )
3514  ->inContentLanguage()->text();
3515  }
3516 
3517  $message = $isSubjectPreview ? 'subject-preview' : 'summary-preview';
3518 
3519  $summary = $this->context->msg( $message )->parse()
3520  . Linker::commentBlock( $summary, $this->mTitle, $isSubjectPreview );
3521  return Xml::tags( 'div', [ 'class' => 'mw-summary-preview' ], $summary );
3522  }
3523 
3524  protected function showFormBeforeText() {
3525  $out = $this->context->getOutput();
3526  $out->addHTML( Html::hidden( 'wpSection', $this->section ) );
3527  $out->addHTML( Html::hidden( 'wpStarttime', $this->starttime ) );
3528  $out->addHTML( Html::hidden( 'wpEdittime', $this->edittime ) );
3529  $out->addHTML( Html::hidden( 'editRevId', $this->editRevId ) );
3530  $out->addHTML( Html::hidden( 'wpScrolltop', $this->scrolltop, [ 'id' => 'wpScrolltop' ] ) );
3531  }
3532 
3533  protected function showFormAfterText() {
3546  $this->context->getOutput()->addHTML(
3547  "\n" .
3548  Html::hidden( "wpEditToken", $this->context->getUser()->getEditToken() ) .
3549  "\n"
3550  );
3551  }
3552 
3561  protected function showContentForm() {
3562  $this->showTextbox1();
3563  }
3564 
3573  protected function showTextbox1( $customAttribs = null, $textoverride = null ) {
3574  if ( $this->wasDeletedSinceLastEdit() && $this->formtype == 'save' ) {
3575  $attribs = [ 'style' => 'display:none;' ];
3576  } else {
3577  $builder = new TextboxBuilder();
3578  $classes = $builder->getTextboxProtectionCSSClasses( $this->getTitle() );
3579 
3580  # Is an old revision being edited?
3581  if ( $this->isOldRev ) {
3582  $classes[] = 'mw-textarea-oldrev';
3583  }
3584 
3585  $attribs = [
3586  'aria-label' => $this->context->msg( 'edit-textarea-aria-label' )->text(),
3587  'tabindex' => 1
3588  ];
3589 
3590  if ( is_array( $customAttribs ) ) {
3591  $attribs += $customAttribs;
3592  }
3593 
3594  $attribs = $builder->mergeClassesIntoAttributes( $classes, $attribs );
3595  }
3596 
3597  $this->showTextbox(
3598  $textoverride ?? $this->textbox1,
3599  'wpTextbox1',
3600  $attribs
3601  );
3602  }
3603 
3604  protected function showTextbox2() {
3605  $this->showTextbox( $this->textbox2, 'wpTextbox2', [ 'tabindex' => 6, 'readonly' ] );
3606  }
3607 
3608  protected function showTextbox( $text, $name, $customAttribs = [] ) {
3609  $builder = new TextboxBuilder();
3610  $attribs = $builder->buildTextboxAttribs(
3611  $name,
3612  $customAttribs,
3613  $this->context->getUser(),
3614  $this->mTitle
3615  );
3616 
3617  $this->context->getOutput()->addHTML(
3618  Html::textarea( $name, $builder->addNewLineAtEnd( $text ), $attribs )
3619  );
3620  }
3621 
3622  protected function displayPreviewArea( $previewOutput, $isOnTop = false ) {
3623  $attribs = [ 'id' => 'wikiPreview' ];
3624  if ( $isOnTop ) {
3625  $attribs['class'] = 'ontop';
3626  }
3627  if ( $this->formtype != 'preview' ) {
3628  $attribs['style'] = 'display: none;';
3629  }
3630 
3631  $out = $this->context->getOutput();
3632  $out->addHTML( Xml::openElement( 'div', $attribs ) );
3633 
3634  if ( $this->formtype == 'preview' ) {
3635  $this->showPreview( $previewOutput );
3636  } else {
3637  // Empty content container for LivePreview
3638  $pageViewLang = $this->mTitle->getPageViewLanguage();
3639  $attribs = [ 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(),
3640  'class' => 'mw-content-' . $pageViewLang->getDir() ];
3641  $out->addHTML( Html::rawElement( 'div', $attribs ) );
3642  }
3643 
3644  $out->addHTML( '</div>' );
3645 
3646  if ( $this->formtype == 'diff' ) {
3647  try {
3648  $this->showDiff();
3649  } catch ( MWContentSerializationException $ex ) {
3650  $msg = $this->context->msg(
3651  'content-failed-to-parse',
3652  $this->contentModel,
3653  $this->contentFormat,
3654  $ex->getMessage()
3655  );
3656  $out->wrapWikiTextAsInterface( 'error', $msg->plain() );
3657  }
3658  }
3659  }
3660 
3667  protected function showPreview( $text ) {
3668  if ( $this->mArticle instanceof CategoryPage ) {
3669  $this->mArticle->openShowCategory();
3670  }
3671  # This hook seems slightly odd here, but makes things more
3672  # consistent for extensions.
3673  $out = $this->context->getOutput();
3674  $this->getHookRunner()->onOutputPageBeforeHTML( $out, $text );
3675  $out->addHTML( $text );
3676  if ( $this->mArticle instanceof CategoryPage ) {
3677  $this->mArticle->closeShowCategory();
3678  }
3679  }
3680 
3688  public function showDiff() {
3689  $oldtitlemsg = 'currentrev';
3690  # if message does not exist, show diff against the preloaded default
3691  if ( $this->mTitle->getNamespace() === NS_MEDIAWIKI && !$this->mTitle->exists() ) {
3692  $oldtext = $this->mTitle->getDefaultMessageText();
3693  if ( $oldtext !== false ) {
3694  $oldtitlemsg = 'defaultmessagetext';
3695  $oldContent = $this->toEditContent( $oldtext );
3696  } else {
3697  $oldContent = null;
3698  }
3699  } else {
3700  $oldContent = $this->getCurrentContent();
3701  }
3702 
3703  $textboxContent = $this->toEditContent( $this->textbox1 );
3704  if ( $this->editRevId !== null ) {
3705  $newContent = $this->page->replaceSectionAtRev(
3706  $this->section, $textboxContent, $this->summary, $this->editRevId
3707  );
3708  } else {
3709  $newContent = $this->page->replaceSectionContent(
3710  $this->section, $textboxContent, $this->summary, $this->edittime
3711  );
3712  }
3713 
3714  if ( $newContent ) {
3715  $this->getHookRunner()->onEditPageGetDiffContent( $this, $newContent );
3716 
3717  $user = $this->context->getUser();
3718  $popts = ParserOptions::newFromUserAndLang( $user,
3719  MediaWikiServices::getInstance()->getContentLanguage() );
3720  $services = MediaWikiServices::getInstance();
3721  $contentTransformer = $services->getContentTransformer();
3722  $newContent = $contentTransformer->preSaveTransform( $newContent, $this->mTitle, $user, $popts );
3723  }
3724 
3725  if ( ( $oldContent && !$oldContent->isEmpty() ) || ( $newContent && !$newContent->isEmpty() ) ) {
3726  $oldtitle = $this->context->msg( $oldtitlemsg )->parse();
3727  $newtitle = $this->context->msg( 'yourtext' )->parse();
3728 
3729  if ( !$oldContent ) {
3730  $oldContent = $newContent->getContentHandler()->makeEmptyContent();
3731  }
3732 
3733  if ( !$newContent ) {
3734  $newContent = $oldContent->getContentHandler()->makeEmptyContent();
3735  }
3736 
3737  $de = $oldContent->getContentHandler()->createDifferenceEngine( $this->context );
3738  $de->setContent( $oldContent, $newContent );
3739 
3740  $difftext = $de->getDiff( $oldtitle, $newtitle );
3741  $de->showDiffStyle();
3742  } else {
3743  $difftext = '';
3744  }
3745 
3746  $this->context->getOutput()->addHTML( '<div id="wikiDiff">' . $difftext . '</div>' );
3747  }
3748 
3749  protected function showHeaderCopyrightWarning() {
3750  $msg = 'editpage-head-copy-warn';
3751  if ( !$this->context->msg( $msg )->isDisabled() ) {
3752  $this->context->getOutput()->wrapWikiMsg( "<div class='editpage-head-copywarn'>\n$1\n</div>",
3753  'editpage-head-copy-warn' );
3754  }
3755  }
3756 
3765  protected function showTosSummary() {
3766  $msg = 'editpage-tos-summary';
3767  $this->getHookRunner()->onEditPageTosSummary( $this->mTitle, $msg );
3768  if ( !$this->context->msg( $msg )->isDisabled() ) {
3769  $out = $this->context->getOutput();
3770  $out->addHTML( '<div class="mw-tos-summary">' );
3771  $out->addWikiMsg( $msg );
3772  $out->addHTML( '</div>' );
3773  }
3774  }
3775 
3780  protected function showEditTools() {
3781  $this->context->getOutput()->addHTML( '<div class="mw-editTools">' .
3782  $this->context->msg( 'edittools' )->inContentLanguage()->parse() .
3783  '</div>' );
3784  }
3785 
3792  protected function getCopywarn() {
3793  return self::getCopyrightWarning( $this->mTitle );
3794  }
3795 
3806  public static function getCopyrightWarning( $title, $format = 'plain', $localizer = null ) {
3807  if ( !$localizer instanceof MessageLocalizer ) {
3809  if ( $localizer !== null ) {
3811  $context->setLanguage( $localizer );
3812  }
3813  $localizer = $context;
3814  }
3815  $rightsText = MediaWikiServices::getInstance()->getMainConfig()->get( 'RightsText' );
3816  if ( $rightsText ) {
3817  $copywarnMsg = [ 'copyrightwarning',
3818  '[[' . $localizer->msg( 'copyrightpage' )->inContentLanguage()->text() . ']]',
3819  $rightsText ];
3820  } else {
3821  $copywarnMsg = [ 'copyrightwarning2',
3822  '[[' . $localizer->msg( 'copyrightpage' )->inContentLanguage()->text() . ']]' ];
3823  }
3824  // Allow for site and per-namespace customization of contribution/copyright notice.
3825  Hooks::runner()->onEditPageCopyrightWarning( $title, $copywarnMsg );
3826 
3827  $msg = $localizer->msg( ...$copywarnMsg )->page( $title );
3828  return "<div id=\"editpage-copywarn\">\n" .
3829  $msg->$format() . "\n</div>";
3830  }
3831 
3839  public static function getPreviewLimitReport( ParserOutput $output = null ) {
3840  if ( !$output || !$output->getLimitReportData() ) {
3841  return '';
3842  }
3843 
3844  $limitReport = Html::rawElement( 'div', [ 'class' => 'mw-limitReportExplanation' ],
3845  wfMessage( 'limitreport-title' )->parseAsBlock()
3846  );
3847 
3848  // Show/hide animation doesn't work correctly on a table, so wrap it in a div.
3849  $limitReport .= Html::openElement( 'div', [ 'class' => 'preview-limit-report-wrapper' ] );
3850 
3851  $limitReport .= Html::openElement( 'table', [
3852  'class' => 'preview-limit-report wikitable'
3853  ] ) .
3854  Html::openElement( 'tbody' );
3855 
3856  foreach ( $output->getLimitReportData() as $key => $value ) {
3857  if ( Hooks::runner()->onParserLimitReportFormat( $key, $value, $limitReport, true, true ) ) {
3858  $keyMsg = wfMessage( $key );
3859  $valueMsg = wfMessage( [ "$key-value-html", "$key-value" ] );
3860  if ( !$valueMsg->exists() ) {
3861  // This is formatted raw, not as localized number.
3862  // If you want the parameter formatted as a number,
3863  // define the `$key-value` message.
3864  $valueMsg = ( new RawMessage( '$1' ) )->params( $value );
3865  } else {
3866  // If you define the `$key-value` or `$key-value-html`
3867  // message then the argument *must* be numeric.
3868  $valueMsg = $valueMsg->numParams( $value );
3869  }
3870  if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) {
3871  $limitReport .= Html::openElement( 'tr' ) .
3872  Html::rawElement( 'th', [], $keyMsg->parse() ) .
3873  Html::rawElement( 'td', [], $valueMsg->parse() ) .
3874  Html::closeElement( 'tr' );
3875  }
3876  }
3877  }
3878 
3879  $limitReport .= Html::closeElement( 'tbody' ) .
3880  Html::closeElement( 'table' ) .
3881  Html::closeElement( 'div' );
3882 
3883  return $limitReport;
3884  }
3885 
3886  protected function showStandardInputs( &$tabindex = 2 ) {
3887  $out = $this->context->getOutput();
3888  $out->addHTML( "<div class='editOptions'>\n" );
3889 
3890  if ( $this->section != 'new' ) {
3891  $this->showSummaryInput( false, $this->summary );
3892  $out->addHTML( $this->getSummaryPreview( false, $this->summary ) );
3893  }
3894 
3895  $checkboxes = $this->getCheckboxesWidget(
3896  $tabindex,
3897  [ 'minor' => $this->minoredit, 'watch' => $this->watchthis ]
3898  );
3899  $checkboxesHTML = new OOUI\HorizontalLayout( [ 'items' => array_values( $checkboxes ) ] );
3900 
3901  $out->addHTML( "<div class='editCheckboxes'>" . $checkboxesHTML . "</div>\n" );
3902 
3903  // Show copyright warning.
3904  $out->addWikiTextAsInterface( $this->getCopywarn() );
3905  $out->addHTML( $this->editFormTextAfterWarn );
3906 
3907  $out->addHTML( "<div class='editButtons'>\n" );
3908  $out->addHTML( implode( "\n", $this->getEditButtons( $tabindex ) ) . "\n" );
3909 
3910  $cancel = $this->getCancelLink( $tabindex++ );
3911 
3912  $message = $this->context->msg( 'edithelppage' )->inContentLanguage()->text();
3913  $edithelpurl = Skin::makeInternalOrExternalUrl( $message );
3914  $edithelp =
3916  $this->context->msg( 'edithelp' )->text(),
3917  [ 'target' => 'helpwindow', 'href' => $edithelpurl ],
3918  [ 'mw-ui-quiet' ]
3919  ) .
3920  $this->context->msg( 'word-separator' )->escaped() .
3921  $this->context->msg( 'newwindow' )->parse();
3922 
3923  $out->addHTML( " <span class='cancelLink'>{$cancel}</span>\n" );
3924  $out->addHTML( " <span class='editHelp'>{$edithelp}</span>\n" );
3925  $out->addHTML( "</div><!-- editButtons -->\n" );
3926 
3927  $this->getHookRunner()->onEditPage__showStandardInputs_options( $this, $out, $tabindex );
3928 
3929  $out->addHTML( "</div><!-- editOptions -->\n" );
3930  }
3931 
3936  protected function showConflict() {
3937  $out = $this->context->getOutput();
3938  // Avoid PHP 7.1 warning of passing $this by reference
3939  $editPage = $this;
3940  if ( $this->getHookRunner()->onEditPageBeforeConflictDiff( $editPage, $out ) ) {
3941  $this->incrementConflictStats();
3942 
3943  $this->getEditConflictHelper()->showEditFormTextAfterFooters();
3944  }
3945  }
3946 
3947  protected function incrementConflictStats() {
3948  $this->getEditConflictHelper()->incrementConflictStats( $this->context->getUser() );
3949  }
3950 
3955  public function getCancelLink( $tabindex = 0 ) {
3956  $cancelParams = [];
3957  if ( !$this->isConflict && $this->oldid > 0 ) {
3958  $cancelParams['oldid'] = $this->oldid;
3959  } elseif ( $this->getContextTitle()->isRedirect() ) {
3960  $cancelParams['redirect'] = 'no';
3961  }
3962 
3963  return new OOUI\ButtonWidget( [
3964  'id' => 'mw-editform-cancel',
3965  'tabIndex' => $tabindex,
3966  'href' => $this->getContextTitle()->getLinkURL( $cancelParams ),
3967  'label' => new OOUI\HtmlSnippet( $this->context->msg( 'cancel' )->parse() ),
3968  'framed' => false,
3969  'infusable' => true,
3970  'flags' => 'destructive',
3971  ] );
3972  }
3973 
3983  protected function getActionURL( Title $title ) {
3984  return $title->getLocalURL( [ 'action' => $this->action ] );
3985  }
3986 
3994  protected function wasDeletedSinceLastEdit() {
3995  if ( $this->deletedSinceEdit !== null ) {
3996  return $this->deletedSinceEdit;
3997  }
3998 
3999  $this->deletedSinceEdit = false;
4000 
4001  if ( !$this->mTitle->exists() && $this->mTitle->hasDeletedEdits() ) {
4002  $this->lastDelete = $this->getLastDelete();
4003  if ( $this->lastDelete ) {
4004  $deleteTime = wfTimestamp( TS_MW, $this->lastDelete->log_timestamp );
4005  if ( $deleteTime > $this->starttime ) {
4006  $this->deletedSinceEdit = true;
4007  }
4008  }
4009  }
4010 
4011  return $this->deletedSinceEdit;
4012  }
4013 
4019  protected function getLastDelete() {
4020  $dbr = wfGetDB( DB_REPLICA );
4021  $commentQuery = CommentStore::getStore()->getJoin( 'log_comment' );
4022  $data = $dbr->selectRow(
4023  array_merge( [ 'logging' ], $commentQuery['tables'], [ 'actor' ] ),
4024  [
4025  'log_type',
4026  'log_action',
4027  'log_timestamp',
4028  'log_namespace',
4029  'log_title',
4030  'log_params',
4031  'log_deleted',
4032  'actor_name'
4033  ] + $commentQuery['fields'],
4034  [
4035  'log_namespace' => $this->mTitle->getNamespace(),
4036  'log_title' => $this->mTitle->getDBkey(),
4037  'log_type' => 'delete',
4038  'log_action' => 'delete',
4039  ],
4040  __METHOD__,
4041  [ 'ORDER BY' => 'log_timestamp DESC' ],
4042  [
4043  'actor' => [ 'JOIN', 'actor_id=log_actor' ],
4044  ] + $commentQuery['joins']
4045  );
4046  // Quick paranoid permission checks...
4047  if ( is_object( $data ) ) {
4048  if ( $data->log_deleted & LogPage::DELETED_USER ) {
4049  $data->actor_name = $this->context->msg( 'rev-deleted-user' )->escaped();
4050  }
4051 
4052  if ( $data->log_deleted & LogPage::DELETED_COMMENT ) {
4053  $data->log_comment_text = $this->context->msg( 'rev-deleted-comment' )->escaped();
4054  $data->log_comment_data = null;
4055  }
4056  }
4057 
4058  return $data;
4059  }
4060 
4066  public function getPreviewText() {
4067  $out = $this->context->getOutput();
4068  $config = $this->context->getConfig();
4069 
4070  if ( $config->get( 'RawHtml' ) && !$this->mTokenOk ) {
4071  // Could be an offsite preview attempt. This is very unsafe if
4072  // HTML is enabled, as it could be an attack.
4073  $parsedNote = '';
4074  if ( $this->textbox1 !== '' ) {
4075  // Do not put big scary notice, if previewing the empty
4076  // string, which happens when you initially edit
4077  // a category page, due to automatic preview-on-open.
4078  $parsedNote = Html::rawElement( 'div', [ 'class' => 'previewnote' ],
4079  $out->parseAsInterface(
4080  $this->context->msg( 'session_fail_preview_html' )->plain()
4081  ) );
4082  }
4083  $this->incrementEditFailureStats( 'session_loss' );
4084  return $parsedNote;
4085  }
4086 
4087  $note = '';
4088 
4089  try {
4090  $content = $this->toEditContent( $this->textbox1 );
4091 
4092  $previewHTML = '';
4093  if ( !$this->getHookRunner()->onAlternateEditPreview(
4094  $this, $content, $previewHTML, $this->mParserOutput )
4095  ) {
4096  return $previewHTML;
4097  }
4098 
4099  # provide a anchor link to the editform
4100  $continueEditing = '<span class="mw-continue-editing">' .
4101  '[[#' . self::EDITFORM_ID . '|' .
4102  $this->context->getLanguage()->getArrow() . ' ' .
4103  $this->context->msg( 'continue-editing' )->text() . ']]</span>';
4104  if ( $this->mTriedSave && !$this->mTokenOk ) {
4105  $note = $this->context->msg( 'session_fail_preview' )->plain();
4106  $this->incrementEditFailureStats( 'session_loss' );
4107  } elseif ( $this->incompleteForm ) {
4108  $note = $this->context->msg( 'edit_form_incomplete' )->plain();
4109  if ( $this->mTriedSave ) {
4110  $this->incrementEditFailureStats( 'incomplete_form' );
4111  }
4112  } else {
4113  $note = $this->context->msg( 'previewnote' )->plain() . ' ' . $continueEditing;
4114  }
4115 
4116  # don't parse non-wikitext pages, show message about preview
4117  if ( $this->mTitle->isUserConfigPage() || $this->mTitle->isSiteConfigPage() ) {
4118  if ( $this->mTitle->isUserConfigPage() ) {
4119  $level = 'user';
4120  } elseif ( $this->mTitle->isSiteConfigPage() ) {
4121  $level = 'site';
4122  } else {
4123  $level = false;
4124  }
4125 
4126  if ( $content->getModel() == CONTENT_MODEL_CSS ) {
4127  $format = 'css';
4128  if ( $level === 'user' && !$config->get( 'AllowUserCss' ) ) {
4129  $format = false;
4130  }
4131  } elseif ( $content->getModel() == CONTENT_MODEL_JSON ) {
4132  $format = 'json';
4133  if ( $level === 'user' /* No comparable 'AllowUserJson' */ ) {
4134  $format = false;
4135  }
4136  } elseif ( $content->getModel() == CONTENT_MODEL_JAVASCRIPT ) {
4137  $format = 'js';
4138  if ( $level === 'user' && !$config->get( 'AllowUserJs' ) ) {
4139  $format = false;
4140  }
4141  } else {
4142  $format = false;
4143  }
4144 
4145  # Used messages to make sure grep find them:
4146  # Messages: usercsspreview, userjsonpreview, userjspreview,
4147  # sitecsspreview, sitejsonpreview, sitejspreview
4148  if ( $level && $format ) {
4149  $note = "<div id='mw-{$level}{$format}preview'>" .
4150  $this->context->msg( "{$level}{$format}preview" )->plain() .
4151  ' ' . $continueEditing . "</div>";
4152  }
4153  }
4154 
4155  # If we're adding a comment, we need to show the
4156  # summary as the headline
4157  if ( $this->section === "new" && $this->summary !== "" ) {
4158  $content = $content->addSectionHeader( $this->summary );
4159  }
4160 
4161  $this->getHookRunner()->onEditPageGetPreviewContent( $this, $content );
4162 
4163  $parserResult = $this->doPreviewParse( $content );
4164  $parserOutput = $parserResult['parserOutput'];
4165  $previewHTML = $parserResult['html'];
4166  $this->mParserOutput = $parserOutput;
4167  $out->addParserOutputMetadata( $parserOutput );
4168  if ( $out->userCanPreview() ) {
4169  $out->addContentOverride( $this->getTitle(), $content );
4170  }
4171 
4172  if ( count( $parserOutput->getWarnings() ) ) {
4173  $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
4174  }
4175 
4176  } catch ( MWContentSerializationException $ex ) {
4177  $m = $this->context->msg(
4178  'content-failed-to-parse',
4179  $this->contentModel,
4180  $this->contentFormat,
4181  $ex->getMessage()
4182  );
4183  $note .= "\n\n" . $m->plain(); # gets parsed down below
4184  $previewHTML = '';
4185  }
4186 
4187  if ( $this->isConflict ) {
4188  $conflict = Html::rawElement(
4189  'div', [ 'id' => 'mw-previewconflict', 'class' => 'warningbox' ],
4190  $this->context->msg( 'previewconflict' )->escaped()
4191  );
4192  } else {
4193  $conflict = '';
4194  }
4195 
4196  $previewhead = Html::rawElement(
4197  'div', [ 'class' => 'previewnote' ],
4199  'h2', [ 'id' => 'mw-previewheader' ],
4200  $this->context->msg( 'preview' )->escaped()
4201  ) .
4202  Html::rawElement( 'div', [ 'class' => 'warningbox' ],
4203  $out->parseAsInterface( $note )
4204  ) . $conflict
4205  );
4206 
4207  $pageViewLang = $this->mTitle->getPageViewLanguage();
4208  $attribs = [ 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(),
4209  'class' => 'mw-content-' . $pageViewLang->getDir() ];
4210  $previewHTML = Html::rawElement( 'div', $attribs, $previewHTML );
4211 
4212  return $previewhead . $previewHTML . $this->previewTextAfterContent;
4213  }
4214 
4215  private function incrementEditFailureStats( $failureType ) {
4216  $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
4217  $stats->increment( 'edit.failures.' . $failureType );
4218  }
4219 
4224  protected function getPreviewParserOptions() {
4225  $parserOptions = $this->page->makeParserOptions( $this->context );
4226  $parserOptions->setIsPreview( true );
4227  $parserOptions->setIsSectionPreview( $this->section !== null && $this->section !== '' );
4228 
4229  // XXX: we could call $parserOptions->setCurrentRevisionRecordCallback here to force the
4230  // current revision to be null during PST, until setupFakeRevision is called on
4231  // the ParserOptions. Currently, we rely on Parser::getRevisionRecordObject() to ignore
4232  // existing revisions in preview mode.
4233 
4234  return $parserOptions;
4235  }
4236 
4246  protected function doPreviewParse( Content $content ) {
4247  $user = $this->context->getUser();
4248  $parserOptions = $this->getPreviewParserOptions();
4249 
4250  // NOTE: preSaveTransform doesn't have a fake revision to operate on.
4251  // Parser::getRevisionRecordObject() will return null in preview mode,
4252  // causing the context user to be used for {{subst:REVISIONUSER}}.
4253  // XXX: Alternatively, we could also call setupFakeRevision() a second time:
4254  // once before PST with $content, and then after PST with $pstContent.
4255  $services = MediaWikiServices::getInstance();
4256  $contentTransformer = $services->getContentTransformer();
4257  $contentRenderer = $services->getContentRenderer();
4258  $pstContent = $contentTransformer->preSaveTransform( $content, $this->mTitle, $user, $parserOptions );
4259  $scopedCallback = $parserOptions->setupFakeRevision( $this->mTitle, $pstContent, $user );
4260  $parserOutput = $contentRenderer->getParserOutput( $pstContent, $this->mTitle, null, $parserOptions );
4261  ScopedCallback::consume( $scopedCallback );
4262  $out = $this->context->getOutput();
4263  $skin = $out->getSkin();
4264  $skinOptions = $skin->getOptions();
4265  return [
4266  'parserOutput' => $parserOutput,
4267  'html' => $parserOutput->getText( [
4268  'injectTOC' => $skinOptions['toc'],
4269  'enableSectionEditLinks' => false,
4270  'includeDebugInfo' => true,
4271  ] )
4272  ];
4273  }
4274 
4278  public function getTemplates() {
4279  if ( $this->preview || $this->section != '' ) {
4280  $templates = [];
4281  if ( !isset( $this->mParserOutput ) ) {
4282  return $templates;
4283  }
4284  foreach ( $this->mParserOutput->getTemplates() as $ns => $template ) {
4285  foreach ( array_keys( $template ) as $dbk ) {
4286  $templates[] = Title::makeTitle( $ns, $dbk );
4287  }
4288  }
4289  return $templates;
4290  } else {
4291  return $this->mTitle->getTemplateLinksFrom();
4292  }
4293  }
4294 
4300  public static function getEditToolbar() {
4301  $startingToolbar = '<div id="toolbar"></div>';
4302  $toolbar = $startingToolbar;
4303 
4304  if ( !Hooks::runner()->onEditPageBeforeEditToolbar( $toolbar ) ) {
4305  return null;
4306  }
4307  // Don't add a pointless `<div>` to the page unless a hook caller populated it
4308  return ( $toolbar === $startingToolbar ) ? null : $toolbar;
4309  }
4310 
4336  public function getCheckboxesDefinition( $checked ) {
4337  $checkboxes = [];
4338 
4339  $user = $this->context->getUser();
4340  // don't show the minor edit checkbox if it's a new page or section
4341  if ( !$this->isNew && $this->permManager->userHasRight( $user, 'minoredit' ) ) {
4342  $checkboxes['wpMinoredit'] = [
4343  'id' => 'wpMinoredit',
4344  'label-message' => 'minoredit',
4345  // Uses messages: tooltip-minoredit, accesskey-minoredit
4346  'tooltip' => 'minoredit',
4347  'label-id' => 'mw-editpage-minoredit',
4348  'legacy-name' => 'minor',
4349  'default' => $checked['minor'],
4350  ];
4351  }
4352 
4353  if ( $user->isRegistered() ) {
4354  $checkboxes = array_merge(
4355  $checkboxes,
4356  $this->getCheckboxesDefinitionForWatchlist( $checked['watch'] )
4357  );
4358  }
4359 
4360  $this->getHookRunner()->onEditPageGetCheckboxesDefinition( $this, $checkboxes );
4361 
4362  return $checkboxes;
4363  }
4364 
4372  private function getCheckboxesDefinitionForWatchlist( $watch ) {
4373  $fieldDefs = [
4374  'wpWatchthis' => [
4375  'id' => 'wpWatchthis',
4376  'label-message' => 'watchthis',
4377  // Uses messages: tooltip-watch, accesskey-watch
4378  'tooltip' => 'watch',
4379  'label-id' => 'mw-editpage-watch',
4380  'legacy-name' => 'watch',
4381  'default' => $watch,
4382  ]
4383  ];
4384  if ( $this->watchlistExpiryEnabled ) {
4385  $watchedItem = $this->watchedItemStore->getWatchedItem( $this->getContext()->getUser(), $this->getTitle() );
4386  $expiryOptions = WatchAction::getExpiryOptions( $this->getContext(), $watchedItem );
4387  // When previewing, override the selected dropdown option to select whatever was posted
4388  // (if it's a valid option) rather than the current value for watchlistExpiry.
4389  // See also above in $this->importFormData().
4390  $expiryFromRequest = $this->getContext()->getRequest()->getText( 'wpWatchlistExpiry' );
4391  if ( ( $this->preview || $this->diff ) && in_array( $expiryFromRequest, $expiryOptions['options'] ) ) {
4392  $expiryOptions['default'] = $expiryFromRequest;
4393  }
4394  // Reformat the options to match what DropdownInputWidget wants.
4395  $options = [];
4396  foreach ( $expiryOptions['options'] as $label => $value ) {
4397  $options[] = [ 'data' => $value, 'label' => $label ];
4398  }
4399  $fieldDefs['wpWatchlistExpiry'] = [
4400  'id' => 'wpWatchlistExpiry',
4401  'label-message' => 'confirm-watch-label',
4402  // Uses messages: tooltip-watchlist-expiry, accesskey-watchlist-expiry
4403  'tooltip' => 'watchlist-expiry',
4404  'label-id' => 'mw-editpage-watchlist-expiry',
4405  'default' => $expiryOptions['default'],
4406  'value-attr' => 'value',
4407  'class' => DropdownInputWidget::class,
4408  'options' => $options,
4409  'invisibleLabel' => true,
4410  ];
4411  }
4412  return $fieldDefs;
4413  }
4414 
4426  public function getCheckboxesWidget( &$tabindex, $checked ) {
4427  $checkboxes = [];
4428  $checkboxesDef = $this->getCheckboxesDefinition( $checked );
4429 
4430  foreach ( $checkboxesDef as $name => $options ) {
4431  $legacyName = $options['legacy-name'] ?? $name;
4432 
4433  $title = null;
4434  $accesskey = null;
4435  if ( isset( $options['tooltip'] ) ) {
4436  $accesskey = $this->context->msg( "accesskey-{$options['tooltip']}" )->text();
4437  $title = Linker::titleAttrib( $options['tooltip'] );
4438  }
4439  if ( isset( $options['title-message'] ) ) {
4440  $title = $this->context->msg( $options['title-message'] )->text();
4441  }
4442  // Allow checkbox definitions to set their own class and value-attribute names.
4443  // See $this->getCheckboxesDefinition() for details.
4444  $className = $options['class'] ?? CheckboxInputWidget::class;
4445  $valueAttr = $options['value-attr'] ?? 'selected';
4446  $checkboxes[ $legacyName ] = new FieldLayout(
4447  new $className( [
4448  'tabIndex' => ++$tabindex,
4449  'accessKey' => $accesskey,
4450  'id' => $options['id'] . 'Widget',
4451  'inputId' => $options['id'],
4452  'name' => $name,
4453  $valueAttr => $options['default'],
4454  'infusable' => true,
4455  'options' => $options['options'] ?? null,
4456  ] ),
4457  [
4458  'align' => 'inline',
4459  'label' => new OOUI\HtmlSnippet( $this->context->msg( $options['label-message'] )->parse() ),
4460  'title' => $title,
4461  'id' => $options['label-id'] ?? null,
4462  'invisibleLabel' => $options['invisibleLabel'] ?? null,
4463  ]
4464  );
4465  }
4466 
4467  return $checkboxes;
4468  }
4469 
4476  protected function getSubmitButtonLabel() {
4477  $labelAsPublish =
4478  $this->context->getConfig()->get( 'EditSubmitButtonLabelPublish' );
4479 
4480  // Can't use $this->isNew as that's also true if we're adding a new section to an extant page
4481  $newPage = !$this->mTitle->exists();
4482 
4483  if ( $labelAsPublish ) {
4484  $buttonLabelKey = $newPage ? 'publishpage' : 'publishchanges';
4485  } else {
4486  $buttonLabelKey = $newPage ? 'savearticle' : 'savechanges';
4487  }
4488 
4489  return $buttonLabelKey;
4490  }
4491 
4502  public function getEditButtons( &$tabindex ) {
4503  $buttons = [];
4504 
4505  $labelAsPublish =
4506  $this->context->getConfig()->get( 'EditSubmitButtonLabelPublish' );
4507 
4508  $buttonLabel = $this->context->msg( $this->getSubmitButtonLabel() )->text();
4509  $buttonTooltip = $labelAsPublish ? 'publish' : 'save';
4510 
4511  $buttons['save'] = new OOUI\ButtonInputWidget( [
4512  'name' => 'wpSave',
4513  'tabIndex' => ++$tabindex,
4514  'id' => 'wpSaveWidget',
4515  'inputId' => 'wpSave',
4516  // Support: IE 6 – Use <input>, otherwise it can't distinguish which button was clicked
4517  'useInputTag' => true,
4518  'flags' => [ 'progressive', 'primary' ],
4519  'label' => $buttonLabel,
4520  'infusable' => true,
4521  'type' => 'submit',
4522  // Messages used: tooltip-save, tooltip-publish
4523  'title' => Linker::titleAttrib( $buttonTooltip ),
4524  // Messages used: accesskey-save, accesskey-publish
4525  'accessKey' => Linker::accesskey( $buttonTooltip ),
4526  ] );
4527 
4528  $buttons['preview'] = new OOUI\ButtonInputWidget( [
4529  'name' => 'wpPreview',
4530  'tabIndex' => ++$tabindex,
4531  'id' => 'wpPreviewWidget',
4532  'inputId' => 'wpPreview',
4533  // Support: IE 6 – Use <input>, otherwise it can't distinguish which button was clicked
4534  'useInputTag' => true,
4535  'label' => $this->context->msg( 'showpreview' )->text(),
4536  'infusable' => true,
4537  'type' => 'submit',
4538  // Message used: tooltip-preview
4539  'title' => Linker::titleAttrib( 'preview' ),
4540  // Message used: accesskey-preview
4541  'accessKey' => Linker::accesskey( 'preview' ),
4542  ] );
4543 
4544  $buttons['diff'] = new OOUI\ButtonInputWidget( [
4545  'name' => 'wpDiff',
4546  'tabIndex' => ++$tabindex,
4547  'id' => 'wpDiffWidget',
4548  'inputId' => 'wpDiff',
4549  // Support: IE 6 – Use <input>, otherwise it can't distinguish which button was clicked
4550  'useInputTag' => true,
4551  'label' => $this->context->msg( 'showdiff' )->text(),
4552  'infusable' => true,
4553  'type' => 'submit',
4554  // Message used: tooltip-diff
4555  'title' => Linker::titleAttrib( 'diff' ),
4556  // Message used: accesskey-diff
4557  'accessKey' => Linker::accesskey( 'diff' ),
4558  ] );
4559 
4560  $this->getHookRunner()->onEditPageBeforeEditButtons( $this, $buttons, $tabindex );
4561 
4562  return $buttons;
4563  }
4564 
4569  private function noSuchSectionPage() {
4570  $out = $this->context->getOutput();
4571  $out->prepareErrorPage( $this->context->msg( 'nosuchsectiontitle' ) );
4572 
4573  $res = $this->context->msg( 'nosuchsectiontext', $this->section )->parseAsBlock();
4574 
4575  $this->getHookRunner()->onEditPageNoSuchSection( $this, $res );
4576  $out->addHTML( $res );
4577 
4578  $out->returnToMain( false, $this->mTitle );
4579  }
4580 
4586  public function spamPageWithContent( $match = false ) {
4587  $this->textbox2 = $this->textbox1;
4588 
4589  if ( is_array( $match ) ) {
4590  $match = $this->context->getLanguage()->listToText( $match );
4591  }
4592  $out = $this->context->getOutput();
4593  $out->prepareErrorPage( $this->context->msg( 'spamprotectiontitle' ) );
4594 
4595  $out->addHTML( '<div id="spamprotected">' );
4596  $out->addWikiMsg( 'spamprotectiontext' );
4597  if ( $match ) {
4598  // @phan-suppress-next-line SecurityCheck-DoubleEscaped false positive
4599  $out->addWikiMsg( 'spamprotectionmatch', wfEscapeWikiText( $match ) );
4600  }
4601  $out->addHTML( '</div>' );
4602 
4603  $out->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
4604  $this->showDiff();
4605 
4606  $out->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
4607  $this->showTextbox2();
4608 
4609  $out->addReturnTo( $this->getContextTitle(), [ 'action' => 'edit' ] );
4610  }
4611 
4615  protected function addEditNotices() {
4616  $out = $this->context->getOutput();
4617  $editNotices = $this->mTitle->getEditNotices( $this->oldid );
4618  if ( count( $editNotices ) ) {
4619  $out->addHTML( implode( "\n", $editNotices ) );
4620  } else {
4621  $msg = $this->context->msg( 'editnotice-notext' );
4622  if ( !$msg->isDisabled() ) {
4623  $out->addHTML(
4624  '<div class="mw-editnotice-notext">'
4625  . $msg->parseAsBlock()
4626  . '</div>'
4627  );
4628  }
4629  }
4630  }
4631 
4635  protected function addTalkPageText() {
4636  if ( $this->mTitle->isTalkPage() ) {
4637  $this->context->getOutput()->addWikiMsg( 'talkpagetext' );
4638  }
4639  }
4640 
4644  protected function addLongPageWarningHeader() {
4645  if ( $this->contentLength === false ) {
4646  $this->contentLength = strlen( $this->textbox1 );
4647  }
4648 
4649  $out = $this->context->getOutput();
4650  $maxArticleSize = $this->context->getConfig()->get( 'MaxArticleSize' );
4651  if ( $this->tooBig || $this->contentLength > $maxArticleSize * 1024 ) {
4652  $lang = $this->context->getLanguage();
4653  $out->wrapWikiMsg( "<div class='error' id='mw-edit-longpageerror'>\n$1\n</div>",
4654  [
4655  'longpageerror',
4656  $lang->formatNum( round( $this->contentLength / 1024, 3 ) ),
4657  $lang->formatNum( $maxArticleSize )
4658  ]
4659  );
4660  } else {
4661  $longPageHint = $this->context->msg( 'longpage-hint' );
4662  if ( !$longPageHint->isDisabled() ) {
4663  $msgText = trim( $longPageHint->sizeParams( $this->contentLength )
4664  ->params( $this->contentLength ) // Keep this unformatted for math inside message
4665  ->text() );
4666  if ( $msgText !== '' && $msgText !== '-' ) {
4667  $out->addWikiTextAsInterface( "<div id='mw-edit-longpage-hint'>\n$msgText\n</div>" );
4668  }
4669  }
4670  }
4671  }
4672 
4676  protected function addPageProtectionWarningHeaders() {
4677  $out = $this->context->getOutput();
4678  if ( $this->mTitle->isProtected( 'edit' ) &&
4679  $this->permManager->getNamespaceRestrictionLevels(
4680  $this->getTitle()->getNamespace()
4681  ) !== [ '' ]
4682  ) {
4683  # Is the title semi-protected?
4684  if ( $this->mTitle->isSemiProtected() ) {
4685  $noticeMsg = 'semiprotectedpagewarning';
4686  } else {
4687  # Then it must be protected based on static groups (regular)
4688  $noticeMsg = 'protectedpagewarning';
4689  }
4690  LogEventsList::showLogExtract( $out, 'protect', $this->mTitle, '',
4691  [ 'lim' => 1, 'msgKey' => [ $noticeMsg ] ] );
4692  }
4693  if ( $this->mTitle->isCascadeProtected() ) {
4694  # Is this page under cascading protection from some source pages?
4695 
4696  list( $cascadeSources, /* $restrictions */ ) = $this->mTitle->getCascadeProtectionSources();
4697  $notice = "<div class='warningbox mw-cascadeprotectedwarning'>\n$1\n";
4698  $cascadeSourcesCount = count( $cascadeSources );
4699  if ( $cascadeSourcesCount > 0 ) {
4700  # Explain, and list the titles responsible
4701  foreach ( $cascadeSources as $page ) {
4702  $notice .= '* [[:' . $page->getPrefixedText() . "]]\n";
4703  }
4704  }
4705  $notice .= '</div>';
4706  $out->wrapWikiMsg( $notice, [ 'cascadeprotectedwarning', $cascadeSourcesCount ] );
4707  }
4708  if ( !$this->mTitle->exists() && $this->mTitle->getRestrictions( 'create' ) ) {
4709  LogEventsList::showLogExtract( $out, 'protect', $this->mTitle, '',
4710  [ 'lim' => 1,
4711  'showIfEmpty' => false,
4712  'msgKey' => [ 'titleprotectedwarning' ],
4713  'wrap' => "<div class=\"mw-titleprotectedwarning\">\n$1</div>" ] );
4714  }
4715  }
4716 
4721  protected function addExplainConflictHeader( OutputPage $out ) {
4722  $out->addHTML(
4723  $this->getEditConflictHelper()->getExplainHeader()
4724  );
4725  }
4726 
4732  protected function addNewLineAtEnd( $wikitext ) {
4733  return ( new TextboxBuilder() )->addNewLineAtEnd( $wikitext );
4734  }
4735 
4746  private function guessSectionName( $text ) {
4747  // Detect Microsoft browsers
4748  $userAgent = $this->context->getRequest()->getHeader( 'User-Agent' );
4749  $parser = MediaWikiServices::getInstance()->getParser();
4750  if ( $userAgent && preg_match( '/MSIE|Edge/', $userAgent ) ) {
4751  // ...and redirect them to legacy encoding, if available
4752  return $parser->guessLegacySectionNameFromWikiText( $text );
4753  }
4754  // Meanwhile, real browsers get real anchors
4755  $name = $parser->guessSectionNameFromWikiText( $text );
4756  // With one little caveat: per T216029, fragments in HTTP redirects need to be urlencoded,
4757  // otherwise Chrome double-escapes the rest of the URL.
4758  return '#' . urlencode( mb_substr( $name, 1 ) );
4759  }
4760 
4767  public function setEditConflictHelperFactory( callable $factory ) {
4768  $this->editConflictHelperFactory = $factory;
4769  $this->editConflictHelper = null;
4770  }
4771 
4775  private function getEditConflictHelper() {
4776  if ( !$this->editConflictHelper ) {
4777  $this->editConflictHelper = call_user_func(
4778  $this->editConflictHelperFactory,
4779  $this->getSubmitButtonLabel()
4780  );
4781  }
4782 
4784  }
4785 
4791  private function newTextConflictHelper( $submitButtonLabel ) {
4792  return new TextConflictHelper(
4793  $this->getTitle(),
4794  $this->getContext()->getOutput(),
4795  MediaWikiServices::getInstance()->getStatsdDataFactory(),
4796  $submitButtonLabel,
4797  MediaWikiServices::getInstance()->getContentHandlerFactory()
4798  );
4799  }
4800 }
ReadOnlyError
Show an error when the wiki is locked/read-only and the user tries to do something that requires writ...
Definition: ReadOnlyError.php:29
EditPage\__construct
__construct(Article $article)
Definition: EditPage.php:431
EditPage\$editFormTextBeforeContent
$editFormTextBeforeContent
Definition: EditPage.php:342
MediaWiki\EditPage\IEditObject\AS_READ_ONLY_PAGE_ANON
const AS_READ_ONLY_PAGE_ANON
Status: this anonymous user is not allowed to edit this page.
Definition: IEditObject.php:50
Page\PageIdentity
Interface for objects (potentially) representing an editable wiki page.
Definition: PageIdentity.php:64
EditPage\$mTriedSave
bool $mTriedSave
Definition: EditPage.php:174
MediaWiki\EditPage\IEditObject\AS_ARTICLE_WAS_DELETED
const AS_ARTICLE_WAS_DELETED
Status: article was deleted while editing and wpRecreate == false or form was not posted.
Definition: IEditObject.php:62
EDIT_AUTOSUMMARY
const EDIT_AUTOSUMMARY
Definition: Defines.php:131
CommentStoreComment\newUnsavedComment
static newUnsavedComment( $comment, array $data=null)
Create a new, unsaved CommentStoreComment.
Definition: CommentStoreComment.php:67
MediaWiki\Revision\RevisionRecord\getContent
getContent( $role, $audience=self::FOR_PUBLIC, Authority $performer=null)
Returns the Content of the given slot of this revision.
Definition: RevisionRecord.php:156
EditPage\$watchlistManager
WatchlistManager $watchlistManager
Definition: EditPage.php:417
EditPage\$contentModel
string $contentModel
Definition: EditPage.php:328
EditPage\showFormBeforeText
showFormBeforeText()
Definition: EditPage.php:3524
Title\newFromText
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition: Title.php:377
EditPage\$lastDelete
bool stdClass $lastDelete
Definition: EditPage.php:168
EditPage\tokenOk
tokenOk(&$request)
Make sure the form isn't faking a user's credentials.
Definition: EditPage.php:1645
EditPage\$editFormPageTop
string $editFormPageTop
Before even the preview.
Definition: EditPage.php:340
EditPage\$redirectLookup
RedirectLookup $redirectLookup
Definition: EditPage.php:425
MediaWiki\EditPage\IEditObject\AS_SUCCESS_NEW_ARTICLE
const AS_SUCCESS_NEW_ARTICLE
Status: Article successfully created.
Definition: IEditObject.php:35
EditPage\showContentForm
showContentForm()
Subpage overridable method for printing the form for page content editing By default this simply outp...
Definition: EditPage.php:3561
EditPage\$mTitle
Title $mTitle
Definition: EditPage.php:138
MediaWiki\Revision\RevisionRecord
Page revision base class.
Definition: RevisionRecord.php:47
EditPage\$watchlistExpiryEnabled
bool $watchlistExpiryEnabled
Corresponds to $wgWatchlistExpiry.
Definition: EditPage.php:245
MediaWiki\EditPage\IEditObject\AS_BLOCKED_PAGE_FOR_USER
const AS_BLOCKED_PAGE_FOR_USER
Status: User is blocked from editing this page.
Definition: IEditObject.php:44
Html\linkButton
static linkButton( $text, array $attrs, array $modifiers=[])
Returns an HTML link element in a string styled as a button (when $wgUseMediaWikiUIEverywhere is enab...
Definition: Html.php:169
Html\textarea
static textarea( $name, $value='', array $attribs=[])
Convenience function to produce a <textarea> element.
Definition: Html.php:850
EditPage\spamPageWithContent
spamPageWithContent( $match=false)
Show "your edit contains spam" page with your diff and text.
Definition: EditPage.php:4586
MediaWiki\EditPage\IEditObject\AS_TEXTBOX_EMPTY
const AS_TEXTBOX_EMPTY
Status: user tried to create a new section without content.
Definition: IEditObject.php:80
EditPage\$section
string $section
Definition: EditPage.php:293
ParserOutput
Definition: ParserOutput.php:35
StatusValue\newFatal
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:70
NS_MEDIAWIKI
const NS_MEDIAWIKI
Definition: Defines.php:72
WikiPage\getRedirectTarget
getRedirectTarget()
If this page is a redirect, get its target.
Definition: WikiPage.php:1033
UserBlockedError
Show an error when the user tries to do something whilst blocked.
Definition: UserBlockedError.php:32
MediaWiki\EditPage\IEditObject\AS_SUMMARY_NEEDED
const AS_SUMMARY_NEEDED
Status: no edit summary given and the user has forceeditsummary set and the user is not editing in hi...
Definition: IEditObject.php:77
MediaWiki\EditPage\IEditObject\AS_CONFLICT_DETECTED
const AS_CONFLICT_DETECTED
Status: (non-resolvable) edit conflict.
Definition: IEditObject.php:71
EditPage\displayPermissionsError
displayPermissionsError(array $permErrors)
Display a permissions error page, like OutputPage::showPermissionsErrorPage(), but with the following...
Definition: EditPage.php:771
MediaWiki\EditPage\IEditObject\AS_CANNOT_USE_CUSTOM_MODEL
const AS_CANNOT_USE_CUSTOM_MODEL
Status: when changing the content model is disallowed due to $wgContentHandlerUseDB being false.
Definition: IEditObject.php:118
EditPage\$editFormTextAfterContent
$editFormTextAfterContent
Definition: EditPage.php:346
EditPage\displayPreviewArea
displayPreviewArea( $previewOutput, $isOnTop=false)
Definition: EditPage.php:3622
MediaWiki\EditPage\Constraint\EditConstraintRunner
Back end to process the edit constraints.
Definition: EditConstraintRunner.php:36
EditPage\$blankArticle
bool $blankArticle
Definition: EditPage.php:192
EditPage\$allowBlankSummary
bool $allowBlankSummary
Definition: EditPage.php:189
EditPage\$editFormTextBottom
$editFormTextBottom
Definition: EditPage.php:345
EditPage\getSummaryInputAttributes
getSummaryInputAttributes(array $inputAttrs=null)
Helper function for summary input functions, which returns the necessary attributes for the input.
Definition: EditPage.php:3419
EditPage\$editFormTextTop
$editFormTextTop
Definition: EditPage.php:341
EditPage\$editintro
string $editintro
Definition: EditPage.php:319
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:204
EditPage\showTextbox2
showTextbox2()
Definition: EditPage.php:3604
$lang
if(!isset( $args[0])) $lang
Definition: testCompression.php:37
EditPage\$summary
string $summary
Definition: EditPage.php:265
MediaWiki\Revision\RevisionStore
Service for looking up page revisions.
Definition: RevisionStore.php:89
EditPage\$textbox2
string $textbox2
Definition: EditPage.php:262
EditPage\getPreloadedContent
getPreloadedContent( $preload, $params=[])
Get the contents to be preloaded into the box, either set by an earlier setPreloadText() or by loadin...
Definition: EditPage.php:1566
EditPage\$mTokenOk
bool $mTokenOk
Definition: EditPage.php:171
EditPage\$contentHandlerFactory
IContentHandlerFactory $contentHandlerFactory
Definition: EditPage.php:397
MediaWiki\EditPage\Constraint\ContentModelChangeConstraint
Verify user permissions if changing content model: Must have editcontentmodel rights Must be able to ...
Definition: ContentModelChangeConstraint.php:36
EditPage\getUndoContent
getUndoContent(RevisionRecord $undoRev, RevisionRecord $oldRev)
Returns the result of a three-way merge when undoing changes.
Definition: EditPage.php:1463
MediaWiki\EditPage\Constraint\UserRateLimitConstraint
Verify user doesn't exceed rate limits.
Definition: UserRateLimitConstraint.php:34
wfTimestamp
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Definition: GlobalFunctions.php:1649
MediaWiki\EditPage\Constraint\PageSizeConstraint
Verify the page isn't larger than the maximum.
Definition: PageSizeConstraint.php:36
EditPage\$oldid
int $oldid
Revision ID the edit is based on, or 0 if it's the current revision.
Definition: EditPage.php:308
MediaWiki\EditPage\TextboxBuilder
Helps EditPage build textboxes.
Definition: TextboxBuilder.php:38
EditPage\getContextTitle
getContextTitle()
Definition: EditPage.php:518
EditPage\getEditToolbar
static getEditToolbar()
Allow extensions to provide a toolbar.
Definition: EditPage.php:4300
MediaWiki\EditPage\Constraint\UserBlockConstraint
Verify user permissions: Must not be blocked from the page.
Definition: UserBlockConstraint.php:36
MediaWiki\EditPage\IEditObject\AS_SPAM_ERROR
const AS_SPAM_ERROR
Status: summary contained spam according to one of the regexes in $wgSummarySpamRegex.
Definition: IEditObject.php:89
EditPage\showTosSummary
showTosSummary()
Give a chance for site and per-namespace customizations of terms of service summary link that might e...
Definition: EditPage.php:3765
CategoryPage
Special handling for category description pages, showing pages, subcategories and file that belong to...
Definition: CategoryPage.php:30
EditPage\$page
WikiPage $page
Definition: EditPage.php:132
EditPage\$save
bool $save
Definition: EditPage.php:230
WatchAction\getExpiryOptions
static getExpiryOptions(MessageLocalizer $msgLocalizer, $watchedItem)
Get options and default for a watchlist expiry select list.
Definition: WatchAction.php:145
EditPage\addPageProtectionWarningHeaders
addPageProtectionWarningHeaders()
Definition: EditPage.php:4676
EditPage\getExpectedParentRevision
getExpectedParentRevision()
Returns the RevisionRecord corresponding to the revision that was current at the time editing was ini...
Definition: EditPage.php:2607
EditPage\setContextTitle
setContextTitle( $title)
Definition: EditPage.php:510
EditPage\handleFailedConstraint
handleFailedConstraint(IEditConstraint $failed)
Apply the specific updates needed for the EditPage fields based on which constraint failed,...
Definition: EditPage.php:2434
getAuthority
getAuthority()
WikiPage
Class representing a MediaWiki article and history.
Definition: WikiPage.php:63
EditPage\edit
edit()
This is the function that gets called for "action=edit".
Definition: EditPage.php:559
ParserOptions\newFromUserAndLang
static newFromUserAndLang(UserIdentity $user, Language $lang)
Get a ParserOptions object from a given user and language.
Definition: ParserOptions.php:1055
EditPage\$autoSumm
string $autoSumm
Definition: EditPage.php:204
MediaWiki\EditPage\IEditObject\AS_END
const AS_END
Status: WikiPage::doEdit() was unsuccessful.
Definition: IEditObject.php:86
$file
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition: router.php:42
EditPage\getLastDelete
getLastDelete()
Get the last log record of this page being deleted, if ever.
Definition: EditPage.php:4019
wfReadOnly
wfReadOnly()
Check whether the wiki is in read-only mode.
Definition: GlobalFunctions.php:1082
EditPage\incrementConflictStats
incrementConflictStats()
Definition: EditPage.php:3947
EditPage\addEditNotices
addEditNotices()
Definition: EditPage.php:4615
MediaWiki\Permissions\Authority\authorizeRead
authorizeRead(string $action, PageIdentity $target, PermissionStatus $status=null)
Authorize read access.
User\newFromName
static newFromName( $name, $validate='valid')
Definition: User.php:595
wfMessage
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Definition: GlobalFunctions.php:1167
EditPage\newSectionSummary
newSectionSummary()
Return the summary to be used for a new section.
Definition: EditPage.php:1863
EditPage\$editFormTextAfterTools
$editFormTextAfterTools
Definition: EditPage.php:344
EditPage\addContentModelChangeLogEntry
addContentModelChangeLogEntry(UserIdentity $user, $oldModel, $newModel, $reason)
Definition: EditPage.php:2515
MediaWiki\EditPage\Constraint\SpamRegexConstraint
Verify summary and text do not match spam regexes.
Definition: SpamRegexConstraint.php:35
SpecialPage\getTitleFor
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
Definition: SpecialPage.php:131
EditPage\getEditPermissionErrors
getEditPermissionErrors( $rigor=PermissionManager::RIGOR_SECURE)
Definition: EditPage.php:734
EditPage\$editFormTextAfterWarn
$editFormTextAfterWarn
Definition: EditPage.php:343
MediaWiki\EditPage\Constraint\EditFilterMergedContentHookConstraint
Verify EditFilterMergedContent hook.
Definition: EditFilterMergedContentHookConstraint.php:37
MediaWiki\EditPage\IEditObject\AS_CONTENT_TOO_BIG
const AS_CONTENT_TOO_BIG
Status: Content too big (> $wgMaxArticleSize)
Definition: IEditObject.php:47
PermissionsError
Show an error when a user tries to do something they do not have the necessary permissions for.
Definition: PermissionsError.php:32
MediaWiki\EditPage\IEditObject\AS_NO_CREATE_PERMISSION
const AS_NO_CREATE_PERMISSION
Status: user tried to create this page, but is not allowed to do that.
Definition: IEditObject.php:65
MediaWiki\EditPage\IEditObject\AS_SUCCESS_UPDATE
const AS_SUCCESS_UPDATE
Status: Article successfully updated.
Definition: IEditObject.php:32
MediaWiki\EditPage\TextConflictHelper\setContentFormat
setContentFormat( $contentFormat)
Definition: TextConflictHelper.php:139
EditPage\showHeaderCopyrightWarning
showHeaderCopyrightWarning()
Definition: EditPage.php:3749
MediaWiki\EditPage\Constraint\AutoSummaryMissingSummaryConstraint
For an edit to an existing page but not with a new section, do not allow the user to post with a summ...
Definition: AutoSummaryMissingSummaryConstraint.php:41
MediaWiki\EditPage\Constraint\DefaultTextConstraint
Don't save a new page if it's blank or if it's a MediaWiki: message with content equivalent to defaul...
Definition: DefaultTextConstraint.php:35
EditPage\getCheckboxesDefinitionForWatchlist
getCheckboxesDefinitionForWatchlist( $watch)
Get the watchthis and watchlistExpiry form field definitions.
Definition: EditPage.php:4372
Html\warningBox
static warningBox( $html, $className='')
Return a warning box.
Definition: Html.php:758
EditPage\$mExpectedParentRevision
RevisionRecord bool null $mExpectedParentRevision
A RevisionRecord corresponding to $this->editRevId or $this->edittime.
Definition: EditPage.php:222
EditPage\$userNameUtils
UserNameUtils $userNameUtils
Definition: EditPage.php:422
EditPage\addLongPageWarningHeader
addLongPageWarningHeader()
Definition: EditPage.php:4644
EditPage\$context
IContextSource $context
Definition: EditPage.php:370
$res
$res
Definition: testCompression.php:57
EditPage\$didSave
$didSave
Definition: EditPage.php:350
Xml\openElement
static openElement( $element, $attribs=null)
This opens an XML element.
Definition: Xml.php:111
MediaWiki\User\UserIdentity
Interface for objects representing user identity.
Definition: UserIdentity.php:39
OutputPage\addHTML
addHTML( $text)
Append $text to the body HTML.
Definition: OutputPage.php:1652
Wikimedia\Message\MessageValue
Value object representing a message for i18n.
Definition: MessageValue.php:18
Linker\formatHiddenCategories
static formatHiddenCategories( $hiddencats)
Returns HTML for the "hidden categories on this page" list.
Definition: Linker.php:2011
EditPage\$mArticle
Article $mArticle
Definition: EditPage.php:129
EditPage\$contentFormat
null string $contentFormat
Definition: EditPage.php:331
MessageLocalizer
Interface for localizing messages in MediaWiki.
Definition: MessageLocalizer.php:29
EditPage\POST_EDIT_COOKIE_DURATION
const POST_EDIT_COOKIE_DURATION
Duration of PostEdit cookie, in seconds.
Definition: EditPage.php:123
$dbr
$dbr
Definition: testCompression.php:54
Status
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: Status.php:44
EditPage\$editConflictHelper
TextConflictHelper null $editConflictHelper
Definition: EditPage.php:392
EditPage\$watchthis
bool $watchthis
Definition: EditPage.php:242
EditPage\$previewTextAfterContent
$previewTextAfterContent
Definition: EditPage.php:347
Html\closeElement
static closeElement( $element)
Returns "</$element>".
Definition: Html.php:319
EditPage\showDiff
showDiff()
Get a diff between the current contents of the edit box and the version of the page we're editing fro...
Definition: EditPage.php:3688
EditPage\$tooBig
bool $tooBig
Definition: EditPage.php:180
Status\getWikiText
getWikiText( $shortContext=false, $longContext=false, $lang=null)
Get the error list as a wikitext formatted list.
Definition: Status.php:189
Config
Interface for configuration instances.
Definition: Config.php:30
MediaWiki\Block\DatabaseBlock
A DatabaseBlock (unlike a SystemBlock) is stored in the database, may give rise to autoblocks and may...
Definition: DatabaseBlock.php:51
Wikimedia\ParamValidator\TypeDef\ExpiryDef
Type definition for expiry timestamps.
Definition: ExpiryDef.php:17
DerivativeContext
An IContextSource implementation which will inherit context from another source but allow individual ...
Definition: DerivativeContext.php:32
getUser
getUser()
CONTENT_MODEL_JSON
const CONTENT_MODEL_JSON
Definition: Defines.php:212
MediaWiki\EditPage\IEditObject\AS_RATE_LIMITED
const AS_RATE_LIMITED
Status: rate limiter for action 'edit' was tripped.
Definition: IEditObject.php:59
EditPage\UNICODE_CHECK
const UNICODE_CHECK
Used for Unicode support checks.
Definition: EditPage.php:97
MWException
MediaWiki exception.
Definition: MWException.php:29
EditPage\toEditContent
toEditContent( $text)
Turns the given text into a Content object by unserializing it.
Definition: EditPage.php:2900
Article\getTitle
getTitle()
Get the title object of the article.
Definition: Article.php:224
EditPage\getEditButtons
getEditButtons(&$tabindex)
Returns an array of html code of the following buttons: save, diff and preview.
Definition: EditPage.php:4502
wfDeprecated
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
Definition: GlobalFunctions.php:997
MediaWiki\EditPage\Constraint\CreationPermissionConstraint
Verify be able to create the page in question if it is a new page.
Definition: CreationPermissionConstraint.php:34
MediaWiki\Logger\LoggerFactory
PSR-3 logger instance factory.
Definition: LoggerFactory.php:45
LogPage\DELETED_COMMENT
const DELETED_COMMENT
Definition: LogPage.php:40
WikiPage\hasDifferencesOutsideMainSlot
static hasDifferencesOutsideMainSlot(RevisionRecord $a, RevisionRecord $b)
Helper method for checking whether two revisions have differences that go beyond the main slot.
Definition: WikiPage.php:1549
EditPage\showSummaryInput
showSummaryInput( $isSubjectPreview, $summary="")
Definition: EditPage.php:3475
MediaWiki\Watchlist\WatchlistManager
WatchlistManager service.
Definition: WatchlistManager.php:52
EditPage\getParentRevId
getParentRevId()
Get the edit's parent revision ID.
Definition: EditPage.php:1523
wfArrayDiff2
wfArrayDiff2( $a, $b)
Like array_diff( $a, $b ) except that it works with two-dimensional arrays.
Definition: GlobalFunctions.php:113
MediaWiki\EditPage\Constraint\MissingCommentConstraint
Do not allow the user to post an empty comment (only used for new section)
Definition: MissingCommentConstraint.php:32
EditPage\isSectionEditSupported
isSectionEditSupported()
Section editing is supported when the page content model allows section edit and we are editing curre...
Definition: EditPage.php:919
EditPage\importFormData
importFormData(&$request)
This function collects the form data and uses it to populate various member variables.
Definition: EditPage.php:938
Status\wrap
static wrap( $sv)
Succinct helper method to wrap a StatusValue.
Definition: Status.php:62
EditPage\getActionURL
getActionURL(Title $title)
Returns the URL to use in the form's action attribute.
Definition: EditPage.php:3983
EditPage\addExplainConflictHeader
addExplainConflictHeader(OutputPage $out)
Definition: EditPage.php:4721
LogPage\DELETED_USER
const DELETED_USER
Definition: LogPage.php:41
wfGetDB
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
Definition: GlobalFunctions.php:2186
EditPage\showIntro
showIntro()
Show all applicable editing introductions.
Definition: EditPage.php:2698
EditPage\$firsttime
bool $firsttime
True the first time the edit form is rendered, false after re-rendering with diff,...
Definition: EditPage.php:165
$matches
$matches
Definition: NoLocalSettings.php:24
EditPage\$missingComment
bool $missingComment
Definition: EditPage.php:183
MediaWiki\EditPage\IEditObject\AS_SELF_REDIRECT
const AS_SELF_REDIRECT
Status: user tried to create self-redirect and wpIgnoreSelfRedirect is false.
Definition: IEditObject.php:104
EditPage\getPreviewLimitReport
static getPreviewLimitReport(ParserOutput $output=null)
Get the Limit report for page previews.
Definition: EditPage.php:3839
EditPage\$editConflictHelperFactory
callable $editConflictHelperFactory
Factory function to create an edit conflict helper.
Definition: EditPage.php:387
MWContentSerializationException
Exception representing a failure to serialize or unserialize a content object.
Definition: MWContentSerializationException.php:8
EditPage\attemptSave
attemptSave(&$resultDetails=false)
Attempt submission.
Definition: EditPage.php:1687
WikiPage\exists
exists()
Definition: WikiPage.php:599
Xml\element
static element( $element, $attribs=null, $contents='', $allowShortTag=true)
Format an XML element with given attributes and, optionally, text content.
Definition: Xml.php:42
Article\getPage
getPage()
Get the WikiPage object of this instance.
Definition: Article.php:234
Article\getContext
getContext()
Gets the context this Article is executed in.
Definition: Article.php:1977
EDIT_NEW
const EDIT_NEW
Definition: Defines.php:125
EditPage\getArticle
getArticle()
Definition: EditPage.php:487
MediaWiki\EditPage\Constraint\ImageRedirectConstraint
Verify user permissions: If creating a redirect in the file namespace, must have upload rights.
Definition: ImageRedirectConstraint.php:36
Page\WikiPageFactory
Definition: WikiPageFactory.php:19
ThrottledError
Show an error when the user hits a rate limit.
Definition: ThrottledError.php:28
EditPage\getCopyrightWarning
static getCopyrightWarning( $title, $format='plain', $localizer=null)
Get the copyright warning, by default returns wikitext.
Definition: EditPage.php:3806
EditPage\getCheckboxesWidget
getCheckboxesWidget(&$tabindex, $checked)
Returns an array of checkboxes for the edit form, including 'minor' and 'watch' checkboxes and any ot...
Definition: EditPage.php:4426
EditPage\previewOnOpen
previewOnOpen()
Should we show a preview when the edit form is first shown?
Definition: EditPage.php:857
EditPage\incrementEditFailureStats
incrementEditFailureStats( $failureType)
Definition: EditPage.php:4215
$title
$title
Definition: testCompression.php:38
EditPage\$allowSelfRedirect
bool $allowSelfRedirect
Definition: EditPage.php:201
EditPage\showEditForm
showEditForm( $formCallback=null)
Send the edit form and related headers to OutputPage.
Definition: EditPage.php:2923
Title\makeTitle
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:648
LogEventsList\showLogExtract
static showLogExtract(&$out, $types=[], $page='', $user='', $param=[])
Show log extract.
Definition: LogEventsList.php:597
EditPage\isUndoClean
isUndoClean(Content $content)
Does checks and compares the automatically generated undo content with the one that was submitted by ...
Definition: EditPage.php:2469
EditPage\wasDeletedSinceLastEdit
wasDeletedSinceLastEdit()
Check if a page was deleted while the user was editing it, before submit.
Definition: EditPage.php:3994
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
EditPage\getTemplates
getTemplates()
Definition: EditPage.php:4278
wfTimestampNow
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
Definition: GlobalFunctions.php:1678
EditPage\getPreviewParserOptions
getPreviewParserOptions()
Get parser options for a preview.
Definition: EditPage.php:4224
EditPage\$mContextTitle
null Title $mContextTitle
Definition: EditPage.php:141
wfDebug
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
Definition: GlobalFunctions.php:894
EditPage\showFormAfterText
showFormAfterText()
Definition: EditPage.php:3533
EditPage\getCancelLink
getCancelLink( $tabindex=0)
Definition: EditPage.php:3955
OutputPage
This is one of the Core classes and should be read at least once by any new developers.
Definition: OutputPage.php:52
EditPage\showPreview
showPreview( $text)
Append preview output to OutputPage.
Definition: EditPage.php:3667
Html\errorBox
static errorBox( $html, $heading='', $className='')
Return an error box.
Definition: Html.php:771
EditPage\initialiseForm
initialiseForm()
Initialise form fields in the object Called on the first invocation, e.g.
Definition: EditPage.php:1190
ContentHandler\makeContent
static makeContent( $text, Title $title=null, $modelId=null, $format=null)
Convenience function for creating a Content object from a given textual representation.
Definition: ContentHandler.php:149
deprecatePublicProperty
deprecatePublicProperty( $property, $version, $class=null, $component=null)
Mark a property as deprecated.
Definition: DeprecationHelper.php:94
MediaWiki\EditPage\Constraint\SelfRedirectConstraint
Verify the page does not redirect to itself unless.
Definition: SelfRedirectConstraint.php:35
EditPage\mergeChangesIntoContent
mergeChangesIntoContent( $editContent)
Attempts to do 3-way merge of edit content with a base revision and current content,...
Definition: EditPage.php:2562
MediaWiki\Storage\EditResult
Object for storing information about the effects of an edit.
Definition: EditResult.php:38
MediaWiki\EditPage\TextConflictHelper\setTextboxes
setTextboxes( $yourtext, $storedversion)
Definition: TextConflictHelper.php:124
MediaWiki\EditPage\TextConflictHelper
Helper for displaying edit conflicts in text content models to users.
Definition: TextConflictHelper.php:44
EditPage\internalAttemptSave
internalAttemptSave(&$result, $markAsBot=false, $markAsMinor=false)
Attempt submission (no UI)
Definition: EditPage.php:1919
MediaWiki\Permissions\Authority
This interface represents the authority associated the current execution context, such as a web reque...
Definition: Authority.php:37
Html\hidden
static hidden( $name, $value, array $attribs=[])
Convenience function to produce an input element with type=hidden.
Definition: Html.php:834
EditPage\$recreate
bool $recreate
Definition: EditPage.php:254
MediaWiki\EditPage\IEditObject\AS_UNICODE_NOT_SUPPORTED
const AS_UNICODE_NOT_SUPPORTED
Status: edit rejected because browser doesn't support Unicode.
Definition: IEditObject.php:121
EditPage\$contentLength
bool int $contentLength
Definition: EditPage.php:360
MediaWiki\EditPage\IEditObject\AS_NO_CHANGE_CONTENT_MODEL
const AS_NO_CHANGE_CONTENT_MODEL
Status: user tried to modify the content model, but is not allowed to do that ( User::isAllowed('edit...
Definition: IEditObject.php:101
MediaWiki\EditPage\IEditObject\AS_READ_ONLY_PAGE_LOGGED
const AS_READ_ONLY_PAGE_LOGGED
Status: this logged in user is not allowed to edit this page.
Definition: IEditObject.php:53
EditPage\showTextbox1
showTextbox1( $customAttribs=null, $textoverride=null)
Method to output wpTextbox1 The $textoverride method can be used by subclasses overriding showContent...
Definition: EditPage.php:3573
MediaWiki\EditPage\Constraint\AccidentalRecreationConstraint
Make sure user doesn't accidentally recreate a page deleted after they started editing.
Definition: AccidentalRecreationConstraint.php:32
EditPage\addTalkPageText
addTalkPageText()
Definition: EditPage.php:4635
MediaWiki\Permissions\PermissionManager
A service class for checking permissions To obtain an instance, use MediaWikiServices::getInstance()-...
Definition: PermissionManager.php:52
$content
$content
Definition: router.php:76
EditPage\getSummaryPreview
getSummaryPreview( $isSubjectPreview, $summary="")
Definition: EditPage.php:3503
EditPage\importContentFormData
importContentFormData(&$request)
Subpage overridable method for extracting the page content data from the posted form to be placed in ...
Definition: EditPage.php:1181
EditPage\$minoredit
bool $minoredit
Definition: EditPage.php:239
EditPage\$isOldRev
bool $isOldRev
Whether an old revision is edited.
Definition: EditPage.php:375
TemplatesOnThisPageFormatter
Handles formatting for the "templates used on this page" lists.
Definition: TemplatesOnThisPageFormatter.php:32
ExternalUserNames\getUserLinkTitle
static getUserLinkTitle( $userName)
Get a target Title to link a username.
Definition: ExternalUserNames.php:63
EditPage\$enableApiEditOverride
bool $enableApiEditOverride
Set in ApiEditPage, based on ContentHandler::allowsDirectApiEditing.
Definition: EditPage.php:365
WikiPage\getContent
getContent( $audience=RevisionRecord::FOR_PUBLIC, Authority $performer=null)
Get the content of the current revision.
Definition: WikiPage.php:840
EditPage\showHeader
showHeader()
Definition: EditPage.php:3235
MediaWiki\EditPage\TextConflictHelper\getEditFormHtmlAfterContent
getEditFormHtmlAfterContent()
Content to go in the edit form after textbox1.
Definition: TextConflictHelper.php:264
ContentHandler\getLocalizedName
static getLocalizedName( $name, Language $lang=null)
Returns the localized name for a given content model.
Definition: ContentHandler.php:310
Linker\commentBlock
static commentBlock( $comment, $title=null, $local=false, $wikiId=null, $useParentheses=true)
Wrap a comment in standard punctuation and formatting if it's non-empty, otherwise return empty strin...
Definition: Linker.php:1549
EditPage\addNewLineAtEnd
addNewLineAtEnd( $wikitext)
Definition: EditPage.php:4732
MediaWiki\Content\IContentHandlerFactory
Definition: IContentHandlerFactory.php:10
StatusValue\newGood
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:82
MediaWiki\EditPage\IEditObject\AS_IMAGE_REDIRECT_ANON
const AS_IMAGE_REDIRECT_ANON
Status: anonymous user is not allowed to upload (User::isAllowed('upload') == false)
Definition: IEditObject.php:92
Message\plaintextParam
static plaintextParam( $plaintext)
Definition: Message.php:1248
EditPage\incrementResolvedConflicts
incrementResolvedConflicts()
Log when a page was successfully saved after the edit conflict view.
Definition: EditPage.php:1706
DB_PRIMARY
const DB_PRIMARY
Definition: defines.php:27
Xml\tags
static tags( $element, $attribs, $contents)
Same as Xml::element(), but does not escape contents.
Definition: Xml.php:133
Hooks\runner
static runner()
Get a HookRunner instance for calling hooks using the new interfaces.
Definition: Hooks.php:173
EditPage\showEditTools
showEditTools()
Inserts optional text shown below edit and upload forms.
Definition: EditPage.php:3780
EditPage\$preview
bool $preview
Definition: EditPage.php:233
EditPage\$isNew
bool $isNew
New page or new section.
Definition: EditPage.php:153
EditPage\getCheckboxesDefinition
getCheckboxesDefinition( $checked)
Return an array of checkbox definitions.
Definition: EditPage.php:4336
MediaWiki\EditPage\Constraint\NewSectionMissingSummaryConstraint
For a new section, do not allow the user to post with an empty summary unless they choose to.
Definition: NewSectionMissingSummaryConstraint.php:32
EditPage\getCopywarn
getCopywarn()
Get the copyright warning.
Definition: EditPage.php:3792
EditPage\setApiEditOverride
setApiEditOverride( $enableOverride)
Allow editing of content that supports API direct editing, but not general direct editing.
Definition: EditPage.php:544
wfEscapeWikiText
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
Definition: GlobalFunctions.php:1440
EditPage\$watchlistExpiry
string null $watchlistExpiry
The expiry time of the watch item, or null if it is not watched temporarily.
Definition: EditPage.php:251
EditPage\newTextConflictHelper
newTextConflictHelper( $submitButtonLabel)
Definition: EditPage.php:4791
EditPage\showCustomIntro
showCustomIntro()
Attempt to show a custom editing introduction, if supplied.
Definition: EditPage.php:2838
EditPage\getContext
getContext()
Definition: EditPage.php:495
CONTENT_MODEL_WIKITEXT
const CONTENT_MODEL_WIKITEXT
Definition: Defines.php:208
MediaWiki\Revision\RevisionStoreRecord
A RevisionRecord representing an existing revision persisted in the revision table.
Definition: RevisionStoreRecord.php:39
NS_USER
const NS_USER
Definition: Defines.php:66
EditPage\EDITFORM_ID
const EDITFORM_ID
HTML id and name for the beginning of the edit form.
Definition: EditPage.php:102
RecentChange\PRC_AUTOPATROLLED
const PRC_AUTOPATROLLED
Definition: RecentChange.php:94
EditPage\extractSectionTitle
static extractSectionTitle( $text)
Extract the section title from current section text, if any.
Definition: EditPage.php:3226
RequestContext\getMain
static getMain()
Get the RequestContext object associated with the main request.
Definition: RequestContext.php:484
EditPage\makeTemplatesOnThisPageList
makeTemplatesOnThisPageList(array $templates)
Wrapper around TemplatesOnThisPageFormatter to make a "templates on this page" list.
Definition: EditPage.php:3202
EditPage\$textbox1
string $textbox1
Page content input field.
Definition: EditPage.php:259
TextContent
Content object implementation for representing flat text.
Definition: TextContent.php:39
EditPage\$parentRevId
int $parentRevId
Revision ID the edit is based on, adjusted when an edit conflict is resolved.
Definition: EditPage.php:316
MediaWiki\EditPage\TextConflictHelper\setContentModel
setContentModel( $contentModel)
Definition: TextConflictHelper.php:132
EditPage\$undidRev
$undidRev
Definition: EditPage.php:351
Linker\titleAttrib
static titleAttrib( $name, $options=null, array $msgParams=[])
Given the id of an interface element, constructs the appropriate title attribute from the system mess...
Definition: Linker.php:2046
MediaWiki\EditPage\Constraint\UnicodeConstraint
Verify unicode constraint.
Definition: UnicodeConstraint.php:31
EditPage\$changeTags
null array $changeTags
Definition: EditPage.php:334
EditPage
The edit page/HTML interface (split from Article) The actual database and text munging is still in Ar...
Definition: EditPage.php:90
EditPage\noSuchSectionPage
noSuchSectionPage()
Creates a basic error page which informs the user that they have attempted to edit a nonexistent sect...
Definition: EditPage.php:4569
IContextSource
Interface for objects which can provide a MediaWiki context on request.
Definition: IContextSource.php:58
EditPage\$formtype
string $formtype
Definition: EditPage.php:159
EDIT_UPDATE
const EDIT_UPDATE
Definition: Defines.php:126
Content
Base interface for content objects.
Definition: Content.php:35
MediaWiki\EditPage\Constraint\EditRightConstraint
Verify user permissions: Must have edit rights.
Definition: EditRightConstraint.php:34
EditPage\getSummaryInputWidget
getSummaryInputWidget( $summary="", $labelText=null, $inputAttrs=null)
Builds a standard summary input with a label.
Definition: EditPage.php:3442
CommentStore\COMMENT_CHARACTER_LIMIT
const COMMENT_CHARACTER_LIMIT
Maximum length of a comment in UTF-8 characters.
Definition: CommentStore.php:48
EditPage\$hasPresetSummary
bool $hasPresetSummary
Has a summary been preset using GET parameter &summary= ?
Definition: EditPage.php:215
MediaWiki\EditPage\IEditObject\AS_BLANK_ARTICLE
const AS_BLANK_ARTICLE
Status: user tried to create a blank page and wpIgnoreBlankArticle == false.
Definition: IEditObject.php:68
EditPage\$mParserOutput
ParserOutput $mParserOutput
Definition: EditPage.php:210
Title
Represents a title within MediaWiki.
Definition: Title.php:47
EditPage\$mShowSummaryField
bool $mShowSummaryField
Definition: EditPage.php:225
EditPage\$sectiontitle
string $sectiontitle
Definition: EditPage.php:296
EditPage\$starttime
string $starttime
Timestamp from the first time the edit form was rendered.
Definition: EditPage.php:301
EditPage\$suppressIntro
$suppressIntro
Definition: EditPage.php:354
EditPage\$permManager
PermissionManager $permManager
Definition: EditPage.php:402
MediaWiki\EditPage\TextConflictHelper\getEditFormHtmlBeforeContent
getEditFormHtmlBeforeContent()
Content to go in the edit form before textbox1.
Definition: TextConflictHelper.php:254
Xml\closeElement
static closeElement( $element)
Shortcut to close an XML element.
Definition: Xml.php:120
wfReadOnlyReason
wfReadOnlyReason()
Check if the site is in read-only mode and return the message if so.
Definition: GlobalFunctions.php:1097
MediaWiki\Revision\RevisionRecord\getId
getId( $wikiId=self::LOCAL)
Get revision ID.
Definition: RevisionRecord.php:279
EditPage\$scrolltop
int null $scrolltop
Definition: EditPage.php:322
EditPage\$deletedSinceEdit
bool $deletedSinceEdit
Definition: EditPage.php:156
MediaWiki\EditPage\IEditObject\AS_PARSE_ERROR
const AS_PARSE_ERROR
Status: can't parse content.
Definition: IEditObject.php:110
EditPage\$selfRedirect
bool $selfRedirect
Definition: EditPage.php:198
EditPage\$edit
bool $edit
Definition: EditPage.php:357
EditPage\isSupportedContentModel
isSupportedContentModel( $modelId)
Returns if the given content model is editable.
Definition: EditPage.php:533
MediaWiki\User\UserNameUtils
UserNameUtils service.
Definition: UserNameUtils.php:42
MediaWiki\EditPage\IEditObject\AS_IMAGE_REDIRECT_LOGGED
const AS_IMAGE_REDIRECT_LOGGED
Status: logged in user is not allowed to upload (User::isAllowed('upload') == false)
Definition: IEditObject.php:95
EditPage\showConflict
showConflict()
Show an edit conflict.
Definition: EditPage.php:3936
EditPage\isPageExistingAndViewable
isPageExistingAndViewable(?PageIdentity $page, Authority $performer)
Verify if a given title exists and the given user is allowed to view it.
Definition: EditPage.php:1634
EditPage\getSubmitButtonLabel
getSubmitButtonLabel()
Get the message key of the label for the button to save the page.
Definition: EditPage.php:4476
MediaWiki\EditPage\IEditObject\AS_MAX_ARTICLE_SIZE_EXCEEDED
const AS_MAX_ARTICLE_SIZE_EXCEEDED
Status: article is too big (> $wgMaxArticleSize), after merging in the new section.
Definition: IEditObject.php:83
EditPage\$watchedItemStore
WatchedItemStoreInterface $watchedItemStore
Definition: EditPage.php:248
EditPage\$unicodeCheck
string null $unicodeCheck
What the user submitted in the 'wpUnicodeCheck' field.
Definition: EditPage.php:380
EditPage\$edittime
string null $edittime
Timestamp of the latest revision of the page when editing was initiated on the client.
Definition: EditPage.php:277
MediaWiki\EditPage\IEditObject\AS_READ_ONLY_PAGE
const AS_READ_ONLY_PAGE
Status: wiki is in readonly mode (wfReadOnly() == true)
Definition: IEditObject.php:56
EditPage\$diff
bool $diff
Definition: EditPage.php:236
CONTENT_MODEL_JAVASCRIPT
const CONTENT_MODEL_JAVASCRIPT
Definition: Defines.php:209
EditPage\doPreviewParse
doPreviewParse(Content $content)
Parse the page for a preview.
Definition: EditPage.php:4246
EditPage\$action
string $action
Definition: EditPage.php:144
Html\openElement
static openElement( $element, $attribs=[])
Identical to rawElement(), but has no third parameter and omits the end tag (and the self-closing '/'...
Definition: Html.php:255
Html\rawElement
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:213
EditPage\setEditConflictHelperFactory
setEditConflictHelperFactory(callable $factory)
Set a factory function to create an EditConflictHelper.
Definition: EditPage.php:4767
NS_USER_TALK
const NS_USER_TALK
Definition: Defines.php:67
EditPage\$wikiPageFactory
WikiPageFactory $wikiPageFactory
Definition: EditPage.php:412
EditPage\showTextbox
showTextbox( $text, $name, $customAttribs=[])
Definition: EditPage.php:3608
EditPage\getTitle
getTitle()
Definition: EditPage.php:503
ManualLogEntry
Class for creating new log entries and inserting them into the database.
Definition: ManualLogEntry.php:45
MediaWiki\EditPage\Constraint\ChangeTagsConstraint
Verify user can add change tags.
Definition: ChangeTagsConstraint.php:34
MWUnknownContentModelException
Exception thrown when an unregistered content model is requested.
Definition: MWUnknownContentModelException.php:11
MediaWiki\EditPage\IEditObject
Serves as a common repository of constants for EditPage edit status results.
Definition: IEditObject.php:30
EditPage\getCurrentContent
getCurrentContent()
Get the current content of the page.
Definition: EditPage.php:1539
wfWarn
wfWarn( $msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
Definition: GlobalFunctions.php:1043
EditPage\$isConflict
bool $isConflict
Whether an edit conflict needs to be resolved.
Definition: EditPage.php:150
EditPage\displayViewSourcePage
displayViewSourcePage(Content $content, $errorMessage='')
Display a read-only View Source page.
Definition: EditPage.php:804
Title\castFromLinkTarget
static castFromLinkTarget( $linkTarget)
Same as newFromLinkTarget, but if passed null, returns null.
Definition: Title.php:313
Article
Class for viewing MediaWiki article and history.
Definition: Article.php:49
EditPage\getContentObject
getContentObject( $def_content=null)
Definition: EditPage.php:1260
Page\RedirectLookup
Interface to handle redirects for a given page like getting the redirect target of an editable wiki p...
Definition: RedirectLookup.php:34
EditPage\showStandardInputs
showStandardInputs(&$tabindex=2)
Definition: EditPage.php:3886
MediaWiki\EditPage\TextConflictHelper\getEditConflictMainTextBox
getEditConflictMainTextBox(array $customAttribs=[])
HTML to build the textbox1 on edit conflicts.
Definition: TextConflictHelper.php:222
NS_FILE
const NS_FILE
Definition: Defines.php:70
EDIT_FORCE_BOT
const EDIT_FORCE_BOT
Definition: Defines.php:129
MediaWiki\EditPage\IEditObject\AS_HOOK_ERROR_EXPECTED
const AS_HOOK_ERROR_EXPECTED
Status: A hook function returned an error.
Definition: IEditObject.php:41
RawMessage
Variant of the Message class.
Definition: RawMessage.php:35
ErrorPageError
An error page which can definitely be safely rendered using the OutputPage.
Definition: ErrorPageError.php:30
WikiPage\isRedirect
isRedirect()
Is the page a redirect, according to secondary tracking tables? If this is true, getRedirectTarget() ...
Definition: WikiPage.php:624
EditPage\$revisionStore
RevisionStore $revisionStore
Definition: EditPage.php:407
EditPage\POST_EDIT_COOKIE_KEY_PREFIX
const POST_EDIT_COOKIE_KEY_PREFIX
Prefix of key for cookie used to pass post-edit state.
Definition: EditPage.php:108
DeprecationHelper
trait DeprecationHelper
Use this trait in classes which have properties for which public access is deprecated or implementati...
Definition: DeprecationHelper.php:60
EDIT_MINOR
const EDIT_MINOR
Definition: Defines.php:127
EditPage\setPostEditCookie
setPostEditCookie( $statusValue)
Sets post-edit cookie indicating the user just saved a particular revision.
Definition: EditPage.php:1666
EditPage\$undoAfter
$undoAfter
Definition: EditPage.php:352
EditPage\getOriginalContent
getOriginalContent(Authority $performer)
Get the content of the wanted revision, without section extraction.
Definition: EditPage.php:1498
Linker\accesskey
static accesskey( $name)
Given the id of an interface element, constructs the appropriate accesskey attribute from the system ...
Definition: Linker.php:2094
WatchedItemStoreInterface
Definition: WatchedItemStoreInterface.php:31
CommentStore\getStore
static getStore()
Definition: CommentStore.php:120
CONTENT_MODEL_CSS
const CONTENT_MODEL_CSS
Definition: Defines.php:210
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:67
DeferredUpdates\addCallableUpdate
static addCallableUpdate( $callable, $stage=self::POSTSEND, $dbw=null)
Add an update to the pending update queue that invokes the specified callback when run.
Definition: DeferredUpdates.php:151
EditPage\$editRevId
int null $editRevId
Revision ID of the latest revision of the page when editing was initiated on the client.
Definition: EditPage.php:290
EditPage\isWrongCaseUserConfigPage
isWrongCaseUserConfigPage()
Checks whether the user entered a skin name in uppercase, e.g.
Definition: EditPage.php:898
EditPage\$incompleteForm
bool $incompleteForm
Definition: EditPage.php:177
EditPage\$missingSummary
bool $missingSummary
Definition: EditPage.php:186
EditPage\getEditConflictHelper
getEditConflictHelper()
Definition: EditPage.php:4775
ExternalUserNames\isExternal
static isExternal( $username)
Tells whether the username is external or not.
Definition: ExternalUserNames.php:149
EditPage\$markAsBot
bool $markAsBot
Definition: EditPage.php:325
MediaWiki\Revision\RevisionRecord\getSlot
getSlot( $role, $audience=self::FOR_PUBLIC, Authority $performer=null)
Returns meta-data for the given slot.
Definition: RevisionRecord.php:180
MediaWiki\EditPage\IEditObject\AS_HOOK_ERROR
const AS_HOOK_ERROR
Status: Article update aborted by a hook function.
Definition: IEditObject.php:38
Skin\makeInternalOrExternalUrl
static makeInternalOrExternalUrl( $name)
If url string starts with http, consider as external URL, else internal.
Definition: Skin.php:1230
MediaWiki\EditPage\Constraint\IEditConstraint
Interface for all constraints that can prevent edits.
Definition: IEditConstraint.php:33
EditPage\updateWatchlist
updateWatchlist()
Register the change of watch status.
Definition: EditPage.php:2532
MediaWiki\Revision\SlotRecord
Value object representing a content slot associated with a page revision.
Definition: SlotRecord.php:40
wfExpandUrl
wfExpandUrl( $url, $defaultProto=PROTO_CURRENT)
Expand a potentially local URL to a fully-qualified URL.
Definition: GlobalFunctions.php:474
EditPage\handleStatus
handleStatus(Status $status, $resultDetails)
Handle status, such as after attempt save.
Definition: EditPage.php:1723
ParserOptions\newFromUser
static newFromUser( $user)
Get a ParserOptions object from a given user.
Definition: ParserOptions.php:1044
EditPage\$hookError
string $hookError
Definition: EditPage.php:207
EditPage\$allowBlankArticle
bool $allowBlankArticle
Definition: EditPage.php:195
EditPage\toEditText
toEditText( $content)
Gets an editable textual representation of $content.
Definition: EditPage.php:2872
Xml\checkLabel
static checkLabel( $label, $name, $id, $checked=false, $attribs=[])
Convenience function to build an HTML checkbox with a label.
Definition: Xml.php:426
EditPage\setHeaders
setHeaders()
Definition: EditPage.php:2627
EditPage\guessSectionName
guessSectionName( $text)
Turns section name wikitext into anchors for use in HTTP redirects.
Definition: EditPage.php:4746
wfArrayToCgi
wfArrayToCgi( $array1, $array2=null, $prefix='')
This function takes one or two arrays as input, and returns a CGI-style string, e....
Definition: GlobalFunctions.php:330
$type
$type
Definition: testCompression.php:52
EditPage\$nosummary
bool $nosummary
If true, hide the summary field.
Definition: EditPage.php:271
EditPage\getPreviewText
getPreviewText()
Get the rendered text for previewing.
Definition: EditPage.php:4066