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