MediaWiki  master
EditPage.php
Go to the documentation of this file.
1 <?php
47 use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
61 use OOUI\CheckboxInputWidget;
62 use OOUI\DropdownInputWidget;
63 use OOUI\FieldLayout;
66 use Wikimedia\ScopedCallback;
67 
88 class EditPage implements IEditObject {
90  use ProtectedHookAccessorTrait;
91 
95  public const UNICODE_CHECK = UnicodeConstraint::VALID_UNICODE;
96 
100  public const EDITFORM_ID = 'editform';
101 
106  public const POST_EDIT_COOKIE_KEY_PREFIX = 'PostEditRevision';
107 
121  public const POST_EDIT_COOKIE_DURATION = 1200;
122 
127  public $mArticle;
128 
130  private $page;
131 
136  public $mTitle;
137 
139  private $mContextTitle = null;
140 
142  public $action = 'submit';
143 
148  public $isConflict = false;
149 
151  public $isNew = false;
152 
155 
157  public $formtype;
158 
163  public $firsttime;
164 
166  private $lastDelete;
167 
169  private $mTokenOk = false;
170 
172  private $mTriedSave = false;
173 
175  private $incompleteForm = false;
176 
178  private $tooBig = false;
179 
181  private $missingComment = false;
182 
184  private $missingSummary = false;
185 
187  private $allowBlankSummary = false;
188 
190  protected $blankArticle = false;
191 
193  protected $allowBlankArticle = false;
194 
196  protected $selfRedirect = false;
197 
199  protected $allowSelfRedirect = false;
200 
202  private $autoSumm = '';
203 
205  private $hookError = '';
206 
208  private $mParserOutput;
209 
213  private $hasPresetSummary = false;
214 
221  private $mExpectedParentRevision = false;
222 
224  public $mShowSummaryField = true;
225 
226  # Form values
227 
229  public $save = false;
230 
232  public $preview = false;
233 
235  public $diff = false;
236 
238  private $minoredit = false;
239 
241  private $watchthis = false;
242 
245 
248 
251 
253  private $recreate = false;
254 
258  public $textbox1 = '';
259 
261  public $textbox2 = '';
262 
264  public $summary = '';
265 
270  private $nosummary = false;
271 
276  public $edittime = '';
277 
289  private $editRevId = null;
290 
292  public $section = '';
293 
295  public $sectiontitle = '';
296 
300  public $starttime = '';
301 
307  public $oldid = 0;
308 
315  private $parentRevId = 0;
316 
318  private $editintro = '';
319 
321  private $scrolltop = null;
322 
324  private $markAsBot = true;
325 
328 
330  public $contentFormat = null;
331 
333  private $changeTags = null;
334 
335  # Placeholders for text injection by hooks (must be HTML)
336  # extensions should take care to _append_ to the present value
337 
339  public $editFormPageTop = '';
340  public $editFormTextTop = '';
344  public $editFormTextBottom = '';
347  public $mPreloadContent = null;
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 
428  public function __construct( Article $article ) {
429  $this->mArticle = $article;
430  $this->page = $article->getPage(); // model object
431  $this->mTitle = $article->getTitle();
432 
433  // Make sure the local context is in sync with other member variables.
434  // Particularly make sure everything is using the same WikiPage instance.
435  // This should probably be the case in Article as well, but it's
436  // particularly important for EditPage, to make use of the in-place caching
437  // facility in WikiPage::prepareContentForEdit.
438  $this->context = new DerivativeContext( $article->getContext() );
439  $this->context->setWikiPage( $this->page );
440  $this->context->setTitle( $this->mTitle );
441 
442  $this->contentModel = $this->mTitle->getContentModel();
443 
444  $services = MediaWikiServices::getInstance();
445  $this->contentHandlerFactory = $services->getContentHandlerFactory();
446  $this->contentFormat = $this->contentHandlerFactory
447  ->getContentHandler( $this->contentModel )
448  ->getDefaultFormat();
449  $this->editConflictHelperFactory = [ $this, 'newTextConflictHelper' ];
450  $this->permManager = $services->getPermissionManager();
451  $this->revisionStore = $services->getRevisionStore();
452  $this->watchlistExpiryEnabled = $this->getContext()->getConfig() instanceof Config
453  && $this->getContext()->getConfig()->get( 'WatchlistExpiry' );
454  $this->watchedItemStore = $services->getWatchedItemStore();
455  $this->wikiPageFactory = $services->getWikiPageFactory();
456  $this->watchlistManager = $services->getWatchlistManager();
457  $this->userNameUtils = $services->getUserNameUtils();
458 
459  $this->deprecatePublicProperty( 'deletedSinceEdit', '1.35', __CLASS__ );
460  $this->deprecatePublicProperty( 'lastDelete', '1.35', __CLASS__ );
461  $this->deprecatePublicProperty( 'mTokenOk', '1.35', __CLASS__ );
462  $this->deprecatePublicProperty( 'mTriedSave', '1.35', __CLASS__ );
463  $this->deprecatePublicProperty( 'incompleteForm', '1.35', __CLASS__ );
464  $this->deprecatePublicProperty( 'tooBig', '1.35', __CLASS__ );
465  $this->deprecatePublicProperty( 'missingComment', '1.35', __CLASS__ );
466  $this->deprecatePublicProperty( 'missingSummary', '1.35', __CLASS__ );
467  $this->deprecatePublicProperty( 'allowBlankSummary', '1.35', __CLASS__ );
468  $this->deprecatePublicProperty( 'autoSumm', '1.35', __CLASS__ );
469  $this->deprecatePublicProperty( 'mParserOutput', '1.35', __CLASS__ );
470  $this->deprecatePublicProperty( 'hasPresetSummary', '1.35', __CLASS__ );
471  $this->deprecatePublicProperty( 'minoredit', '1.35', __CLASS__ );
472  $this->deprecatePublicProperty( 'watchthis', '1.35', __CLASS__ );
473  $this->deprecatePublicProperty( 'recreate', '1.35', __CLASS__ );
474  $this->deprecatePublicProperty( 'nosummaryparentRevId', '1.35', __CLASS__ );
475  $this->deprecatePublicProperty( 'editintro', '1.35', __CLASS__ );
476  $this->deprecatePublicProperty( 'scrolltop', '1.35', __CLASS__ );
477  $this->deprecatePublicProperty( 'markAsBot', '1.35', __CLASS__ );
478  }
479 
483  public function getArticle() {
484  return $this->mArticle;
485  }
486 
491  public function getContext() {
492  return $this->context;
493  }
494 
499  public function getTitle() {
500  return $this->mTitle;
501  }
502 
506  public function setContextTitle( $title ) {
507  $this->mContextTitle = $title;
508  }
509 
514  public function getContextTitle() {
515  if ( $this->mContextTitle === null ) {
516  throw new RuntimeException( "EditPage does not have a context title set" );
517  } else {
518  return $this->mContextTitle;
519  }
520  }
521 
529  public function isSupportedContentModel( $modelId ) {
530  return $this->enableApiEditOverride === true ||
531  $this->contentHandlerFactory->getContentHandler( $modelId )->supportsDirectEditing();
532  }
533 
540  public function setApiEditOverride( $enableOverride ) {
541  $this->enableApiEditOverride = $enableOverride;
542  }
543 
555  public function edit() {
556  // Allow extensions to modify/prevent this form or submission
557  if ( !$this->getHookRunner()->onAlternateEdit( $this ) ) {
558  return;
559  }
560 
561  wfDebug( __METHOD__ . ": enter" );
562 
563  $request = $this->context->getRequest();
564  // If they used redlink=1 and the page exists, redirect to the main article
565  if ( $request->getBool( 'redlink' ) && $this->mTitle->exists() ) {
566  $this->context->getOutput()->redirect( $this->mTitle->getFullURL() );
567  return;
568  }
569 
570  $this->importFormData( $request );
571  $this->firsttime = false;
572 
573  if ( wfReadOnly() && $this->save ) {
574  // Force preview
575  $this->save = false;
576  $this->preview = true;
577  }
578 
579  if ( $this->save ) {
580  $this->formtype = 'save';
581  } elseif ( $this->preview ) {
582  $this->formtype = 'preview';
583  } elseif ( $this->diff ) {
584  $this->formtype = 'diff';
585  } else { # First time through
586  $this->firsttime = true;
587  if ( $this->previewOnOpen() ) {
588  $this->formtype = 'preview';
589  } else {
590  $this->formtype = 'initial';
591  }
592  }
593 
594  $permErrors = $this->getEditPermissionErrors(
595  $this->save ? PermissionManager::RIGOR_SECURE : PermissionManager::RIGOR_FULL
596  );
597  if ( $permErrors ) {
598  wfDebug( __METHOD__ . ": User can't edit" );
599 
600  if ( $this->context->getUser()->getBlock() ) {
601  // Auto-block user's IP if the account was "hard" blocked
602  if ( !wfReadOnly() ) {
604  $this->context->getUser()->spreadAnyEditBlock();
605  } );
606  }
607  }
608  $this->displayPermissionsError( $permErrors );
609 
610  return;
611  }
612 
613  $revRecord = $this->mArticle->fetchRevisionRecord();
614  // Disallow editing revisions with content models different from the current one
615  // Undo edits being an exception in order to allow reverting content model changes.
616  $revContentModel = $revRecord ?
617  $revRecord->getSlot( SlotRecord::MAIN, RevisionRecord::RAW )->getModel() :
618  false;
619  if ( $revContentModel && $revContentModel !== $this->contentModel ) {
620  $prevRevRecord = null;
621  $prevContentModel = false;
622  if ( $this->undidRev ) {
623  $undidRevRecord = $this->revisionStore
624  ->getRevisionById( $this->undidRev );
625  $prevRevRecord = $undidRevRecord ?
626  $this->revisionStore->getPreviousRevision( $undidRevRecord ) :
627  null;
628 
629  $prevContentModel = $prevRevRecord ?
630  $prevRevRecord
631  ->getSlot( SlotRecord::MAIN, RevisionRecord::RAW )
632  ->getModel() :
633  '';
634  }
635 
636  if ( !$this->undidRev
637  || !$prevRevRecord
638  || $prevContentModel !== $this->contentModel
639  ) {
640  $this->displayViewSourcePage(
641  $this->getContentObject(),
642  $this->context->msg(
643  'contentmodelediterror',
644  $revContentModel,
645  $this->contentModel
646  )->plain()
647  );
648  return;
649  }
650  }
651 
652  $this->isConflict = false;
653 
654  # Show applicable editing introductions
655  if ( $this->formtype == 'initial' || $this->firsttime ) {
656  $this->showIntro();
657  }
658 
659  # Attempt submission here. This will check for edit conflicts,
660  # and redundantly check for locked database, blocked IPs, etc.
661  # that edit() already checked just in case someone tries to sneak
662  # in the back door with a hand-edited submission URL.
663 
664  if ( $this->formtype == 'save' ) {
665  $resultDetails = null;
666  $status = $this->attemptSave( $resultDetails );
667  if ( !$this->handleStatus( $status, $resultDetails ) ) {
668  return;
669  }
670  }
671 
672  # First time through: get contents, set time for conflict
673  # checking, etc.
674  if ( $this->formtype == 'initial' || $this->firsttime ) {
675  if ( $this->initialiseForm() === false ) {
676  return;
677  }
678 
679  if ( !$this->mTitle->getArticleID() ) {
680  $this->getHookRunner()->onEditFormPreloadText( $this->textbox1, $this->mTitle );
681  } else {
682  $this->getHookRunner()->onEditFormInitialText( $this );
683  }
684 
685  }
686 
687  // If we're displaying an old revision, and there are differences between it and the
688  // current revision outside the main slot, then we can't allow the old revision to be
689  // editable, as what would happen to the non-main-slot data if someone saves the old
690  // revision is undefined.
691  // When this is the case, display a read-only version of the page instead, with a link
692  // to a diff page from which the old revision can be restored
693  $curRevisionRecord = $this->page->getRevisionRecord();
694  if ( $curRevisionRecord
695  && $revRecord
696  && $curRevisionRecord->getId() !== $revRecord->getId()
698  $revRecord,
699  $curRevisionRecord
700  ) || !$this->isSupportedContentModel(
701  $revRecord->getSlot(
702  SlotRecord::MAIN,
703  RevisionRecord::RAW
704  )->getModel()
705  ) )
706  ) {
707  $restoreLink = $this->mTitle->getFullURL(
708  [
709  'action' => 'mcrrestore',
710  'restore' => $revRecord->getId(),
711  ]
712  );
713  $this->displayViewSourcePage(
714  $this->getContentObject(),
715  $this->context->msg(
716  'nonmain-slot-differences-therefore-readonly',
717  $restoreLink
718  )->plain()
719  );
720  return;
721  }
722 
723  $this->showEditForm();
724  }
725 
730  protected function getEditPermissionErrors( $rigor = PermissionManager::RIGOR_SECURE ) {
731  $user = $this->context->getUser();
732  $permErrors = $this->permManager->getPermissionErrors(
733  'edit',
734  $user,
735  $this->mTitle,
736  $rigor
737  );
738  # Can this title be created?
739  if ( !$this->mTitle->exists() ) {
740  $permErrors = array_merge(
741  $permErrors,
742  wfArrayDiff2(
743  $this->permManager->getPermissionErrors(
744  'create',
745  $user,
746  $this->mTitle,
747  $rigor
748  ),
749  $permErrors
750  )
751  );
752  }
753  # Ignore some permissions errors when a user is just previewing/viewing diffs
754  $remove = [];
755  foreach ( $permErrors as $error ) {
756  if ( ( $this->preview || $this->diff )
757  && (
758  $error[0] == 'blockedtext' ||
759  $error[0] == 'autoblockedtext' ||
760  $error[0] == 'systemblockedtext'
761  )
762  ) {
763  $remove[] = $error;
764  }
765  }
766  $permErrors = wfArrayDiff2( $permErrors, $remove );
767 
768  return $permErrors;
769  }
770 
783  protected function displayPermissionsError( array $permErrors ) {
784  $out = $this->context->getOutput();
785  if ( $this->context->getRequest()->getBool( 'redlink' ) ) {
786  // The edit page was reached via a red link.
787  // Redirect to the article page and let them click the edit tab if
788  // they really want a permission error.
789  $out->redirect( $this->mTitle->getFullURL() );
790  return;
791  }
792 
793  $content = $this->getContentObject();
794 
795  # Use the normal message if there's nothing to display
796  if ( $this->firsttime && ( !$content || $content->isEmpty() ) ) {
797  $action = $this->mTitle->exists() ? 'edit' :
798  ( $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage' );
799  throw new PermissionsError( $action, $permErrors );
800  }
801 
802  $this->displayViewSourcePage(
803  $content,
804  $out->formatPermissionsErrorMessage( $permErrors, 'edit' )
805  );
806  }
807 
813  protected function displayViewSourcePage( Content $content, $errorMessage = '' ) {
814  $out = $this->context->getOutput();
815  $this->getHookRunner()->onEditPage__showReadOnlyForm_initial( $this, $out );
816 
817  $out->setRobotPolicy( 'noindex,nofollow' );
818  $out->setPageTitle( $this->context->msg(
819  'viewsource-title',
820  $this->getContextTitle()->getPrefixedText()
821  ) );
822  $out->addBacklinkSubtitle( $this->getContextTitle() );
823  $out->addHTML( $this->editFormPageTop );
824  $out->addHTML( $this->editFormTextTop );
825 
826  if ( $errorMessage !== '' ) {
827  $out->addWikiTextAsInterface( $errorMessage );
828  $out->addHTML( "<hr />\n" );
829  }
830 
831  # If the user made changes, preserve them when showing the markup
832  # (This happens when a user is blocked during edit, for instance)
833  if ( !$this->firsttime ) {
834  $text = $this->textbox1;
835  $out->addWikiMsg( 'viewyourtext' );
836  } else {
837  try {
838  $text = $this->toEditText( $content );
839  } catch ( MWException $e ) {
840  # Serialize using the default format if the content model is not supported
841  # (e.g. for an old revision with a different model)
842  $text = $content->serialize();
843  }
844  $out->addWikiMsg( 'viewsourcetext' );
845  }
846 
847  $out->addHTML( $this->editFormTextBeforeContent );
848  $this->showTextbox( $text, 'wpTextbox1', [ 'readonly' ] );
849  $out->addHTML( $this->editFormTextAfterContent );
850 
851  $out->addHTML( $this->makeTemplatesOnThisPageList( $this->getTemplates() ) );
852 
853  $out->addModules( 'mediawiki.action.edit.collapsibleFooter' );
854 
855  $out->addHTML( $this->editFormTextBottom );
856  if ( $this->mTitle->exists() ) {
857  $out->returnToMain( null, $this->mTitle );
858  }
859  }
860 
866  protected function previewOnOpen() {
867  $config = $this->context->getConfig();
868  $previewOnOpenNamespaces = $config->get( 'PreviewOnOpenNamespaces' );
869  $request = $this->context->getRequest();
870  if ( $config->get( 'RawHtml' ) ) {
871  // If raw HTML is enabled, disable preview on open
872  // since it has to be posted with a token for
873  // security reasons
874  return false;
875  }
876  if ( $request->getVal( 'preview' ) == 'yes' ) {
877  // Explicit override from request
878  return true;
879  } elseif ( $request->getVal( 'preview' ) == 'no' ) {
880  // Explicit override from request
881  return false;
882  } elseif ( $this->section == 'new' ) {
883  // Nothing *to* preview for new sections
884  return false;
885  } elseif ( ( $request->getCheck( 'preload' ) || $this->mTitle->exists() )
886  && $this->context->getUser()->getOption( 'previewonfirst' )
887  ) {
888  // Standard preference behavior
889  return true;
890  } elseif ( !$this->mTitle->exists()
891  && isset( $previewOnOpenNamespaces[$this->mTitle->getNamespace()] )
892  && $previewOnOpenNamespaces[$this->mTitle->getNamespace()]
893  ) {
894  // Categories are special
895  return true;
896  } else {
897  return false;
898  }
899  }
900 
907  protected function isWrongCaseUserConfigPage() {
908  if ( $this->mTitle->isUserConfigPage() ) {
909  $name = $this->mTitle->getSkinFromConfigSubpage();
910  $skinFactory = MediaWikiServices::getInstance()->getSkinFactory();
911  $skins = array_merge(
912  array_keys( $skinFactory->getSkinNames() ),
913  [ 'common' ]
914  );
915  return !in_array( $name, $skins )
916  && in_array( strtolower( $name ), $skins );
917  } else {
918  return false;
919  }
920  }
921 
928  protected function isSectionEditSupported() {
929  $currentRev = $this->page->getRevisionRecord();
930 
931  // $currentRev is null for non-existing pages, use the page default content model.
932  $revContentModel = $currentRev
933  ? $currentRev->getSlot( SlotRecord::MAIN, RevisionRecord::RAW )->getModel()
934  : $this->page->getContentModel();
935 
936  return (
937  ( $this->mArticle->getRevIdFetched() === $this->page->getLatest() ) &&
938  $this->contentHandlerFactory->getContentHandler( $revContentModel )->supportsSections()
939  );
940  }
941 
947  public function importFormData( &$request ) {
948  # Section edit can come from either the form or a link
949  $this->section = $request->getVal( 'wpSection', $request->getVal( 'section' ) );
950 
951  if ( $this->section !== null && $this->section !== '' && !$this->isSectionEditSupported() ) {
952  throw new ErrorPageError( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
953  }
954 
955  $this->isNew = !$this->mTitle->exists() || $this->section == 'new';
956 
957  if ( $request->wasPosted() ) {
958  # These fields need to be checked for encoding.
959  # Also remove trailing whitespace, but don't remove _initial_
960  # whitespace from the text boxes. This may be significant formatting.
961  $this->textbox1 = rtrim( $request->getText( 'wpTextbox1' ) );
962  if ( !$request->getCheck( 'wpTextbox2' ) ) {
963  // Skip this if wpTextbox2 has input, it indicates that we came
964  // from a conflict page with raw page text, not a custom form
965  // modified by subclasses
966  $textbox1 = $this->importContentFormData( $request );
967  if ( $textbox1 !== null ) {
968  $this->textbox1 = $textbox1;
969  }
970  }
971 
972  $this->unicodeCheck = $request->getText( 'wpUnicodeCheck' );
973 
974  $this->summary = $request->getText( 'wpSummary' );
975 
976  # If the summary consists of a heading, e.g. '==Foobar==', extract the title from the
977  # header syntax, e.g. 'Foobar'. This is mainly an issue when we are using wpSummary for
978  # section titles.
979  $this->summary = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->summary );
980 
981  # Treat sectiontitle the same way as summary.
982  # Note that wpSectionTitle is not yet a part of the actual edit form, as wpSummary is
983  # currently doing double duty as both edit summary and section title. Right now this
984  # is just to allow API edits to work around this limitation, but this should be
985  # incorporated into the actual edit form when EditPage is rewritten (T20654, T28312).
986  $this->sectiontitle = $request->getText( 'wpSectionTitle' );
987  $this->sectiontitle = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->sectiontitle );
988 
989  $this->edittime = $request->getVal( 'wpEdittime' );
990  $this->editRevId = $request->getIntOrNull( 'editRevId' );
991  $this->starttime = $request->getVal( 'wpStarttime' );
992 
993  $undidRev = $request->getInt( 'wpUndidRevision' );
994  if ( $undidRev ) {
995  $this->undidRev = $undidRev;
996  }
997  $undoAfter = $request->getInt( 'wpUndoAfter' );
998  if ( $undoAfter ) {
999  $this->undoAfter = $undoAfter;
1000  }
1001 
1002  $this->scrolltop = $request->getIntOrNull( 'wpScrolltop' );
1003 
1004  if ( $this->textbox1 === '' && !$request->getCheck( 'wpTextbox1' ) ) {
1005  // wpTextbox1 field is missing, possibly due to being "too big"
1006  // according to some filter rules such as Suhosin's setting for
1007  // suhosin.request.max_value_length (d'oh)
1008  $this->incompleteForm = true;
1009  } else {
1010  // If we receive the last parameter of the request, we can fairly
1011  // claim the POST request has not been truncated.
1012  $this->incompleteForm = !$request->getVal( 'wpUltimateParam' );
1013  }
1014  if ( $this->incompleteForm ) {
1015  # If the form is incomplete, force to preview.
1016  wfDebug( __METHOD__ . ": Form data appears to be incomplete" );
1017  wfDebug( "POST DATA: " . var_export( $request->getPostValues(), true ) );
1018  $this->preview = true;
1019  } else {
1020  $this->preview = $request->getCheck( 'wpPreview' );
1021  $this->diff = $request->getCheck( 'wpDiff' );
1022 
1023  // Remember whether a save was requested, so we can indicate
1024  // if we forced preview due to session failure.
1025  $this->mTriedSave = !$this->preview;
1026 
1027  if ( $this->tokenOk( $request ) ) {
1028  # Some browsers will not report any submit button
1029  # if the user hits enter in the comment box.
1030  # The unmarked state will be assumed to be a save,
1031  # if the form seems otherwise complete.
1032  wfDebug( __METHOD__ . ": Passed token check." );
1033  } elseif ( $this->diff ) {
1034  # Failed token check, but only requested "Show Changes".
1035  wfDebug( __METHOD__ . ": Failed token check; Show Changes requested." );
1036  } else {
1037  # Page might be a hack attempt posted from
1038  # an external site. Preview instead of saving.
1039  wfDebug( __METHOD__ . ": Failed token check; forcing preview" );
1040  $this->preview = true;
1041  }
1042  }
1043  $this->save = !$this->preview && !$this->diff;
1044  if ( !preg_match( '/^\d{14}$/', $this->edittime ) ) {
1045  $this->edittime = null;
1046  }
1047 
1048  if ( !preg_match( '/^\d{14}$/', $this->starttime ) ) {
1049  $this->starttime = null;
1050  }
1051 
1052  $this->recreate = $request->getCheck( 'wpRecreate' );
1053 
1054  $user = $this->getContext()->getUser();
1055 
1056  $this->minoredit = $request->getCheck( 'wpMinoredit' );
1057  $this->watchthis = $request->getCheck( 'wpWatchthis' );
1058  $expiry = $request->getText( 'wpWatchlistExpiry' );
1059  if ( $this->watchlistExpiryEnabled && $expiry !== '' ) {
1060  // This parsing of the user-posted expiry is done for both preview and saving. This
1061  // is necessary because ApiEditPage uses preview when it saves (yuck!). Note that it
1062  // only works because the unnormalized value is retrieved again below in
1063  // getCheckboxesDefinitionForWatchlist().
1064  $expiry = ExpiryDef::normalizeExpiry( $expiry, TS_ISO_8601 );
1065  if ( $expiry !== false ) {
1066  $this->watchlistExpiry = $expiry;
1067  }
1068  }
1069 
1070  # Don't force edit summaries when a user is editing their own user or talk page
1071  if ( ( $this->mTitle->mNamespace === NS_USER || $this->mTitle->mNamespace === NS_USER_TALK )
1072  && $this->mTitle->getText() == $user->getName()
1073  ) {
1074  $this->allowBlankSummary = true;
1075  } else {
1076  $this->allowBlankSummary = $request->getBool( 'wpIgnoreBlankSummary' )
1077  || !$user->getOption( 'forceeditsummary' );
1078  }
1079 
1080  $this->autoSumm = $request->getText( 'wpAutoSummary' );
1081 
1082  $this->allowBlankArticle = $request->getBool( 'wpIgnoreBlankArticle' );
1083  $this->allowSelfRedirect = $request->getBool( 'wpIgnoreSelfRedirect' );
1084 
1085  $changeTags = $request->getVal( 'wpChangeTags' );
1086  if ( $changeTags === null || $changeTags === '' ) {
1087  $this->changeTags = [];
1088  } else {
1089  $this->changeTags = array_filter( array_map( 'trim', explode( ',',
1090  $changeTags ) ) );
1091  }
1092  } else {
1093  # Not a posted form? Start with nothing.
1094  wfDebug( __METHOD__ . ": Not a posted form." );
1095  $this->textbox1 = '';
1096  $this->summary = '';
1097  $this->sectiontitle = '';
1098  $this->edittime = '';
1099  $this->editRevId = null;
1100  $this->starttime = wfTimestampNow();
1101  $this->edit = false;
1102  $this->preview = false;
1103  $this->save = false;
1104  $this->diff = false;
1105  $this->minoredit = false;
1106  // Watch may be overridden by request parameters
1107  $this->watchthis = $request->getBool( 'watchthis', false );
1108  if ( $this->watchlistExpiryEnabled ) {
1109  $this->watchlistExpiry = null;
1110  }
1111  $this->recreate = false;
1112 
1113  // When creating a new section, we can preload a section title by passing it as the
1114  // preloadtitle parameter in the URL (T15100)
1115  if ( $this->section == 'new' && $request->getVal( 'preloadtitle' ) ) {
1116  $this->sectiontitle = $request->getVal( 'preloadtitle' );
1117  // Once wpSummary isn't being use for setting section titles, we should delete this.
1118  $this->summary = $request->getVal( 'preloadtitle' );
1119  } elseif ( $this->section != 'new' && $request->getVal( 'summary' ) !== '' ) {
1120  $this->summary = $request->getText( 'summary' );
1121  if ( $this->summary !== '' ) {
1122  $this->hasPresetSummary = true;
1123  }
1124  }
1125 
1126  if ( $request->getVal( 'minor' ) ) {
1127  $this->minoredit = true;
1128  }
1129  }
1130 
1131  $this->oldid = $request->getInt( 'oldid' );
1132  $this->parentRevId = $request->getInt( 'parentRevId' );
1133 
1134  $this->markAsBot = $request->getBool( 'bot', true );
1135  $this->nosummary = $request->getBool( 'nosummary' );
1136 
1137  // May be overridden by revision.
1138  $this->contentModel = $request->getText( 'model', $this->contentModel );
1139  // May be overridden by revision.
1140  $this->contentFormat = $request->getText( 'format', $this->contentFormat );
1141 
1142  try {
1143  $handler = $this->contentHandlerFactory->getContentHandler( $this->contentModel );
1144  } catch ( MWUnknownContentModelException $e ) {
1145  throw new ErrorPageError(
1146  'editpage-invalidcontentmodel-title',
1147  'editpage-invalidcontentmodel-text',
1148  [ wfEscapeWikiText( $this->contentModel ) ]
1149  );
1150  }
1151 
1152  if ( !$handler->isSupportedFormat( $this->contentFormat ) ) {
1153  throw new ErrorPageError(
1154  'editpage-notsupportedcontentformat-title',
1155  'editpage-notsupportedcontentformat-text',
1156  [
1157  wfEscapeWikiText( $this->contentFormat ),
1158  wfEscapeWikiText( ContentHandler::getLocalizedName( $this->contentModel ) )
1159  ]
1160  );
1161  }
1162 
1169  $this->editintro = $request->getText( 'editintro',
1170  // Custom edit intro for new sections
1171  $this->section === 'new' ? 'MediaWiki:addsection-editintro' : '' );
1172 
1173  // Allow extensions to modify form data
1174  $this->getHookRunner()->onEditPage__importFormData( $this, $request );
1175  }
1176 
1186  protected function importContentFormData( &$request ) {
1187  return null; // Don't do anything, EditPage already extracted wpTextbox1
1188  }
1189 
1195  private function initialiseForm() {
1196  $this->edittime = $this->page->getTimestamp();
1197  $this->editRevId = $this->page->getLatest();
1198 
1199  $dummy = $this->contentHandlerFactory
1200  ->getContentHandler( $this->contentModel )
1201  ->makeEmptyContent();
1202  $content = $this->getContentObject( $dummy ); # TODO: track content object?!
1203  if ( $content === $dummy ) { // Invalid section
1204  $this->noSuchSectionPage();
1205  return false;
1206  }
1207 
1208  if ( !$content ) {
1209  $out = $this->context->getOutput();
1210  $this->editFormPageTop .= Html::rawElement(
1211  'div', [ 'class' => 'errorbox' ],
1212  $out->parseAsInterface( $this->context->msg( 'missing-revision-content',
1213  $this->oldid,
1214  Message::plaintextParam( $this->mTitle->getPrefixedText() )
1215  ) )
1216  );
1217  } elseif ( !$this->isSupportedContentModel( $content->getModel() ) ) {
1218  $modelMsg = $this->getContext()->msg( 'content-model-' . $content->getModel() );
1219  $modelName = $modelMsg->exists() ? $modelMsg->text() : $content->getModel();
1220 
1221  $out = $this->context->getOutput();
1222  $out->showErrorPage(
1223  'modeleditnotsupported-title',
1224  'modeleditnotsupported-text',
1225  [ $modelName ]
1226  );
1227  return false;
1228  }
1229 
1230  $this->textbox1 = $this->toEditText( $content );
1231 
1232  $user = $this->context->getUser();
1233  // activate checkboxes if user wants them to be always active
1234  # Sort out the "watch" checkbox
1235  if ( $user->getOption( 'watchdefault' ) ) {
1236  # Watch all edits
1237  $this->watchthis = true;
1238  } elseif ( $user->getOption( 'watchcreations' ) && !$this->mTitle->exists() ) {
1239  # Watch creations
1240  $this->watchthis = true;
1241  } elseif ( $this->watchlistManager->isWatched( $user, $this->mTitle ) ) {
1242  # Already watched
1243  $this->watchthis = true;
1244  }
1245  if ( $this->watchthis && $this->watchlistExpiryEnabled ) {
1246  $watchedItem = $this->watchedItemStore->getWatchedItem( $user, $this->getTitle() );
1247  $this->watchlistExpiry = $watchedItem ? $watchedItem->getExpiry() : null;
1248  }
1249  if ( $user->getOption( 'minordefault' ) && !$this->isNew ) {
1250  $this->minoredit = true;
1251  }
1252  if ( $this->textbox1 === false ) {
1253  return false;
1254  }
1255  return true;
1256  }
1257 
1265  protected function getContentObject( $def_content = null ) {
1266  global $wgDisableAnonTalk;
1267 
1268  $content = false;
1269 
1270  $user = $this->context->getUser();
1271  $request = $this->context->getRequest();
1272  // For message page not locally set, use the i18n message.
1273  // For other non-existent articles, use preload text if any.
1274  if ( !$this->mTitle->exists() || $this->section == 'new' ) {
1275  if ( $this->mTitle->getNamespace() === NS_MEDIAWIKI && $this->section != 'new' ) {
1276  # If this is a system message, get the default text.
1277  $msg = $this->mTitle->getDefaultMessageText();
1278 
1279  $content = $this->toEditContent( $msg );
1280  }
1281  if ( $content === false ) {
1282  # If requested, preload some text.
1283  $preload = $request->getVal( 'preload',
1284  // Custom preload text for new sections
1285  $this->section === 'new' ? 'MediaWiki:addsection-preload' : '' );
1286  $params = $request->getArray( 'preloadparams', [] );
1287 
1288  $content = $this->getPreloadedContent( $preload, $params );
1289  }
1290  // For existing pages, get text based on "undo" or section parameters.
1291  } elseif ( $this->section != '' ) {
1292  // Get section edit text (returns $def_text for invalid sections)
1293  $orig = $this->getOriginalContent( $user );
1294  $content = $orig ? $orig->getSection( $this->section ) : null;
1295 
1296  if ( !$content ) {
1297  $content = $def_content;
1298  }
1299  } else {
1300  $undoafter = $request->getInt( 'undoafter' );
1301  $undo = $request->getInt( 'undo' );
1302 
1303  if ( $undo > 0 && $undoafter > 0 ) {
1304  $undorev = $this->revisionStore->getRevisionById( $undo );
1305  $oldrev = $this->revisionStore->getRevisionById( $undoafter );
1306  $undoMsg = null;
1307 
1308  # Sanity check, make sure it's the right page,
1309  # the revisions exist and they were not deleted.
1310  # Otherwise, $content will be left as-is.
1311  if ( $undorev !== null && $oldrev !== null &&
1312  !$undorev->isDeleted( RevisionRecord::DELETED_TEXT ) &&
1313  !$oldrev->isDeleted( RevisionRecord::DELETED_TEXT )
1314  ) {
1315  if ( WikiPage::hasDifferencesOutsideMainSlot( $undorev, $oldrev )
1316  || !$this->isSupportedContentModel(
1317  $oldrev->getSlot( SlotRecord::MAIN, RevisionRecord::RAW )->getModel()
1318  )
1319  ) {
1320  // Hack for undo while EditPage can't handle multi-slot editing
1321  $this->context->getOutput()->redirect( $this->mTitle->getFullURL( [
1322  'action' => 'mcrundo',
1323  'undo' => $undo,
1324  'undoafter' => $undoafter,
1325  ] ) );
1326  return false;
1327  } else {
1328  $content = $this->getUndoContent( $undorev, $oldrev );
1329 
1330  if ( $content === false ) {
1331  # Warn the user that something went wrong
1332  $undoMsg = 'failure';
1333  }
1334  }
1335 
1336  if ( $undoMsg === null ) {
1337  $oldContent = $this->page->getContent( RevisionRecord::RAW );
1339  $user, MediaWikiServices::getInstance()->getContentLanguage() );
1340  $newContent = $content->preSaveTransform( $this->mTitle, $user, $popts );
1341  if ( $newContent->getModel() !== $oldContent->getModel() ) {
1342  // The undo may change content
1343  // model if its reverting the top
1344  // edit. This can result in
1345  // mismatched content model/format.
1346  $this->contentModel = $newContent->getModel();
1347  $oldMainSlot = $oldrev->getSlot(
1348  SlotRecord::MAIN,
1349  RevisionRecord::RAW
1350  );
1351  $this->contentFormat = $oldMainSlot->getFormat();
1352  if ( $this->contentFormat === null ) {
1353  $this->contentFormat = $this->contentHandlerFactory
1354  ->getContentHandler( $oldMainSlot->getModel() )
1355  ->getDefaultFormat();
1356  }
1357  }
1358 
1359  if ( $newContent->equals( $oldContent ) ) {
1360  # Tell the user that the undo results in no change,
1361  # i.e. the revisions were already undone.
1362  $undoMsg = 'nochange';
1363  $content = false;
1364  } else {
1365  # Inform the user of our success and set an automatic edit summary
1366  $undoMsg = 'success';
1367 
1368  # If we just undid one rev, use an autosummary
1369  $firstrev = $this->revisionStore->getNextRevision( $oldrev );
1370  if ( $firstrev && $firstrev->getId() == $undo ) {
1371  $userText = $undorev->getUser() ?
1372  $undorev->getUser()->getName() :
1373  '';
1374  if ( $userText === '' ) {
1375  $undoSummary = $this->context->msg(
1376  'undo-summary-username-hidden',
1377  $undo
1378  )->inContentLanguage()->text();
1379  // Handle external users (imported revisions)
1380  } elseif ( ExternalUserNames::isExternal( $userText ) ) {
1381  $userLinkTitle = ExternalUserNames::getUserLinkTitle( $userText );
1382  if ( $userLinkTitle ) {
1383  $userLink = $userLinkTitle->getPrefixedText();
1384  $undoSummary = $this->context->msg(
1385  'undo-summary-import',
1386  $undo,
1387  $userLink,
1388  $userText
1389  )->inContentLanguage()->text();
1390  } else {
1391  $undoSummary = $this->context->msg(
1392  'undo-summary-import2',
1393  $undo,
1394  $userText
1395  )->inContentLanguage()->text();
1396  }
1397  } else {
1398  $undoIsAnon = $undorev->getUser() ?
1399  !$undorev->getUser()->isRegistered() :
1400  true;
1401  $undoMessage = ( $undoIsAnon && $wgDisableAnonTalk ) ?
1402  'undo-summary-anon' :
1403  'undo-summary';
1404  $undoSummary = $this->context->msg(
1405  $undoMessage,
1406  $undo,
1407  $userText
1408  )->inContentLanguage()->text();
1409  }
1410  if ( $this->summary === '' ) {
1411  $this->summary = $undoSummary;
1412  } else {
1413  $this->summary = $undoSummary . $this->context->msg( 'colon-separator' )
1414  ->inContentLanguage()->text() . $this->summary;
1415  }
1416  }
1417  $this->undidRev = $undo;
1418  $this->undoAfter = $undoafter;
1419  $this->formtype = 'diff';
1420  }
1421  }
1422  } else {
1423  // Failed basic sanity checks.
1424  // Older revisions may have been removed since the link
1425  // was created, or we may simply have got bogus input.
1426  $undoMsg = 'norev';
1427  }
1428 
1429  $out = $this->context->getOutput();
1430  // Messages: undo-success, undo-failure, undo-main-slot-only, undo-norev,
1431  // undo-nochange.
1432  $class = ( $undoMsg == 'success' ? '' : 'error ' ) . "mw-undo-{$undoMsg}";
1433  $this->editFormPageTop .= Html::rawElement(
1434  'div', [ 'class' => $class ],
1435  $out->parseAsInterface(
1436  $this->context->msg( 'undo-' . $undoMsg )->plain()
1437  )
1438  );
1439  }
1440 
1441  if ( $content === false ) {
1442  $content = $this->getOriginalContent( $user );
1443  }
1444  }
1445 
1446  return $content;
1447  }
1448 
1459  private function getUndoContent( RevisionRecord $undoRev, RevisionRecord $oldRev ) {
1460  $handler = $this->contentHandlerFactory
1461  ->getContentHandler( $undoRev->getSlot(
1462  SlotRecord::MAIN,
1463  RevisionRecord::RAW
1464  )->getModel() );
1465  $currentContent = $this->page->getRevisionRecord()
1466  ->getContent( SlotRecord::MAIN );
1467  $undoContent = $undoRev->getContent( SlotRecord::MAIN );
1468  $undoAfterContent = $oldRev->getContent( SlotRecord::MAIN );
1469  $undoIsLatest = $this->page->getRevisionRecord()->getId() === $undoRev->getId();
1470 
1471  return $handler->getUndoContent(
1472  $currentContent,
1473  $undoContent,
1474  $undoAfterContent,
1475  $undoIsLatest
1476  );
1477  }
1478 
1494  private function getOriginalContent( Authority $performer ) {
1495  if ( $this->section == 'new' ) {
1496  return $this->getCurrentContent();
1497  }
1498  $revRecord = $this->mArticle->fetchRevisionRecord();
1499  if ( $revRecord === null ) {
1500  return $this->contentHandlerFactory
1501  ->getContentHandler( $this->contentModel )
1502  ->makeEmptyContent();
1503  }
1504  return $revRecord->getContent( SlotRecord::MAIN, RevisionRecord::FOR_THIS_USER, $performer );
1505  }
1506 
1519  public function getParentRevId() {
1520  if ( $this->parentRevId ) {
1521  return $this->parentRevId;
1522  } else {
1523  return $this->mArticle->getRevIdFetched();
1524  }
1525  }
1526 
1535  protected function getCurrentContent() {
1536  $revRecord = $this->page->getRevisionRecord();
1537  $content = $revRecord ? $revRecord->getContent(
1538  SlotRecord::MAIN,
1539  RevisionRecord::RAW
1540  ) : null;
1541 
1542  if ( $content === null ) {
1543  return $this->contentHandlerFactory
1544  ->getContentHandler( $this->contentModel )
1545  ->makeEmptyContent();
1546  }
1547 
1548  return $content;
1549  }
1550 
1562  protected function getPreloadedContent( $preload, $params = [] ) {
1563  if ( !empty( $this->mPreloadContent ) ) {
1564  return $this->mPreloadContent;
1565  }
1566 
1567  $handler = $this->contentHandlerFactory->getContentHandler( $this->contentModel );
1568 
1569  if ( $preload === '' ) {
1570  return $handler->makeEmptyContent();
1571  }
1572 
1573  $user = $this->context->getUser();
1574  $title = Title::newFromText( $preload );
1575 
1576  # Check for existence to avoid getting MediaWiki:Noarticletext
1577  if ( !$this->isPageExistingAndViewable( $title, $this->getContext()->getAuthority() ) ) {
1578  // TODO: somehow show a warning to the user!
1579  return $handler->makeEmptyContent();
1580  }
1581 
1582  $page = $this->wikiPageFactory->newFromTitle( $title );
1583  if ( $page->isRedirect() ) {
1585  # Same as before
1586  if ( !$this->isPageExistingAndViewable( $title, $this->getContext()->getAuthority() ) ) {
1587  // TODO: somehow show a warning to the user!
1588  return $handler->makeEmptyContent();
1589  }
1590  $page = $this->wikiPageFactory->newFromTitle( $title );
1591  }
1592 
1593  $parserOptions = ParserOptions::newFromUser( $user );
1594  $content = $page->getContent( RevisionRecord::RAW );
1595 
1596  if ( !$content ) {
1597  // TODO: somehow show a warning to the user!
1598  return $handler->makeEmptyContent();
1599  }
1600 
1601  if ( $content->getModel() !== $handler->getModelID() ) {
1602  $converted = $content->convert( $handler->getModelID() );
1603 
1604  if ( !$converted ) {
1605  // TODO: somehow show a warning to the user!
1606  wfDebug( "Attempt to preload incompatible content: " .
1607  "can't convert " . $content->getModel() .
1608  " to " . $handler->getModelID() );
1609 
1610  return $handler->makeEmptyContent();
1611  }
1612 
1613  $content = $converted;
1614  }
1615 
1616  return $content->preloadTransform( $title, $parserOptions, $params );
1617  }
1618 
1628  private function isPageExistingAndViewable( ?PageIdentity $page, Authority $performer ) {
1629  return $page && $page->exists() && $performer->authorizeRead( 'read', $page );
1630  }
1631 
1639  public function tokenOk( &$request ) {
1640  $token = $request->getVal( 'wpEditToken' );
1641  $user = $this->context->getUser();
1642  $this->mTokenOk = $user->matchEditToken( $token );
1643  return $this->mTokenOk;
1644  }
1645 
1660  protected function setPostEditCookie( $statusValue ) {
1661  $revisionId = $this->page->getLatest();
1662  $postEditKey = self::POST_EDIT_COOKIE_KEY_PREFIX . $revisionId;
1663 
1664  $val = 'saved';
1665  if ( $statusValue == self::AS_SUCCESS_NEW_ARTICLE ) {
1666  $val = 'created';
1667  } elseif ( $this->oldid ) {
1668  $val = 'restored';
1669  }
1670 
1671  $response = $this->context->getRequest()->response();
1672  $response->setCookie( $postEditKey, $val, time() + self::POST_EDIT_COOKIE_DURATION );
1673  }
1674 
1681  public function attemptSave( &$resultDetails = false ) {
1682  // TODO: MCR:
1683  // * treat $this->minoredit like $this->markAsBot and check isAllowed( 'minoredit' )!
1684  // * add $this->autopatrol like $this->markAsBot and check isAllowed( 'autopatrol' )!
1685  // This is needed since PageUpdater no longer checks these rights!
1686 
1687  // Allow bots to exempt some edits from bot flagging
1688  $markAsBot = $this->markAsBot
1689  && $this->permManager->userHasRight( $this->context->getUser(), 'bot' );
1690  $status = $this->internalAttemptSave( $resultDetails, $markAsBot );
1691 
1692  $this->getHookRunner()->onEditPage__attemptSave_after( $this, $status, $resultDetails );
1693 
1694  return $status;
1695  }
1696 
1700  private function incrementResolvedConflicts() {
1701  if ( $this->context->getRequest()->getText( 'mode' ) !== 'conflict' ) {
1702  return;
1703  }
1704 
1705  $this->getEditConflictHelper()->incrementResolvedStats( $this->context->getUser() );
1706  }
1707 
1717  private function handleStatus( Status $status, $resultDetails ) {
1718  $statusValue = is_int( $status->value ) ? $status->value : 0;
1719 
1724  if ( $statusValue == self::AS_SUCCESS_UPDATE
1725  || $statusValue == self::AS_SUCCESS_NEW_ARTICLE
1726  ) {
1727  $this->incrementResolvedConflicts();
1728 
1729  $this->didSave = true;
1730  if ( !$resultDetails['nullEdit'] ) {
1731  $this->setPostEditCookie( $statusValue );
1732  }
1733  }
1734 
1735  $out = $this->context->getOutput();
1736 
1737  // "wpExtraQueryRedirect" is a hidden input to modify
1738  // after save URL and is not used by actual edit form
1739  $request = $this->context->getRequest();
1740  $extraQueryRedirect = $request->getVal( 'wpExtraQueryRedirect' );
1741 
1742  switch ( $statusValue ) {
1750  case self::AS_END:
1753  return true;
1754 
1755  case self::AS_HOOK_ERROR:
1756  return false;
1757 
1759  wfDeprecated(
1760  __METHOD__ . ' with $statusValue == AS_CANNOT_USE_CUSTOM_MODEL',
1761  '1.35'
1762  );
1763  // ...and fall through to next case
1764  case self::AS_PARSE_ERROR:
1766  $out->wrapWikiTextAsInterface( 'error',
1767  $status->getWikiText( false, false, $this->context->getLanguage() )
1768  );
1769  return true;
1770 
1772  $query = $resultDetails['redirect'] ? 'redirect=no' : '';
1773  if ( $extraQueryRedirect ) {
1774  if ( $query !== '' ) {
1775  $query .= '&';
1776  }
1777  $query .= $extraQueryRedirect;
1778  }
1779  $anchor = $resultDetails['sectionanchor'] ?? '';
1780  $out->redirect( $this->mTitle->getFullURL( $query ) . $anchor );
1781  return false;
1782 
1784  $extraQuery = '';
1785  $sectionanchor = $resultDetails['sectionanchor'];
1786 
1787  // Give extensions a chance to modify URL query on update
1788  $this->getHookRunner()->onArticleUpdateBeforeRedirect( $this->mArticle,
1789  $sectionanchor, $extraQuery );
1790 
1791  if ( $resultDetails['redirect'] ) {
1792  if ( $extraQuery !== '' ) {
1793  $extraQuery = '&' . $extraQuery;
1794  }
1795  $extraQuery = 'redirect=no' . $extraQuery;
1796  }
1797  if ( $extraQueryRedirect ) {
1798  if ( $extraQuery !== '' ) {
1799  $extraQuery .= '&';
1800  }
1801  $extraQuery .= $extraQueryRedirect;
1802  }
1803 
1804  $out->redirect( $this->mTitle->getFullURL( $extraQuery ) . $sectionanchor );
1805  return false;
1806 
1807  case self::AS_SPAM_ERROR:
1808  $this->spamPageWithContent( $resultDetails['spam'] ?? false );
1809  return false;
1810 
1812  throw new UserBlockedError(
1813  $this->context->getUser()->getBlock(),
1814  $this->context->getUser(),
1815  $this->context->getLanguage(),
1816  $request->getIP()
1817  );
1818 
1821  throw new PermissionsError( 'upload' );
1822 
1825  throw new PermissionsError( 'edit' );
1826 
1828  throw new ReadOnlyError;
1829 
1830  case self::AS_RATE_LIMITED:
1831  throw new ThrottledError();
1832 
1834  $permission = $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage';
1835  throw new PermissionsError( $permission );
1836 
1838  throw new PermissionsError( 'editcontentmodel' );
1839 
1840  default:
1841  // We don't recognize $statusValue. The only way that can happen
1842  // is if an extension hook aborted from inside ArticleSave.
1843  // Render the status object into $this->hookError
1844  // FIXME this sucks, we should just use the Status object throughout
1845  $this->hookError = Html::errorBox(
1846  "\n" . $status->getWikiText( false, false, $this->context->getLanguage() )
1847  );
1848  return true;
1849  }
1850  }
1851 
1857  private function newSectionSummary() : array {
1858  $newSectionSummary = $this->summary;
1859  $newSectionAnchor = '';
1860  $services = MediaWikiServices::getInstance();
1861  $parser = $services->getParser();
1862  $textFormatter = $services->getMessageFormatterFactory()->getTextFormatter(
1863  $services->getContentLanguage()->getCode()
1864  );
1865 
1866  if ( $this->sectiontitle !== '' ) {
1867  $newSectionAnchor = $this->guessSectionName( $this->sectiontitle );
1868  // If no edit summary was specified, create one automatically from the section
1869  // title and have it link to the new section. Otherwise, respect the summary as
1870  // passed.
1871  if ( $this->summary === '' ) {
1872  $messageValue = MessageValue::new( 'newsectionsummary' )
1873  ->plaintextParams( $parser->stripSectionName( $this->sectiontitle ) );
1874  $newSectionSummary = $textFormatter->format( $messageValue );
1875  }
1876  } elseif ( $this->summary !== '' ) {
1877  $newSectionAnchor = $this->guessSectionName( $this->summary );
1878  // This is a new section, so create a link to the new section
1879  // in the revision summary.
1880  $messageValue = MessageValue::new( 'newsectionsummary' )
1881  ->plaintextParams( $parser->stripSectionName( $this->summary ) );
1882  $newSectionSummary = $textFormatter->format( $messageValue );
1883  }
1884  return [ $newSectionSummary, $newSectionAnchor ];
1885  }
1886 
1912  public function internalAttemptSave( &$result, $markAsBot = false ) {
1913  if ( !$this->getHookRunner()->onEditPage__attemptSave( $this ) ) {
1914  wfDebug( "Hook 'EditPage::attemptSave' aborted article saving" );
1915  $status = Status::newFatal( 'hookaborted' );
1916  $status->value = self::AS_HOOK_ERROR;
1917  return $status;
1918  }
1919 
1920  if ( !$this->getHookRunner()->onEditFilter( $this, $this->textbox1, $this->section,
1921  $this->hookError, $this->summary )
1922  ) {
1923  # Error messages etc. could be handled within the hook...
1924  $status = Status::newFatal( 'hookaborted' );
1925  $status->value = self::AS_HOOK_ERROR;
1926  return $status;
1927  } elseif ( $this->hookError != '' ) {
1928  # ...or the hook could be expecting us to produce an error
1929  $status = Status::newFatal( 'hookaborted' );
1930  $status->value = self::AS_HOOK_ERROR_EXPECTED;
1931  return $status;
1932  }
1933 
1934  try {
1935  # Construct Content object
1936  $textbox_content = $this->toEditContent( $this->textbox1 );
1937  } catch ( MWContentSerializationException $ex ) {
1938  $status = Status::newFatal(
1939  'content-failed-to-parse',
1940  $this->contentModel,
1941  $this->contentFormat,
1942  $ex->getMessage()
1943  );
1944  $status->value = self::AS_PARSE_ERROR;
1945  return $status;
1946  }
1947 
1948  $this->contentLength = strlen( $this->textbox1 );
1949  $user = $this->context->getUser();
1950 
1951  $changingContentModel = false;
1952  if ( $this->contentModel !== $this->mTitle->getContentModel() ) {
1953  $changingContentModel = true;
1954  $oldContentModel = $this->mTitle->getContentModel();
1955  }
1956 
1957  // BEGINNING OF MIGRATION TO EDITCONSTRAINT SYSTEM (see T157658)
1958  $constraintFactory = MediaWikiServices::getInstance()->getService( '_EditConstraintFactory' );
1959  $constraintRunner = new EditConstraintRunner();
1960 
1961  // UnicodeConstraint: ensure that `$this->unicodeCheck` is the correct unicode
1962  $constraintRunner->addConstraint(
1963  new UnicodeConstraint( $this->unicodeCheck )
1964  );
1965 
1966  // SimpleAntiSpamConstraint: ensure that the context request does not have
1967  // `wpAntispam` set
1968  $constraintRunner->addConstraint(
1969  $constraintFactory->newSimpleAntiSpamConstraint(
1970  $this->context->getRequest()->getText( 'wpAntispam' ),
1971  $user,
1972  $this->mTitle
1973  )
1974  );
1975 
1976  // SpamRegexConstraint: ensure that the summary and text don't match the spam regex
1977  // FIXME $this->section is documented to always be a string, but it can be null
1978  // since importFormData does not provide a default when getting the section from
1979  // WebRequest, and the default default is null.
1980  $constraintRunner->addConstraint(
1981  $constraintFactory->newSpamRegexConstraint(
1982  $this->summary,
1983  $this->section ?? '',
1984  $this->sectiontitle,
1985  $this->textbox1,
1986  $this->context->getRequest()->getIP(),
1987  $this->mTitle
1988  )
1989  );
1990  $constraintRunner->addConstraint(
1991  new EditRightConstraint( $user )
1992  );
1993  $constraintRunner->addConstraint(
1995  $textbox_content,
1996  $this->mTitle,
1997  $user
1998  )
1999  );
2000  $constraintRunner->addConstraint(
2001  $constraintFactory->newUserBlockConstraint( $this->mTitle, $user )
2002  );
2003  $constraintRunner->addConstraint(
2005  $user,
2006  $this->mTitle,
2007  $this->contentModel
2008  )
2009  );
2010 
2011  $constraintRunner->addConstraint(
2012  $constraintFactory->newReadOnlyConstraint()
2013  );
2014  $constraintRunner->addConstraint(
2015  new UserRateLimitConstraint( $user, $this->mTitle, $this->contentModel )
2016  );
2017  $constraintRunner->addConstraint(
2018  // Same constraint is used to check size before and after merging the
2019  // edits, which use different failure codes
2020  $constraintFactory->newPageSizeConstraint(
2021  $this->contentLength,
2022  PageSizeConstraint::BEFORE_MERGE
2023  )
2024  );
2025  $constraintRunner->addConstraint(
2026  new ChangeTagsConstraint( $user, $this->changeTags )
2027  );
2028 
2029  // If the article has been deleted while editing, don't save it without
2030  // confirmation
2031  $constraintRunner->addConstraint(
2033  $this->wasDeletedSinceLastEdit(),
2034  $this->recreate
2035  )
2036  );
2037 
2038  // Check the constraints
2039  if ( $constraintRunner->checkConstraints() === false ) {
2040  $failed = $constraintRunner->getFailedConstraint();
2041 
2042  // Need to check SpamRegexConstraint here, to avoid needing to pass
2043  // $result by reference again
2044  if ( $failed instanceof SpamRegexConstraint ) {
2045  $result['spam'] = $failed->getMatch();
2046  } else {
2047  $this->handleFailedConstraint( $failed );
2048  }
2049 
2050  return Status::wrap( $failed->getLegacyStatus() );
2051  }
2052  // END OF MIGRATION TO EDITCONSTRAINT SYSTEM (continued below)
2053 
2054  # Load the page data from the primary DB. If anything changes in the meantime,
2055  # we detect it by using page_latest like a token in a 1 try compare-and-swap.
2056  $this->page->loadPageData( 'fromdbmaster' );
2057  $new = !$this->page->exists();
2058 
2059  if ( $new ) {
2060  // BEGINNING OF MIGRATION TO EDITCONSTRAINT SYSTEM (see T157658)
2061  // Create a new runner to avoid rechecking the prior constraints, use the same factory
2062  $constraintRunner = new EditConstraintRunner();
2063  // Late check for create permission, just in case *PARANOIA*
2064  $constraintRunner->addConstraint(
2065  new CreationPermissionConstraint( $user, $this->mTitle )
2066  );
2067 
2068  // Don't save a new page if it's blank or if it's a MediaWiki:
2069  // message with content equivalent to default (allow empty pages
2070  // in this case to disable messages, see T52124)
2071  $constraintRunner->addConstraint(
2073  $this->mTitle,
2074  $this->allowBlankArticle,
2075  $this->textbox1
2076  )
2077  );
2078 
2079  $constraintRunner->addConstraint(
2080  $constraintFactory->newEditFilterMergedContentHookConstraint(
2081  $textbox_content,
2082  $this->context,
2083  $this->summary,
2084  $this->minoredit
2085  )
2086  );
2087 
2088  // Check the constraints
2089  if ( $constraintRunner->checkConstraints() === false ) {
2090  $failed = $constraintRunner->getFailedConstraint();
2091  $this->handleFailedConstraint( $failed );
2092  return Status::wrap( $failed->getLegacyStatus() );
2093  }
2094  // END OF MIGRATION TO EDITCONSTRAINT SYSTEM (continued below)
2095 
2096  $content = $textbox_content;
2097 
2098  $result['sectionanchor'] = '';
2099  if ( $this->section == 'new' ) {
2100  if ( $this->sectiontitle !== '' ) {
2101  // Insert the section title above the content.
2102  $content = $content->addSectionHeader( $this->sectiontitle );
2103  } elseif ( $this->summary !== '' ) {
2104  // Insert the section title above the content.
2105  $content = $content->addSectionHeader( $this->summary );
2106  }
2107 
2108  list( $newSectionSummary, $anchor ) = $this->newSectionSummary();
2109  $this->summary = $newSectionSummary;
2110  $result['sectionanchor'] = $anchor;
2111  }
2112  } else { # not $new
2113 
2114  # Article exists. Check for edit conflict.
2115 
2116  $timestamp = $this->page->getTimestamp();
2117  $latest = $this->page->getLatest();
2118 
2119  wfDebug( "timestamp: {$timestamp}, edittime: {$this->edittime}" );
2120  wfDebug( "revision: {$latest}, editRevId: {$this->editRevId}" );
2121 
2122  $editConflictLogger = LoggerFactory::getInstance( 'EditConflict' );
2123  // An edit conflict is detected if the current revision is different from the
2124  // revision that was current when editing was initiated on the client.
2125  // This is checked based on the timestamp and revision ID.
2126  // TODO: the timestamp based check can probably go away now.
2127  if ( ( $this->edittime !== null && $this->edittime != $timestamp )
2128  || ( $this->editRevId !== null && $this->editRevId != $latest )
2129  ) {
2130  $this->isConflict = true;
2131  list( $newSectionSummary, $newSectionAnchor ) = $this->newSectionSummary();
2132  if ( $this->section == 'new' ) {
2133  if ( $this->page->getUserText() == $user->getName() &&
2134  $this->page->getComment() == $newSectionSummary
2135  ) {
2136  // Probably a duplicate submission of a new comment.
2137  // This can happen when CDN resends a request after
2138  // a timeout but the first one actually went through.
2139  $editConflictLogger->debug(
2140  'Duplicate new section submission; trigger edit conflict!'
2141  );
2142  } else {
2143  // New comment; suppress conflict.
2144  $this->isConflict = false;
2145  $editConflictLogger->debug( 'Conflict suppressed; new section' );
2146  }
2147  } elseif ( $this->section == ''
2148  && $this->edittime
2149  && $this->revisionStore->userWasLastToEdit(
2150  wfGetDB( DB_PRIMARY ),
2151  $this->mTitle->getArticleID(),
2152  $user->getId(),
2153  $this->edittime
2154  )
2155  ) {
2156  # Suppress edit conflict with self, except for section edits where merging is required.
2157  $editConflictLogger->debug( 'Suppressing edit conflict, same user.' );
2158  $this->isConflict = false;
2159  }
2160  }
2161 
2162  // If sectiontitle is set, use it, otherwise use the summary as the section title.
2163  if ( $this->sectiontitle !== '' ) {
2164  $sectionTitle = $this->sectiontitle;
2165  } else {
2166  $sectionTitle = $this->summary;
2167  }
2168 
2169  $content = null;
2170 
2171  if ( $this->isConflict ) {
2172  $editConflictLogger->debug(
2173  'Conflict! Getting section {section} for time {editTime}'
2174  . ' (id {editRevId}, article time {timestamp})',
2175  [
2176  'section' => $this->section,
2177  'editTime' => $this->edittime,
2178  'editRevId' => $this->editRevId,
2179  'timestamp' => $timestamp,
2180  ]
2181  );
2182  // @TODO: replaceSectionAtRev() with base ID (not prior current) for ?oldid=X case
2183  // ...or disable section editing for non-current revisions (not exposed anyway).
2184  if ( $this->editRevId !== null ) {
2185  $content = $this->page->replaceSectionAtRev(
2186  $this->section,
2187  $textbox_content,
2188  $sectionTitle,
2189  $this->editRevId
2190  );
2191  } else {
2192  $content = $this->page->replaceSectionContent(
2193  $this->section,
2194  $textbox_content,
2195  $sectionTitle,
2196  $this->edittime
2197  );
2198  }
2199  } else {
2200  $editConflictLogger->debug(
2201  'Getting section {section}',
2202  [ 'section' => $this->section ]
2203  );
2204  $content = $this->page->replaceSectionContent(
2205  $this->section,
2206  $textbox_content,
2207  $sectionTitle
2208  );
2209  }
2210 
2211  if ( $content === null ) {
2212  $editConflictLogger->debug( 'Activating conflict; section replace failed.' );
2213  $this->isConflict = true;
2214  $content = $textbox_content; // do not try to merge here!
2215  } elseif ( $this->isConflict ) {
2216  // Attempt merge
2217  $mergedChange = $this->mergeChangesIntoContent( $content );
2218  if ( $mergedChange !== false ) {
2219  // Successful merge! Maybe we should tell the user the good news?
2220  $content = $mergedChange[0];
2221  $this->parentRevId = $mergedChange[1];
2222  $this->isConflict = false;
2223  $editConflictLogger->debug( 'Suppressing edit conflict, successful merge.' );
2224  } else {
2225  $this->section = '';
2226  $this->textbox1 = ContentHandler::getContentText( $content );
2227  $editConflictLogger->debug( 'Keeping edit conflict, failed merge.' );
2228  }
2229  }
2230 
2231  if ( $this->isConflict ) {
2232  $status = Status::newGood( self::AS_CONFLICT_DETECTED );
2233  $status->setOK( false );
2234  return $status;
2235  }
2236 
2237  // BEGINNING OF MIGRATION TO EDITCONSTRAINT SYSTEM (see T157658)
2238  // Create a new runner to avoid rechecking the prior constraints, use the same factory
2239  $constraintRunner = new EditConstraintRunner();
2240  $constraintRunner->addConstraint(
2241  $constraintFactory->newEditFilterMergedContentHookConstraint(
2242  $content,
2243  $this->context,
2244  $this->summary,
2245  $this->minoredit
2246  )
2247  );
2248 
2249  if ( $this->section == 'new' ) {
2250  $constraintRunner->addConstraint(
2252  $this->summary,
2253  $this->allowBlankSummary
2254  )
2255  );
2256  $constraintRunner->addConstraint(
2257  new MissingCommentConstraint( $this->textbox1 )
2258  );
2259  } else {
2260  $constraintRunner->addConstraint(
2262  $this->summary,
2263  $this->autoSumm,
2264  $this->allowBlankSummary,
2265  $content,
2266  $this->getOriginalContent( $user )
2267  )
2268  );
2269  }
2270  // Check the constraints
2271  if ( $constraintRunner->checkConstraints() === false ) {
2272  $failed = $constraintRunner->getFailedConstraint();
2273  $this->handleFailedConstraint( $failed );
2274  return Status::wrap( $failed->getLegacyStatus() );
2275  }
2276  // END OF MIGRATION TO EDITCONSTRAINT SYSTEM (continued below)
2277 
2278  # All's well
2279  $sectionAnchor = '';
2280  if ( $this->section == 'new' ) {
2281  list( $newSectionSummary, $anchor ) = $this->newSectionSummary();
2282  $this->summary = $newSectionSummary;
2283  $sectionAnchor = $anchor;
2284  } elseif ( $this->section != '' ) {
2285  # Try to get a section anchor from the section source, redirect
2286  # to edited section if header found.
2287  # XXX: Might be better to integrate this into WikiPage::replaceSectionAtRev
2288  # for duplicate heading checking and maybe parsing.
2289  $hasmatch = preg_match( "/^ *([=]{1,6})(.*?)(\\1) *\\n/i", $this->textbox1, $matches );
2290  # We can't deal with anchors, includes, html etc in the header for now,
2291  # headline would need to be parsed to improve this.
2292  if ( $hasmatch && strlen( $matches[2] ) > 0 ) {
2293  $sectionAnchor = $this->guessSectionName( $matches[2] );
2294  }
2295  }
2296  $result['sectionanchor'] = $sectionAnchor;
2297 
2298  // Save errors may fall down to the edit form, but we've now
2299  // merged the section into full text. Clear the section field
2300  // so that later submission of conflict forms won't try to
2301  // replace that into a duplicated mess.
2302  $this->textbox1 = $this->toEditText( $content );
2303  $this->section = '';
2304  }
2305 
2306  // Check for length errors again now that the section is merged in
2307  $this->contentLength = strlen( $this->toEditText( $content ) );
2308 
2309  // BEGINNING OF MIGRATION TO EDITCONSTRAINT SYSTEM (see T157658)
2310  // Create a new runner to avoid rechecking the prior constraints, use the same factory
2311  $constraintRunner = new EditConstraintRunner();
2312  $constraintRunner->addConstraint(
2314  $this->allowSelfRedirect,
2315  $content,
2316  $this->getCurrentContent(),
2317  $this->getTitle()
2318  )
2319  );
2320  $constraintRunner->addConstraint(
2321  // Same constraint is used to check size before and after merging the
2322  // edits, which use different failure codes
2323  $constraintFactory->newPageSizeConstraint(
2324  $this->contentLength,
2325  PageSizeConstraint::AFTER_MERGE
2326  )
2327  );
2328  // Check the constraints
2329  if ( $constraintRunner->checkConstraints() === false ) {
2330  $failed = $constraintRunner->getFailedConstraint();
2331  $this->handleFailedConstraint( $failed );
2332  return Status::wrap( $failed->getLegacyStatus() );
2333  }
2334  // END OF MIGRATION TO EDITCONSTRAINT SYSTEM
2335 
2336  $flags = EDIT_AUTOSUMMARY |
2337  ( $new ? EDIT_NEW : EDIT_UPDATE ) |
2338  ( ( $this->minoredit && !$this->isNew ) ? EDIT_MINOR : 0 ) |
2339  ( $markAsBot ? EDIT_FORCE_BOT : 0 );
2340 
2341  $isUndo = false;
2342  if ( $this->undidRev ) {
2343  // As the user can change the edit's content before saving, we only mark
2344  // "clean" undos as reverts. This is to avoid abuse by marking irrelevant
2345  // edits as undos.
2346  $isUndo = $this->isUndoClean( $content );
2347  }
2348 
2349  $doEditStatus = $this->page->doEditContent(
2350  $content,
2351  $this->summary,
2352  $flags,
2353  $isUndo && $this->undoAfter ? $this->undoAfter : false,
2354  $user,
2355  $content->getDefaultFormat(),
2356  $this->changeTags,
2357  $isUndo ? $this->undidRev : 0
2358  );
2359 
2360  if ( !$doEditStatus->isOK() ) {
2361  // Failure from doEdit()
2362  // Show the edit conflict page for certain recognized errors from doEdit(),
2363  // but don't show it for errors from extension hooks
2364  $errors = $doEditStatus->getErrorsArray();
2365  if ( in_array( $errors[0][0],
2366  [ 'edit-gone-missing', 'edit-conflict', 'edit-already-exists' ] )
2367  ) {
2368  $this->isConflict = true;
2369  // Destroys data doEdit() put in $status->value but who cares
2370  $doEditStatus->value = self::AS_END;
2371  }
2372  return $doEditStatus;
2373  }
2374 
2375  $result['nullEdit'] = $doEditStatus->hasMessage( 'edit-no-change' );
2376  if ( $result['nullEdit'] ) {
2377  // We don't know if it was a null edit until now, so increment here
2378  $user->pingLimiter( 'linkpurge' );
2379  }
2380  $result['redirect'] = $content->isRedirect();
2381 
2382  $this->updateWatchlist();
2383 
2384  // If the content model changed, add a log entry
2385  if ( $changingContentModel ) {
2387  $user,
2388  $new ? false : $oldContentModel,
2389  $this->contentModel,
2390  $this->summary
2391  );
2392  }
2393 
2394  // Instead of carrying the same status object throughout, it is created right
2395  // when it is returned, either at an earlier point due to an error or here
2396  // due to a successful edit.
2397  $statusCode = ( $new ? self::AS_SUCCESS_NEW_ARTICLE : self::AS_SUCCESS_UPDATE );
2398  $status = Status::newGood( $statusCode );
2399  return $status;
2400  }
2401 
2410  private function handleFailedConstraint( IEditConstraint $failed ) {
2411  if ( $failed instanceof PageSizeConstraint ) {
2412  // Error will be displayed by showEditForm()
2413  $this->tooBig = true;
2414  } elseif ( $failed instanceof UserBlockConstraint ) {
2415  // Auto-block user's IP if the account was "hard" blocked
2416  if ( !wfReadOnly() ) {
2417  $this->context->getUser()->spreadAnyEditBlock();
2418  }
2419  } elseif ( $failed instanceof DefaultTextConstraint ) {
2420  $this->blankArticle = true;
2421  } elseif ( $failed instanceof EditFilterMergedContentHookConstraint ) {
2422  $this->hookError = $failed->getHookError();
2423  } elseif (
2424  $failed instanceof AutoSummaryMissingSummaryConstraint ||
2425  $failed instanceof NewSectionMissingSummaryConstraint
2426  ) {
2427  $this->missingSummary = true;
2428  } elseif ( $failed instanceof MissingCommentConstraint ) {
2429  $this->missingComment = true;
2430  } elseif ( $failed instanceof SelfRedirectConstraint ) {
2431  $this->selfRedirect = true;
2432  }
2433  }
2434 
2445  private function isUndoClean( Content $content ) : bool {
2446  // Check whether the undo was "clean", that is the user has not modified
2447  // the automatically generated content.
2448  $undoRev = $this->revisionStore->getRevisionById( $this->undidRev );
2449  if ( $undoRev === null ) {
2450  return false;
2451  }
2452 
2453  if ( $this->undoAfter ) {
2454  $oldRev = $this->revisionStore->getRevisionById( $this->undoAfter );
2455  } else {
2456  $oldRev = $this->revisionStore->getPreviousRevision( $undoRev );
2457  }
2458 
2459  // Sanity checks
2460  if ( $oldRev === null ||
2461  $undoRev->isDeleted( RevisionRecord::DELETED_TEXT ) ||
2462  $oldRev->isDeleted( RevisionRecord::DELETED_TEXT )
2463  ) {
2464  return false;
2465  }
2466 
2467  $undoContent = $this->getUndoContent( $undoRev, $oldRev );
2468  if ( !$undoContent ) {
2469  return false;
2470  }
2471 
2472  // Do a pre-save transform on the retrieved undo content
2473  $contentLanguage = MediaWikiServices::getInstance()->getContentLanguage();
2474  $user = $this->context->getUser();
2475  $parserOptions = ParserOptions::newFromUserAndLang( $user, $contentLanguage );
2476  $undoContent = $undoContent->preSaveTransform( $this->mTitle, $user, $parserOptions );
2477 
2478  if ( $undoContent->equals( $content ) ) {
2479  return true;
2480  }
2481  return false;
2482  }
2483 
2490  protected function addContentModelChangeLogEntry( UserIdentity $user, $oldModel, $newModel, $reason ) {
2491  $new = $oldModel === false;
2492  $log = new ManualLogEntry( 'contentmodel', $new ? 'new' : 'change' );
2493  $log->setPerformer( $user );
2494  $log->setTarget( $this->mTitle );
2495  $log->setComment( $reason );
2496  $log->setParameters( [
2497  '4::oldmodel' => $oldModel,
2498  '5::newmodel' => $newModel
2499  ] );
2500  $logid = $log->insert();
2501  $log->publish( $logid );
2502  }
2503 
2507  protected function updateWatchlist() {
2508  $performer = $this->context->getAuthority();
2509  if ( !$performer->getUser()->isRegistered() ) {
2510  return;
2511  }
2512 
2514  $watch = $this->watchthis;
2516 
2517  // This can't run as a DeferredUpdate due to a possible race condition
2518  // when the post-edit redirect happens if the pendingUpdates queue is
2519  // too large to finish in time (T259564)
2520  $this->watchlistManager->setWatch( $watch, $performer, $title, $watchlistExpiry );
2521 
2522  $this->watchedItemStore->maybeEnqueueWatchlistExpiryJob();
2523  }
2524 
2537  private function mergeChangesIntoContent( $editContent ) {
2538  // This is the revision that was current at the time editing was initiated on the client,
2539  // even if the edit was based on an old revision.
2540  $baseRevRecord = $this->getExpectedParentRevision();
2541  $baseContent = $baseRevRecord ?
2542  $baseRevRecord->getContent( SlotRecord::MAIN ) :
2543  null;
2544 
2545  if ( $baseContent === null ) {
2546  return false;
2547  }
2548 
2549  // The current state, we want to merge updates into it
2550  $currentRevisionRecord = $this->revisionStore->getRevisionByTitle(
2551  $this->mTitle,
2552  0,
2553  RevisionStore::READ_LATEST
2554  );
2555  $currentContent = $currentRevisionRecord
2556  ? $currentRevisionRecord->getContent( SlotRecord::MAIN )
2557  : null;
2558 
2559  if ( $currentContent === null ) {
2560  return false;
2561  }
2562 
2563  $mergedContent = $this->contentHandlerFactory
2564  ->getContentHandler( $baseContent->getModel() )
2565  ->merge3( $baseContent, $editContent, $currentContent );
2566 
2567  if ( $mergedContent ) {
2568  // Also need to update parentRevId to what we just merged.
2569  return [ $mergedContent, $currentRevisionRecord->getId() ];
2570  }
2571 
2572  return false;
2573  }
2574 
2582  public function getExpectedParentRevision() {
2583  if ( $this->mExpectedParentRevision === false ) {
2584  $revRecord = null;
2585  if ( $this->editRevId ) {
2586  $revRecord = $this->revisionStore->getRevisionById(
2587  $this->editRevId,
2588  RevisionStore::READ_LATEST
2589  );
2590  } elseif ( $this->edittime ) {
2591  $revRecord = $this->revisionStore->getRevisionByTimestamp(
2592  $this->getTitle(),
2593  $this->edittime,
2594  RevisionStore::READ_LATEST
2595  );
2596  }
2597  $this->mExpectedParentRevision = $revRecord;
2598  }
2600  }
2601 
2602  public function setHeaders() {
2603  $out = $this->context->getOutput();
2604 
2605  $out->addModules( 'mediawiki.action.edit' );
2606  $out->addModuleStyles( 'mediawiki.action.edit.styles' );
2607  $out->addModuleStyles( 'mediawiki.editfont.styles' );
2608 
2609  $user = $this->context->getUser();
2610 
2611  if ( $user->getOption( 'uselivepreview' ) ) {
2612  $out->addModules( 'mediawiki.action.edit.preview' );
2613  }
2614 
2615  if ( $user->getOption( 'useeditwarning' ) ) {
2616  $out->addModules( 'mediawiki.action.edit.editWarning' );
2617  }
2618 
2619  # Enabled article-related sidebar, toplinks, etc.
2620  $out->setArticleRelated( true );
2621 
2622  $contextTitle = $this->getContextTitle();
2623  if ( $this->isConflict ) {
2624  $msg = 'editconflict';
2625  } elseif ( $contextTitle->exists() && $this->section != '' ) {
2626  $msg = $this->section == 'new' ? 'editingcomment' : 'editingsection';
2627  } else {
2628  $msg = $contextTitle->exists()
2629  || ( $contextTitle->getNamespace() === NS_MEDIAWIKI
2630  && $contextTitle->getDefaultMessageText() !== false
2631  )
2632  ? 'editing'
2633  : 'creating';
2634  }
2635 
2636  # Use the title defined by DISPLAYTITLE magic word when present
2637  # NOTE: getDisplayTitle() returns HTML while getPrefixedText() returns plain text.
2638  # setPageTitle() treats the input as wikitext, which should be safe in either case.
2639  $displayTitle = isset( $this->mParserOutput ) ? $this->mParserOutput->getDisplayTitle() : false;
2640  if ( $displayTitle === false ) {
2641  $displayTitle = $contextTitle->getPrefixedText();
2642  } else {
2643  $out->setDisplayTitle( $displayTitle );
2644  }
2645  $out->setPageTitle( $this->context->msg( $msg, $displayTitle ) );
2646 
2647  $config = $this->context->getConfig();
2648 
2649  # Transmit the name of the message to JavaScript for live preview
2650  # Keep Resources.php/mediawiki.action.edit.preview in sync with the possible keys
2651  $out->addJsConfigVars( [
2652  'wgEditMessage' => $msg,
2653  'wgAjaxEditStash' => $config->get( 'AjaxEditStash' ),
2654  ] );
2655 
2656  // Add whether to use 'save' or 'publish' messages to JavaScript for post-edit, other
2657  // editors, etc.
2658  $out->addJsConfigVars(
2659  'wgEditSubmitButtonLabelPublish',
2660  $config->get( 'EditSubmitButtonLabelPublish' )
2661  );
2662  }
2663 
2667  protected function showIntro() {
2668  if ( $this->suppressIntro ) {
2669  return;
2670  }
2671 
2672  $out = $this->context->getOutput();
2673  $namespace = $this->mTitle->getNamespace();
2674 
2675  if ( $namespace === NS_MEDIAWIKI ) {
2676  # Show a warning if editing an interface message
2677  $out->wrapWikiMsg( "<div class='mw-editinginterface'>\n$1\n</div>", 'editinginterface' );
2678  # If this is a default message (but not css, json, or js),
2679  # show a hint that it is translatable on translatewiki.net
2680  if (
2681  !$this->mTitle->hasContentModel( CONTENT_MODEL_CSS )
2682  && !$this->mTitle->hasContentModel( CONTENT_MODEL_JSON )
2683  && !$this->mTitle->hasContentModel( CONTENT_MODEL_JAVASCRIPT )
2684  ) {
2685  $defaultMessageText = $this->mTitle->getDefaultMessageText();
2686  if ( $defaultMessageText !== false ) {
2687  $out->wrapWikiMsg( "<div class='mw-translateinterface'>\n$1\n</div>",
2688  'translateinterface' );
2689  }
2690  }
2691  } elseif ( $namespace === NS_FILE ) {
2692  # Show a hint to shared repo
2693  $file = MediaWikiServices::getInstance()->getRepoGroup()->findFile( $this->mTitle );
2694  if ( $file && !$file->isLocal() ) {
2695  $descUrl = $file->getDescriptionUrl();
2696  # there must be a description url to show a hint to shared repo
2697  if ( $descUrl ) {
2698  if ( !$this->mTitle->exists() ) {
2699  $out->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-create\">\n$1\n</div>", [
2700  'sharedupload-desc-create', $file->getRepo()->getDisplayName(), $descUrl
2701  ] );
2702  } else {
2703  $out->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-edit\">\n$1\n</div>", [
2704  'sharedupload-desc-edit', $file->getRepo()->getDisplayName(), $descUrl
2705  ] );
2706  }
2707  }
2708  }
2709  }
2710 
2711  # Show a warning message when someone creates/edits a user (talk) page but the user does not exist
2712  # Show log extract when the user is currently blocked
2713  if ( $namespace === NS_USER || $namespace === NS_USER_TALK ) {
2714  $username = explode( '/', $this->mTitle->getText(), 2 )[0];
2715  $user = User::newFromName( $username, false /* allow IP users */ );
2716  $ip = $this->userNameUtils->isIP( $username );
2717  $block = DatabaseBlock::newFromTarget( $user, $user );
2718 
2719  $userExists = ( $user && $user->isRegistered() );
2720  if ( $userExists && $user->isHidden() &&
2721  !$this->permManager->userHasRight( $this->context->getUser(), 'hideuser' )
2722  ) {
2723  // If the user exists, but is hidden, and the viewer cannot see hidden
2724  // users, pretend like they don't exist at all. See T120883
2725  $userExists = false;
2726  }
2727 
2728  if ( !$userExists && !$ip ) { # User does not exist
2729  $out->wrapWikiMsg( "<div class=\"mw-userpage-userdoesnotexist error\">\n$1\n</div>",
2730  [ 'userpage-userdoesnotexist', wfEscapeWikiText( $username ) ] );
2731  } elseif (
2732  $block !== null &&
2733  $block->getType() != DatabaseBlock::TYPE_AUTO &&
2734  ( $block->isSitewide() || $user->isBlockedFrom( $this->mTitle ) )
2735  ) {
2736  // Show log extract if the user is sitewide blocked or is partially
2737  // blocked and not allowed to edit their user page or user talk page
2739  $out,
2740  'block',
2741  MediaWikiServices::getInstance()->getNamespaceInfo()->
2742  getCanonicalName( NS_USER ) . ':' . $block->getTargetName(),
2743  '',
2744  [
2745  'lim' => 1,
2746  'showIfEmpty' => false,
2747  'msgKey' => [
2748  'blocked-notice-logextract',
2749  $user->getName() # Support GENDER in notice
2750  ]
2751  ]
2752  );
2753  }
2754  }
2755  # Try to add a custom edit intro, or use the standard one if this is not possible.
2756  if ( !$this->showCustomIntro() && !$this->mTitle->exists() ) {
2758  $this->context->msg( 'helppage' )->inContentLanguage()->text()
2759  ) );
2760  if ( $this->context->getUser()->isRegistered() ) {
2761  $out->wrapWikiMsg(
2762  // Suppress the external link icon, consider the help url an internal one
2763  "<div class=\"mw-newarticletext plainlinks\">\n$1\n</div>",
2764  [
2765  'newarticletext',
2766  $helpLink
2767  ]
2768  );
2769  } else {
2770  $out->wrapWikiMsg(
2771  // Suppress the external link icon, consider the help url an internal one
2772  "<div class=\"mw-newarticletextanon plainlinks\">\n$1\n</div>",
2773  [
2774  'newarticletextanon',
2775  $helpLink
2776  ]
2777  );
2778  }
2779  }
2780  # Give a notice if the user is editing a deleted/moved page...
2781  if ( !$this->mTitle->exists() ) {
2782  $dbr = wfGetDB( DB_REPLICA );
2783 
2784  LogEventsList::showLogExtract( $out, [ 'delete', 'move' ], $this->mTitle,
2785  '',
2786  [
2787  'lim' => 10,
2788  'conds' => [ 'log_action != ' . $dbr->addQuotes( 'revision' ) ],
2789  'showIfEmpty' => false,
2790  'msgKey' => [ 'recreate-moveddeleted-warn' ]
2791  ]
2792  );
2793  }
2794  }
2795 
2801  protected function showCustomIntro() {
2802  if ( $this->editintro ) {
2803  $title = Title::newFromText( $this->editintro );
2804  if ( $this->isPageExistingAndViewable( $title, $this->context->getUser() ) ) {
2805  // Added using template syntax, to take <noinclude>'s into account.
2806  $this->context->getOutput()->addWikiTextAsContent(
2807  '<div class="mw-editintro">{{:' . $title->getFullText() . '}}</div>',
2808  /*linestart*/true,
2809  $this->mTitle
2810  );
2811  return true;
2812  }
2813  }
2814  return false;
2815  }
2816 
2835  protected function toEditText( $content ) {
2836  if ( $content === null || $content === false || is_string( $content ) ) {
2837  return $content;
2838  }
2839 
2840  if ( !$this->isSupportedContentModel( $content->getModel() ) ) {
2841  throw new MWException( 'This content model is not supported: ' . $content->getModel() );
2842  }
2843 
2844  return $content->serialize( $this->contentFormat );
2845  }
2846 
2863  protected function toEditContent( $text ) {
2864  if ( $text === false || $text === null ) {
2865  return $text;
2866  }
2867 
2868  $content = ContentHandler::makeContent( $text, $this->getTitle(),
2869  $this->contentModel, $this->contentFormat );
2870 
2871  if ( !$this->isSupportedContentModel( $content->getModel() ) ) {
2872  throw new MWException( 'This content model is not supported: ' . $content->getModel() );
2873  }
2874 
2875  return $content;
2876  }
2877 
2886  public function showEditForm( $formCallback = null ) {
2887  # need to parse the preview early so that we know which templates are used,
2888  # otherwise users with "show preview after edit box" will get a blank list
2889  # we parse this near the beginning so that setHeaders can do the title
2890  # setting work instead of leaving it in getPreviewText
2891  $previewOutput = '';
2892  if ( $this->formtype == 'preview' ) {
2893  $previewOutput = $this->getPreviewText();
2894  }
2895 
2896  $out = $this->context->getOutput();
2897 
2898  $this->getHookRunner()->onEditPage__showEditForm_initial( $this, $out );
2899 
2900  $this->setHeaders();
2901 
2902  $this->addTalkPageText();
2903  $this->addEditNotices();
2904 
2905  if ( !$this->isConflict &&
2906  $this->section != '' &&
2907  !$this->isSectionEditSupported() ) {
2908  // We use $this->section to much before this and getVal('wgSection') directly in other places
2909  // at this point we can't reset $this->section to '' to fallback to non-section editing.
2910  // Someone is welcome to try refactoring though
2911  $out->showErrorPage( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
2912  return;
2913  }
2914 
2915  $this->showHeader();
2916 
2917  $out->addHTML( $this->editFormPageTop );
2918 
2919  $user = $this->context->getUser();
2920  if ( $user->getOption( 'previewontop' ) ) {
2921  $this->displayPreviewArea( $previewOutput, true );
2922  }
2923 
2924  $out->addHTML( $this->editFormTextTop );
2925 
2926  if ( $this->wasDeletedSinceLastEdit() && $this->formtype !== 'save' ) {
2927  $out->addHTML( Html::errorBox(
2928  $out->msg( 'deletedwhileediting' )->parse(),
2929  '',
2930  'mw-deleted-while-editing'
2931  ) );
2932  }
2933 
2934  // @todo add EditForm plugin interface and use it here!
2935  // search for textarea1 and textarea2, and allow EditForm to override all uses.
2936  $out->addHTML( Html::openElement(
2937  'form',
2938  [
2939  'class' => 'mw-editform',
2940  'id' => self::EDITFORM_ID,
2941  'name' => self::EDITFORM_ID,
2942  'method' => 'post',
2943  'action' => $this->getActionURL( $this->getContextTitle() ),
2944  'enctype' => 'multipart/form-data'
2945  ]
2946  ) );
2947 
2948  if ( is_callable( $formCallback ) ) {
2949  wfWarn( 'The $formCallback parameter to ' . __METHOD__ . 'is deprecated' );
2950  call_user_func_array( $formCallback, [ &$out ] );
2951  }
2952 
2953  // Add a check for Unicode support
2954  $out->addHTML( Html::hidden( 'wpUnicodeCheck', self::UNICODE_CHECK ) );
2955 
2956  // Add an empty field to trip up spambots
2957  $out->addHTML(
2958  Xml::openElement( 'div', [ 'id' => 'antispam-container', 'style' => 'display: none;' ] )
2959  . Html::rawElement(
2960  'label',
2961  [ 'for' => 'wpAntispam' ],
2962  $this->context->msg( 'simpleantispam-label' )->parse()
2963  )
2964  . Xml::element(
2965  'input',
2966  [
2967  'type' => 'text',
2968  'name' => 'wpAntispam',
2969  'id' => 'wpAntispam',
2970  'value' => ''
2971  ]
2972  )
2973  . Xml::closeElement( 'div' )
2974  );
2975 
2976  $this->getHookRunner()->onEditPage__showEditForm_fields( $this, $out );
2977 
2978  // Put these up at the top to ensure they aren't lost on early form submission
2979  $this->showFormBeforeText();
2980 
2981  if ( $this->wasDeletedSinceLastEdit() && $this->formtype == 'save' ) {
2982  $username = $this->lastDelete->actor_name;
2983  $comment = CommentStore::getStore()
2984  ->getComment( 'log_comment', $this->lastDelete )->text;
2985 
2986  // It is better to not parse the comment at all than to have templates expanded in the middle
2987  // TODO: can the checkLabel be moved outside of the div so that wrapWikiMsg could be used?
2988  $key = $comment === ''
2989  ? 'confirmrecreate-noreason'
2990  : 'confirmrecreate';
2991  $out->addHTML(
2992  '<div class="mw-confirm-recreate">' .
2993  $this->context->msg( $key, $username, "<nowiki>$comment</nowiki>" )->parse() .
2994  Xml::checkLabel( $this->context->msg( 'recreate' )->text(), 'wpRecreate', 'wpRecreate', false,
2995  [ 'title' => Linker::titleAttrib( 'recreate' ), 'tabindex' => 1, 'id' => 'wpRecreate' ]
2996  ) .
2997  '</div>'
2998  );
2999  }
3000 
3001  # When the summary is hidden, also hide them on preview/show changes
3002  if ( $this->nosummary ) {
3003  $out->addHTML( Html::hidden( 'nosummary', true ) );
3004  }
3005 
3006  # If a blank edit summary was previously provided, and the appropriate
3007  # user preference is active, pass a hidden tag as wpIgnoreBlankSummary. This will stop the
3008  # user being bounced back more than once in the event that a summary
3009  # is not required.
3010  # ####
3011  # For a bit more sophisticated detection of blank summaries, hash the
3012  # automatic one and pass that in the hidden field wpAutoSummary.
3013  if (
3014  $this->missingSummary ||
3015  ( $this->section == 'new' && $this->nosummary ) ||
3016  $this->allowBlankSummary
3017  ) {
3018  $out->addHTML( Html::hidden( 'wpIgnoreBlankSummary', true ) );
3019  }
3020 
3021  if ( $this->undidRev ) {
3022  $out->addHTML( Html::hidden( 'wpUndidRevision', $this->undidRev ) );
3023  }
3024  if ( $this->undoAfter ) {
3025  $out->addHTML( Html::hidden( 'wpUndoAfter', $this->undoAfter ) );
3026  }
3027 
3028  if ( $this->selfRedirect ) {
3029  $out->addHTML( Html::hidden( 'wpIgnoreSelfRedirect', true ) );
3030  }
3031 
3032  if ( $this->hasPresetSummary ) {
3033  // If a summary has been preset using &summary= we don't want to prompt for
3034  // a different summary. Only prompt for a summary if the summary is blanked.
3035  // (T19416)
3036  $this->autoSumm = md5( '' );
3037  }
3038 
3039  $autosumm = $this->autoSumm !== '' ? $this->autoSumm : md5( $this->summary );
3040  $out->addHTML( Html::hidden( 'wpAutoSummary', $autosumm ) );
3041 
3042  $out->addHTML( Html::hidden( 'oldid', $this->oldid ) );
3043  $out->addHTML( Html::hidden( 'parentRevId', $this->getParentRevId() ) );
3044 
3045  $out->addHTML( Html::hidden( 'format', $this->contentFormat ) );
3046  $out->addHTML( Html::hidden( 'model', $this->contentModel ) );
3047 
3048  $out->enableOOUI();
3049 
3050  if ( $this->section == 'new' ) {
3051  $this->showSummaryInput( true, $this->summary );
3052  $out->addHTML( $this->getSummaryPreview( true, $this->summary ) );
3053  }
3054 
3055  $out->addHTML( $this->editFormTextBeforeContent );
3056  if ( $this->isConflict ) {
3057  // In an edit conflict, we turn textbox2 into the user's text,
3058  // and textbox1 into the stored version
3059  $this->textbox2 = $this->textbox1;
3060 
3061  $content = $this->getCurrentContent();
3062  $this->textbox1 = $this->toEditText( $content );
3063 
3065  $editConflictHelper->setTextboxes( $this->textbox2, $this->textbox1 );
3066  $editConflictHelper->setContentModel( $this->contentModel );
3067  $editConflictHelper->setContentFormat( $this->contentFormat );
3069  }
3070 
3071  if ( !$this->mTitle->isUserConfigPage() ) {
3072  $out->addHTML( self::getEditToolbar() );
3073  }
3074 
3075  if ( $this->blankArticle ) {
3076  $out->addHTML( Html::hidden( 'wpIgnoreBlankArticle', true ) );
3077  }
3078 
3079  if ( $this->isConflict ) {
3080  // In an edit conflict bypass the overridable content form method
3081  // and fallback to the raw wpTextbox1 since editconflicts can't be
3082  // resolved between page source edits and custom ui edits using the
3083  // custom edit ui.
3084  $conflictTextBoxAttribs = [];
3085  if ( $this->wasDeletedSinceLastEdit() ) {
3086  $conflictTextBoxAttribs['style'] = 'display:none;';
3087  } elseif ( $this->isOldRev ) {
3088  $conflictTextBoxAttribs['class'] = 'mw-textarea-oldrev';
3089  }
3090 
3091  $out->addHTML( $editConflictHelper->getEditConflictMainTextBox( $conflictTextBoxAttribs ) );
3093  } else {
3094  $this->showContentForm();
3095  }
3096 
3097  $out->addHTML( $this->editFormTextAfterContent );
3098 
3099  $this->showStandardInputs();
3100 
3101  $this->showFormAfterText();
3102 
3103  $this->showTosSummary();
3104 
3105  $this->showEditTools();
3106 
3107  $out->addHTML( $this->editFormTextAfterTools . "\n" );
3108 
3109  $out->addHTML( $this->makeTemplatesOnThisPageList( $this->getTemplates() ) );
3110 
3111  $out->addHTML( Html::rawElement( 'div', [ 'class' => 'hiddencats' ],
3112  Linker::formatHiddenCategories( $this->page->getHiddenCategories() ) ) );
3113 
3114  $out->addHTML( Html::rawElement( 'div', [ 'class' => 'limitreport' ],
3115  self::getPreviewLimitReport( $this->mParserOutput ) ) );
3116 
3117  $out->addModules( 'mediawiki.action.edit.collapsibleFooter' );
3118 
3119  if ( $this->isConflict ) {
3120  try {
3121  $this->showConflict();
3122  } catch ( MWContentSerializationException $ex ) {
3123  // this can't really happen, but be nice if it does.
3124  $msg = $this->context->msg(
3125  'content-failed-to-parse',
3126  $this->contentModel,
3127  $this->contentFormat,
3128  $ex->getMessage()
3129  );
3130  $out->wrapWikiTextAsInterface( 'error', $msg->plain() );
3131  }
3132  }
3133 
3134  // Set a hidden field so JS knows what edit form mode we are in
3135  if ( $this->isConflict ) {
3136  $mode = 'conflict';
3137  } elseif ( $this->preview ) {
3138  $mode = 'preview';
3139  } elseif ( $this->diff ) {
3140  $mode = 'diff';
3141  } else {
3142  $mode = 'text';
3143  }
3144  $out->addHTML( Html::hidden( 'mode', $mode, [ 'id' => 'mw-edit-mode' ] ) );
3145 
3146  // Marker for detecting truncated form data. This must be the last
3147  // parameter sent in order to be of use, so do not move me.
3148  $out->addHTML( Html::hidden( 'wpUltimateParam', true ) );
3149  $out->addHTML( $this->editFormTextBottom . "\n</form>\n" );
3150 
3151  if ( !$user->getOption( 'previewontop' ) ) {
3152  $this->displayPreviewArea( $previewOutput, false );
3153  }
3154  }
3155 
3163  public function makeTemplatesOnThisPageList( array $templates ) {
3164  $templateListFormatter = new TemplatesOnThisPageFormatter(
3165  $this->context, MediaWikiServices::getInstance()->getLinkRenderer()
3166  );
3167 
3168  // preview if preview, else section if section, else false
3169  $type = false;
3170  if ( $this->preview ) {
3171  $type = 'preview';
3172  } elseif ( $this->section != '' ) {
3173  $type = 'section';
3174  }
3175 
3176  return Html::rawElement( 'div', [ 'class' => 'templatesUsed' ],
3177  $templateListFormatter->format( $templates, $type )
3178  );
3179  }
3180 
3187  private static function extractSectionTitle( $text ) {
3188  if ( preg_match( "/^(=+)(.+)\\1\\s*(\n|$)/i", $text, $matches ) ) {
3189  return MediaWikiServices::getInstance()->getParser()
3190  ->stripSectionName( trim( $matches[2] ) );
3191  } else {
3192  return false;
3193  }
3194  }
3195 
3196  protected function showHeader() {
3197  $out = $this->context->getOutput();
3198  $user = $this->context->getUser();
3199  if ( $this->isConflict ) {
3200  $this->addExplainConflictHeader( $out );
3201  $this->editRevId = $this->page->getLatest();
3202  } else {
3203  if ( $this->section != '' && $this->section != 'new' && !$this->summary &&
3204  !$this->preview && !$this->diff
3205  ) {
3206  $sectionTitle = self::extractSectionTitle( $this->textbox1 ); // FIXME: use Content object
3207  if ( $sectionTitle !== false ) {
3208  $this->summary = "/* $sectionTitle */ ";
3209  }
3210  }
3211 
3212  $buttonLabel = $this->context->msg( $this->getSubmitButtonLabel() )->text();
3213 
3214  if ( $this->missingComment ) {
3215  $out->wrapWikiMsg( "<div id='mw-missingcommenttext'>\n$1\n</div>", 'missingcommenttext' );
3216  }
3217 
3218  if ( $this->missingSummary && $this->section != 'new' ) {
3219  $out->wrapWikiMsg(
3220  "<div id='mw-missingsummary'>\n$1\n</div>",
3221  [ 'missingsummary', $buttonLabel ]
3222  );
3223  }
3224 
3225  if ( $this->missingSummary && $this->section == 'new' ) {
3226  $out->wrapWikiMsg(
3227  "<div id='mw-missingcommentheader'>\n$1\n</div>",
3228  [ 'missingcommentheader', $buttonLabel ]
3229  );
3230  }
3231 
3232  if ( $this->blankArticle ) {
3233  $out->wrapWikiMsg(
3234  "<div id='mw-blankarticle'>\n$1\n</div>",
3235  [ 'blankarticle', $buttonLabel ]
3236  );
3237  }
3238 
3239  if ( $this->selfRedirect ) {
3240  $out->wrapWikiMsg(
3241  "<div id='mw-selfredirect'>\n$1\n</div>",
3242  [ 'selfredirect', $buttonLabel ]
3243  );
3244  }
3245 
3246  if ( $this->hookError !== '' ) {
3247  $out->addWikiTextAsInterface( $this->hookError );
3248  }
3249 
3250  if ( $this->section != 'new' ) {
3251  $revRecord = $this->mArticle->fetchRevisionRecord();
3252  if ( $revRecord && $revRecord instanceof RevisionStoreRecord ) {
3253  // Let sysop know that this will make private content public if saved
3254 
3255  if ( !$revRecord->userCan( RevisionRecord::DELETED_TEXT, $user ) ) {
3256  $out->addHtml(
3258  $out->msg( 'rev-deleted-text-permission', $this->mTitle->getPrefixedDBkey() )->parse(),
3259  'plainlinks'
3260  )
3261  );
3262  } elseif ( $revRecord->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
3263  $out->addHtml(
3265  // title used in wikilinks, should not contain whitespaces
3266  $out->msg( 'rev-deleted-text-view', $this->mTitle->getPrefixedDBkey() )->parse(),
3267  'plainlinks'
3268  )
3269  );
3270  }
3271 
3272  if ( !$revRecord->isCurrent() ) {
3273  $this->mArticle->setOldSubtitle( $revRecord->getId() );
3274  $out->wrapWikiMsg(
3275  Html::warningBox( "\n$1\n" ),
3276  'editingold'
3277  );
3278  $this->isOldRev = true;
3279  }
3280  } elseif ( $this->mTitle->exists() ) {
3281  // Something went wrong
3282 
3283  $out->wrapWikiMsg( "<div class='errorbox'>\n$1\n</div>\n",
3284  [ 'missing-revision', $this->oldid ] );
3285  }
3286  }
3287  }
3288 
3289  if ( wfReadOnly() ) {
3290  $out->wrapWikiMsg(
3291  "<div id=\"mw-read-only-warning\">\n$1\n</div>",
3292  [ 'readonlywarning', wfReadOnlyReason() ]
3293  );
3294  } elseif ( $user->isAnon() ) {
3295  if ( $this->formtype != 'preview' ) {
3296  $returntoquery = array_diff_key(
3297  $this->context->getRequest()->getValues(),
3298  [ 'title' => true, 'returnto' => true, 'returntoquery' => true ]
3299  );
3300  $out->wrapWikiMsg(
3301  "<div id='mw-anon-edit-warning' class='warningbox'>\n$1\n</div>",
3302  [ 'anoneditwarning',
3303  // Log-in link
3304  SpecialPage::getTitleFor( 'Userlogin' )->getFullURL( [
3305  'returnto' => $this->getTitle()->getPrefixedDBkey(),
3306  'returntoquery' => wfArrayToCgi( $returntoquery ),
3307  ] ),
3308  // Sign-up link
3309  SpecialPage::getTitleFor( 'CreateAccount' )->getFullURL( [
3310  'returnto' => $this->getTitle()->getPrefixedDBkey(),
3311  'returntoquery' => wfArrayToCgi( $returntoquery ),
3312  ] )
3313  ]
3314  );
3315  } else {
3316  $out->wrapWikiMsg( "<div id=\"mw-anon-preview-warning\" class=\"warningbox\">\n$1</div>",
3317  'anonpreviewwarning'
3318  );
3319  }
3320  } elseif ( $this->mTitle->isUserConfigPage() ) {
3321  # Check the skin exists
3322  if ( $this->isWrongCaseUserConfigPage() ) {
3323  $out->wrapWikiMsg(
3324  "<div class='errorbox' id='mw-userinvalidconfigtitle'>\n$1\n</div>",
3325  [ 'userinvalidconfigtitle', $this->mTitle->getSkinFromConfigSubpage() ]
3326  );
3327  }
3328  if ( $this->getTitle()->isSubpageOf( $user->getUserPage() ) ) {
3329  $isUserCssConfig = $this->mTitle->isUserCssConfigPage();
3330  $isUserJsonConfig = $this->mTitle->isUserJsonConfigPage();
3331  $isUserJsConfig = $this->mTitle->isUserJsConfigPage();
3332 
3333  $warning = $isUserCssConfig
3334  ? 'usercssispublic'
3335  : ( $isUserJsonConfig ? 'userjsonispublic' : 'userjsispublic' );
3336 
3337  $out->wrapWikiMsg( '<div class="mw-userconfigpublic">$1</div>', $warning );
3338 
3339  if ( $isUserJsConfig ) {
3340  $out->wrapWikiMsg( '<div class="mw-userconfigdangerous">$1</div>', 'userjsdangerous' );
3341  }
3342 
3343  if ( $this->formtype !== 'preview' ) {
3344  $config = $this->context->getConfig();
3345  if ( $isUserCssConfig && $config->get( 'AllowUserCss' ) ) {
3346  $out->wrapWikiMsg(
3347  "<div id='mw-usercssyoucanpreview'>\n$1\n</div>",
3348  [ 'usercssyoucanpreview' ]
3349  );
3350  } elseif ( $isUserJsonConfig /* No comparable 'AllowUserJson' */ ) {
3351  $out->wrapWikiMsg(
3352  "<div id='mw-userjsonyoucanpreview'>\n$1\n</div>",
3353  [ 'userjsonyoucanpreview' ]
3354  );
3355  } elseif ( $isUserJsConfig && $config->get( 'AllowUserJs' ) ) {
3356  $out->wrapWikiMsg(
3357  "<div id='mw-userjsyoucanpreview'>\n$1\n</div>",
3358  [ 'userjsyoucanpreview' ]
3359  );
3360  }
3361  }
3362  }
3363  }
3364 
3366 
3367  $this->addLongPageWarningHeader();
3368 
3369  # Add header copyright warning
3370  $this->showHeaderCopyrightWarning();
3371  }
3372 
3380  private function getSummaryInputAttributes( array $inputAttrs = null ) {
3381  // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
3382  // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
3383  // Unicode codepoints.
3384  return ( is_array( $inputAttrs ) ? $inputAttrs : [] ) + [
3385  'id' => 'wpSummary',
3386  'name' => 'wpSummary',
3388  'tabindex' => 1,
3389  'size' => 60,
3390  'spellcheck' => 'true',
3391  ];
3392  }
3393 
3403  private function getSummaryInputWidget( $summary = "", $labelText = null, $inputAttrs = null ) {
3404  $inputAttrs = OOUI\Element::configFromHtmlAttributes(
3405  $this->getSummaryInputAttributes( $inputAttrs )
3406  );
3407  $inputAttrs += [
3408  'title' => Linker::titleAttrib( 'summary' ),
3409  'accessKey' => Linker::accesskey( 'summary' ),
3410  ];
3411 
3412  // For compatibility with old scripts and extensions, we want the legacy 'id' on the `<input>`
3413  $inputAttrs['inputId'] = $inputAttrs['id'];
3414  $inputAttrs['id'] = 'wpSummaryWidget';
3415 
3416  return new OOUI\FieldLayout(
3417  new OOUI\TextInputWidget( [
3418  'value' => $summary,
3419  'infusable' => true,
3420  ] + $inputAttrs ),
3421  [
3422  'label' => new OOUI\HtmlSnippet( $labelText ),
3423  'align' => 'top',
3424  'id' => 'wpSummaryLabel',
3425  'classes' => [ $this->missingSummary ? 'mw-summarymissed' : 'mw-summary' ],
3426  ]
3427  );
3428  }
3429 
3436  protected function showSummaryInput( $isSubjectPreview, $summary = "" ) {
3437  # Add a class if 'missingsummary' is triggered to allow styling of the summary line
3438  $summaryClass = $this->missingSummary ? 'mw-summarymissed' : 'mw-summary';
3439  if ( $isSubjectPreview ) {
3440  if ( $this->nosummary ) {
3441  return;
3442  }
3443  } elseif ( !$this->mShowSummaryField ) {
3444  return;
3445  }
3446 
3447  $labelText = $this->context->msg( $isSubjectPreview ? 'subject' : 'summary' )->parse();
3448  $this->context->getOutput()->addHTML( $this->getSummaryInputWidget(
3449  $summary,
3450  $labelText,
3451  [ 'class' => $summaryClass ]
3452  ) );
3453  }
3454 
3462  protected function getSummaryPreview( $isSubjectPreview, $summary = "" ) {
3463  // avoid spaces in preview, gets always trimmed on save
3464  $summary = trim( $summary );
3465  if ( !$summary || ( !$this->preview && !$this->diff ) ) {
3466  return "";
3467  }
3468 
3469  if ( $isSubjectPreview ) {
3470  $summary = $this->context->msg( 'newsectionsummary' )
3471  ->rawParams( MediaWikiServices::getInstance()->getParser()
3472  ->stripSectionName( $summary ) )
3473  ->inContentLanguage()->text();
3474  }
3475 
3476  $message = $isSubjectPreview ? 'subject-preview' : 'summary-preview';
3477 
3478  $summary = $this->context->msg( $message )->parse()
3479  . Linker::commentBlock( $summary, $this->mTitle, $isSubjectPreview );
3480  return Xml::tags( 'div', [ 'class' => 'mw-summary-preview' ], $summary );
3481  }
3482 
3483  protected function showFormBeforeText() {
3484  $out = $this->context->getOutput();
3485  $out->addHTML( Html::hidden( 'wpSection', $this->section ) );
3486  $out->addHTML( Html::hidden( 'wpStarttime', $this->starttime ) );
3487  $out->addHTML( Html::hidden( 'wpEdittime', $this->edittime ) );
3488  $out->addHTML( Html::hidden( 'editRevId', $this->editRevId ) );
3489  $out->addHTML( Html::hidden( 'wpScrolltop', $this->scrolltop, [ 'id' => 'wpScrolltop' ] ) );
3490  }
3491 
3492  protected function showFormAfterText() {
3505  $this->context->getOutput()->addHTML(
3506  "\n" .
3507  Html::hidden( "wpEditToken", $this->context->getUser()->getEditToken() ) .
3508  "\n"
3509  );
3510  }
3511 
3520  protected function showContentForm() {
3521  $this->showTextbox1();
3522  }
3523 
3532  protected function showTextbox1( $customAttribs = null, $textoverride = null ) {
3533  if ( $this->wasDeletedSinceLastEdit() && $this->formtype == 'save' ) {
3534  $attribs = [ 'style' => 'display:none;' ];
3535  } else {
3536  $builder = new TextboxBuilder();
3537  $classes = $builder->getTextboxProtectionCSSClasses( $this->getTitle() );
3538 
3539  # Is an old revision being edited?
3540  if ( $this->isOldRev ) {
3541  $classes[] = 'mw-textarea-oldrev';
3542  }
3543 
3544  $attribs = [
3545  'aria-label' => $this->context->msg( 'edit-textarea-aria-label' )->text(),
3546  'tabindex' => 1
3547  ];
3548 
3549  if ( is_array( $customAttribs ) ) {
3550  $attribs += $customAttribs;
3551  }
3552 
3553  $attribs = $builder->mergeClassesIntoAttributes( $classes, $attribs );
3554  }
3555 
3556  $this->showTextbox(
3557  $textoverride ?? $this->textbox1,
3558  'wpTextbox1',
3559  $attribs
3560  );
3561  }
3562 
3563  protected function showTextbox2() {
3564  $this->showTextbox( $this->textbox2, 'wpTextbox2', [ 'tabindex' => 6, 'readonly' ] );
3565  }
3566 
3567  protected function showTextbox( $text, $name, $customAttribs = [] ) {
3568  $builder = new TextboxBuilder();
3569  $attribs = $builder->buildTextboxAttribs(
3570  $name,
3571  $customAttribs,
3572  $this->context->getUser(),
3573  $this->mTitle
3574  );
3575 
3576  $this->context->getOutput()->addHTML(
3577  Html::textarea( $name, $builder->addNewLineAtEnd( $text ), $attribs )
3578  );
3579  }
3580 
3581  protected function displayPreviewArea( $previewOutput, $isOnTop = false ) {
3582  $attribs = [ 'id' => 'wikiPreview' ];
3583  if ( $isOnTop ) {
3584  $attribs['class'] = 'ontop';
3585  }
3586  if ( $this->formtype != 'preview' ) {
3587  $attribs['style'] = 'display: none;';
3588  }
3589 
3590  $out = $this->context->getOutput();
3591  $out->addHTML( Xml::openElement( 'div', $attribs ) );
3592 
3593  if ( $this->formtype == 'preview' ) {
3594  $this->showPreview( $previewOutput );
3595  } else {
3596  // Empty content container for LivePreview
3597  $pageViewLang = $this->mTitle->getPageViewLanguage();
3598  $attribs = [ 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(),
3599  'class' => 'mw-content-' . $pageViewLang->getDir() ];
3600  $out->addHTML( Html::rawElement( 'div', $attribs ) );
3601  }
3602 
3603  $out->addHTML( '</div>' );
3604 
3605  if ( $this->formtype == 'diff' ) {
3606  try {
3607  $this->showDiff();
3608  } catch ( MWContentSerializationException $ex ) {
3609  $msg = $this->context->msg(
3610  'content-failed-to-parse',
3611  $this->contentModel,
3612  $this->contentFormat,
3613  $ex->getMessage()
3614  );
3615  $out->wrapWikiTextAsInterface( 'error', $msg->plain() );
3616  }
3617  }
3618  }
3619 
3626  protected function showPreview( $text ) {
3627  if ( $this->mArticle instanceof CategoryPage ) {
3628  $this->mArticle->openShowCategory();
3629  }
3630  # This hook seems slightly odd here, but makes things more
3631  # consistent for extensions.
3632  $out = $this->context->getOutput();
3633  $this->getHookRunner()->onOutputPageBeforeHTML( $out, $text );
3634  $out->addHTML( $text );
3635  if ( $this->mArticle instanceof CategoryPage ) {
3636  $this->mArticle->closeShowCategory();
3637  }
3638  }
3639 
3647  public function showDiff() {
3648  $oldtitlemsg = 'currentrev';
3649  # if message does not exist, show diff against the preloaded default
3650  if ( $this->mTitle->getNamespace() === NS_MEDIAWIKI && !$this->mTitle->exists() ) {
3651  $oldtext = $this->mTitle->getDefaultMessageText();
3652  if ( $oldtext !== false ) {
3653  $oldtitlemsg = 'defaultmessagetext';
3654  $oldContent = $this->toEditContent( $oldtext );
3655  } else {
3656  $oldContent = null;
3657  }
3658  } else {
3659  $oldContent = $this->getCurrentContent();
3660  }
3661 
3662  $textboxContent = $this->toEditContent( $this->textbox1 );
3663  if ( $this->editRevId !== null ) {
3664  $newContent = $this->page->replaceSectionAtRev(
3665  $this->section, $textboxContent, $this->summary, $this->editRevId
3666  );
3667  } else {
3668  $newContent = $this->page->replaceSectionContent(
3669  $this->section, $textboxContent, $this->summary, $this->edittime
3670  );
3671  }
3672 
3673  if ( $newContent ) {
3674  $this->getHookRunner()->onEditPageGetDiffContent( $this, $newContent );
3675 
3676  $user = $this->context->getUser();
3677  $popts = ParserOptions::newFromUserAndLang( $user,
3678  MediaWikiServices::getInstance()->getContentLanguage() );
3679  $newContent = $newContent->preSaveTransform( $this->mTitle, $user, $popts );
3680  }
3681 
3682  if ( ( $oldContent && !$oldContent->isEmpty() ) || ( $newContent && !$newContent->isEmpty() ) ) {
3683  $oldtitle = $this->context->msg( $oldtitlemsg )->parse();
3684  $newtitle = $this->context->msg( 'yourtext' )->parse();
3685 
3686  if ( !$oldContent ) {
3687  $oldContent = $newContent->getContentHandler()->makeEmptyContent();
3688  }
3689 
3690  if ( !$newContent ) {
3691  $newContent = $oldContent->getContentHandler()->makeEmptyContent();
3692  }
3693 
3694  $de = $oldContent->getContentHandler()->createDifferenceEngine( $this->context );
3695  $de->setContent( $oldContent, $newContent );
3696 
3697  $difftext = $de->getDiff( $oldtitle, $newtitle );
3698  $de->showDiffStyle();
3699  } else {
3700  $difftext = '';
3701  }
3702 
3703  $this->context->getOutput()->addHTML( '<div id="wikiDiff">' . $difftext . '</div>' );
3704  }
3705 
3706  protected function showHeaderCopyrightWarning() {
3707  $msg = 'editpage-head-copy-warn';
3708  if ( !$this->context->msg( $msg )->isDisabled() ) {
3709  $this->context->getOutput()->wrapWikiMsg( "<div class='editpage-head-copywarn'>\n$1\n</div>",
3710  'editpage-head-copy-warn' );
3711  }
3712  }
3713 
3722  protected function showTosSummary() {
3723  $msg = 'editpage-tos-summary';
3724  $this->getHookRunner()->onEditPageTosSummary( $this->mTitle, $msg );
3725  if ( !$this->context->msg( $msg )->isDisabled() ) {
3726  $out = $this->context->getOutput();
3727  $out->addHTML( '<div class="mw-tos-summary">' );
3728  $out->addWikiMsg( $msg );
3729  $out->addHTML( '</div>' );
3730  }
3731  }
3732 
3737  protected function showEditTools() {
3738  $this->context->getOutput()->addHTML( '<div class="mw-editTools">' .
3739  $this->context->msg( 'edittools' )->inContentLanguage()->parse() .
3740  '</div>' );
3741  }
3742 
3749  protected function getCopywarn() {
3750  return self::getCopyrightWarning( $this->mTitle );
3751  }
3752 
3761  public static function getCopyrightWarning( $title, $format = 'plain', $langcode = null ) {
3762  global $wgRightsText;
3763  if ( $wgRightsText ) {
3764  $copywarnMsg = [ 'copyrightwarning',
3765  '[[' . wfMessage( 'copyrightpage' )->inContentLanguage()->text() . ']]',
3766  $wgRightsText ];
3767  } else {
3768  $copywarnMsg = [ 'copyrightwarning2',
3769  '[[' . wfMessage( 'copyrightpage' )->inContentLanguage()->text() . ']]' ];
3770  }
3771  // Allow for site and per-namespace customization of contribution/copyright notice.
3772  Hooks::runner()->onEditPageCopyrightWarning( $title, $copywarnMsg );
3773 
3774  $msg = wfMessage( ...$copywarnMsg )->page( $title );
3775  if ( $langcode ) {
3776  $msg->inLanguage( $langcode );
3777  }
3778  return "<div id=\"editpage-copywarn\">\n" .
3779  $msg->$format() . "\n</div>";
3780  }
3781 
3789  public static function getPreviewLimitReport( ParserOutput $output = null ) {
3790  if ( !$output || !$output->getLimitReportData() ) {
3791  return '';
3792  }
3793 
3794  $limitReport = Html::rawElement( 'div', [ 'class' => 'mw-limitReportExplanation' ],
3795  wfMessage( 'limitreport-title' )->parseAsBlock()
3796  );
3797 
3798  // Show/hide animation doesn't work correctly on a table, so wrap it in a div.
3799  $limitReport .= Html::openElement( 'div', [ 'class' => 'preview-limit-report-wrapper' ] );
3800 
3801  $limitReport .= Html::openElement( 'table', [
3802  'class' => 'preview-limit-report wikitable'
3803  ] ) .
3804  Html::openElement( 'tbody' );
3805 
3806  foreach ( $output->getLimitReportData() as $key => $value ) {
3807  if ( Hooks::runner()->onParserLimitReportFormat( $key, $value, $limitReport, true, true ) ) {
3808  $keyMsg = wfMessage( $key );
3809  $valueMsg = wfMessage( [ "$key-value-html", "$key-value" ] );
3810  if ( !$valueMsg->exists() ) {
3811  // This is formatted raw, not as localized number.
3812  // If you want the parameter formatted as a number,
3813  // define the `$key-value` message.
3814  $valueMsg = ( new RawMessage( '$1' ) )->params( $value );
3815  } else {
3816  // If you define the `$key-value` or `$key-value-html`
3817  // message then the argument *must* be numeric.
3818  $valueMsg = $valueMsg->numParams( $value );
3819  }
3820  if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) {
3821  $limitReport .= Html::openElement( 'tr' ) .
3822  Html::rawElement( 'th', null, $keyMsg->parse() ) .
3823  Html::rawElement( 'td', null, $valueMsg->parse() ) .
3824  Html::closeElement( 'tr' );
3825  }
3826  }
3827  }
3828 
3829  $limitReport .= Html::closeElement( 'tbody' ) .
3830  Html::closeElement( 'table' ) .
3831  Html::closeElement( 'div' );
3832 
3833  return $limitReport;
3834  }
3835 
3836  protected function showStandardInputs( &$tabindex = 2 ) {
3837  $out = $this->context->getOutput();
3838  $out->addHTML( "<div class='editOptions'>\n" );
3839 
3840  if ( $this->section != 'new' ) {
3841  $this->showSummaryInput( false, $this->summary );
3842  $out->addHTML( $this->getSummaryPreview( false, $this->summary ) );
3843  }
3844 
3845  $checkboxes = $this->getCheckboxesWidget(
3846  $tabindex,
3847  [ 'minor' => $this->minoredit, 'watch' => $this->watchthis ]
3848  );
3849  $checkboxesHTML = new OOUI\HorizontalLayout( [ 'items' => $checkboxes ] );
3850 
3851  $out->addHTML( "<div class='editCheckboxes'>" . $checkboxesHTML . "</div>\n" );
3852 
3853  // Show copyright warning.
3854  $out->addWikiTextAsInterface( $this->getCopywarn() );
3855  $out->addHTML( $this->editFormTextAfterWarn );
3856 
3857  $out->addHTML( "<div class='editButtons'>\n" );
3858  $out->addHTML( implode( "\n", $this->getEditButtons( $tabindex ) ) . "\n" );
3859 
3860  $cancel = $this->getCancelLink( $tabindex++ );
3861 
3862  $message = $this->context->msg( 'edithelppage' )->inContentLanguage()->text();
3863  $edithelpurl = Skin::makeInternalOrExternalUrl( $message );
3864  $edithelp =
3866  $this->context->msg( 'edithelp' )->text(),
3867  [ 'target' => 'helpwindow', 'href' => $edithelpurl ],
3868  [ 'mw-ui-quiet' ]
3869  ) .
3870  $this->context->msg( 'word-separator' )->escaped() .
3871  $this->context->msg( 'newwindow' )->parse();
3872 
3873  $out->addHTML( " <span class='cancelLink'>{$cancel}</span>\n" );
3874  $out->addHTML( " <span class='editHelp'>{$edithelp}</span>\n" );
3875  $out->addHTML( "</div><!-- editButtons -->\n" );
3876 
3877  $this->getHookRunner()->onEditPage__showStandardInputs_options( $this, $out, $tabindex );
3878 
3879  $out->addHTML( "</div><!-- editOptions -->\n" );
3880  }
3881 
3886  protected function showConflict() {
3887  $out = $this->context->getOutput();
3888  // Avoid PHP 7.1 warning of passing $this by reference
3889  $editPage = $this;
3890  if ( $this->getHookRunner()->onEditPageBeforeConflictDiff( $editPage, $out ) ) {
3891  $this->incrementConflictStats();
3892 
3893  $this->getEditConflictHelper()->showEditFormTextAfterFooters();
3894  }
3895  }
3896 
3897  protected function incrementConflictStats() {
3898  $this->getEditConflictHelper()->incrementConflictStats( $this->context->getUser() );
3899  }
3900 
3905  public function getCancelLink( $tabindex = 0 ) {
3906  $cancelParams = [];
3907  if ( !$this->isConflict && $this->oldid > 0 ) {
3908  $cancelParams['oldid'] = $this->oldid;
3909  } elseif ( $this->getContextTitle()->isRedirect() ) {
3910  $cancelParams['redirect'] = 'no';
3911  }
3912 
3913  return new OOUI\ButtonWidget( [
3914  'id' => 'mw-editform-cancel',
3915  'tabIndex' => $tabindex,
3916  'href' => $this->getContextTitle()->getLinkURL( $cancelParams ),
3917  'label' => new OOUI\HtmlSnippet( $this->context->msg( 'cancel' )->parse() ),
3918  'framed' => false,
3919  'infusable' => true,
3920  'flags' => 'destructive',
3921  ] );
3922  }
3923 
3933  protected function getActionURL( Title $title ) {
3934  return $title->getLocalURL( [ 'action' => $this->action ] );
3935  }
3936 
3944  protected function wasDeletedSinceLastEdit() {
3945  if ( $this->deletedSinceEdit !== null ) {
3946  return $this->deletedSinceEdit;
3947  }
3948 
3949  $this->deletedSinceEdit = false;
3950 
3951  if ( !$this->mTitle->exists() && $this->mTitle->hasDeletedEdits() ) {
3952  $this->lastDelete = $this->getLastDelete();
3953  if ( $this->lastDelete ) {
3954  $deleteTime = wfTimestamp( TS_MW, $this->lastDelete->log_timestamp );
3955  if ( $deleteTime > $this->starttime ) {
3956  $this->deletedSinceEdit = true;
3957  }
3958  }
3959  }
3960 
3961  return $this->deletedSinceEdit;
3962  }
3963 
3969  protected function getLastDelete() {
3970  $dbr = wfGetDB( DB_REPLICA );
3971  $commentQuery = CommentStore::getStore()->getJoin( 'log_comment' );
3972  $data = $dbr->selectRow(
3973  array_merge( [ 'logging' ], $commentQuery['tables'], [ 'actor' ] ),
3974  [
3975  'log_type',
3976  'log_action',
3977  'log_timestamp',
3978  'log_namespace',
3979  'log_title',
3980  'log_params',
3981  'log_deleted',
3982  'actor_name'
3983  ] + $commentQuery['fields'],
3984  [
3985  'log_namespace' => $this->mTitle->getNamespace(),
3986  'log_title' => $this->mTitle->getDBkey(),
3987  'log_type' => 'delete',
3988  'log_action' => 'delete',
3989  ],
3990  __METHOD__,
3991  [ 'LIMIT' => 1, 'ORDER BY' => 'log_timestamp DESC' ],
3992  [
3993  'actor' => [ 'JOIN', 'actor_id=log_actor' ],
3994  ] + $commentQuery['joins']
3995  );
3996  // Quick paranoid permission checks...
3997  if ( is_object( $data ) ) {
3998  if ( $data->log_deleted & LogPage::DELETED_USER ) {
3999  $data->actor_name = $this->context->msg( 'rev-deleted-user' )->escaped();
4000  }
4001 
4002  if ( $data->log_deleted & LogPage::DELETED_COMMENT ) {
4003  $data->log_comment_text = $this->context->msg( 'rev-deleted-comment' )->escaped();
4004  $data->log_comment_data = null;
4005  }
4006  }
4007 
4008  return $data;
4009  }
4010 
4016  public function getPreviewText() {
4017  $out = $this->context->getOutput();
4018  $config = $this->context->getConfig();
4019 
4020  if ( $config->get( 'RawHtml' ) && !$this->mTokenOk ) {
4021  // Could be an offsite preview attempt. This is very unsafe if
4022  // HTML is enabled, as it could be an attack.
4023  $parsedNote = '';
4024  if ( $this->textbox1 !== '' ) {
4025  // Do not put big scary notice, if previewing the empty
4026  // string, which happens when you initially edit
4027  // a category page, due to automatic preview-on-open.
4028  $parsedNote = Html::rawElement( 'div', [ 'class' => 'previewnote' ],
4029  $out->parseAsInterface(
4030  $this->context->msg( 'session_fail_preview_html' )->plain()
4031  ) );
4032  }
4033  $this->incrementEditFailureStats( 'session_loss' );
4034  return $parsedNote;
4035  }
4036 
4037  $note = '';
4038 
4039  try {
4040  $content = $this->toEditContent( $this->textbox1 );
4041 
4042  $previewHTML = '';
4043  if ( !$this->getHookRunner()->onAlternateEditPreview(
4044  $this, $content, $previewHTML, $this->mParserOutput )
4045  ) {
4046  return $previewHTML;
4047  }
4048 
4049  # provide a anchor link to the editform
4050  $continueEditing = '<span class="mw-continue-editing">' .
4051  '[[#' . self::EDITFORM_ID . '|' .
4052  $this->context->getLanguage()->getArrow() . ' ' .
4053  $this->context->msg( 'continue-editing' )->text() . ']]</span>';
4054  if ( $this->mTriedSave && !$this->mTokenOk ) {
4055  $note = $this->context->msg( 'session_fail_preview' )->plain();
4056  $this->incrementEditFailureStats( 'session_loss' );
4057  } elseif ( $this->incompleteForm ) {
4058  $note = $this->context->msg( 'edit_form_incomplete' )->plain();
4059  if ( $this->mTriedSave ) {
4060  $this->incrementEditFailureStats( 'incomplete_form' );
4061  }
4062  } else {
4063  $note = $this->context->msg( 'previewnote' )->plain() . ' ' . $continueEditing;
4064  }
4065 
4066  # don't parse non-wikitext pages, show message about preview
4067  if ( $this->mTitle->isUserConfigPage() || $this->mTitle->isSiteConfigPage() ) {
4068  if ( $this->mTitle->isUserConfigPage() ) {
4069  $level = 'user';
4070  } elseif ( $this->mTitle->isSiteConfigPage() ) {
4071  $level = 'site';
4072  } else {
4073  $level = false;
4074  }
4075 
4076  if ( $content->getModel() == CONTENT_MODEL_CSS ) {
4077  $format = 'css';
4078  if ( $level === 'user' && !$config->get( 'AllowUserCss' ) ) {
4079  $format = false;
4080  }
4081  } elseif ( $content->getModel() == CONTENT_MODEL_JSON ) {
4082  $format = 'json';
4083  if ( $level === 'user' /* No comparable 'AllowUserJson' */ ) {
4084  $format = false;
4085  }
4086  } elseif ( $content->getModel() == CONTENT_MODEL_JAVASCRIPT ) {
4087  $format = 'js';
4088  if ( $level === 'user' && !$config->get( 'AllowUserJs' ) ) {
4089  $format = false;
4090  }
4091  } else {
4092  $format = false;
4093  }
4094 
4095  # Used messages to make sure grep find them:
4096  # Messages: usercsspreview, userjsonpreview, userjspreview,
4097  # sitecsspreview, sitejsonpreview, sitejspreview
4098  if ( $level && $format ) {
4099  $note = "<div id='mw-{$level}{$format}preview'>" .
4100  $this->context->msg( "{$level}{$format}preview" )->plain() .
4101  ' ' . $continueEditing . "</div>";
4102  }
4103  }
4104 
4105  # If we're adding a comment, we need to show the
4106  # summary as the headline
4107  if ( $this->section === "new" && $this->summary !== "" ) {
4108  $content = $content->addSectionHeader( $this->summary );
4109  }
4110 
4111  $this->getHookRunner()->onEditPageGetPreviewContent( $this, $content );
4112 
4113  $parserResult = $this->doPreviewParse( $content );
4114  $parserOutput = $parserResult['parserOutput'];
4115  $previewHTML = $parserResult['html'];
4116  $this->mParserOutput = $parserOutput;
4117  $out->addParserOutputMetadata( $parserOutput );
4118  if ( $out->userCanPreview() ) {
4119  $out->addContentOverride( $this->getTitle(), $content );
4120  }
4121 
4122  if ( count( $parserOutput->getWarnings() ) ) {
4123  $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
4124  }
4125 
4126  } catch ( MWContentSerializationException $ex ) {
4127  $m = $this->context->msg(
4128  'content-failed-to-parse',
4129  $this->contentModel,
4130  $this->contentFormat,
4131  $ex->getMessage()
4132  );
4133  $note .= "\n\n" . $m->plain(); # gets parsed down below
4134  $previewHTML = '';
4135  }
4136 
4137  if ( $this->isConflict ) {
4138  $conflict = Html::rawElement(
4139  'div', [ 'id' => 'mw-previewconflict', 'class' => 'warningbox' ],
4140  $this->context->msg( 'previewconflict' )->escaped()
4141  );
4142  } else {
4143  $conflict = '';
4144  }
4145 
4146  $previewhead = Html::rawElement(
4147  'div', [ 'class' => 'previewnote' ],
4149  'h2', [ 'id' => 'mw-previewheader' ],
4150  $this->context->msg( 'preview' )->escaped()
4151  ) .
4152  Html::rawElement( 'div', [ 'class' => 'warningbox' ],
4153  $out->parseAsInterface( $note )
4154  ) . $conflict
4155  );
4156 
4157  $pageViewLang = $this->mTitle->getPageViewLanguage();
4158  $attribs = [ 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(),
4159  'class' => 'mw-content-' . $pageViewLang->getDir() ];
4160  $previewHTML = Html::rawElement( 'div', $attribs, $previewHTML );
4161 
4162  return $previewhead . $previewHTML . $this->previewTextAfterContent;
4163  }
4164 
4165  private function incrementEditFailureStats( $failureType ) {
4166  $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
4167  $stats->increment( 'edit.failures.' . $failureType );
4168  }
4169 
4174  protected function getPreviewParserOptions() {
4175  $parserOptions = $this->page->makeParserOptions( $this->context );
4176  $parserOptions->setIsPreview( true );
4177  $parserOptions->setIsSectionPreview( $this->section !== null && $this->section !== '' );
4178  $parserOptions->enableLimitReport();
4179 
4180  // XXX: we could call $parserOptions->setCurrentRevisionRecordCallback here to force the
4181  // current revision to be null during PST, until setupFakeRevision is called on
4182  // the ParserOptions. Currently, we rely on Parser::getRevisionRecordObject() to ignore
4183  // existing revisions in preview mode.
4184 
4185  return $parserOptions;
4186  }
4187 
4197  protected function doPreviewParse( Content $content ) {
4198  $user = $this->context->getUser();
4199  $parserOptions = $this->getPreviewParserOptions();
4200 
4201  // NOTE: preSaveTransform doesn't have a fake revision to operate on.
4202  // Parser::getRevisionRecordObject() will return null in preview mode,
4203  // causing the context user to be used for {{subst:REVISIONUSER}}.
4204  // XXX: Alternatively, we could also call setupFakeRevision() a second time:
4205  // once before PST with $content, and then after PST with $pstContent.
4206  $pstContent = $content->preSaveTransform( $this->mTitle, $user, $parserOptions );
4207  $scopedCallback = $parserOptions->setupFakeRevision( $this->mTitle, $pstContent, $user );
4208  $parserOutput = $pstContent->getParserOutput( $this->mTitle, null, $parserOptions );
4209  ScopedCallback::consume( $scopedCallback );
4210  return [
4211  'parserOutput' => $parserOutput,
4212  'html' => $parserOutput->getText( [
4213  'enableSectionEditLinks' => false
4214  ] )
4215  ];
4216  }
4217 
4221  public function getTemplates() {
4222  if ( $this->preview || $this->section != '' ) {
4223  $templates = [];
4224  if ( !isset( $this->mParserOutput ) ) {
4225  return $templates;
4226  }
4227  foreach ( $this->mParserOutput->getTemplates() as $ns => $template ) {
4228  foreach ( array_keys( $template ) as $dbk ) {
4229  $templates[] = Title::makeTitle( $ns, $dbk );
4230  }
4231  }
4232  return $templates;
4233  } else {
4234  return $this->mTitle->getTemplateLinksFrom();
4235  }
4236  }
4237 
4243  public static function getEditToolbar() {
4244  $startingToolbar = '<div id="toolbar"></div>';
4245  $toolbar = $startingToolbar;
4246 
4247  if ( !Hooks::runner()->onEditPageBeforeEditToolbar( $toolbar ) ) {
4248  return null;
4249  }
4250  // Don't add a pointless `<div>` to the page unless a hook caller populated it
4251  return ( $toolbar === $startingToolbar ) ? null : $toolbar;
4252  }
4253 
4279  public function getCheckboxesDefinition( $checked ) {
4280  $checkboxes = [];
4281 
4282  $user = $this->context->getUser();
4283  // don't show the minor edit checkbox if it's a new page or section
4284  if ( !$this->isNew && $this->permManager->userHasRight( $user, 'minoredit' ) ) {
4285  $checkboxes['wpMinoredit'] = [
4286  'id' => 'wpMinoredit',
4287  'label-message' => 'minoredit',
4288  // Uses messages: tooltip-minoredit, accesskey-minoredit
4289  'tooltip' => 'minoredit',
4290  'label-id' => 'mw-editpage-minoredit',
4291  'legacy-name' => 'minor',
4292  'default' => $checked['minor'],
4293  ];
4294  }
4295 
4296  if ( $user->isRegistered() ) {
4297  $checkboxes = array_merge(
4298  $checkboxes,
4299  $this->getCheckboxesDefinitionForWatchlist( $checked['watch'] )
4300  );
4301  }
4302 
4303  $this->getHookRunner()->onEditPageGetCheckboxesDefinition( $this, $checkboxes );
4304 
4305  return $checkboxes;
4306  }
4307 
4315  private function getCheckboxesDefinitionForWatchlist( $watch ) {
4316  $fieldDefs = [
4317  'wpWatchthis' => [
4318  'id' => 'wpWatchthis',
4319  'label-message' => 'watchthis',
4320  // Uses messages: tooltip-watch, accesskey-watch
4321  'tooltip' => 'watch',
4322  'label-id' => 'mw-editpage-watch',
4323  'legacy-name' => 'watch',
4324  'default' => $watch,
4325  ]
4326  ];
4327  if ( $this->watchlistExpiryEnabled ) {
4328  $watchedItem = $this->watchedItemStore->getWatchedItem( $this->getContext()->getUser(), $this->getTitle() );
4329  $expiryOptions = WatchAction::getExpiryOptions( $this->getContext(), $watchedItem );
4330  // When previewing, override the selected dropdown option to select whatever was posted
4331  // (if it's a valid option) rather than the current value for watchlistExpiry.
4332  // See also above in $this->importFormData().
4333  $expiryFromRequest = $this->getContext()->getRequest()->getText( 'wpWatchlistExpiry' );
4334  if ( $this->preview && in_array( $expiryFromRequest, $expiryOptions['options'] ) ) {
4335  $expiryOptions['default'] = $expiryFromRequest;
4336  }
4337  // Reformat the options to match what DropdownInputWidget wants.
4338  $options = [];
4339  foreach ( $expiryOptions['options'] as $label => $value ) {
4340  $options[] = [ 'data' => $value, 'label' => $label ];
4341  }
4342  $fieldDefs['wpWatchlistExpiry'] = [
4343  'id' => 'wpWatchlistExpiry',
4344  'label-message' => 'confirm-watch-label',
4345  // Uses messages: tooltip-watchlist-expiry, accesskey-watchlist-expiry
4346  'tooltip' => 'watchlist-expiry',
4347  'label-id' => 'mw-editpage-watchlist-expiry',
4348  'default' => $expiryOptions['default'],
4349  'value-attr' => 'value',
4350  'class' => DropdownInputWidget::class,
4351  'options' => $options,
4352  'invisibleLabel' => true,
4353  ];
4354  }
4355  return $fieldDefs;
4356  }
4357 
4369  public function getCheckboxesWidget( &$tabindex, $checked ) {
4370  $checkboxes = [];
4371  $checkboxesDef = $this->getCheckboxesDefinition( $checked );
4372 
4373  foreach ( $checkboxesDef as $name => $options ) {
4374  $legacyName = $options['legacy-name'] ?? $name;
4375 
4376  $title = null;
4377  $accesskey = null;
4378  if ( isset( $options['tooltip'] ) ) {
4379  $accesskey = $this->context->msg( "accesskey-{$options['tooltip']}" )->text();
4380  $title = Linker::titleAttrib( $options['tooltip'] );
4381  }
4382  if ( isset( $options['title-message'] ) ) {
4383  $title = $this->context->msg( $options['title-message'] )->text();
4384  }
4385  // Allow checkbox definitions to set their own class and value-attribute names.
4386  // See $this->getCheckboxesDefinition() for details.
4387  $className = $options['class'] ?? CheckboxInputWidget::class;
4388  $valueAttr = $options['value-attr'] ?? 'selected';
4389  $checkboxes[ $legacyName ] = new FieldLayout(
4390  new $className( [
4391  'tabIndex' => ++$tabindex,
4392  'accessKey' => $accesskey,
4393  'id' => $options['id'] . 'Widget',
4394  'inputId' => $options['id'],
4395  'name' => $name,
4396  $valueAttr => $options['default'],
4397  'infusable' => true,
4398  'options' => $options['options'] ?? null,
4399  ] ),
4400  [
4401  'align' => 'inline',
4402  'label' => new OOUI\HtmlSnippet( $this->context->msg( $options['label-message'] )->parse() ),
4403  'title' => $title,
4404  'id' => $options['label-id'] ?? null,
4405  'invisibleLabel' => $options['invisibleLabel'] ?? null,
4406  ]
4407  );
4408  }
4409 
4410  return $checkboxes;
4411  }
4412 
4419  protected function getSubmitButtonLabel() {
4420  $labelAsPublish =
4421  $this->context->getConfig()->get( 'EditSubmitButtonLabelPublish' );
4422 
4423  // Can't use $this->isNew as that's also true if we're adding a new section to an extant page
4424  $newPage = !$this->mTitle->exists();
4425 
4426  if ( $labelAsPublish ) {
4427  $buttonLabelKey = $newPage ? 'publishpage' : 'publishchanges';
4428  } else {
4429  $buttonLabelKey = $newPage ? 'savearticle' : 'savechanges';
4430  }
4431 
4432  return $buttonLabelKey;
4433  }
4434 
4443  public function getEditButtons( &$tabindex ) {
4444  $buttons = [];
4445 
4446  $labelAsPublish =
4447  $this->context->getConfig()->get( 'EditSubmitButtonLabelPublish' );
4448 
4449  $buttonLabel = $this->context->msg( $this->getSubmitButtonLabel() )->text();
4450  $buttonTooltip = $labelAsPublish ? 'publish' : 'save';
4451 
4452  $buttons['save'] = new OOUI\ButtonInputWidget( [
4453  'name' => 'wpSave',
4454  'tabIndex' => ++$tabindex,
4455  'id' => 'wpSaveWidget',
4456  'inputId' => 'wpSave',
4457  // Support: IE 6 – Use <input>, otherwise it can't distinguish which button was clicked
4458  'useInputTag' => true,
4459  'flags' => [ 'progressive', 'primary' ],
4460  'label' => $buttonLabel,
4461  'infusable' => true,
4462  'type' => 'submit',
4463  // Messages used: tooltip-save, tooltip-publish
4464  'title' => Linker::titleAttrib( $buttonTooltip ),
4465  // Messages used: accesskey-save, accesskey-publish
4466  'accessKey' => Linker::accesskey( $buttonTooltip ),
4467  ] );
4468 
4469  $buttons['preview'] = new OOUI\ButtonInputWidget( [
4470  'name' => 'wpPreview',
4471  'tabIndex' => ++$tabindex,
4472  'id' => 'wpPreviewWidget',
4473  'inputId' => 'wpPreview',
4474  // Support: IE 6 – Use <input>, otherwise it can't distinguish which button was clicked
4475  'useInputTag' => true,
4476  'label' => $this->context->msg( 'showpreview' )->text(),
4477  'infusable' => true,
4478  'type' => 'submit',
4479  // Message used: tooltip-preview
4480  'title' => Linker::titleAttrib( 'preview' ),
4481  // Message used: accesskey-preview
4482  'accessKey' => Linker::accesskey( 'preview' ),
4483  ] );
4484 
4485  $buttons['diff'] = new OOUI\ButtonInputWidget( [
4486  'name' => 'wpDiff',
4487  'tabIndex' => ++$tabindex,
4488  'id' => 'wpDiffWidget',
4489  'inputId' => 'wpDiff',
4490  // Support: IE 6 – Use <input>, otherwise it can't distinguish which button was clicked
4491  'useInputTag' => true,
4492  'label' => $this->context->msg( 'showdiff' )->text(),
4493  'infusable' => true,
4494  'type' => 'submit',
4495  // Message used: tooltip-diff
4496  'title' => Linker::titleAttrib( 'diff' ),
4497  // Message used: accesskey-diff
4498  'accessKey' => Linker::accesskey( 'diff' ),
4499  ] );
4500 
4501  $this->getHookRunner()->onEditPageBeforeEditButtons( $this, $buttons, $tabindex );
4502 
4503  return $buttons;
4504  }
4505 
4510  private function noSuchSectionPage() {
4511  $out = $this->context->getOutput();
4512  $out->prepareErrorPage( $this->context->msg( 'nosuchsectiontitle' ) );
4513 
4514  $res = $this->context->msg( 'nosuchsectiontext', $this->section )->parseAsBlock();
4515 
4516  $this->getHookRunner()->onEditPageNoSuchSection( $this, $res );
4517  $out->addHTML( $res );
4518 
4519  $out->returnToMain( false, $this->mTitle );
4520  }
4521 
4527  public function spamPageWithContent( $match = false ) {
4528  $this->textbox2 = $this->textbox1;
4529 
4530  if ( is_array( $match ) ) {
4531  $match = $this->context->getLanguage()->listToText( $match );
4532  }
4533  $out = $this->context->getOutput();
4534  $out->prepareErrorPage( $this->context->msg( 'spamprotectiontitle' ) );
4535 
4536  $out->addHTML( '<div id="spamprotected">' );
4537  $out->addWikiMsg( 'spamprotectiontext' );
4538  if ( $match ) {
4539  // @phan-suppress-next-line SecurityCheck-DoubleEscaped false positive
4540  $out->addWikiMsg( 'spamprotectionmatch', wfEscapeWikiText( $match ) );
4541  }
4542  $out->addHTML( '</div>' );
4543 
4544  $out->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
4545  $this->showDiff();
4546 
4547  $out->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
4548  $this->showTextbox2();
4549 
4550  $out->addReturnTo( $this->getContextTitle(), [ 'action' => 'edit' ] );
4551  }
4552 
4556  protected function addEditNotices() {
4557  $out = $this->context->getOutput();
4558  $editNotices = $this->mTitle->getEditNotices( $this->oldid );
4559  if ( count( $editNotices ) ) {
4560  $out->addHTML( implode( "\n", $editNotices ) );
4561  } else {
4562  $msg = $this->context->msg( 'editnotice-notext' );
4563  if ( !$msg->isDisabled() ) {
4564  $out->addHTML(
4565  '<div class="mw-editnotice-notext">'
4566  . $msg->parseAsBlock()
4567  . '</div>'
4568  );
4569  }
4570  }
4571  }
4572 
4576  protected function addTalkPageText() {
4577  if ( $this->mTitle->isTalkPage() ) {
4578  $this->context->getOutput()->addWikiMsg( 'talkpagetext' );
4579  }
4580  }
4581 
4585  protected function addLongPageWarningHeader() {
4586  if ( $this->contentLength === false ) {
4587  $this->contentLength = strlen( $this->textbox1 );
4588  }
4589 
4590  $out = $this->context->getOutput();
4591  $lang = $this->context->getLanguage();
4592  $maxArticleSize = $this->context->getConfig()->get( 'MaxArticleSize' );
4593  if ( $this->tooBig || $this->contentLength > $maxArticleSize * 1024 ) {
4594  $out->wrapWikiMsg( "<div class='error' id='mw-edit-longpageerror'>\n$1\n</div>",
4595  [
4596  'longpageerror',
4597  $lang->formatNum( round( $this->contentLength / 1024, 3 ) ),
4598  $lang->formatNum( $maxArticleSize )
4599  ]
4600  );
4601  } else {
4602  $longPageHint = $this->context->msg( 'longpage-hint' );
4603  if ( !$longPageHint->isDisabled() ) {
4604  $msgText = trim( $longPageHint->params( $lang->formatSize( $this->contentLength ),
4605  $this->contentLength )->text() );
4606  if ( $msgText !== '' && $msgText !== '-' ) {
4607  $out->addWikiTextAsInterface( "<div id='mw-edit-longpage-hint'>\n$msgText\n</div>" );
4608  }
4609  }
4610  }
4611  }
4612 
4616  protected function addPageProtectionWarningHeaders() {
4617  $out = $this->context->getOutput();
4618  if ( $this->mTitle->isProtected( 'edit' ) &&
4619  $this->permManager->getNamespaceRestrictionLevels(
4620  $this->getTitle()->getNamespace()
4621  ) !== [ '' ]
4622  ) {
4623  # Is the title semi-protected?
4624  if ( $this->mTitle->isSemiProtected() ) {
4625  $noticeMsg = 'semiprotectedpagewarning';
4626  } else {
4627  # Then it must be protected based on static groups (regular)
4628  $noticeMsg = 'protectedpagewarning';
4629  }
4630  LogEventsList::showLogExtract( $out, 'protect', $this->mTitle, '',
4631  [ 'lim' => 1, 'msgKey' => [ $noticeMsg ] ] );
4632  }
4633  if ( $this->mTitle->isCascadeProtected() ) {
4634  # Is this page under cascading protection from some source pages?
4635 
4636  list( $cascadeSources, /* $restrictions */ ) = $this->mTitle->getCascadeProtectionSources();
4637  $notice = "<div class='warningbox mw-cascadeprotectedwarning'>\n$1\n";
4638  $cascadeSourcesCount = count( $cascadeSources );
4639  if ( $cascadeSourcesCount > 0 ) {
4640  # Explain, and list the titles responsible
4641  foreach ( $cascadeSources as $page ) {
4642  $notice .= '* [[:' . $page->getPrefixedText() . "]]\n";
4643  }
4644  }
4645  $notice .= '</div>';
4646  $out->wrapWikiMsg( $notice, [ 'cascadeprotectedwarning', $cascadeSourcesCount ] );
4647  }
4648  if ( !$this->mTitle->exists() && $this->mTitle->getRestrictions( 'create' ) ) {
4649  LogEventsList::showLogExtract( $out, 'protect', $this->mTitle, '',
4650  [ 'lim' => 1,
4651  'showIfEmpty' => false,
4652  'msgKey' => [ 'titleprotectedwarning' ],
4653  'wrap' => "<div class=\"mw-titleprotectedwarning\">\n$1</div>" ] );
4654  }
4655  }
4656 
4661  protected function addExplainConflictHeader( OutputPage $out ) {
4662  $out->addHTML(
4663  $this->getEditConflictHelper()->getExplainHeader()
4664  );
4665  }
4666 
4672  protected function addNewLineAtEnd( $wikitext ) {
4673  return ( new TextboxBuilder() )->addNewLineAtEnd( $wikitext );
4674  }
4675 
4686  private function guessSectionName( $text ) {
4687  // Detect Microsoft browsers
4688  $userAgent = $this->context->getRequest()->getHeader( 'User-Agent' );
4689  $parser = MediaWikiServices::getInstance()->getParser();
4690  if ( $userAgent && preg_match( '/MSIE|Edge/', $userAgent ) ) {
4691  // ...and redirect them to legacy encoding, if available
4692  return $parser->guessLegacySectionNameFromWikiText( $text );
4693  }
4694  // Meanwhile, real browsers get real anchors
4695  $name = $parser->guessSectionNameFromWikiText( $text );
4696  // With one little caveat: per T216029, fragments in HTTP redirects need to be urlencoded,
4697  // otherwise Chrome double-escapes the rest of the URL.
4698  return '#' . urlencode( mb_substr( $name, 1 ) );
4699  }
4700 
4707  public function setEditConflictHelperFactory( callable $factory ) {
4708  $this->editConflictHelperFactory = $factory;
4709  $this->editConflictHelper = null;
4710  }
4711 
4715  private function getEditConflictHelper() {
4716  if ( !$this->editConflictHelper ) {
4717  $this->editConflictHelper = call_user_func(
4718  $this->editConflictHelperFactory,
4719  $this->getSubmitButtonLabel()
4720  );
4721  }
4722 
4724  }
4725 
4731  private function newTextConflictHelper( $submitButtonLabel ) {
4732  return new TextConflictHelper(
4733  $this->getTitle(),
4734  $this->getContext()->getOutput(),
4735  MediaWikiServices::getInstance()->getStatsdDataFactory(),
4736  $submitButtonLabel,
4737  MediaWikiServices::getInstance()->getContentHandlerFactory()
4738  );
4739  }
4740 }
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:428
EditPage\$editFormTextBeforeContent
$editFormTextBeforeContent
Definition: EditPage.php:341
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:172
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
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:327
EditPage\showFormBeforeText
showFormBeforeText()
Definition: EditPage.php:3483
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:385
EditPage\$lastDelete
bool stdClass $lastDelete
Definition: EditPage.php:166
EditPage\tokenOk
tokenOk(&$request)
Make sure the form isn't faking a user's credentials.
Definition: EditPage.php:1639
EditPage\$editFormPageTop
string $editFormPageTop
Before even the preview.
Definition: EditPage.php:339
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:3520
EditPage\$mTitle
Title $mTitle
Definition: EditPage.php:136
MediaWiki\Revision\RevisionRecord
Page revision base class.
Definition: RevisionRecord.php:47
EditPage\$watchlistExpiryEnabled
bool $watchlistExpiryEnabled
Corresponds to $wgWatchlistExpiry.
Definition: EditPage.php:244
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:168
Html\textarea
static textarea( $name, $value='', array $attribs=[])
Convenience function to produce a <textarea> element.
Definition: Html.php:821
EditPage\spamPageWithContent
spamPageWithContent( $match=false)
Show "your edit contains spam" page with your diff and text.
Definition: EditPage.php:4527
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:292
ParserOutput
Definition: ParserOutput.php:31
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:1074
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:783
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:345
EditPage\displayPreviewArea
displayPreviewArea( $previewOutput, $isOnTop=false)
Definition: EditPage.php:3581
MediaWiki\EditPage\Constraint\EditConstraintRunner
Back end to process the edit constraints.
Definition: EditConstraintRunner.php:36
EditPage\$blankArticle
bool $blankArticle
Definition: EditPage.php:190
EditPage\$allowBlankSummary
bool $allowBlankSummary
Definition: EditPage.php:187
$wgRightsText
$wgRightsText
If either $wgRightsUrl or $wgRightsPage is specified then this variable gives the text for the link.
Definition: DefaultSettings.php:8281
EditPage\$editFormTextBottom
$editFormTextBottom
Definition: EditPage.php:344
EditPage\getSummaryInputAttributes
getSummaryInputAttributes(array $inputAttrs=null)
Helper function for summary input functions, which returns the necessary attributes for the input.
Definition: EditPage.php:3380
EditPage\$editFormTextTop
$editFormTextTop
Definition: EditPage.php:340
EditPage\$editintro
string $editintro
Definition: EditPage.php:318
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:180
EditPage\showTextbox2
showTextbox2()
Definition: EditPage.php:3563
$lang
if(!isset( $args[0])) $lang
Definition: testCompression.php:37
EditPage\$summary
string $summary
Definition: EditPage.php:264
MediaWiki\Revision\RevisionStore
Service for looking up page revisions.
Definition: RevisionStore.php:88
EditPage\$textbox2
string $textbox2
Definition: EditPage.php:261
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:1562
EditPage\$mTokenOk
bool $mTokenOk
Definition: EditPage.php:169
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:1459
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:1692
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:307
MediaWiki\EditPage\TextboxBuilder
Helps EditPage build textboxes.
Definition: TextboxBuilder.php:37
EditPage\getContextTitle
getContextTitle()
Definition: EditPage.php:514
EditPage\getEditToolbar
static getEditToolbar()
Allow extensions to provide a toolbar.
Definition: EditPage.php:4243
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:3722
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:130
EditPage\$save
bool $save
Definition: EditPage.php:229
WatchAction\getExpiryOptions
static getExpiryOptions(MessageLocalizer $msgLocalizer, $watchedItem)
Get options and default for a watchlist expiry select list.
Definition: WatchAction.php:135
EditPage\addPageProtectionWarningHeaders
addPageProtectionWarningHeaders()
Definition: EditPage.php:4616
EditPage\getExpectedParentRevision
getExpectedParentRevision()
Returns the RevisionRecord corresponding to the revision that was current at the time editing was ini...
Definition: EditPage.php:2582
EditPage\setContextTitle
setContextTitle( $title)
Definition: EditPage.php:506
EditPage\handleFailedConstraint
handleFailedConstraint(IEditConstraint $failed)
Apply the specific updates needed for the EditPage fields based on which constraint failed,...
Definition: EditPage.php:2410
getAuthority
getAuthority()
WikiPage
Class representing a MediaWiki article and history.
Definition: WikiPage.php:62
EditPage\edit
edit()
This is the function that gets called for "action=edit".
Definition: EditPage.php:555
ParserOptions\newFromUserAndLang
static newFromUserAndLang(UserIdentity $user, Language $lang)
Get a ParserOptions object from a given user and language.
Definition: ParserOptions.php:1088
EditPage\$autoSumm
string $autoSumm
Definition: EditPage.php:202
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:3969
wfReadOnly
wfReadOnly()
Check whether the wiki is in read-only mode.
Definition: GlobalFunctions.php:1099
EditPage\incrementConflictStats
incrementConflictStats()
Definition: EditPage.php:3897
EditPage\addEditNotices
addEditNotices()
Definition: EditPage.php:4556
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:589
wfMessage
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Definition: GlobalFunctions.php:1182
EditPage\newSectionSummary
newSectionSummary()
Return the summary to be used for a new section.
Definition: EditPage.php:1857
EditPage\$editFormTextAfterTools
$editFormTextAfterTools
Definition: EditPage.php:343
EditPage\addContentModelChangeLogEntry
addContentModelChangeLogEntry(UserIdentity $user, $oldModel, $newModel, $reason)
Definition: EditPage.php:2490
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:107
EditPage\getEditPermissionErrors
getEditPermissionErrors( $rigor=PermissionManager::RIGOR_SECURE)
Definition: EditPage.php:730
EditPage\$editFormTextAfterWarn
$editFormTextAfterWarn
Definition: EditPage.php:342
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:3706
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:4315
Html\warningBox
static warningBox( $html, $className='')
Return a warning box.
Definition: Html.php:729
EditPage\$mExpectedParentRevision
RevisionRecord bool null $mExpectedParentRevision
A RevisionRecord corresponding to $this->editRevId or $this->edittime Replaced $mBaseRevision.
Definition: EditPage.php:221
EditPage\$userNameUtils
UserNameUtils $userNameUtils
Definition: EditPage.php:422
EditPage\addLongPageWarningHeader
addLongPageWarningHeader()
Definition: EditPage.php:4585
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:110
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:1629
Wikimedia\Message\MessageValue
Value object representing a message for i18n.
Definition: MessageValue.php:16
Linker\formatHiddenCategories
static formatHiddenCategories( $hiddencats)
Returns HTML for the "hidden categories on this page" list.
Definition: Linker.php:2075
EditPage\$mArticle
Article $mArticle
Definition: EditPage.php:127
EditPage\$contentFormat
null string $contentFormat
Definition: EditPage.php:330
EditPage\POST_EDIT_COOKIE_DURATION
const POST_EDIT_COOKIE_DURATION
Duration of PostEdit cookie, in seconds.
Definition: EditPage.php:121
$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:241
EditPage\$previewTextAfterContent
$previewTextAfterContent
Definition: EditPage.php:346
Html\closeElement
static closeElement( $element)
Returns "</$element>".
Definition: Html.php:318
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:3647
EditPage\$tooBig
bool $tooBig
Definition: EditPage.php:178
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:52
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
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:95
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:2863
Article\getTitle
getTitle()
Get the title object of the article.
Definition: Article.php:227
EditPage\getEditButtons
getEditButtons(&$tabindex)
Returns an array of html code of the following buttons: save, diff and preview.
Definition: EditPage.php:4443
wfDeprecated
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that $function is deprecated.
Definition: GlobalFunctions.php:996
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:1583
EditPage\$editRevId
int $editRevId
Revision ID of the latest revision of the page when editing was initiated on the client.
Definition: EditPage.php:289
EditPage\showSummaryInput
showSummaryInput( $isSubjectPreview, $summary="")
Definition: EditPage.php:3436
MediaWiki\Watchlist\WatchlistManager
WatchlistManager service.
Definition: WatchlistManager.php:52
EditPage\getParentRevId
getParentRevId()
Get the edit's parent revision ID.
Definition: EditPage.php:1519
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:928
EditPage\importFormData
importFormData(&$request)
This function collects the form data and uses it to populate various member variables.
Definition: EditPage.php:947
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:3933
EditPage\addExplainConflictHeader
addExplainConflictHeader(OutputPage $out)
Definition: EditPage.php:4661
LogPage\DELETED_USER
const DELETED_USER
Definition: LogPage.php:41
wfGetDB
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
Definition: GlobalFunctions.php:2306
EditPage\showIntro
showIntro()
Show all applicable editing introductions.
Definition: EditPage.php:2667
EditPage\$firsttime
bool $firsttime
True the first time the edit form is rendered, false after re-rendering with diff,...
Definition: EditPage.php:163
$matches
$matches
Definition: NoLocalSettings.php:24
EditPage\$missingComment
bool $missingComment
Definition: EditPage.php:181
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:3789
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:1681
WikiPage\exists
exists()
Definition: WikiPage.php:635
Xml\element
static element( $element, $attribs=null, $contents='', $allowShortTag=true)
Format an XML element with given attributes and, optionally, text content.
Definition: Xml.php:41
Article\getPage
getPage()
Get the WikiPage object of this instance.
Definition: Article.php:237
Article\getContext
getContext()
Gets the context this Article is executed in.
Definition: Article.php:2332
EDIT_NEW
const EDIT_NEW
Definition: Defines.php:125
EditPage\getArticle
getArticle()
Definition: EditPage.php:483
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:20
ThrottledError
Show an error when the user hits a rate limit.
Definition: ThrottledError.php:28
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:4369
EditPage\previewOnOpen
previewOnOpen()
Should we show a preview when the edit form is first shown?
Definition: EditPage.php:866
EditPage\incrementEditFailureStats
incrementEditFailureStats( $failureType)
Definition: EditPage.php:4165
$title
$title
Definition: testCompression.php:38
EditPage\$allowSelfRedirect
bool $allowSelfRedirect
Definition: EditPage.php:199
EditPage\showEditForm
showEditForm( $formCallback=null)
Send the edit form and related headers to OutputPage.
Definition: EditPage.php:2886
Title\makeTitle
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:650
LogEventsList\showLogExtract
static showLogExtract(&$out, $types=[], $page='', $user='', $param=[])
Show log extract.
Definition: LogEventsList.php:607
EditPage\isUndoClean
isUndoClean(Content $content)
Does sanity checks and compares the automatically generated undo content with the one that was submit...
Definition: EditPage.php:2445
EditPage\wasDeletedSinceLastEdit
wasDeletedSinceLastEdit()
Check if a page was deleted while the user was editing it, before submit.
Definition: EditPage.php:3944
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
EditPage\getTemplates
getTemplates()
Definition: EditPage.php:4221
wfTimestampNow
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
Definition: GlobalFunctions.php:1721
EditPage\getPreviewParserOptions
getPreviewParserOptions()
Get parser options for a preview.
Definition: EditPage.php:4174
EditPage\$mContextTitle
null Title $mContextTitle
Definition: EditPage.php:139
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:3492
EditPage\getCancelLink
getCancelLink( $tabindex=0)
Definition: EditPage.php:3905
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:3626
Html\errorBox
static errorBox( $html, $heading='', $className='')
Return an error box.
Definition: Html.php:742
EditPage\initialiseForm
initialiseForm()
Initialise form fields in the object Called on the first invocation, e.g.
Definition: EditPage.php:1195
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:144
deprecatePublicProperty
deprecatePublicProperty( $property, $version, $class=null, $component=null)
Mark a property as deprecated.
Definition: DeprecationHelper.php:87
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:2537
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
MediaWiki\Permissions\Authority
This interface represents the authority associated the current execution context, such as a web reque...
Definition: Authority.php:35
Html\hidden
static hidden( $name, $value, array $attribs=[])
Convenience function to produce an input element with type=hidden.
Definition: Html.php:805
EditPage\$recreate
bool $recreate
Definition: EditPage.php:253
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:3532
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:4576
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:3462
EditPage\importContentFormData
importContentFormData(&$request)
Subpage overridable method for extracting the page content data from the posted form to be placed in ...
Definition: EditPage.php:1186
EditPage\$minoredit
bool $minoredit
Definition: EditPage.php:238
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:888
EditPage\showHeader
showHeader()
Definition: EditPage.php:3196
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:303
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:1583
EditPage\addNewLineAtEnd
addNewLineAtEnd( $wikitext)
Definition: EditPage.php:4672
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:1202
EditPage\incrementResolvedConflicts
incrementResolvedConflicts()
Log when a page was successfully saved after the edit conflict view.
Definition: EditPage.php:1700
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:132
Hooks\runner
static runner()
Get a HookRunner instance for calling hooks using the new interfaces.
Definition: Hooks.php:172
EditPage\showEditTools
showEditTools()
Inserts optional text shown below edit and upload forms.
Definition: EditPage.php:3737
EditPage\$preview
bool $preview
Definition: EditPage.php:232
EditPage\$isNew
bool $isNew
New page or new section.
Definition: EditPage.php:151
EditPage\getCheckboxesDefinition
getCheckboxesDefinition( $checked)
Return an array of checkbox definitions.
Definition: EditPage.php:4279
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\internalAttemptSave
internalAttemptSave(&$result, $markAsBot=false)
Attempt submission (no UI)
Definition: EditPage.php:1912
EditPage\getCopywarn
getCopywarn()
Get the copyright warning.
Definition: EditPage.php:3749
EditPage\setApiEditOverride
setApiEditOverride( $enableOverride)
Allow editing of content that supports API direct editing, but not general direct editing.
Definition: EditPage.php:540
wfEscapeWikiText
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
Definition: GlobalFunctions.php:1456
EditPage\$watchlistExpiry
string null $watchlistExpiry
The expiry time of the watch item, or null if it is not watched temporarily.
Definition: EditPage.php:250
EditPage\newTextConflictHelper
newTextConflictHelper( $submitButtonLabel)
Definition: EditPage.php:4731
EditPage\showCustomIntro
showCustomIntro()
Attempt to show a custom editing introduction, if supplied.
Definition: EditPage.php:2801
EditPage\getContext
getContext()
Definition: EditPage.php:491
MediaWiki\Revision\RevisionStoreRecord
A RevisionRecord representing an existing revision persisted in the revision table.
Definition: RevisionStoreRecord.php:40
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:100
EditPage\extractSectionTitle
static extractSectionTitle( $text)
Extract the section title from current section text, if any.
Definition: EditPage.php:3187
EditPage\makeTemplatesOnThisPageList
makeTemplatesOnThisPageList(array $templates)
Wrapper around TemplatesOnThisPageFormatter to make a "templates on this page" list.
Definition: EditPage.php:3163
EditPage\$textbox1
string $textbox1
Page content input field.
Definition: EditPage.php:258
EditPage\$parentRevId
int $parentRevId
Revision ID the edit is based on, adjusted when an edit conflict is resolved.
Definition: EditPage.php:315
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:2110
MediaWiki\EditPage\Constraint\UnicodeConstraint
Verify unicode constraint.
Definition: UnicodeConstraint.php:31
EditPage\$changeTags
null array $changeTags
Definition: EditPage.php:333
EditPage
The edit page/HTML interface (split from Article) The actual database and text munging is still in Ar...
Definition: EditPage.php:88
EditPage\noSuchSectionPage
noSuchSectionPage()
Creates a basic error page which informs the user that they have attempted to edit a nonexistent sect...
Definition: EditPage.php:4510
IContextSource
Interface for objects which can provide a MediaWiki context on request.
Definition: IContextSource.php:57
EditPage\$formtype
string $formtype
Definition: EditPage.php:157
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:3403
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:213
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:208
Title
Represents a title within MediaWiki.
Definition: Title.php:49
EditPage\$mShowSummaryField
bool $mShowSummaryField
Definition: EditPage.php:224
EditPage\$sectiontitle
string $sectiontitle
Definition: EditPage.php:295
EditPage\$starttime
string $starttime
Timestamp from the first time the edit form was rendered.
Definition: EditPage.php:300
EditPage\$suppressIntro
$suppressIntro
Definition: EditPage.php:354
ContentHandler\getContentText
static getContentText(Content $content=null)
Convenience function for getting flat text from a Content object.
Definition: ContentHandler.php:93
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:119
wfReadOnlyReason
wfReadOnlyReason()
Check if the site is in read-only mode and return the message if so.
Definition: GlobalFunctions.php:1112
MediaWiki\Revision\RevisionRecord\getId
getId( $wikiId=self::LOCAL)
Get revision ID.
Definition: RevisionRecord.php:279
EditPage\$scrolltop
int null $scrolltop
Definition: EditPage.php:321
EditPage\$deletedSinceEdit
bool $deletedSinceEdit
Definition: EditPage.php:154
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:196
EditPage\$edit
bool $edit
Definition: EditPage.php:357
EditPage\isSupportedContentModel
isSupportedContentModel( $modelId)
Returns if the given content model is editable.
Definition: EditPage.php:529
EditPage\$mPreloadContent
$mPreloadContent
Definition: EditPage.php:347
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:3886
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:1628
EditPage\getSubmitButtonLabel
getSubmitButtonLabel()
Get the message key of the label for the button to save the page.
Definition: EditPage.php:4419
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:247
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:276
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:235
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:4197
EditPage\$action
string $action
Definition: EditPage.php:142
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:254
Html\rawElement
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:212
EditPage\setEditConflictHelperFactory
setEditConflictHelperFactory(callable $factory)
Set a factory function to create an EditConflictHelper.
Definition: EditPage.php:4707
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:3567
EditPage\getTitle
getTitle()
Definition: EditPage.php:499
ManualLogEntry
Class for creating new log entries and inserting them into the database.
Definition: ManualLogEntry.php:43
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:1535
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:148
EditPage\displayViewSourcePage
displayViewSourcePage(Content $content, $errorMessage='')
Display a read-only View Source page.
Definition: EditPage.php:813
Article
Class for viewing MediaWiki article and history.
Definition: Article.php:49
EditPage\getContentObject
getContentObject( $def_content=null)
Definition: EditPage.php:1265
EditPage\showStandardInputs
showStandardInputs(&$tabindex=2)
Definition: EditPage.php:3836
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:660
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:106
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:1660
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:1494
Linker\accesskey
static accesskey( $name)
Given the id of an interface element, constructs the appropriate accesskey attribute from the system ...
Definition: Linker.php:2158
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:145
EditPage\isWrongCaseUserConfigPage
isWrongCaseUserConfigPage()
Checks whether the user entered a skin name in uppercase, e.g.
Definition: EditPage.php:907
EditPage\$incompleteForm
bool $incompleteForm
Definition: EditPage.php:175
EditPage\$missingSummary
bool $missingSummary
Definition: EditPage.php:184
EditPage\getEditConflictHelper
getEditConflictHelper()
Definition: EditPage.php:4715
ExternalUserNames\isExternal
static isExternal( $username)
Tells whether the username is external or not.
Definition: ExternalUserNames.php:149
EditPage\$markAsBot
bool $markAsBot
Definition: EditPage.php:324
EditPage\getCopyrightWarning
static getCopyrightWarning( $title, $format='plain', $langcode=null)
Get the copyright warning, by default returns wikitext.
Definition: EditPage.php:3761
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
$wgDisableAnonTalk
$wgDisableAnonTalk
Disable links to talk pages of anonymous users (IPs) in listings on special pages like page history,...
Definition: DefaultSettings.php:8111
Skin\makeInternalOrExternalUrl
static makeInternalOrExternalUrl( $name)
If url string starts with http, consider as external URL, else internal.
Definition: Skin.php:1370
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:2507
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:1717
ParserOptions\newFromUser
static newFromUser( $user)
Get a ParserOptions object from a given user.
Definition: ParserOptions.php:1075
EditPage\$hookError
string $hookError
Definition: EditPage.php:205
EditPage\$allowBlankArticle
bool $allowBlankArticle
Definition: EditPage.php:193
EditPage\toEditText
toEditText( $content)
Gets an editable textual representation of $content.
Definition: EditPage.php:2835
Xml\checkLabel
static checkLabel( $label, $name, $id, $checked=false, $attribs=[])
Convenience function to build an HTML checkbox with a label.
Definition: Xml.php:425
EditPage\setHeaders
setHeaders()
Definition: EditPage.php:2602
EditPage\guessSectionName
guessSectionName( $text)
Turns section name wikitext into anchors for use in HTTP redirects.
Definition: EditPage.php:4686
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:270
EditPage\getPreviewText
getPreviewText()
Get the rendered text for previewing.
Definition: EditPage.php:4016