MediaWiki  master
EditPage.php
Go to the documentation of this file.
1 <?php
34 use Wikimedia\ScopedCallback;
35 
51 class EditPage {
52 
54 
58  public const UNICODE_CHECK = 'ℳ𝒲♥𝓊𝓃𝒾𝒸ℴ𝒹ℯ';
59 
63  public const AS_SUCCESS_UPDATE = 200;
64 
68  public const AS_SUCCESS_NEW_ARTICLE = 201;
69 
73  public const AS_HOOK_ERROR = 210;
74 
78  public const AS_HOOK_ERROR_EXPECTED = 212;
79 
83  public const AS_BLOCKED_PAGE_FOR_USER = 215;
84 
88  public const AS_CONTENT_TOO_BIG = 216;
89 
93  public const AS_READ_ONLY_PAGE_ANON = 218;
94 
98  public const AS_READ_ONLY_PAGE_LOGGED = 219;
99 
103  public const AS_READ_ONLY_PAGE = 220;
104 
108  public const AS_RATE_LIMITED = 221;
109 
114  public const AS_ARTICLE_WAS_DELETED = 222;
115 
120  public const AS_NO_CREATE_PERMISSION = 223;
121 
125  public const AS_BLANK_ARTICLE = 224;
126 
130  public const AS_CONFLICT_DETECTED = 225;
131 
136  public const AS_SUMMARY_NEEDED = 226;
137 
141  public const AS_TEXTBOX_EMPTY = 228;
142 
146  public const AS_MAX_ARTICLE_SIZE_EXCEEDED = 229;
147 
151  public const AS_END = 231;
152 
156  public const AS_SPAM_ERROR = 232;
157 
161  public const AS_IMAGE_REDIRECT_ANON = 233;
162 
166  public const AS_IMAGE_REDIRECT_LOGGED = 234;
167 
172  public const AS_NO_CHANGE_CONTENT_MODEL = 235;
173 
178  public const AS_SELF_REDIRECT = 236;
179 
184  public const AS_CHANGE_TAG_ERROR = 237;
185 
189  public const AS_PARSE_ERROR = 240;
190 
197  public const AS_CANNOT_USE_CUSTOM_MODEL = 241;
198 
202  public const AS_UNICODE_NOT_SUPPORTED = 242;
203 
207  public const EDITFORM_ID = 'editform';
208 
213  public const POST_EDIT_COOKIE_KEY_PREFIX = 'PostEditRevision';
214 
228  public const POST_EDIT_COOKIE_DURATION = 1200;
229 
234  public $mArticle;
236  private $page;
237 
242  public $mTitle;
243 
245  private $mContextTitle = null;
246 
248  public $action = 'submit';
249 
254  public $isConflict = false;
255 
257  public $isNew = false;
258 
261 
263  public $formtype;
264 
269  public $firsttime;
270 
272  public $lastDelete;
273 
275  public $mTokenOk = false;
276 
278  public $mTokenOkExceptSuffix = false;
279 
281  public $mTriedSave = false;
282 
284  public $incompleteForm = false;
285 
287  public $tooBig = false;
288 
290  public $missingComment = false;
291 
293  public $missingSummary = false;
294 
296  public $allowBlankSummary = false;
297 
299  protected $blankArticle = false;
300 
302  protected $allowBlankArticle = false;
303 
305  protected $selfRedirect = false;
306 
308  protected $allowSelfRedirect = false;
309 
311  public $autoSumm = '';
312 
314  private $hookError = '';
315 
318 
320  public $hasPresetSummary = false;
321 
330  protected $mBaseRevision = false;
331 
338  private $mExpectedParentRevision = false;
339 
341  public $mShowSummaryField = true;
342 
343  # Form values
344 
346  public $save = false;
347 
349  public $preview = false;
350 
352  public $diff = false;
353 
355  public $minoredit = false;
356 
358  public $watchthis = false;
359 
361  public $recreate = false;
362 
366  public $textbox1 = '';
367 
369  public $textbox2 = '';
370 
372  public $summary = '';
373 
377  public $nosummary = false;
378 
383  public $edittime = '';
384 
396  private $editRevId = null;
397 
399  public $section = '';
400 
402  public $sectiontitle = '';
403 
407  public $starttime = '';
408 
414  public $oldid = 0;
415 
421  public $parentRevId = 0;
422 
424  public $editintro = '';
425 
427  public $scrolltop = null;
428 
430  public $markAsBot = true;
431 
434 
436  public $contentFormat = null;
437 
439  private $changeTags = null;
440 
441  # Placeholders for text injection by hooks (must be HTML)
442  # extensions should take care to _append_ to the present value
443 
445  public $editFormPageTop = '';
446  public $editFormTextTop = '';
450  public $editFormTextBottom = '';
453  public $mPreloadContent = null;
454 
455  /* $didSave should be set to true whenever an article was successfully altered. */
456  public $didSave = false;
457  public $undidRev = 0;
458 
459  public $suppressIntro = false;
460 
462  protected $edit;
463 
465  protected $contentLength = false;
466 
470  private $enableApiEditOverride = false;
471 
475  protected $context;
476 
480  private $isOldRev = false;
481 
485  private $unicodeCheck;
486 
493 
498 
503 
507  private $permManager;
508 
512  private $revisionStore;
513 
517  public function __construct( Article $article ) {
518  $this->mArticle = $article;
519  $this->page = $article->getPage(); // model object
520  $this->mTitle = $article->getTitle();
521 
522  // Make sure the local context is in sync with other member variables.
523  // Particularly make sure everything is using the same WikiPage instance.
524  // This should probably be the case in Article as well, but it's
525  // particularly important for EditPage, to make use of the in-place caching
526  // facility in WikiPage::prepareContentForEdit.
527  $this->context = new DerivativeContext( $article->getContext() );
528  $this->context->setWikiPage( $this->page );
529  $this->context->setTitle( $this->mTitle );
530 
531  $this->contentModel = $this->mTitle->getContentModel();
532 
533  $services = MediaWikiServices::getInstance();
534  $this->contentHandlerFactory = $services->getContentHandlerFactory();
535  $this->contentFormat = $this->contentHandlerFactory
536  ->getContentHandler( $this->contentModel )
537  ->getDefaultFormat();
538  $this->editConflictHelperFactory = [ $this, 'newTextConflictHelper' ];
539  $this->permManager = $services->getPermissionManager();
540  $this->revisionStore = $services->getRevisionStore();
541 
542  $this->deprecatePublicProperty( 'mBaseRevision', '1.35', __CLASS__ );
543  }
544 
548  public function getArticle() {
549  return $this->mArticle;
550  }
551 
556  public function getContext() {
557  return $this->context;
558  }
559 
564  public function getTitle() {
565  return $this->mTitle;
566  }
567 
573  public function setContextTitle( $title ) {
574  $this->mContextTitle = $title;
575  }
576 
585  public function getContextTitle() {
586  if ( $this->mContextTitle === null ) {
587  wfDeprecated( get_class( $this ) . '::getContextTitle called with no title set', '1.32' );
588  global $wgTitle;
589  return $wgTitle;
590  } else {
591  return $this->mContextTitle;
592  }
593  }
594 
602  public function isSupportedContentModel( $modelId ) {
603  return $this->enableApiEditOverride === true ||
604  $this->contentHandlerFactory->getContentHandler( $modelId )->supportsDirectEditing();
605  }
606 
613  public function setApiEditOverride( $enableOverride ) {
614  $this->enableApiEditOverride = $enableOverride;
615  }
616 
628  public function edit() {
629  // Allow extensions to modify/prevent this form or submission
630  if ( !Hooks::run( 'AlternateEdit', [ $this ] ) ) {
631  return;
632  }
633 
634  wfDebug( __METHOD__ . ": enter\n" );
635 
636  $request = $this->context->getRequest();
637  // If they used redlink=1 and the page exists, redirect to the main article
638  if ( $request->getBool( 'redlink' ) && $this->mTitle->exists() ) {
639  $this->context->getOutput()->redirect( $this->mTitle->getFullURL() );
640  return;
641  }
642 
643  $this->importFormData( $request );
644  $this->firsttime = false;
645 
646  if ( wfReadOnly() && $this->save ) {
647  // Force preview
648  $this->save = false;
649  $this->preview = true;
650  }
651 
652  if ( $this->save ) {
653  $this->formtype = 'save';
654  } elseif ( $this->preview ) {
655  $this->formtype = 'preview';
656  } elseif ( $this->diff ) {
657  $this->formtype = 'diff';
658  } else { # First time through
659  $this->firsttime = true;
660  if ( $this->previewOnOpen() ) {
661  $this->formtype = 'preview';
662  } else {
663  $this->formtype = 'initial';
664  }
665  }
666 
667  $permErrors = $this->getEditPermissionErrors(
668  $this->save ? PermissionManager::RIGOR_SECURE : PermissionManager::RIGOR_FULL
669  );
670  if ( $permErrors ) {
671  wfDebug( __METHOD__ . ": User can't edit\n" );
672 
673  if ( $this->context->getUser()->getBlock() ) {
674  // Auto-block user's IP if the account was "hard" blocked
675  if ( !wfReadOnly() ) {
677  $this->context->getUser()->spreadAnyEditBlock();
678  } );
679  }
680  }
681  $this->displayPermissionsError( $permErrors );
682 
683  return;
684  }
685 
686  $revRecord = $this->mArticle->fetchRevisionRecord();
687  // Disallow editing revisions with content models different from the current one
688  // Undo edits being an exception in order to allow reverting content model changes.
689  $revContentModel = $revRecord ?
690  $revRecord->getSlot( SlotRecord::MAIN, RevisionRecord::RAW )->getModel() :
691  false;
692  if ( $revContentModel && $revContentModel !== $this->contentModel ) {
693  $prevRev = null;
694  if ( $this->undidRev ) {
695  $undidRevRecord = $this->revisionStore
696  ->getRevisionById( $this->undidRev );
697  $prevRevRecord = $undidRevRecord ?
698  $this->revisionStore->getPreviousRevision( $undidRevRecord ) :
699  null;
700 
701  $prevContentModel = $prevRevRecord ?
702  $prevRevRecord
703  ->getSlot( SlotRecord::MAIN, RevisionRecord::RAW )
704  ->getModel() :
705  '';
706  }
707 
708  if ( !$this->undidRev
709  || !$prevRevRecord
710  || $prevContentModel !== $this->contentModel
711  ) {
712  $this->displayViewSourcePage(
713  $this->getContentObject(),
714  $this->context->msg(
715  'contentmodelediterror',
716  $revContentModel,
717  $this->contentModel
718  )->plain()
719  );
720  return;
721  }
722  }
723 
724  $this->isConflict = false;
725 
726  # Show applicable editing introductions
727  if ( $this->formtype == 'initial' || $this->firsttime ) {
728  $this->showIntro();
729  }
730 
731  # Attempt submission here. This will check for edit conflicts,
732  # and redundantly check for locked database, blocked IPs, etc.
733  # that edit() already checked just in case someone tries to sneak
734  # in the back door with a hand-edited submission URL.
735 
736  if ( $this->formtype == 'save' ) {
737  $resultDetails = null;
738  $status = $this->attemptSave( $resultDetails );
739  if ( !$this->handleStatus( $status, $resultDetails ) ) {
740  return;
741  }
742  }
743 
744  # First time through: get contents, set time for conflict
745  # checking, etc.
746  if ( $this->formtype == 'initial' || $this->firsttime ) {
747  if ( $this->initialiseForm() === false ) {
748  return;
749  }
750 
751  if ( !$this->mTitle->getArticleID() ) {
752  Hooks::run( 'EditFormPreloadText', [ &$this->textbox1, &$this->mTitle ] );
753  } else {
754  Hooks::run( 'EditFormInitialText', [ $this ] );
755  }
756 
757  }
758 
759  $this->showEditForm();
760  }
761 
766  protected function getEditPermissionErrors( $rigor = PermissionManager::RIGOR_SECURE ) {
767  $user = $this->context->getUser();
768  $permErrors = $this->permManager->getPermissionErrors(
769  'edit',
770  $user,
771  $this->mTitle,
772  $rigor
773  );
774  # Can this title be created?
775  if ( !$this->mTitle->exists() ) {
776  $permErrors = array_merge(
777  $permErrors,
778  wfArrayDiff2(
779  $this->permManager->getPermissionErrors(
780  'create',
781  $user,
782  $this->mTitle,
783  $rigor
784  ),
785  $permErrors
786  )
787  );
788  }
789  # Ignore some permissions errors when a user is just previewing/viewing diffs
790  $remove = [];
791  foreach ( $permErrors as $error ) {
792  if ( ( $this->preview || $this->diff )
793  && (
794  $error[0] == 'blockedtext' ||
795  $error[0] == 'autoblockedtext' ||
796  $error[0] == 'systemblockedtext'
797  )
798  ) {
799  $remove[] = $error;
800  }
801  }
802  $permErrors = wfArrayDiff2( $permErrors, $remove );
803 
804  return $permErrors;
805  }
806 
819  protected function displayPermissionsError( array $permErrors ) {
820  $out = $this->context->getOutput();
821  if ( $this->context->getRequest()->getBool( 'redlink' ) ) {
822  // The edit page was reached via a red link.
823  // Redirect to the article page and let them click the edit tab if
824  // they really want a permission error.
825  $out->redirect( $this->mTitle->getFullURL() );
826  return;
827  }
828 
829  $content = $this->getContentObject();
830 
831  # Use the normal message if there's nothing to display
832  if ( $this->firsttime && ( !$content || $content->isEmpty() ) ) {
833  $action = $this->mTitle->exists() ? 'edit' :
834  ( $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage' );
835  throw new PermissionsError( $action, $permErrors );
836  }
837 
838  $this->displayViewSourcePage(
839  $content,
840  $out->formatPermissionsErrorMessage( $permErrors, 'edit' )
841  );
842  }
843 
849  protected function displayViewSourcePage( Content $content, $errorMessage = '' ) {
850  $out = $this->context->getOutput();
851  Hooks::run( 'EditPage::showReadOnlyForm:initial', [ $this, &$out ] );
852 
853  $out->setRobotPolicy( 'noindex,nofollow' );
854  $out->setPageTitle( $this->context->msg(
855  'viewsource-title',
856  $this->getContextTitle()->getPrefixedText()
857  ) );
858  $out->addBacklinkSubtitle( $this->getContextTitle() );
859  $out->addHTML( $this->editFormPageTop );
860  $out->addHTML( $this->editFormTextTop );
861 
862  if ( $errorMessage !== '' ) {
863  $out->addWikiTextAsInterface( $errorMessage );
864  $out->addHTML( "<hr />\n" );
865  }
866 
867  # If the user made changes, preserve them when showing the markup
868  # (This happens when a user is blocked during edit, for instance)
869  if ( !$this->firsttime ) {
870  $text = $this->textbox1;
871  $out->addWikiMsg( 'viewyourtext' );
872  } else {
873  try {
874  $text = $this->toEditText( $content );
875  } catch ( MWException $e ) {
876  # Serialize using the default format if the content model is not supported
877  # (e.g. for an old revision with a different model)
878  $text = $content->serialize();
879  }
880  $out->addWikiMsg( 'viewsourcetext' );
881  }
882 
883  $out->addHTML( $this->editFormTextBeforeContent );
884  $this->showTextbox( $text, 'wpTextbox1', [ 'readonly' ] );
885  $out->addHTML( $this->editFormTextAfterContent );
886 
887  $out->addHTML( $this->makeTemplatesOnThisPageList( $this->getTemplates() ) );
888 
889  $out->addModules( 'mediawiki.action.edit.collapsibleFooter' );
890 
891  $out->addHTML( $this->editFormTextBottom );
892  if ( $this->mTitle->exists() ) {
893  $out->returnToMain( null, $this->mTitle );
894  }
895  }
896 
902  protected function previewOnOpen() {
903  $config = $this->context->getConfig();
904  $previewOnOpenNamespaces = $config->get( 'PreviewOnOpenNamespaces' );
905  $request = $this->context->getRequest();
906  if ( $config->get( 'RawHtml' ) ) {
907  // If raw HTML is enabled, disable preview on open
908  // since it has to be posted with a token for
909  // security reasons
910  return false;
911  }
912  if ( $request->getVal( 'preview' ) == 'yes' ) {
913  // Explicit override from request
914  return true;
915  } elseif ( $request->getVal( 'preview' ) == 'no' ) {
916  // Explicit override from request
917  return false;
918  } elseif ( $this->section == 'new' ) {
919  // Nothing *to* preview for new sections
920  return false;
921  } elseif ( ( $request->getCheck( 'preload' ) || $this->mTitle->exists() )
922  && $this->context->getUser()->getOption( 'previewonfirst' )
923  ) {
924  // Standard preference behavior
925  return true;
926  } elseif ( !$this->mTitle->exists()
927  && isset( $previewOnOpenNamespaces[$this->mTitle->getNamespace()] )
928  && $previewOnOpenNamespaces[$this->mTitle->getNamespace()]
929  ) {
930  // Categories are special
931  return true;
932  } else {
933  return false;
934  }
935  }
936 
943  protected function isWrongCaseUserConfigPage() {
944  if ( $this->mTitle->isUserConfigPage() ) {
945  $name = $this->mTitle->getSkinFromConfigSubpage();
946  $skins = array_merge(
947  array_keys( Skin::getSkinNames() ),
948  [ 'common' ]
949  );
950  return !in_array( $name, $skins )
951  && in_array( strtolower( $name ), $skins );
952  } else {
953  return false;
954  }
955  }
956 
964  protected function isSectionEditSupported() {
965  return $this->contentHandlerFactory
966  ->getContentHandler( $this->mTitle->getContentModel() )
967  ->supportsSections();
968  }
969 
975  public function importFormData( &$request ) {
976  # Section edit can come from either the form or a link
977  $this->section = $request->getVal( 'wpSection', $request->getVal( 'section' ) );
978 
979  if ( $this->section !== null && $this->section !== '' && !$this->isSectionEditSupported() ) {
980  throw new ErrorPageError( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
981  }
982 
983  $this->isNew = !$this->mTitle->exists() || $this->section == 'new';
984 
985  if ( $request->wasPosted() ) {
986  # These fields need to be checked for encoding.
987  # Also remove trailing whitespace, but don't remove _initial_
988  # whitespace from the text boxes. This may be significant formatting.
989  $this->textbox1 = rtrim( $request->getText( 'wpTextbox1' ) );
990  if ( !$request->getCheck( 'wpTextbox2' ) ) {
991  // Skip this if wpTextbox2 has input, it indicates that we came
992  // from a conflict page with raw page text, not a custom form
993  // modified by subclasses
994  $textbox1 = $this->importContentFormData( $request );
995  if ( $textbox1 !== null ) {
996  $this->textbox1 = $textbox1;
997  }
998  }
999 
1000  $this->unicodeCheck = $request->getText( 'wpUnicodeCheck' );
1001 
1002  $this->summary = $request->getText( 'wpSummary' );
1003 
1004  # If the summary consists of a heading, e.g. '==Foobar==', extract the title from the
1005  # header syntax, e.g. 'Foobar'. This is mainly an issue when we are using wpSummary for
1006  # section titles.
1007  $this->summary = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->summary );
1008 
1009  # Treat sectiontitle the same way as summary.
1010  # Note that wpSectionTitle is not yet a part of the actual edit form, as wpSummary is
1011  # currently doing double duty as both edit summary and section title. Right now this
1012  # is just to allow API edits to work around this limitation, but this should be
1013  # incorporated into the actual edit form when EditPage is rewritten (T20654, T28312).
1014  $this->sectiontitle = $request->getText( 'wpSectionTitle' );
1015  $this->sectiontitle = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->sectiontitle );
1016 
1017  $this->edittime = $request->getVal( 'wpEdittime' );
1018  $this->editRevId = $request->getIntOrNull( 'editRevId' );
1019  $this->starttime = $request->getVal( 'wpStarttime' );
1020 
1021  $undidRev = $request->getInt( 'wpUndidRevision' );
1022  if ( $undidRev ) {
1023  $this->undidRev = $undidRev;
1024  }
1025 
1026  $this->scrolltop = $request->getIntOrNull( 'wpScrolltop' );
1027 
1028  if ( $this->textbox1 === '' && !$request->getCheck( 'wpTextbox1' ) ) {
1029  // wpTextbox1 field is missing, possibly due to being "too big"
1030  // according to some filter rules such as Suhosin's setting for
1031  // suhosin.request.max_value_length (d'oh)
1032  $this->incompleteForm = true;
1033  } else {
1034  // If we receive the last parameter of the request, we can fairly
1035  // claim the POST request has not been truncated.
1036  $this->incompleteForm = !$request->getVal( 'wpUltimateParam' );
1037  }
1038  if ( $this->incompleteForm ) {
1039  # If the form is incomplete, force to preview.
1040  wfDebug( __METHOD__ . ": Form data appears to be incomplete\n" );
1041  wfDebug( "POST DATA: " . var_export( $request->getPostValues(), true ) . "\n" );
1042  $this->preview = true;
1043  } else {
1044  $this->preview = $request->getCheck( 'wpPreview' );
1045  $this->diff = $request->getCheck( 'wpDiff' );
1046 
1047  // Remember whether a save was requested, so we can indicate
1048  // if we forced preview due to session failure.
1049  $this->mTriedSave = !$this->preview;
1050 
1051  if ( $this->tokenOk( $request ) ) {
1052  # Some browsers will not report any submit button
1053  # if the user hits enter in the comment box.
1054  # The unmarked state will be assumed to be a save,
1055  # if the form seems otherwise complete.
1056  wfDebug( __METHOD__ . ": Passed token check.\n" );
1057  } elseif ( $this->diff ) {
1058  # Failed token check, but only requested "Show Changes".
1059  wfDebug( __METHOD__ . ": Failed token check; Show Changes requested.\n" );
1060  } else {
1061  # Page might be a hack attempt posted from
1062  # an external site. Preview instead of saving.
1063  wfDebug( __METHOD__ . ": Failed token check; forcing preview\n" );
1064  $this->preview = true;
1065  }
1066  }
1067  $this->save = !$this->preview && !$this->diff;
1068  if ( !preg_match( '/^\d{14}$/', $this->edittime ) ) {
1069  $this->edittime = null;
1070  }
1071 
1072  if ( !preg_match( '/^\d{14}$/', $this->starttime ) ) {
1073  $this->starttime = null;
1074  }
1075 
1076  $this->recreate = $request->getCheck( 'wpRecreate' );
1077 
1078  $this->minoredit = $request->getCheck( 'wpMinoredit' );
1079  $this->watchthis = $request->getCheck( 'wpWatchthis' );
1080 
1081  $user = $this->context->getUser();
1082  # Don't force edit summaries when a user is editing their own user or talk page
1083  if ( ( $this->mTitle->mNamespace == NS_USER || $this->mTitle->mNamespace == NS_USER_TALK )
1084  && $this->mTitle->getText() == $user->getName()
1085  ) {
1086  $this->allowBlankSummary = true;
1087  } else {
1088  $this->allowBlankSummary = $request->getBool( 'wpIgnoreBlankSummary' )
1089  || !$user->getOption( 'forceeditsummary' );
1090  }
1091 
1092  $this->autoSumm = $request->getText( 'wpAutoSummary' );
1093 
1094  $this->allowBlankArticle = $request->getBool( 'wpIgnoreBlankArticle' );
1095  $this->allowSelfRedirect = $request->getBool( 'wpIgnoreSelfRedirect' );
1096 
1097  $changeTags = $request->getVal( 'wpChangeTags' );
1098  if ( $changeTags === null || $changeTags === '' ) {
1099  $this->changeTags = [];
1100  } else {
1101  $this->changeTags = array_filter( array_map( 'trim', explode( ',',
1102  $changeTags ) ) );
1103  }
1104  } else {
1105  # Not a posted form? Start with nothing.
1106  wfDebug( __METHOD__ . ": Not a posted form.\n" );
1107  $this->textbox1 = '';
1108  $this->summary = '';
1109  $this->sectiontitle = '';
1110  $this->edittime = '';
1111  $this->editRevId = null;
1112  $this->starttime = wfTimestampNow();
1113  $this->edit = false;
1114  $this->preview = false;
1115  $this->save = false;
1116  $this->diff = false;
1117  $this->minoredit = false;
1118  // Watch may be overridden by request parameters
1119  $this->watchthis = $request->getBool( 'watchthis', false );
1120  $this->recreate = false;
1121 
1122  // When creating a new section, we can preload a section title by passing it as the
1123  // preloadtitle parameter in the URL (T15100)
1124  if ( $this->section == 'new' && $request->getVal( 'preloadtitle' ) ) {
1125  $this->sectiontitle = $request->getVal( 'preloadtitle' );
1126  // Once wpSummary isn't being use for setting section titles, we should delete this.
1127  $this->summary = $request->getVal( 'preloadtitle' );
1128  } elseif ( $this->section != 'new' && $request->getVal( 'summary' ) !== '' ) {
1129  $this->summary = $request->getText( 'summary' );
1130  if ( $this->summary !== '' ) {
1131  $this->hasPresetSummary = true;
1132  }
1133  }
1134 
1135  if ( $request->getVal( 'minor' ) ) {
1136  $this->minoredit = true;
1137  }
1138  }
1139 
1140  $this->oldid = $request->getInt( 'oldid' );
1141  $this->parentRevId = $request->getInt( 'parentRevId' );
1142 
1143  $this->markAsBot = $request->getBool( 'bot', true );
1144  $this->nosummary = $request->getBool( 'nosummary' );
1145 
1146  // May be overridden by revision.
1147  $this->contentModel = $request->getText( 'model', $this->contentModel );
1148  // May be overridden by revision.
1149  $this->contentFormat = $request->getText( 'format', $this->contentFormat );
1150 
1151  try {
1152  $handler = $this->contentHandlerFactory->getContentHandler( $this->contentModel );
1153  } catch ( MWUnknownContentModelException $e ) {
1154  throw new ErrorPageError(
1155  'editpage-invalidcontentmodel-title',
1156  'editpage-invalidcontentmodel-text',
1157  [ wfEscapeWikiText( $this->contentModel ) ]
1158  );
1159  }
1160 
1161  if ( !$handler->isSupportedFormat( $this->contentFormat ) ) {
1162  throw new ErrorPageError(
1163  'editpage-notsupportedcontentformat-title',
1164  'editpage-notsupportedcontentformat-text',
1165  [
1166  wfEscapeWikiText( $this->contentFormat ),
1167  wfEscapeWikiText( ContentHandler::getLocalizedName( $this->contentModel ) )
1168  ]
1169  );
1170  }
1171 
1178  $this->editintro = $request->getText( 'editintro',
1179  // Custom edit intro for new sections
1180  $this->section === 'new' ? 'MediaWiki:addsection-editintro' : '' );
1181 
1182  // Allow extensions to modify form data
1183  Hooks::run( 'EditPage::importFormData', [ $this, $request ] );
1184  }
1185 
1195  protected function importContentFormData( &$request ) {
1196  return null; // Don't do anything, EditPage already extracted wpTextbox1
1197  }
1198 
1204  public function initialiseForm() {
1205  $this->edittime = $this->page->getTimestamp();
1206  $this->editRevId = $this->page->getLatest();
1207 
1208  $dummy = $this->contentHandlerFactory
1209  ->getContentHandler( $this->contentModel )
1210  ->makeEmptyContent();
1211  $content = $this->getContentObject( $dummy ); # TODO: track content object?!
1212  if ( $content === $dummy ) { // Invalid section
1213  $this->noSuchSectionPage();
1214  return false;
1215  }
1216 
1217  if ( !$content ) {
1218  $out = $this->context->getOutput();
1219  $this->editFormPageTop .= Html::rawElement(
1220  'div', [ 'class' => 'errorbox' ],
1221  $out->parseAsInterface( $this->context->msg( 'missing-revision-content',
1222  $this->oldid,
1223  Message::plaintextParam( $this->mTitle->getPrefixedText() )
1224  ) )
1225  );
1226  } elseif ( !$this->isSupportedContentModel( $content->getModel() ) ) {
1227  $modelMsg = $this->getContext()->msg( 'content-model-' . $content->getModel() );
1228  $modelName = $modelMsg->exists() ? $modelMsg->text() : $content->getModel();
1229 
1230  $out = $this->context->getOutput();
1231  $out->showErrorPage(
1232  'modeleditnotsupported-title',
1233  'modeleditnotsupported-text',
1234  [ $modelName ]
1235  );
1236  return false;
1237  }
1238 
1239  $this->textbox1 = $this->toEditText( $content );
1240 
1241  $user = $this->context->getUser();
1242  // activate checkboxes if user wants them to be always active
1243  # Sort out the "watch" checkbox
1244  if ( $user->getOption( 'watchdefault' ) ) {
1245  # Watch all edits
1246  $this->watchthis = true;
1247  } elseif ( $user->getOption( 'watchcreations' ) && !$this->mTitle->exists() ) {
1248  # Watch creations
1249  $this->watchthis = true;
1250  } elseif ( $user->isWatched( $this->mTitle ) ) {
1251  # Already watched
1252  $this->watchthis = true;
1253  }
1254  if ( $user->getOption( 'minordefault' ) && !$this->isNew ) {
1255  $this->minoredit = true;
1256  }
1257  if ( $this->textbox1 === false ) {
1258  return false;
1259  }
1260  return true;
1261  }
1262 
1270  protected function getContentObject( $def_content = null ) {
1271  global $wgDisableAnonTalk;
1272 
1273  $content = false;
1274 
1275  $user = $this->context->getUser();
1276  $request = $this->context->getRequest();
1277  // For message page not locally set, use the i18n message.
1278  // For other non-existent articles, use preload text if any.
1279  if ( !$this->mTitle->exists() || $this->section == 'new' ) {
1280  if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && $this->section != 'new' ) {
1281  # If this is a system message, get the default text.
1282  $msg = $this->mTitle->getDefaultMessageText();
1283 
1284  $content = $this->toEditContent( $msg );
1285  }
1286  if ( $content === false ) {
1287  # If requested, preload some text.
1288  $preload = $request->getVal( 'preload',
1289  // Custom preload text for new sections
1290  $this->section === 'new' ? 'MediaWiki:addsection-preload' : '' );
1291  $params = $request->getArray( 'preloadparams', [] );
1292 
1293  $content = $this->getPreloadedContent( $preload, $params );
1294  }
1295  // For existing pages, get text based on "undo" or section parameters.
1296  } elseif ( $this->section != '' ) {
1297  // Get section edit text (returns $def_text for invalid sections)
1298  $orig = $this->getOriginalContent( $user );
1299  $content = $orig ? $orig->getSection( $this->section ) : null;
1300 
1301  if ( !$content ) {
1302  $content = $def_content;
1303  }
1304  } else {
1305  $undoafter = $request->getInt( 'undoafter' );
1306  $undo = $request->getInt( 'undo' );
1307 
1308  if ( $undo > 0 && $undoafter > 0 ) {
1309  $undorev = $this->revisionStore->getRevisionById( $undo );
1310  $oldrev = $this->revisionStore->getRevisionById( $undoafter );
1311  $undoMsg = null;
1312 
1313  # Sanity check, make sure it's the right page,
1314  # the revisions exist and they were not deleted.
1315  # Otherwise, $content will be left as-is.
1316  if ( $undorev !== null && $oldrev !== null &&
1317  !$undorev->isDeleted( RevisionRecord::DELETED_TEXT ) &&
1318  !$oldrev->isDeleted( RevisionRecord::DELETED_TEXT )
1319  ) {
1320  if ( WikiPage::hasDifferencesOutsideMainSlot( $undorev, $oldrev )
1321  || !$this->isSupportedContentModel(
1322  $oldrev->getSlot( SlotRecord::MAIN, RevisionRecord::RAW )->getModel()
1323  )
1324  ) {
1325  // Hack for undo while EditPage can't handle multi-slot editing
1326  $this->context->getOutput()->redirect( $this->mTitle->getFullURL( [
1327  'action' => 'mcrundo',
1328  'undo' => $undo,
1329  'undoafter' => $undoafter,
1330  ] ) );
1331  return false;
1332  } else {
1333  $handler = $this->contentHandlerFactory
1334  ->getContentHandler( $undorev->getSlot(
1335  SlotRecord::MAIN,
1336  RevisionRecord::RAW
1337  )->getModel() );
1338  $currentContent = $this->page->getRevisionRecord()
1339  ->getContent( SlotRecord::MAIN );
1340  $undoContent = $undorev->getContent( SlotRecord::MAIN );
1341  $undoAfterContent = $oldrev->getContent( SlotRecord::MAIN );
1342  $undoIsLatest = $this->page->getRevisionRecord()->getId() === $undorev->getId();
1343  $content = $handler->getUndoContent(
1344  $currentContent,
1345  $undoContent,
1346  $undoAfterContent,
1347  $undoIsLatest
1348  );
1349 
1350  if ( $content === false ) {
1351  # Warn the user that something went wrong
1352  $undoMsg = 'failure';
1353  }
1354  }
1355 
1356  if ( $undoMsg === null ) {
1357  $oldContent = $this->page->getContent( RevisionRecord::RAW );
1359  $user, MediaWikiServices::getInstance()->getContentLanguage() );
1360  $newContent = $content->preSaveTransform( $this->mTitle, $user, $popts );
1361  if ( $newContent->getModel() !== $oldContent->getModel() ) {
1362  // The undo may change content
1363  // model if its reverting the top
1364  // edit. This can result in
1365  // mismatched content model/format.
1366  $this->contentModel = $newContent->getModel();
1367  $oldMainSlot = $oldrev->getSlot(
1368  SlotRecord::MAIN,
1369  RevisionRecord::RAW
1370  );
1371  $this->contentFormat = $oldMainSlot->getFormat();
1372  if ( $this->contentFormat === null ) {
1373  $this->contentFormat = $this->contentHandlerFactory
1374  ->getContentHandler( $oldMainSlot->getModel() )
1375  ->getDefaultFormat();
1376  }
1377  }
1378 
1379  if ( $newContent->equals( $oldContent ) ) {
1380  # Tell the user that the undo results in no change,
1381  # i.e. the revisions were already undone.
1382  $undoMsg = 'nochange';
1383  $content = false;
1384  } else {
1385  # Inform the user of our success and set an automatic edit summary
1386  $undoMsg = 'success';
1387 
1388  # If we just undid one rev, use an autosummary
1389  $firstrev = $this->revisionStore->getNextRevision( $oldrev );
1390  if ( $firstrev && $firstrev->getId() == $undo ) {
1391  $userText = $undorev->getUser() ?
1392  $undorev->getUser()->getName() :
1393  '';
1394  if ( $userText === '' ) {
1395  $undoSummary = $this->context->msg(
1396  'undo-summary-username-hidden',
1397  $undo
1398  )->inContentLanguage()->text();
1399  // Handle external users (imported revisions)
1400  } elseif ( ExternalUserNames::isExternal( $userText ) ) {
1401  $userLinkTitle = ExternalUserNames::getUserLinkTitle( $userText );
1402  if ( $userLinkTitle ) {
1403  $userLink = $userLinkTitle->getPrefixedText();
1404  $undoSummary = $this->context->msg(
1405  'undo-summary-import',
1406  $undo,
1407  $userLink,
1408  $userText
1409  )->inContentLanguage()->text();
1410  } else {
1411  $undoSummary = $this->context->msg(
1412  'undo-summary-import2',
1413  $undo,
1414  $userText
1415  )->inContentLanguage()->text();
1416  }
1417  } else {
1418  $undoIsAnon = $undorev->getUser() ?
1419  !$undorev->getUser()->isRegistered() :
1420  true;
1421  $undoMessage = ( $undoIsAnon && $wgDisableAnonTalk ) ?
1422  'undo-summary-anon' :
1423  'undo-summary';
1424  $undoSummary = $this->context->msg(
1425  $undoMessage,
1426  $undo,
1427  $userText
1428  )->inContentLanguage()->text();
1429  }
1430  if ( $this->summary === '' ) {
1431  $this->summary = $undoSummary;
1432  } else {
1433  $this->summary = $undoSummary . $this->context->msg( 'colon-separator' )
1434  ->inContentLanguage()->text() . $this->summary;
1435  }
1436  $this->undidRev = $undo;
1437  }
1438  $this->formtype = 'diff';
1439  }
1440  }
1441  } else {
1442  // Failed basic sanity checks.
1443  // Older revisions may have been removed since the link
1444  // was created, or we may simply have got bogus input.
1445  $undoMsg = 'norev';
1446  }
1447 
1448  $out = $this->context->getOutput();
1449  // Messages: undo-success, undo-failure, undo-main-slot-only, undo-norev,
1450  // undo-nochange.
1451  $class = ( $undoMsg == 'success' ? '' : 'error ' ) . "mw-undo-{$undoMsg}";
1452  $this->editFormPageTop .= Html::rawElement(
1453  'div', [ 'class' => $class ],
1454  $out->parseAsInterface(
1455  $this->context->msg( 'undo-' . $undoMsg )->plain()
1456  )
1457  );
1458  }
1459 
1460  if ( $content === false ) {
1461  // Hack for restoring old revisions while EditPage
1462  // can't handle multi-slot editing.
1463  $curRevisionRecord = $this->page->getRevisionRecord();
1464  $oldRevisionRecord = $this->mArticle->fetchRevisionRecord();
1465 
1466  if ( $curRevisionRecord
1467  && $oldRevisionRecord
1468  && $curRevisionRecord->getId() !== $oldRevisionRecord->getId()
1470  $oldRevisionRecord,
1471  $curRevisionRecord
1472  ) || !$this->isSupportedContentModel(
1473  $oldRevisionRecord->getSlot(
1474  SlotRecord::MAIN,
1475  RevisionRecord::RAW
1476  )->getModel()
1477  ) )
1478  ) {
1479  $this->context->getOutput()->redirect(
1480  $this->mTitle->getFullURL(
1481  [
1482  'action' => 'mcrrestore',
1483  'restore' => $oldRevisionRecord->getId(),
1484  ]
1485  )
1486  );
1487 
1488  return false;
1489  }
1490  }
1491 
1492  if ( $content === false ) {
1493  $content = $this->getOriginalContent( $user );
1494  }
1495  }
1496 
1497  return $content;
1498  }
1499 
1515  private function getOriginalContent( User $user ) {
1516  if ( $this->section == 'new' ) {
1517  return $this->getCurrentContent();
1518  }
1519  $revRecord = $this->mArticle->fetchRevisionRecord();
1520  if ( $revRecord === null ) {
1521  return $this->contentHandlerFactory
1522  ->getContentHandler( $this->contentModel )
1523  ->makeEmptyContent();
1524  }
1525  return $revRecord->getContent( SlotRecord::MAIN, RevisionRecord::FOR_THIS_USER, $user );
1526  }
1527 
1540  public function getParentRevId() {
1541  if ( $this->parentRevId ) {
1542  return $this->parentRevId;
1543  } else {
1544  return $this->mArticle->getRevIdFetched();
1545  }
1546  }
1547 
1556  protected function getCurrentContent() {
1557  $revRecord = $this->page->getRevisionRecord();
1558  $content = $revRecord ? $revRecord->getContent(
1559  SlotRecord::MAIN,
1560  RevisionRecord::RAW
1561  ) : null;
1562 
1563  if ( $content === false || $content === null ) {
1564  return $this->contentHandlerFactory
1565  ->getContentHandler( $this->contentModel )
1566  ->makeEmptyContent();
1567  } elseif ( !$this->undidRev ) {
1568  $mainSlot = $revRecord->getSlot( SlotRecord::MAIN, RevisionRecord::RAW );
1569 
1570  // Content models should always be the same since we error
1571  // out if they are different before this point (in ->edit()).
1572  // The exception being, during an undo, the current revision might
1573  // differ from the prior revision.
1574  $logger = LoggerFactory::getInstance( 'editpage' );
1575  if ( $this->contentModel !== $mainSlot->getModel() ) {
1576  $logger->warning( "Overriding content model from current edit {prev} to {new}", [
1577  'prev' => $this->contentModel,
1578  'new' => $mainSlot->getModel(),
1579  'title' => $this->getTitle()->getPrefixedDBkey(),
1580  'method' => __METHOD__
1581  ] );
1582  $this->contentModel = $mainSlot->getModel();
1583  }
1584 
1585  // Given that the content models should match, the current selected
1586  // format should be supported.
1587  if ( !$content->isSupportedFormat( $this->contentFormat ) ) {
1588  $revFormat = $mainSlot->getFormat();
1589  if ( $revFormat === null ) {
1590  $revFormat = $this->contentHandlerFactory
1591  ->getContentHandler( $mainSlot->getModel() )
1592  ->getDefaultFormat();
1593  }
1594 
1595  $logger->warning( "Current revision content format unsupported. Overriding {prev} to {new}", [
1596  'prev' => $this->contentFormat,
1597  'new' => $revFormat,
1598  'title' => $this->getTitle()->getPrefixedDBkey(),
1599  'method' => __METHOD__
1600  ] );
1601  $this->contentFormat = $revFormat;
1602  }
1603  }
1604  return $content;
1605  }
1606 
1614  public function setPreloadedContent( Content $content ) {
1615  $this->mPreloadContent = $content;
1616  }
1617 
1629  protected function getPreloadedContent( $preload, $params = [] ) {
1630  if ( !empty( $this->mPreloadContent ) ) {
1631  return $this->mPreloadContent;
1632  }
1633 
1634  $handler = $this->contentHandlerFactory->getContentHandler( $this->contentModel );
1635 
1636  if ( $preload === '' ) {
1637  return $handler->makeEmptyContent();
1638  }
1639 
1640  $user = $this->context->getUser();
1641  $title = Title::newFromText( $preload );
1642 
1643  # Check for existence to avoid getting MediaWiki:Noarticletext
1644  if ( !$this->isPageExistingAndViewable( $title, $user ) ) {
1645  // TODO: somehow show a warning to the user!
1646  return $handler->makeEmptyContent();
1647  }
1648 
1650  if ( $page->isRedirect() ) {
1652  # Same as before
1653  if ( !$this->isPageExistingAndViewable( $title, $user ) ) {
1654  // TODO: somehow show a warning to the user!
1655  return $handler->makeEmptyContent();
1656  }
1658  }
1659 
1660  $parserOptions = ParserOptions::newFromUser( $user );
1661  $content = $page->getContent( RevisionRecord::RAW );
1662 
1663  if ( !$content ) {
1664  // TODO: somehow show a warning to the user!
1665  return $handler->makeEmptyContent();
1666  }
1667 
1668  if ( $content->getModel() !== $handler->getModelID() ) {
1669  $converted = $content->convert( $handler->getModelID() );
1670 
1671  if ( !$converted ) {
1672  // TODO: somehow show a warning to the user!
1673  wfDebug( "Attempt to preload incompatible content: " .
1674  "can't convert " . $content->getModel() .
1675  " to " . $handler->getModelID() );
1676 
1677  return $handler->makeEmptyContent();
1678  }
1679 
1680  $content = $converted;
1681  }
1682 
1683  return $content->preloadTransform( $title, $parserOptions, $params );
1684  }
1685 
1695  private function isPageExistingAndViewable( $title, User $user ) {
1696  return $title && $title->exists() && $this->permManager->userCan( 'read', $user, $title );
1697  }
1698 
1706  public function tokenOk( &$request ) {
1707  $token = $request->getVal( 'wpEditToken' );
1708  $user = $this->context->getUser();
1709  $this->mTokenOk = $user->matchEditToken( $token );
1710  $this->mTokenOkExceptSuffix = $user->matchEditTokenNoSuffix( $token );
1711  return $this->mTokenOk;
1712  }
1713 
1728  protected function setPostEditCookie( $statusValue ) {
1729  $revisionId = $this->page->getLatest();
1730  $postEditKey = self::POST_EDIT_COOKIE_KEY_PREFIX . $revisionId;
1731 
1732  $val = 'saved';
1733  if ( $statusValue == self::AS_SUCCESS_NEW_ARTICLE ) {
1734  $val = 'created';
1735  } elseif ( $this->oldid ) {
1736  $val = 'restored';
1737  }
1738 
1739  $response = $this->context->getRequest()->response();
1740  $response->setCookie( $postEditKey, $val, time() + self::POST_EDIT_COOKIE_DURATION );
1741  }
1742 
1749  public function attemptSave( &$resultDetails = false ) {
1750  // TODO: MCR:
1751  // * treat $this->minoredit like $this->markAsBot and check isAllowed( 'minoredit' )!
1752  // * add $this->autopatrol like $this->markAsBot and check isAllowed( 'autopatrol' )!
1753  // This is needed since PageUpdater no longer checks these rights!
1754 
1755  // Allow bots to exempt some edits from bot flagging
1756  $markAsBot = $this->markAsBot
1757  && $this->permManager->userHasRight( $this->context->getUser(), 'bot' );
1758  $status = $this->internalAttemptSave( $resultDetails, $markAsBot );
1759 
1760  Hooks::run( 'EditPage::attemptSave:after', [ $this, $status, $resultDetails ] );
1761 
1762  return $status;
1763  }
1764 
1768  private function incrementResolvedConflicts() {
1769  if ( $this->context->getRequest()->getText( 'mode' ) !== 'conflict' ) {
1770  return;
1771  }
1772 
1773  $this->getEditConflictHelper()->incrementResolvedStats( $this->context->getUser() );
1774  }
1775 
1785  private function handleStatus( Status $status, $resultDetails ) {
1790  if ( $status->value == self::AS_SUCCESS_UPDATE
1791  || $status->value == self::AS_SUCCESS_NEW_ARTICLE
1792  ) {
1793  $this->incrementResolvedConflicts();
1794 
1795  $this->didSave = true;
1796  if ( !$resultDetails['nullEdit'] ) {
1797  $this->setPostEditCookie( $status->value );
1798  }
1799  }
1800 
1801  $out = $this->context->getOutput();
1802 
1803  // "wpExtraQueryRedirect" is a hidden input to modify
1804  // after save URL and is not used by actual edit form
1805  $request = $this->context->getRequest();
1806  $extraQueryRedirect = $request->getVal( 'wpExtraQueryRedirect' );
1807 
1808  switch ( $status->value ) {
1816  case self::AS_END:
1819  return true;
1820 
1821  case self::AS_HOOK_ERROR:
1822  return false;
1823 
1825  wfDeprecated(
1826  __METHOD__ . ' with $status->value == AS_CANNOT_USE_CUSTOM_MODEL',
1827  '1.35'
1828  );
1829  // ...and fall through to next case
1830  case self::AS_PARSE_ERROR:
1832  $out->wrapWikiTextAsInterface( 'error',
1833  $status->getWikiText( false, false, $this->context->getLanguage() )
1834  );
1835  return true;
1836 
1838  $query = $resultDetails['redirect'] ? 'redirect=no' : '';
1839  if ( $extraQueryRedirect ) {
1840  if ( $query !== '' ) {
1841  $query .= '&';
1842  }
1843  $query .= $extraQueryRedirect;
1844  }
1845  $anchor = $resultDetails['sectionanchor'] ?? '';
1846  $out->redirect( $this->mTitle->getFullURL( $query ) . $anchor );
1847  return false;
1848 
1850  $extraQuery = '';
1851  $sectionanchor = $resultDetails['sectionanchor'];
1852 
1853  // Give extensions a chance to modify URL query on update
1854  Hooks::run(
1855  'ArticleUpdateBeforeRedirect',
1856  [ $this->mArticle, &$sectionanchor, &$extraQuery ]
1857  );
1858 
1859  if ( $resultDetails['redirect'] ) {
1860  if ( $extraQuery !== '' ) {
1861  $extraQuery = '&' . $extraQuery;
1862  }
1863  $extraQuery = 'redirect=no' . $extraQuery;
1864  }
1865  if ( $extraQueryRedirect ) {
1866  if ( $extraQuery !== '' ) {
1867  $extraQuery .= '&';
1868  }
1869  $extraQuery .= $extraQueryRedirect;
1870  }
1871 
1872  $out->redirect( $this->mTitle->getFullURL( $extraQuery ) . $sectionanchor );
1873  return false;
1874 
1875  case self::AS_SPAM_ERROR:
1876  $this->spamPageWithContent( $resultDetails['spam'] ?? false );
1877  return false;
1878 
1880  throw new UserBlockedError(
1881  $this->context->getUser()->getBlock(),
1882  $this->context->getUser(),
1883  $this->context->getLanguage(),
1884  $request->getIP()
1885  );
1886 
1889  throw new PermissionsError( 'upload' );
1890 
1893  throw new PermissionsError( 'edit' );
1894 
1896  throw new ReadOnlyError;
1897 
1898  case self::AS_RATE_LIMITED:
1899  throw new ThrottledError();
1900 
1902  $permission = $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage';
1903  throw new PermissionsError( $permission );
1904 
1906  throw new PermissionsError( 'editcontentmodel' );
1907 
1908  default:
1909  // We don't recognize $status->value. The only way that can happen
1910  // is if an extension hook aborted from inside ArticleSave.
1911  // Render the status object into $this->hookError
1912  // FIXME this sucks, we should just use the Status object throughout
1913  $this->hookError = '<div class="error">' . "\n" .
1914  $status->getWikiText( false, false, $this->context->getLanguage() ) .
1915  '</div>';
1916  return true;
1917  }
1918  }
1919 
1929  protected function runPostMergeFilters( Content $content, Status $status, User $user ) {
1930  // Run old style post-section-merge edit filter
1931  if ( $this->hookError != '' ) {
1932  # ...or the hook could be expecting us to produce an error
1933  $status->fatal( 'hookaborted' );
1934  $status->value = self::AS_HOOK_ERROR_EXPECTED;
1935  return false;
1936  }
1937 
1938  // Run new style post-section-merge edit filter
1939  if ( !Hooks::run( 'EditFilterMergedContent',
1940  [ $this->context, $content, $status, $this->summary,
1941  $user, $this->minoredit ] )
1942  ) {
1943  # Error messages etc. could be handled within the hook...
1944  if ( $status->isGood() ) {
1945  $status->fatal( 'hookaborted' );
1946  // Not setting $this->hookError here is a hack to allow the hook
1947  // to cause a return to the edit page without $this->hookError
1948  // being set. This is used by ConfirmEdit to display a captcha
1949  // without any error message cruft.
1950  } else {
1951  $this->hookError = $this->formatStatusErrors( $status );
1952  }
1953  // Use the existing $status->value if the hook set it
1954  if ( !$status->value ) {
1955  $status->value = self::AS_HOOK_ERROR;
1956  }
1957  return false;
1958  } elseif ( !$status->isOK() ) {
1959  # ...or the hook could be expecting us to produce an error
1960  // FIXME this sucks, we should just use the Status object throughout
1961  if ( !$status->getErrors() ) {
1962  // Provide a fallback error message if none was set
1963  $status->fatal( 'hookaborted' );
1964  }
1965  $this->hookError = $this->formatStatusErrors( $status );
1966  $status->value = self::AS_HOOK_ERROR_EXPECTED;
1967  return false;
1968  }
1969 
1970  return true;
1971  }
1972 
1979  private function formatStatusErrors( Status $status ) {
1980  $errmsg = $status->getWikiText(
1981  'edit-error-short',
1982  'edit-error-long',
1983  $this->context->getLanguage()
1984  );
1985  return <<<ERROR
1986 <div class="errorbox">
1987 {$errmsg}
1988 </div>
1989 <br clear="all" />
1990 ERROR;
1991  }
1992 
1999  private function newSectionSummary( &$sectionanchor = null ) {
2000  if ( $this->sectiontitle !== '' ) {
2001  $sectionanchor = $this->guessSectionName( $this->sectiontitle );
2002  // If no edit summary was specified, create one automatically from the section
2003  // title and have it link to the new section. Otherwise, respect the summary as
2004  // passed.
2005  if ( $this->summary === '' ) {
2006  $cleanSectionTitle = MediaWikiServices::getInstance()->getParser()
2007  ->stripSectionName( $this->sectiontitle );
2008  return $this->context->msg( 'newsectionsummary' )
2009  ->plaintextParams( $cleanSectionTitle )->inContentLanguage()->text();
2010  }
2011  } elseif ( $this->summary !== '' ) {
2012  $sectionanchor = $this->guessSectionName( $this->summary );
2013  # This is a new section, so create a link to the new section
2014  # in the revision summary.
2015  $cleanSummary = MediaWikiServices::getInstance()->getParser()
2016  ->stripSectionName( $this->summary );
2017  return $this->context->msg( 'newsectionsummary' )
2018  ->plaintextParams( $cleanSummary )->inContentLanguage()->text();
2019  }
2020  return $this->summary;
2021  }
2022 
2048  public function internalAttemptSave( &$result, $markAsBot = false ) {
2049  $status = Status::newGood();
2050  $user = $this->context->getUser();
2051 
2052  if ( !Hooks::run( 'EditPage::attemptSave', [ $this ] ) ) {
2053  wfDebug( "Hook 'EditPage::attemptSave' aborted article saving\n" );
2054  $status->fatal( 'hookaborted' );
2055  $status->value = self::AS_HOOK_ERROR;
2056  return $status;
2057  }
2058 
2059  if ( $this->unicodeCheck !== self::UNICODE_CHECK ) {
2060  $status->fatal( 'unicode-support-fail' );
2061  $status->value = self::AS_UNICODE_NOT_SUPPORTED;
2062  return $status;
2063  }
2064 
2065  $request = $this->context->getRequest();
2066  $spam = $request->getText( 'wpAntispam' );
2067  if ( $spam !== '' ) {
2068  wfDebugLog(
2069  'SimpleAntiSpam',
2070  $user->getName() .
2071  ' editing "' .
2072  $this->mTitle->getPrefixedText() .
2073  '" submitted bogus field "' .
2074  $spam .
2075  '"'
2076  );
2077  $status->fatal( 'spamprotectionmatch', false );
2078  $status->value = self::AS_SPAM_ERROR;
2079  return $status;
2080  }
2081 
2082  try {
2083  # Construct Content object
2084  $textbox_content = $this->toEditContent( $this->textbox1 );
2085  } catch ( MWContentSerializationException $ex ) {
2086  $status->fatal(
2087  'content-failed-to-parse',
2088  $this->contentModel,
2089  $this->contentFormat,
2090  $ex->getMessage()
2091  );
2092  $status->value = self::AS_PARSE_ERROR;
2093  return $status;
2094  }
2095 
2096  # Check image redirect
2097  if ( $this->mTitle->getNamespace() == NS_FILE &&
2098  $textbox_content->isRedirect() &&
2099  !$this->permManager->userHasRight( $user, 'upload' )
2100  ) {
2102  $status->setResult( false, $code );
2103 
2104  return $status;
2105  }
2106 
2107  # Check for spam
2108  $spamRegexChecker = MediaWikiServices::getInstance()->getSpamChecker();
2109  $match = $spamRegexChecker->checkSummary( $this->summary );
2110  if ( $match === false && $this->section == 'new' ) {
2111  # $wgSpamRegex is enforced on this new heading/summary because, unlike
2112  # regular summaries, it is added to the actual wikitext.
2113  if ( $this->sectiontitle !== '' ) {
2114  # This branch is taken when the API is used with the 'sectiontitle' parameter.
2115  $match = $spamRegexChecker->checkContent( $this->sectiontitle );
2116  } else {
2117  # This branch is taken when the "Add Topic" user interface is used, or the API
2118  # is used with the 'summary' parameter.
2119  $match = $spamRegexChecker->checkContent( $this->summary );
2120  }
2121  }
2122  if ( $match === false ) {
2123  $match = $spamRegexChecker->checkContent( $this->textbox1 );
2124  }
2125  if ( $match !== false ) {
2126  $result['spam'] = $match;
2127  $ip = $request->getIP();
2128  $pdbk = $this->mTitle->getPrefixedDBkey();
2129  $match = str_replace( "\n", '', $match );
2130  wfDebugLog( 'SpamRegex', "$ip spam regex hit [[$pdbk]]: \"$match\"" );
2131  $status->fatal( 'spamprotectionmatch', $match );
2132  $status->value = self::AS_SPAM_ERROR;
2133  return $status;
2134  }
2135  if ( !Hooks::run(
2136  'EditFilter',
2137  [ $this, $this->textbox1, $this->section, &$this->hookError, $this->summary ] )
2138  ) {
2139  # Error messages etc. could be handled within the hook...
2140  $status->fatal( 'hookaborted' );
2141  $status->value = self::AS_HOOK_ERROR;
2142  return $status;
2143  } elseif ( $this->hookError != '' ) {
2144  # ...or the hook could be expecting us to produce an error
2145  $status->fatal( 'hookaborted' );
2146  $status->value = self::AS_HOOK_ERROR_EXPECTED;
2147  return $status;
2148  }
2149 
2150  if ( $this->permManager->isBlockedFrom( $user, $this->mTitle ) ) {
2151  // Auto-block user's IP if the account was "hard" blocked
2152  if ( !wfReadOnly() ) {
2153  $user->spreadAnyEditBlock();
2154  }
2155  # Check block state against master, thus 'false'.
2156  $status->setResult( false, self::AS_BLOCKED_PAGE_FOR_USER );
2157  return $status;
2158  }
2159 
2160  $this->contentLength = strlen( $this->textbox1 );
2161  $config = $this->context->getConfig();
2162  $maxArticleSize = $config->get( 'MaxArticleSize' );
2163  if ( $this->contentLength > $maxArticleSize * 1024 ) {
2164  // Error will be displayed by showEditForm()
2165  $this->tooBig = true;
2166  $status->setResult( false, self::AS_CONTENT_TOO_BIG );
2167  return $status;
2168  }
2169 
2170  if ( !$this->permManager->userHasRight( $user, 'edit' ) ) {
2171  if ( $user->isAnon() ) {
2172  $status->setResult( false, self::AS_READ_ONLY_PAGE_ANON );
2173  return $status;
2174  } else {
2175  $status->fatal( 'readonlytext' );
2176  $status->value = self::AS_READ_ONLY_PAGE_LOGGED;
2177  return $status;
2178  }
2179  }
2180 
2181  $changingContentModel = false;
2182  if ( $this->contentModel !== $this->mTitle->getContentModel() ) {
2183  if ( !$this->permManager->userHasRight( $user, 'editcontentmodel' ) ) {
2184  $status->setResult( false, self::AS_NO_CHANGE_CONTENT_MODEL );
2185  return $status;
2186  }
2187  // Make sure the user can edit the page under the new content model too
2188  $titleWithNewContentModel = clone $this->mTitle;
2189  $titleWithNewContentModel->setContentModel( $this->contentModel );
2190 
2191  $canEditModel = $this->permManager->userCan(
2192  'editcontentmodel',
2193  $user,
2194  $titleWithNewContentModel
2195  );
2196 
2197  if (
2198  !$canEditModel
2199  || !$this->permManager->userCan( 'edit', $user, $titleWithNewContentModel )
2200  ) {
2201  $status->setResult( false, self::AS_NO_CHANGE_CONTENT_MODEL );
2202 
2203  return $status;
2204  }
2205 
2206  $changingContentModel = true;
2207  $oldContentModel = $this->mTitle->getContentModel();
2208  }
2209 
2210  if ( $this->changeTags ) {
2211  $changeTagsStatus = ChangeTags::canAddTagsAccompanyingChange(
2212  $this->changeTags, $user );
2213  if ( !$changeTagsStatus->isOK() ) {
2214  $changeTagsStatus->value = self::AS_CHANGE_TAG_ERROR;
2215  return $changeTagsStatus;
2216  }
2217  }
2218 
2219  if ( wfReadOnly() ) {
2220  $status->fatal( 'readonlytext' );
2221  $status->value = self::AS_READ_ONLY_PAGE;
2222  return $status;
2223  }
2224  if ( $user->pingLimiter() || $user->pingLimiter( 'linkpurge', 0 )
2225  || ( $changingContentModel && $user->pingLimiter( 'editcontentmodel' ) )
2226  ) {
2227  $status->fatal( 'actionthrottledtext' );
2228  $status->value = self::AS_RATE_LIMITED;
2229  return $status;
2230  }
2231 
2232  # If the article has been deleted while editing, don't save it without
2233  # confirmation
2234  if ( $this->wasDeletedSinceLastEdit() && !$this->recreate ) {
2235  $status->setResult( false, self::AS_ARTICLE_WAS_DELETED );
2236  return $status;
2237  }
2238 
2239  # Load the page data from the master. If anything changes in the meantime,
2240  # we detect it by using page_latest like a token in a 1 try compare-and-swap.
2241  $this->page->loadPageData( 'fromdbmaster' );
2242  $new = !$this->page->exists();
2243 
2244  if ( $new ) {
2245  // Late check for create permission, just in case *PARANOIA*
2246  if ( !$this->permManager->userCan( 'create', $user, $this->mTitle ) ) {
2247  $status->fatal( 'nocreatetext' );
2248  $status->value = self::AS_NO_CREATE_PERMISSION;
2249  wfDebug( __METHOD__ . ": no create permission\n" );
2250  return $status;
2251  }
2252 
2253  // Don't save a new page if it's blank or if it's a MediaWiki:
2254  // message with content equivalent to default (allow empty pages
2255  // in this case to disable messages, see T52124)
2256  $defaultMessageText = $this->mTitle->getDefaultMessageText();
2257  if ( $this->mTitle->getNamespace() === NS_MEDIAWIKI && $defaultMessageText !== false ) {
2258  $defaultText = $defaultMessageText;
2259  } else {
2260  $defaultText = '';
2261  }
2262 
2263  if ( !$this->allowBlankArticle && $this->textbox1 === $defaultText ) {
2264  $this->blankArticle = true;
2265  $status->fatal( 'blankarticle' );
2266  $status->setResult( false, self::AS_BLANK_ARTICLE );
2267  return $status;
2268  }
2269 
2270  if ( !$this->runPostMergeFilters( $textbox_content, $status, $user ) ) {
2271  return $status;
2272  }
2273 
2274  $content = $textbox_content;
2275 
2276  $result['sectionanchor'] = '';
2277  if ( $this->section == 'new' ) {
2278  if ( $this->sectiontitle !== '' ) {
2279  // Insert the section title above the content.
2280  $content = $content->addSectionHeader( $this->sectiontitle );
2281  } elseif ( $this->summary !== '' ) {
2282  // Insert the section title above the content.
2283  $content = $content->addSectionHeader( $this->summary );
2284  }
2285  $this->summary = $this->newSectionSummary( $result['sectionanchor'] );
2286  }
2287 
2288  $status->value = self::AS_SUCCESS_NEW_ARTICLE;
2289 
2290  } else { # not $new
2291 
2292  # Article exists. Check for edit conflict.
2293 
2294  $this->page->clear(); # Force reload of dates, etc.
2295  $timestamp = $this->page->getTimestamp();
2296  $latest = $this->page->getLatest();
2297 
2298  wfDebug( "timestamp: {$timestamp}, edittime: {$this->edittime}\n" );
2299  wfDebug( "revision: {$latest}, editRevId: {$this->editRevId}\n" );
2300 
2301  // An edit conflict is detected if the current revision is different from the
2302  // revision that was current when editing was initiated on the client.
2303  // This is checked based on the timestamp and revision ID.
2304  // TODO: the timestamp based check can probably go away now.
2305  if ( ( $this->edittime !== null && $this->edittime != $timestamp )
2306  || ( $this->editRevId !== null && $this->editRevId != $latest )
2307  ) {
2308  $this->isConflict = true;
2309  if ( $this->section == 'new' ) {
2310  if ( $this->page->getUserText() == $user->getName() &&
2311  $this->page->getComment() == $this->newSectionSummary()
2312  ) {
2313  // Probably a duplicate submission of a new comment.
2314  // This can happen when CDN resends a request after
2315  // a timeout but the first one actually went through.
2316  wfDebug( __METHOD__
2317  . ": duplicate new section submission; trigger edit conflict!\n" );
2318  } else {
2319  // New comment; suppress conflict.
2320  $this->isConflict = false;
2321  wfDebug( __METHOD__ . ": conflict suppressed; new section\n" );
2322  }
2323  } elseif ( $this->section == ''
2324  && $this->edittime
2325  && $this->revisionStore->userWasLastToEdit(
2326  wfGetDB( DB_MASTER ),
2327  $this->mTitle->getArticleID(),
2328  $user->getId(),
2329  $this->edittime
2330  )
2331  ) {
2332  # Suppress edit conflict with self, except for section edits where merging is required.
2333  wfDebug( __METHOD__ . ": Suppressing edit conflict, same user.\n" );
2334  $this->isConflict = false;
2335  }
2336  }
2337 
2338  // If sectiontitle is set, use it, otherwise use the summary as the section title.
2339  if ( $this->sectiontitle !== '' ) {
2340  $sectionTitle = $this->sectiontitle;
2341  } else {
2342  $sectionTitle = $this->summary;
2343  }
2344 
2345  $content = null;
2346 
2347  if ( $this->isConflict ) {
2348  wfDebug( __METHOD__
2349  . ": conflict! getting section '{$this->section}' for time '{$this->edittime}'"
2350  . " (id '{$this->editRevId}') (article time '{$timestamp}')\n" );
2351  // @TODO: replaceSectionAtRev() with base ID (not prior current) for ?oldid=X case
2352  // ...or disable section editing for non-current revisions (not exposed anyway).
2353  if ( $this->editRevId !== null ) {
2354  $content = $this->page->replaceSectionAtRev(
2355  $this->section,
2356  $textbox_content,
2357  $sectionTitle,
2358  $this->editRevId
2359  );
2360  } else {
2361  $content = $this->page->replaceSectionContent(
2362  $this->section,
2363  $textbox_content,
2364  $sectionTitle,
2365  $this->edittime
2366  );
2367  }
2368  } else {
2369  wfDebug( __METHOD__ . ": getting section '{$this->section}'\n" );
2370  $content = $this->page->replaceSectionContent(
2371  $this->section,
2372  $textbox_content,
2373  $sectionTitle
2374  );
2375  }
2376 
2377  if ( $content === null ) {
2378  wfDebug( __METHOD__ . ": activating conflict; section replace failed.\n" );
2379  $this->isConflict = true;
2380  $content = $textbox_content; // do not try to merge here!
2381  } elseif ( $this->isConflict ) {
2382  # Attempt merge
2383  if ( $this->mergeChangesIntoContent( $content ) ) {
2384  // Successful merge! Maybe we should tell the user the good news?
2385  $this->isConflict = false;
2386  wfDebug( __METHOD__ . ": Suppressing edit conflict, successful merge.\n" );
2387  } else {
2388  $this->section = '';
2389  $this->textbox1 = ContentHandler::getContentText( $content );
2390  wfDebug( __METHOD__ . ": Keeping edit conflict, failed merge.\n" );
2391  }
2392  }
2393 
2394  if ( $this->isConflict ) {
2395  $status->setResult( false, self::AS_CONFLICT_DETECTED );
2396  return $status;
2397  }
2398 
2399  if ( !$this->runPostMergeFilters( $content, $status, $user ) ) {
2400  return $status;
2401  }
2402 
2403  if ( $this->section == 'new' ) {
2404  // Handle the user preference to force summaries here
2405  if ( !$this->allowBlankSummary && trim( $this->summary ) == '' ) {
2406  $this->missingSummary = true;
2407  $status->fatal( 'missingsummary' ); // or 'missingcommentheader' if $section == 'new'. Blegh
2408  $status->value = self::AS_SUMMARY_NEEDED;
2409  return $status;
2410  }
2411 
2412  // Do not allow the user to post an empty comment
2413  if ( $this->textbox1 == '' ) {
2414  $this->missingComment = true;
2415  $status->fatal( 'missingcommenttext' );
2416  $status->value = self::AS_TEXTBOX_EMPTY;
2417  return $status;
2418  }
2419  } elseif ( !$this->allowBlankSummary
2420  && !$content->equals( $this->getOriginalContent( $user ) )
2421  && !$content->isRedirect()
2422  && md5( $this->summary ) == $this->autoSumm
2423  ) {
2424  $this->missingSummary = true;
2425  $status->fatal( 'missingsummary' );
2426  $status->value = self::AS_SUMMARY_NEEDED;
2427  return $status;
2428  }
2429 
2430  # All's well
2431  $sectionanchor = '';
2432  if ( $this->section == 'new' ) {
2433  $this->summary = $this->newSectionSummary( $sectionanchor );
2434  } elseif ( $this->section != '' ) {
2435  # Try to get a section anchor from the section source, redirect
2436  # to edited section if header found.
2437  # XXX: Might be better to integrate this into Article::replaceSectionAtRev
2438  # for duplicate heading checking and maybe parsing.
2439  $hasmatch = preg_match( "/^ *([=]{1,6})(.*?)(\\1) *\\n/i", $this->textbox1, $matches );
2440  # We can't deal with anchors, includes, html etc in the header for now,
2441  # headline would need to be parsed to improve this.
2442  if ( $hasmatch && strlen( $matches[2] ) > 0 ) {
2443  $sectionanchor = $this->guessSectionName( $matches[2] );
2444  }
2445  }
2446  $result['sectionanchor'] = $sectionanchor;
2447 
2448  // Save errors may fall down to the edit form, but we've now
2449  // merged the section into full text. Clear the section field
2450  // so that later submission of conflict forms won't try to
2451  // replace that into a duplicated mess.
2452  $this->textbox1 = $this->toEditText( $content );
2453  $this->section = '';
2454 
2455  $status->value = self::AS_SUCCESS_UPDATE;
2456  }
2457 
2458  if ( !$this->allowSelfRedirect
2459  && $content->isRedirect()
2460  && $content->getRedirectTarget()->equals( $this->getTitle() )
2461  ) {
2462  // If the page already redirects to itself, don't warn.
2463  $currentTarget = $this->getCurrentContent()->getRedirectTarget();
2464  if ( !$currentTarget || !$currentTarget->equals( $this->getTitle() ) ) {
2465  $this->selfRedirect = true;
2466  $status->fatal( 'selfredirect' );
2467  $status->value = self::AS_SELF_REDIRECT;
2468  return $status;
2469  }
2470  }
2471 
2472  // Check for length errors again now that the section is merged in
2473  $this->contentLength = strlen( $this->toEditText( $content ) );
2474  if ( $this->contentLength > $maxArticleSize * 1024 ) {
2475  $this->tooBig = true;
2476  $status->setResult( false, self::AS_MAX_ARTICLE_SIZE_EXCEEDED );
2477  return $status;
2478  }
2479 
2480  $flags = EDIT_AUTOSUMMARY |
2481  ( $new ? EDIT_NEW : EDIT_UPDATE ) |
2482  ( ( $this->minoredit && !$this->isNew ) ? EDIT_MINOR : 0 ) |
2483  ( $markAsBot ? EDIT_FORCE_BOT : 0 );
2484 
2485  $doEditStatus = $this->page->doEditContent(
2486  $content,
2487  $this->summary,
2488  $flags,
2489  false,
2490  $user,
2491  $content->getDefaultFormat(),
2494  );
2495 
2496  if ( !$doEditStatus->isOK() ) {
2497  // Failure from doEdit()
2498  // Show the edit conflict page for certain recognized errors from doEdit(),
2499  // but don't show it for errors from extension hooks
2500  $errors = $doEditStatus->getErrorsArray();
2501  if ( in_array( $errors[0][0],
2502  [ 'edit-gone-missing', 'edit-conflict', 'edit-already-exists' ] )
2503  ) {
2504  $this->isConflict = true;
2505  // Destroys data doEdit() put in $status->value but who cares
2506  $doEditStatus->value = self::AS_END;
2507  }
2508  return $doEditStatus;
2509  }
2510 
2511  $result['nullEdit'] = $doEditStatus->hasMessage( 'edit-no-change' );
2512  if ( $result['nullEdit'] ) {
2513  // We don't know if it was a null edit until now, so increment here
2514  $user->pingLimiter( 'linkpurge' );
2515  }
2516  $result['redirect'] = $content->isRedirect();
2517 
2518  $this->updateWatchlist();
2519 
2520  // If the content model changed, add a log entry
2521  if ( $changingContentModel ) {
2523  $user,
2524  $new ? false : $oldContentModel,
2525  $this->contentModel,
2526  $this->summary
2527  );
2528  }
2529 
2530  return $status;
2531  }
2532 
2539  protected function addContentModelChangeLogEntry( User $user, $oldModel, $newModel, $reason ) {
2540  $new = $oldModel === false;
2541  $log = new ManualLogEntry( 'contentmodel', $new ? 'new' : 'change' );
2542  $log->setPerformer( $user );
2543  $log->setTarget( $this->mTitle );
2544  $log->setComment( $reason );
2545  $log->setParameters( [
2546  '4::oldmodel' => $oldModel,
2547  '5::newmodel' => $newModel
2548  ] );
2549  $logid = $log->insert();
2550  $log->publish( $logid );
2551  }
2552 
2556  protected function updateWatchlist() {
2557  $user = $this->context->getUser();
2558  if ( !$user->isLoggedIn() ) {
2559  return;
2560  }
2561 
2563  $watch = $this->watchthis;
2564  // Do this in its own transaction to reduce contention...
2565  DeferredUpdates::addCallableUpdate( function () use ( $user, $title, $watch ) {
2566  if ( $watch == $user->isWatched( $title, User::IGNORE_USER_RIGHTS ) ) {
2567  return; // nothing to change
2568  }
2569  WatchAction::doWatchOrUnwatch( $watch, $title, $user );
2570  } );
2571 
2572  // Add a job to purge expired watchlist items. Jobs will only be added at the rate
2573  // specified by $wgWatchlistPurgeRate, which by default is every tenth edit.
2574  $config = $this->context->getConfig();
2575  if ( $config->get( 'WatchlistExpiry' ) ) {
2576  MediaWikiServices::getInstance()
2577  ->getWatchedItemStore()
2578  ->enqueueWatchlistExpiryJob( $config->get( 'WatchlistPurgeRate' ) );
2579  }
2580  }
2581 
2593  private function mergeChangesIntoContent( &$editContent ) {
2594  // This is the revision that was current at the time editing was initiated on the client,
2595  // even if the edit was based on an old revision.
2596  $baseRevRecord = $this->getExpectedParentRevision();
2597  $baseContent = $baseRevRecord ?
2598  $baseRevRecord->getContent( SlotRecord::MAIN ) :
2599  null;
2600 
2601  if ( $baseContent === null ) {
2602  return false;
2603  }
2604 
2605  // The current state, we want to merge updates into it
2606  $currentRevisionRecord = $this->revisionStore->getRevisionByTitle(
2607  $this->mTitle,
2608  0,
2609  RevisionStore::READ_LATEST
2610  );
2611  $currentContent = $currentRevisionRecord
2612  ? $currentRevisionRecord->getContent( SlotRecord::MAIN )
2613  : null;
2614 
2615  if ( $currentContent === null ) {
2616  return false;
2617  }
2618 
2619  $result = $this->contentHandlerFactory
2620  ->getContentHandler( $baseContent->getModel() )
2621  ->merge3( $baseContent, $editContent, $currentContent );
2622 
2623  if ( $result ) {
2624  $editContent = $result;
2625  // Update parentRevId to what we just merged.
2626  $this->parentRevId = $currentRevisionRecord->getId();
2627  return true;
2628  }
2629 
2630  return false;
2631  }
2632 
2647  public function getBaseRevision() {
2648  wfDeprecated( __METHOD__, '1.35' );
2649  if ( $this->mBaseRevision === false ) {
2650  $revRecord = $this->getExpectedParentRevision();
2651  $this->mBaseRevision = $revRecord ? new Revision( $revRecord ) : null;
2652  }
2653  return $this->mBaseRevision;
2654  }
2655 
2663  public function getExpectedParentRevision() {
2664  if ( $this->mExpectedParentRevision === false ) {
2665  $revRecord = null;
2666  if ( $this->editRevId ) {
2667  $revRecord = $this->revisionStore->getRevisionById(
2668  $this->editRevId,
2669  RevisionStore::READ_LATEST
2670  );
2671  } else {
2672  $revRecord = $this->revisionStore->getRevisionByTimestamp(
2673  $this->getTitle(),
2674  $this->edittime,
2675  RevisionStore::READ_LATEST
2676  );
2677  }
2678  $this->mExpectedParentRevision = $revRecord;
2679  }
2681  }
2682 
2692  public static function matchSpamRegex( $text ) {
2693  wfDeprecated( __METHOD__, '1.35' );
2694  return MediaWikiServices::getInstance()->getSpamChecker()->checkContent( $text );
2695  }
2696 
2706  public static function matchSummarySpamRegex( $text ) {
2707  wfDeprecated( __METHOD__, '1.35' );
2708  return MediaWikiServices::getInstance()->getSpamChecker()->checkSummary( $text );
2709  }
2710 
2711  public function setHeaders() {
2712  $out = $this->context->getOutput();
2713 
2714  $out->addModules( 'mediawiki.action.edit' );
2715  $out->addModuleStyles( 'mediawiki.action.edit.styles' );
2716  $out->addModuleStyles( 'mediawiki.editfont.styles' );
2717 
2718  $user = $this->context->getUser();
2719 
2720  if ( $user->getOption( 'uselivepreview' ) ) {
2721  $out->addModules( 'mediawiki.action.edit.preview' );
2722  }
2723 
2724  if ( $user->getOption( 'useeditwarning' ) ) {
2725  $out->addModules( 'mediawiki.action.edit.editWarning' );
2726  }
2727 
2728  # Enabled article-related sidebar, toplinks, etc.
2729  $out->setArticleRelated( true );
2730 
2731  $contextTitle = $this->getContextTitle();
2732  if ( $this->isConflict ) {
2733  $msg = 'editconflict';
2734  } elseif ( $contextTitle->exists() && $this->section != '' ) {
2735  $msg = $this->section == 'new' ? 'editingcomment' : 'editingsection';
2736  } else {
2737  $msg = $contextTitle->exists()
2738  || ( $contextTitle->getNamespace() == NS_MEDIAWIKI
2739  && $contextTitle->getDefaultMessageText() !== false
2740  )
2741  ? 'editing'
2742  : 'creating';
2743  }
2744 
2745  # Use the title defined by DISPLAYTITLE magic word when present
2746  # NOTE: getDisplayTitle() returns HTML while getPrefixedText() returns plain text.
2747  # setPageTitle() treats the input as wikitext, which should be safe in either case.
2748  $displayTitle = isset( $this->mParserOutput ) ? $this->mParserOutput->getDisplayTitle() : false;
2749  if ( $displayTitle === false ) {
2750  $displayTitle = $contextTitle->getPrefixedText();
2751  } else {
2752  $out->setDisplayTitle( $displayTitle );
2753  }
2754  $out->setPageTitle( $this->context->msg( $msg, $displayTitle ) );
2755 
2756  $config = $this->context->getConfig();
2757 
2758  # Transmit the name of the message to JavaScript for live preview
2759  # Keep Resources.php/mediawiki.action.edit.preview in sync with the possible keys
2760  $out->addJsConfigVars( [
2761  'wgEditMessage' => $msg,
2762  'wgAjaxEditStash' => $config->get( 'AjaxEditStash' ),
2763  ] );
2764 
2765  // Add whether to use 'save' or 'publish' messages to JavaScript for post-edit, other
2766  // editors, etc.
2767  $out->addJsConfigVars(
2768  'wgEditSubmitButtonLabelPublish',
2769  $config->get( 'EditSubmitButtonLabelPublish' )
2770  );
2771  }
2772 
2776  protected function showIntro() {
2777  if ( $this->suppressIntro ) {
2778  return;
2779  }
2780 
2781  $out = $this->context->getOutput();
2782  $namespace = $this->mTitle->getNamespace();
2783 
2784  if ( $namespace == NS_MEDIAWIKI ) {
2785  # Show a warning if editing an interface message
2786  $out->wrapWikiMsg( "<div class='mw-editinginterface'>\n$1\n</div>", 'editinginterface' );
2787  # If this is a default message (but not css, json, or js),
2788  # show a hint that it is translatable on translatewiki.net
2789  if (
2790  !$this->mTitle->hasContentModel( CONTENT_MODEL_CSS )
2791  && !$this->mTitle->hasContentModel( CONTENT_MODEL_JSON )
2792  && !$this->mTitle->hasContentModel( CONTENT_MODEL_JAVASCRIPT )
2793  ) {
2794  $defaultMessageText = $this->mTitle->getDefaultMessageText();
2795  if ( $defaultMessageText !== false ) {
2796  $out->wrapWikiMsg( "<div class='mw-translateinterface'>\n$1\n</div>",
2797  'translateinterface' );
2798  }
2799  }
2800  } elseif ( $namespace == NS_FILE ) {
2801  # Show a hint to shared repo
2802  $file = MediaWikiServices::getInstance()->getRepoGroup()->findFile( $this->mTitle );
2803  if ( $file && !$file->isLocal() ) {
2804  $descUrl = $file->getDescriptionUrl();
2805  # there must be a description url to show a hint to shared repo
2806  if ( $descUrl ) {
2807  if ( !$this->mTitle->exists() ) {
2808  $out->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-create\">\n$1\n</div>", [
2809  'sharedupload-desc-create', $file->getRepo()->getDisplayName(), $descUrl
2810  ] );
2811  } else {
2812  $out->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-edit\">\n$1\n</div>", [
2813  'sharedupload-desc-edit', $file->getRepo()->getDisplayName(), $descUrl
2814  ] );
2815  }
2816  }
2817  }
2818  }
2819 
2820  # Show a warning message when someone creates/edits a user (talk) page but the user does not exist
2821  # Show log extract when the user is currently blocked
2822  if ( $namespace == NS_USER || $namespace == NS_USER_TALK ) {
2823  $username = explode( '/', $this->mTitle->getText(), 2 )[0];
2824  $user = User::newFromName( $username, false /* allow IP users */ );
2825  $ip = User::isIP( $username );
2826  $block = DatabaseBlock::newFromTarget( $user, $user );
2827  if ( !( $user && $user->isLoggedIn() ) && !$ip ) { # User does not exist
2828  $out->wrapWikiMsg( "<div class=\"mw-userpage-userdoesnotexist error\">\n$1\n</div>",
2829  [ 'userpage-userdoesnotexist', wfEscapeWikiText( $username ) ] );
2830  } elseif (
2831  $block !== null &&
2832  $block->getType() != DatabaseBlock::TYPE_AUTO &&
2833  ( $block->isSitewide() || $user->isBlockedFrom( $this->mTitle ) )
2834  ) {
2835  // Show log extract if the user is sitewide blocked or is partially
2836  // blocked and not allowed to edit their user page or user talk page
2838  $out,
2839  'block',
2840  MediaWikiServices::getInstance()->getNamespaceInfo()->
2841  getCanonicalName( NS_USER ) . ':' . $block->getTarget(),
2842  '',
2843  [
2844  'lim' => 1,
2845  'showIfEmpty' => false,
2846  'msgKey' => [
2847  'blocked-notice-logextract',
2848  $user->getName() # Support GENDER in notice
2849  ]
2850  ]
2851  );
2852  }
2853  }
2854  # Try to add a custom edit intro, or use the standard one if this is not possible.
2855  if ( !$this->showCustomIntro() && !$this->mTitle->exists() ) {
2857  $this->context->msg( 'helppage' )->inContentLanguage()->text()
2858  ) );
2859  if ( $this->context->getUser()->isLoggedIn() ) {
2860  $out->wrapWikiMsg(
2861  // Suppress the external link icon, consider the help url an internal one
2862  "<div class=\"mw-newarticletext plainlinks\">\n$1\n</div>",
2863  [
2864  'newarticletext',
2865  $helpLink
2866  ]
2867  );
2868  } else {
2869  $out->wrapWikiMsg(
2870  // Suppress the external link icon, consider the help url an internal one
2871  "<div class=\"mw-newarticletextanon plainlinks\">\n$1\n</div>",
2872  [
2873  'newarticletextanon',
2874  $helpLink
2875  ]
2876  );
2877  }
2878  }
2879  # Give a notice if the user is editing a deleted/moved page...
2880  if ( !$this->mTitle->exists() ) {
2881  $dbr = wfGetDB( DB_REPLICA );
2882 
2883  LogEventsList::showLogExtract( $out, [ 'delete', 'move' ], $this->mTitle,
2884  '',
2885  [
2886  'lim' => 10,
2887  'conds' => [ 'log_action != ' . $dbr->addQuotes( 'revision' ) ],
2888  'showIfEmpty' => false,
2889  'msgKey' => [ 'recreate-moveddeleted-warn' ]
2890  ]
2891  );
2892  }
2893  }
2894 
2900  protected function showCustomIntro() {
2901  if ( $this->editintro ) {
2902  $title = Title::newFromText( $this->editintro );
2903  if ( $this->isPageExistingAndViewable( $title, $this->context->getUser() ) ) {
2904  // Added using template syntax, to take <noinclude>'s into account.
2905  $this->context->getOutput()->addWikiTextAsContent(
2906  '<div class="mw-editintro">{{:' . $title->getFullText() . '}}</div>',
2907  /*linestart*/true,
2908  $this->mTitle
2909  );
2910  return true;
2911  }
2912  }
2913  return false;
2914  }
2915 
2934  protected function toEditText( $content ) {
2935  if ( $content === null || $content === false || is_string( $content ) ) {
2936  return $content;
2937  }
2938 
2939  if ( !$this->isSupportedContentModel( $content->getModel() ) ) {
2940  throw new MWException( 'This content model is not supported: ' . $content->getModel() );
2941  }
2942 
2943  return $content->serialize( $this->contentFormat );
2944  }
2945 
2962  protected function toEditContent( $text ) {
2963  if ( $text === false || $text === null ) {
2964  return $text;
2965  }
2966 
2967  $content = ContentHandler::makeContent( $text, $this->getTitle(),
2968  $this->contentModel, $this->contentFormat );
2969 
2970  if ( !$this->isSupportedContentModel( $content->getModel() ) ) {
2971  throw new MWException( 'This content model is not supported: ' . $content->getModel() );
2972  }
2973 
2974  return $content;
2975  }
2976 
2985  public function showEditForm( $formCallback = null ) {
2986  # need to parse the preview early so that we know which templates are used,
2987  # otherwise users with "show preview after edit box" will get a blank list
2988  # we parse this near the beginning so that setHeaders can do the title
2989  # setting work instead of leaving it in getPreviewText
2990  $previewOutput = '';
2991  if ( $this->formtype == 'preview' ) {
2992  $previewOutput = $this->getPreviewText();
2993  }
2994 
2995  $out = $this->context->getOutput();
2996 
2997  // Avoid PHP 7.1 warning of passing $this by reference
2998  $editPage = $this;
2999  Hooks::run( 'EditPage::showEditForm:initial', [ &$editPage, &$out ] );
3000 
3001  $this->setHeaders();
3002 
3003  $this->addTalkPageText();
3004  $this->addEditNotices();
3005 
3006  if ( !$this->isConflict &&
3007  $this->section != '' &&
3008  !$this->isSectionEditSupported() ) {
3009  // We use $this->section to much before this and getVal('wgSection') directly in other places
3010  // at this point we can't reset $this->section to '' to fallback to non-section editing.
3011  // Someone is welcome to try refactoring though
3012  $out->showErrorPage( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
3013  return;
3014  }
3015 
3016  $this->showHeader();
3017 
3018  $out->addHTML( $this->editFormPageTop );
3019 
3020  $user = $this->context->getUser();
3021  if ( $user->getOption( 'previewontop' ) ) {
3022  $this->displayPreviewArea( $previewOutput, true );
3023  }
3024 
3025  $out->addHTML( $this->editFormTextTop );
3026 
3027  if ( $this->wasDeletedSinceLastEdit() && $this->formtype !== 'save' ) {
3028  $out->wrapWikiMsg( "<div class='error mw-deleted-while-editing'>\n$1\n</div>",
3029  'deletedwhileediting' );
3030  }
3031 
3032  // @todo add EditForm plugin interface and use it here!
3033  // search for textarea1 and textarea2, and allow EditForm to override all uses.
3034  $out->addHTML( Html::openElement(
3035  'form',
3036  [
3037  'class' => 'mw-editform',
3038  'id' => self::EDITFORM_ID,
3039  'name' => self::EDITFORM_ID,
3040  'method' => 'post',
3041  'action' => $this->getActionURL( $this->getContextTitle() ),
3042  'enctype' => 'multipart/form-data'
3043  ]
3044  ) );
3045 
3046  if ( is_callable( $formCallback ) ) {
3047  wfWarn( 'The $formCallback parameter to ' . __METHOD__ . 'is deprecated' );
3048  call_user_func_array( $formCallback, [ &$out ] );
3049  }
3050 
3051  // Add a check for Unicode support
3052  $out->addHTML( Html::hidden( 'wpUnicodeCheck', self::UNICODE_CHECK ) );
3053 
3054  // Add an empty field to trip up spambots
3055  $out->addHTML(
3056  Xml::openElement( 'div', [ 'id' => 'antispam-container', 'style' => 'display: none;' ] )
3057  . Html::rawElement(
3058  'label',
3059  [ 'for' => 'wpAntispam' ],
3060  $this->context->msg( 'simpleantispam-label' )->parse()
3061  )
3062  . Xml::element(
3063  'input',
3064  [
3065  'type' => 'text',
3066  'name' => 'wpAntispam',
3067  'id' => 'wpAntispam',
3068  'value' => ''
3069  ]
3070  )
3071  . Xml::closeElement( 'div' )
3072  );
3073 
3074  // Avoid PHP 7.1 warning of passing $this by reference
3075  $editPage = $this;
3076  Hooks::run( 'EditPage::showEditForm:fields', [ &$editPage, &$out ] );
3077 
3078  // Put these up at the top to ensure they aren't lost on early form submission
3079  $this->showFormBeforeText();
3080 
3081  if ( $this->wasDeletedSinceLastEdit() && $this->formtype == 'save' ) {
3082  $username = $this->lastDelete->user_name;
3083  $comment = CommentStore::getStore()
3084  ->getComment( 'log_comment', $this->lastDelete )->text;
3085 
3086  // It is better to not parse the comment at all than to have templates expanded in the middle
3087  // TODO: can the checkLabel be moved outside of the div so that wrapWikiMsg could be used?
3088  $key = $comment === ''
3089  ? 'confirmrecreate-noreason'
3090  : 'confirmrecreate';
3091  $out->addHTML(
3092  '<div class="mw-confirm-recreate">' .
3093  $this->context->msg( $key, $username, "<nowiki>$comment</nowiki>" )->parse() .
3094  Xml::checkLabel( $this->context->msg( 'recreate' )->text(), 'wpRecreate', 'wpRecreate', false,
3095  [ 'title' => Linker::titleAttrib( 'recreate' ), 'tabindex' => 1, 'id' => 'wpRecreate' ]
3096  ) .
3097  '</div>'
3098  );
3099  }
3100 
3101  # When the summary is hidden, also hide them on preview/show changes
3102  if ( $this->nosummary ) {
3103  $out->addHTML( Html::hidden( 'nosummary', true ) );
3104  }
3105 
3106  # If a blank edit summary was previously provided, and the appropriate
3107  # user preference is active, pass a hidden tag as wpIgnoreBlankSummary. This will stop the
3108  # user being bounced back more than once in the event that a summary
3109  # is not required.
3110  # ####
3111  # For a bit more sophisticated detection of blank summaries, hash the
3112  # automatic one and pass that in the hidden field wpAutoSummary.
3113  if ( $this->missingSummary || ( $this->section == 'new' && $this->nosummary ) ) {
3114  $out->addHTML( Html::hidden( 'wpIgnoreBlankSummary', true ) );
3115  }
3116 
3117  if ( $this->undidRev ) {
3118  $out->addHTML( Html::hidden( 'wpUndidRevision', $this->undidRev ) );
3119  }
3120 
3121  if ( $this->selfRedirect ) {
3122  $out->addHTML( Html::hidden( 'wpIgnoreSelfRedirect', true ) );
3123  }
3124 
3125  if ( $this->hasPresetSummary ) {
3126  // If a summary has been preset using &summary= we don't want to prompt for
3127  // a different summary. Only prompt for a summary if the summary is blanked.
3128  // (T19416)
3129  $this->autoSumm = md5( '' );
3130  }
3131 
3132  $autosumm = $this->autoSumm !== '' ? $this->autoSumm : md5( $this->summary );
3133  $out->addHTML( Html::hidden( 'wpAutoSummary', $autosumm ) );
3134 
3135  $out->addHTML( Html::hidden( 'oldid', $this->oldid ) );
3136  $out->addHTML( Html::hidden( 'parentRevId', $this->getParentRevId() ) );
3137 
3138  $out->addHTML( Html::hidden( 'format', $this->contentFormat ) );
3139  $out->addHTML( Html::hidden( 'model', $this->contentModel ) );
3140 
3141  $out->enableOOUI();
3142 
3143  if ( $this->section == 'new' ) {
3144  $this->showSummaryInput( true, $this->summary );
3145  $out->addHTML( $this->getSummaryPreview( true, $this->summary ) );
3146  }
3147 
3148  $out->addHTML( $this->editFormTextBeforeContent );
3149  if ( $this->isConflict ) {
3150  // In an edit conflict, we turn textbox2 into the user's text,
3151  // and textbox1 into the stored version
3152  $this->textbox2 = $this->textbox1;
3153 
3154  $content = $this->getCurrentContent();
3155  $this->textbox1 = $this->toEditText( $content );
3156 
3158  $editConflictHelper->setTextboxes( $this->textbox2, $this->textbox1 );
3159  $editConflictHelper->setContentModel( $this->contentModel );
3160  $editConflictHelper->setContentFormat( $this->contentFormat );
3162  }
3163 
3164  if ( !$this->mTitle->isUserConfigPage() ) {
3165  $out->addHTML( self::getEditToolbar() );
3166  }
3167 
3168  if ( $this->blankArticle ) {
3169  $out->addHTML( Html::hidden( 'wpIgnoreBlankArticle', true ) );
3170  }
3171 
3172  if ( $this->isConflict ) {
3173  // In an edit conflict bypass the overridable content form method
3174  // and fallback to the raw wpTextbox1 since editconflicts can't be
3175  // resolved between page source edits and custom ui edits using the
3176  // custom edit ui.
3177  $conflictTextBoxAttribs = [];
3178  if ( $this->wasDeletedSinceLastEdit() ) {
3179  $conflictTextBoxAttribs['style'] = 'display:none;';
3180  } elseif ( $this->isOldRev ) {
3181  $conflictTextBoxAttribs['class'] = 'mw-textarea-oldrev';
3182  }
3183 
3184  $out->addHTML( $editConflictHelper->getEditConflictMainTextBox( $conflictTextBoxAttribs ) );
3186  } else {
3187  $this->showContentForm();
3188  }
3189 
3190  $out->addHTML( $this->editFormTextAfterContent );
3191 
3192  $this->showStandardInputs();
3193 
3194  $this->showFormAfterText();
3195 
3196  $this->showTosSummary();
3197 
3198  $this->showEditTools();
3199 
3200  $out->addHTML( $this->editFormTextAfterTools . "\n" );
3201 
3202  $out->addHTML( $this->makeTemplatesOnThisPageList( $this->getTemplates() ) );
3203 
3204  $out->addHTML( Html::rawElement( 'div', [ 'class' => 'hiddencats' ],
3205  Linker::formatHiddenCategories( $this->page->getHiddenCategories() ) ) );
3206 
3207  $out->addHTML( Html::rawElement( 'div', [ 'class' => 'limitreport' ],
3208  self::getPreviewLimitReport( $this->mParserOutput ) ) );
3209 
3210  $out->addModules( 'mediawiki.action.edit.collapsibleFooter' );
3211 
3212  if ( $this->isConflict ) {
3213  try {
3214  $this->showConflict();
3215  } catch ( MWContentSerializationException $ex ) {
3216  // this can't really happen, but be nice if it does.
3217  $msg = $this->context->msg(
3218  'content-failed-to-parse',
3219  $this->contentModel,
3220  $this->contentFormat,
3221  $ex->getMessage()
3222  );
3223  $out->wrapWikiTextAsInterface( 'error', $msg->plain() );
3224  }
3225  }
3226 
3227  // Set a hidden field so JS knows what edit form mode we are in
3228  if ( $this->isConflict ) {
3229  $mode = 'conflict';
3230  } elseif ( $this->preview ) {
3231  $mode = 'preview';
3232  } elseif ( $this->diff ) {
3233  $mode = 'diff';
3234  } else {
3235  $mode = 'text';
3236  }
3237  $out->addHTML( Html::hidden( 'mode', $mode, [ 'id' => 'mw-edit-mode' ] ) );
3238 
3239  // Marker for detecting truncated form data. This must be the last
3240  // parameter sent in order to be of use, so do not move me.
3241  $out->addHTML( Html::hidden( 'wpUltimateParam', true ) );
3242  $out->addHTML( $this->editFormTextBottom . "\n</form>\n" );
3243 
3244  if ( !$user->getOption( 'previewontop' ) ) {
3245  $this->displayPreviewArea( $previewOutput, false );
3246  }
3247  }
3248 
3256  public function makeTemplatesOnThisPageList( array $templates ) {
3257  $templateListFormatter = new TemplatesOnThisPageFormatter(
3258  $this->context, MediaWikiServices::getInstance()->getLinkRenderer()
3259  );
3260 
3261  // preview if preview, else section if section, else false
3262  $type = false;
3263  if ( $this->preview ) {
3264  $type = 'preview';
3265  } elseif ( $this->section != '' ) {
3266  $type = 'section';
3267  }
3268 
3269  return Html::rawElement( 'div', [ 'class' => 'templatesUsed' ],
3270  $templateListFormatter->format( $templates, $type )
3271  );
3272  }
3273 
3280  public static function extractSectionTitle( $text ) {
3281  preg_match( "/^(=+)(.+)\\1\\s*(\n|$)/i", $text, $matches );
3282  if ( !empty( $matches[2] ) ) {
3283  return MediaWikiServices::getInstance()->getParser()
3284  ->stripSectionName( trim( $matches[2] ) );
3285  } else {
3286  return false;
3287  }
3288  }
3289 
3290  protected function showHeader() {
3291  $out = $this->context->getOutput();
3292  $user = $this->context->getUser();
3293  if ( $this->isConflict ) {
3294  $this->addExplainConflictHeader( $out );
3295  $this->editRevId = $this->page->getLatest();
3296  } else {
3297  if ( $this->section != '' && $this->section != 'new' && !$this->summary &&
3298  !$this->preview && !$this->diff
3299  ) {
3300  $sectionTitle = self::extractSectionTitle( $this->textbox1 ); // FIXME: use Content object
3301  if ( $sectionTitle !== false ) {
3302  $this->summary = "/* $sectionTitle */ ";
3303  }
3304  }
3305 
3306  $buttonLabel = $this->context->msg( $this->getSubmitButtonLabel() )->text();
3307 
3308  if ( $this->missingComment ) {
3309  $out->wrapWikiMsg( "<div id='mw-missingcommenttext'>\n$1\n</div>", 'missingcommenttext' );
3310  }
3311 
3312  if ( $this->missingSummary && $this->section != 'new' ) {
3313  $out->wrapWikiMsg(
3314  "<div id='mw-missingsummary'>\n$1\n</div>",
3315  [ 'missingsummary', $buttonLabel ]
3316  );
3317  }
3318 
3319  if ( $this->missingSummary && $this->section == 'new' ) {
3320  $out->wrapWikiMsg(
3321  "<div id='mw-missingcommentheader'>\n$1\n</div>",
3322  [ 'missingcommentheader', $buttonLabel ]
3323  );
3324  }
3325 
3326  if ( $this->blankArticle ) {
3327  $out->wrapWikiMsg(
3328  "<div id='mw-blankarticle'>\n$1\n</div>",
3329  [ 'blankarticle', $buttonLabel ]
3330  );
3331  }
3332 
3333  if ( $this->selfRedirect ) {
3334  $out->wrapWikiMsg(
3335  "<div id='mw-selfredirect'>\n$1\n</div>",
3336  [ 'selfredirect', $buttonLabel ]
3337  );
3338  }
3339 
3340  if ( $this->hookError !== '' ) {
3341  $out->addWikiTextAsInterface( $this->hookError );
3342  }
3343 
3344  if ( $this->section != 'new' ) {
3345  $revRecord = $this->mArticle->fetchRevisionRecord();
3346  if ( $revRecord && $revRecord instanceof RevisionStoreRecord ) {
3347  // Let sysop know that this will make private content public if saved
3348 
3349  if ( !RevisionRecord::userCanBitfield(
3350  $revRecord->getVisibility(),
3351  RevisionRecord::DELETED_TEXT,
3352  $user
3353  ) ) {
3354  $out->wrapWikiMsg(
3355  "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
3356  'rev-deleted-text-permission'
3357  );
3358  } elseif ( $revRecord->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
3359  $out->wrapWikiMsg(
3360  "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
3361  'rev-deleted-text-view'
3362  );
3363  }
3364 
3365  if ( !$revRecord->isCurrent() ) {
3366  $this->mArticle->setOldSubtitle( $revRecord->getId() );
3367  $out->wrapWikiMsg(
3368  Html::warningBox( "\n$1\n" ),
3369  'editingold'
3370  );
3371  $this->isOldRev = true;
3372  }
3373  } elseif ( $this->mTitle->exists() ) {
3374  // Something went wrong
3375 
3376  $out->wrapWikiMsg( "<div class='errorbox'>\n$1\n</div>\n",
3377  [ 'missing-revision', $this->oldid ] );
3378  }
3379  }
3380  }
3381 
3382  if ( wfReadOnly() ) {
3383  $out->wrapWikiMsg(
3384  "<div id=\"mw-read-only-warning\">\n$1\n</div>",
3385  [ 'readonlywarning', wfReadOnlyReason() ]
3386  );
3387  } elseif ( $user->isAnon() ) {
3388  if ( $this->formtype != 'preview' ) {
3389  $returntoquery = array_diff_key(
3390  $this->context->getRequest()->getValues(),
3391  [ 'title' => true, 'returnto' => true, 'returntoquery' => true ]
3392  );
3393  $out->wrapWikiMsg(
3394  "<div id='mw-anon-edit-warning' class='warningbox'>\n$1\n</div>",
3395  [ 'anoneditwarning',
3396  // Log-in link
3397  SpecialPage::getTitleFor( 'Userlogin' )->getFullURL( [
3398  'returnto' => $this->getTitle()->getPrefixedDBkey(),
3399  'returntoquery' => wfArrayToCgi( $returntoquery ),
3400  ] ),
3401  // Sign-up link
3402  SpecialPage::getTitleFor( 'CreateAccount' )->getFullURL( [
3403  'returnto' => $this->getTitle()->getPrefixedDBkey(),
3404  'returntoquery' => wfArrayToCgi( $returntoquery ),
3405  ] )
3406  ]
3407  );
3408  } else {
3409  $out->wrapWikiMsg( "<div id=\"mw-anon-preview-warning\" class=\"warningbox\">\n$1</div>",
3410  'anonpreviewwarning'
3411  );
3412  }
3413  } elseif ( $this->mTitle->isUserConfigPage() ) {
3414  # Check the skin exists
3415  if ( $this->isWrongCaseUserConfigPage() ) {
3416  $out->wrapWikiMsg(
3417  "<div class='error' id='mw-userinvalidconfigtitle'>\n$1\n</div>",
3418  [ 'userinvalidconfigtitle', $this->mTitle->getSkinFromConfigSubpage() ]
3419  );
3420  }
3421  if ( $this->getTitle()->isSubpageOf( $user->getUserPage() ) ) {
3422  $isUserCssConfig = $this->mTitle->isUserCssConfigPage();
3423  $isUserJsonConfig = $this->mTitle->isUserJsonConfigPage();
3424  $isUserJsConfig = $this->mTitle->isUserJsConfigPage();
3425 
3426  $warning = $isUserCssConfig
3427  ? 'usercssispublic'
3428  : ( $isUserJsonConfig ? 'userjsonispublic' : 'userjsispublic' );
3429 
3430  $out->wrapWikiMsg( '<div class="mw-userconfigpublic">$1</div>', $warning );
3431 
3432  if ( $isUserJsConfig ) {
3433  $out->wrapWikiMsg( '<div class="mw-userconfigdangerous">$1</div>', 'userjsdangerous' );
3434  }
3435 
3436  if ( $this->formtype !== 'preview' ) {
3437  $config = $this->context->getConfig();
3438  if ( $isUserCssConfig && $config->get( 'AllowUserCss' ) ) {
3439  $out->wrapWikiMsg(
3440  "<div id='mw-usercssyoucanpreview'>\n$1\n</div>",
3441  [ 'usercssyoucanpreview' ]
3442  );
3443  } elseif ( $isUserJsonConfig /* No comparable 'AllowUserJson' */ ) {
3444  $out->wrapWikiMsg(
3445  "<div id='mw-userjsonyoucanpreview'>\n$1\n</div>",
3446  [ 'userjsonyoucanpreview' ]
3447  );
3448  } elseif ( $isUserJsConfig && $config->get( 'AllowUserJs' ) ) {
3449  $out->wrapWikiMsg(
3450  "<div id='mw-userjsyoucanpreview'>\n$1\n</div>",
3451  [ 'userjsyoucanpreview' ]
3452  );
3453  }
3454  }
3455  }
3456  }
3457 
3459 
3460  $this->addLongPageWarningHeader();
3461 
3462  # Add header copyright warning
3463  $this->showHeaderCopyrightWarning();
3464  }
3465 
3473  private function getSummaryInputAttributes( array $inputAttrs = null ) {
3474  // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
3475  // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
3476  // Unicode codepoints.
3477  return ( is_array( $inputAttrs ) ? $inputAttrs : [] ) + [
3478  'id' => 'wpSummary',
3479  'name' => 'wpSummary',
3481  'tabindex' => 1,
3482  'size' => 60,
3483  'spellcheck' => 'true',
3484  ];
3485  }
3486 
3496  public function getSummaryInputWidget( $summary = "", $labelText = null, $inputAttrs = null ) {
3497  $inputAttrs = OOUI\Element::configFromHtmlAttributes(
3498  $this->getSummaryInputAttributes( $inputAttrs )
3499  );
3500  $inputAttrs += [
3501  'title' => Linker::titleAttrib( 'summary' ),
3502  'accessKey' => Linker::accesskey( 'summary' ),
3503  ];
3504 
3505  // For compatibility with old scripts and extensions, we want the legacy 'id' on the `<input>`
3506  $inputAttrs['inputId'] = $inputAttrs['id'];
3507  $inputAttrs['id'] = 'wpSummaryWidget';
3508 
3509  return new OOUI\FieldLayout(
3510  new OOUI\TextInputWidget( [
3511  'value' => $summary,
3512  'infusable' => true,
3513  ] + $inputAttrs ),
3514  [
3515  'label' => new OOUI\HtmlSnippet( $labelText ),
3516  'align' => 'top',
3517  'id' => 'wpSummaryLabel',
3518  'classes' => [ $this->missingSummary ? 'mw-summarymissed' : 'mw-summary' ],
3519  ]
3520  );
3521  }
3522 
3529  protected function showSummaryInput( $isSubjectPreview, $summary = "" ) {
3530  # Add a class if 'missingsummary' is triggered to allow styling of the summary line
3531  $summaryClass = $this->missingSummary ? 'mw-summarymissed' : 'mw-summary';
3532  if ( $isSubjectPreview ) {
3533  if ( $this->nosummary ) {
3534  return;
3535  }
3536  } elseif ( !$this->mShowSummaryField ) {
3537  return;
3538  }
3539 
3540  $labelText = $this->context->msg( $isSubjectPreview ? 'subject' : 'summary' )->parse();
3541  $this->context->getOutput()->addHTML( $this->getSummaryInputWidget(
3542  $summary,
3543  $labelText,
3544  [ 'class' => $summaryClass ]
3545  ) );
3546  }
3547 
3555  protected function getSummaryPreview( $isSubjectPreview, $summary = "" ) {
3556  // avoid spaces in preview, gets always trimmed on save
3557  $summary = trim( $summary );
3558  if ( !$summary || ( !$this->preview && !$this->diff ) ) {
3559  return "";
3560  }
3561 
3562  if ( $isSubjectPreview ) {
3563  $summary = $this->context->msg( 'newsectionsummary' )
3564  ->rawParams( MediaWikiServices::getInstance()->getParser()
3565  ->stripSectionName( $summary ) )
3566  ->inContentLanguage()->text();
3567  }
3568 
3569  $message = $isSubjectPreview ? 'subject-preview' : 'summary-preview';
3570 
3571  $summary = $this->context->msg( $message )->parse()
3572  . Linker::commentBlock( $summary, $this->mTitle, $isSubjectPreview );
3573  return Xml::tags( 'div', [ 'class' => 'mw-summary-preview' ], $summary );
3574  }
3575 
3576  protected function showFormBeforeText() {
3577  $out = $this->context->getOutput();
3578  $out->addHTML( Html::hidden( 'wpSection', $this->section ) );
3579  $out->addHTML( Html::hidden( 'wpStarttime', $this->starttime ) );
3580  $out->addHTML( Html::hidden( 'wpEdittime', $this->edittime ) );
3581  $out->addHTML( Html::hidden( 'editRevId', $this->editRevId ) );
3582  $out->addHTML( Html::hidden( 'wpScrolltop', $this->scrolltop, [ 'id' => 'wpScrolltop' ] ) );
3583  }
3584 
3585  protected function showFormAfterText() {
3598  $this->context->getOutput()->addHTML(
3599  "\n" .
3600  Html::hidden( "wpEditToken", $this->context->getUser()->getEditToken() ) .
3601  "\n"
3602  );
3603  }
3604 
3613  protected function showContentForm() {
3614  $this->showTextbox1();
3615  }
3616 
3625  protected function showTextbox1( $customAttribs = null, $textoverride = null ) {
3626  if ( $this->wasDeletedSinceLastEdit() && $this->formtype == 'save' ) {
3627  $attribs = [ 'style' => 'display:none;' ];
3628  } else {
3629  $builder = new TextboxBuilder();
3630  $classes = $builder->getTextboxProtectionCSSClasses( $this->getTitle() );
3631 
3632  # Is an old revision being edited?
3633  if ( $this->isOldRev ) {
3634  $classes[] = 'mw-textarea-oldrev';
3635  }
3636 
3637  $attribs = [
3638  'aria-label' => $this->context->msg( 'edit-textarea-aria-label' )->text(),
3639  'tabindex' => 1
3640  ];
3641 
3642  if ( is_array( $customAttribs ) ) {
3643  $attribs += $customAttribs;
3644  }
3645 
3646  $attribs = $builder->mergeClassesIntoAttributes( $classes, $attribs );
3647  }
3648 
3649  $this->showTextbox(
3650  $textoverride ?? $this->textbox1,
3651  'wpTextbox1',
3652  $attribs
3653  );
3654  }
3655 
3656  protected function showTextbox2() {
3657  $this->showTextbox( $this->textbox2, 'wpTextbox2', [ 'tabindex' => 6, 'readonly' ] );
3658  }
3659 
3660  protected function showTextbox( $text, $name, $customAttribs = [] ) {
3661  $builder = new TextboxBuilder();
3662  $attribs = $builder->buildTextboxAttribs(
3663  $name,
3664  $customAttribs,
3665  $this->context->getUser(),
3666  $this->mTitle
3667  );
3668 
3669  $this->context->getOutput()->addHTML(
3670  Html::textarea( $name, $builder->addNewLineAtEnd( $text ), $attribs )
3671  );
3672  }
3673 
3674  protected function displayPreviewArea( $previewOutput, $isOnTop = false ) {
3675  $classes = [];
3676  if ( $isOnTop ) {
3677  $classes[] = 'ontop';
3678  }
3679 
3680  $attribs = [ 'id' => 'wikiPreview', 'class' => implode( ' ', $classes ) ];
3681 
3682  if ( $this->formtype != 'preview' ) {
3683  $attribs['style'] = 'display: none;';
3684  }
3685 
3686  $out = $this->context->getOutput();
3687  $out->addHTML( Xml::openElement( 'div', $attribs ) );
3688 
3689  if ( $this->formtype == 'preview' ) {
3690  $this->showPreview( $previewOutput );
3691  } else {
3692  // Empty content container for LivePreview
3693  $pageViewLang = $this->mTitle->getPageViewLanguage();
3694  $attribs = [ 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(),
3695  'class' => 'mw-content-' . $pageViewLang->getDir() ];
3696  $out->addHTML( Html::rawElement( 'div', $attribs ) );
3697  }
3698 
3699  $out->addHTML( '</div>' );
3700 
3701  if ( $this->formtype == 'diff' ) {
3702  try {
3703  $this->showDiff();
3704  } catch ( MWContentSerializationException $ex ) {
3705  $msg = $this->context->msg(
3706  'content-failed-to-parse',
3707  $this->contentModel,
3708  $this->contentFormat,
3709  $ex->getMessage()
3710  );
3711  $out->wrapWikiTextAsInterface( 'error', $msg->plain() );
3712  }
3713  }
3714  }
3715 
3722  protected function showPreview( $text ) {
3723  if ( $this->mArticle instanceof CategoryPage ) {
3724  $this->mArticle->openShowCategory();
3725  }
3726  # This hook seems slightly odd here, but makes things more
3727  # consistent for extensions.
3728  $out = $this->context->getOutput();
3729  Hooks::run( 'OutputPageBeforeHTML', [ &$out, &$text ] );
3730  $out->addHTML( $text );
3731  if ( $this->mArticle instanceof CategoryPage ) {
3732  $this->mArticle->closeShowCategory();
3733  }
3734  }
3735 
3743  public function showDiff() {
3744  $oldtitlemsg = 'currentrev';
3745  # if message does not exist, show diff against the preloaded default
3746  if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && !$this->mTitle->exists() ) {
3747  $oldtext = $this->mTitle->getDefaultMessageText();
3748  if ( $oldtext !== false ) {
3749  $oldtitlemsg = 'defaultmessagetext';
3750  $oldContent = $this->toEditContent( $oldtext );
3751  } else {
3752  $oldContent = null;
3753  }
3754  } else {
3755  $oldContent = $this->getCurrentContent();
3756  }
3757 
3758  $textboxContent = $this->toEditContent( $this->textbox1 );
3759  if ( $this->editRevId !== null ) {
3760  $newContent = $this->page->replaceSectionAtRev(
3761  $this->section, $textboxContent, $this->summary, $this->editRevId
3762  );
3763  } else {
3764  $newContent = $this->page->replaceSectionContent(
3765  $this->section, $textboxContent, $this->summary, $this->edittime
3766  );
3767  }
3768 
3769  if ( $newContent ) {
3770  Hooks::run( 'EditPageGetDiffContent', [ $this, &$newContent ] );
3771 
3772  $user = $this->context->getUser();
3773  $popts = ParserOptions::newFromUserAndLang( $user,
3774  MediaWikiServices::getInstance()->getContentLanguage() );
3775  $newContent = $newContent->preSaveTransform( $this->mTitle, $user, $popts );
3776  }
3777 
3778  if ( ( $oldContent && !$oldContent->isEmpty() ) || ( $newContent && !$newContent->isEmpty() ) ) {
3779  $oldtitle = $this->context->msg( $oldtitlemsg )->parse();
3780  $newtitle = $this->context->msg( 'yourtext' )->parse();
3781 
3782  if ( !$oldContent ) {
3783  $oldContent = $newContent->getContentHandler()->makeEmptyContent();
3784  }
3785 
3786  if ( !$newContent ) {
3787  $newContent = $oldContent->getContentHandler()->makeEmptyContent();
3788  }
3789 
3790  $de = $oldContent->getContentHandler()->createDifferenceEngine( $this->context );
3791  $de->setContent( $oldContent, $newContent );
3792 
3793  $difftext = $de->getDiff( $oldtitle, $newtitle );
3794  $de->showDiffStyle();
3795  } else {
3796  $difftext = '';
3797  }
3798 
3799  $this->context->getOutput()->addHTML( '<div id="wikiDiff">' . $difftext . '</div>' );
3800  }
3801 
3805  protected function showHeaderCopyrightWarning() {
3806  $msg = 'editpage-head-copy-warn';
3807  if ( !$this->context->msg( $msg )->isDisabled() ) {
3808  $this->context->getOutput()->wrapWikiMsg( "<div class='editpage-head-copywarn'>\n$1\n</div>",
3809  'editpage-head-copy-warn' );
3810  }
3811  }
3812 
3821  protected function showTosSummary() {
3822  $msg = 'editpage-tos-summary';
3823  Hooks::run( 'EditPageTosSummary', [ $this->mTitle, &$msg ] );
3824  if ( !$this->context->msg( $msg )->isDisabled() ) {
3825  $out = $this->context->getOutput();
3826  $out->addHTML( '<div class="mw-tos-summary">' );
3827  $out->addWikiMsg( $msg );
3828  $out->addHTML( '</div>' );
3829  }
3830  }
3831 
3836  protected function showEditTools() {
3837  $this->context->getOutput()->addHTML( '<div class="mw-editTools">' .
3838  $this->context->msg( 'edittools' )->inContentLanguage()->parse() .
3839  '</div>' );
3840  }
3841 
3848  protected function getCopywarn() {
3849  return self::getCopyrightWarning( $this->mTitle );
3850  }
3851 
3860  public static function getCopyrightWarning( $title, $format = 'plain', $langcode = null ) {
3861  global $wgRightsText;
3862  if ( $wgRightsText ) {
3863  $copywarnMsg = [ 'copyrightwarning',
3864  '[[' . wfMessage( 'copyrightpage' )->inContentLanguage()->text() . ']]',
3865  $wgRightsText ];
3866  } else {
3867  $copywarnMsg = [ 'copyrightwarning2',
3868  '[[' . wfMessage( 'copyrightpage' )->inContentLanguage()->text() . ']]' ];
3869  }
3870  // Allow for site and per-namespace customization of contribution/copyright notice.
3871  Hooks::run( 'EditPageCopyrightWarning', [ $title, &$copywarnMsg ] );
3872 
3873  $msg = wfMessage( ...$copywarnMsg )->title( $title );
3874  if ( $langcode ) {
3875  $msg->inLanguage( $langcode );
3876  }
3877  return "<div id=\"editpage-copywarn\">\n" .
3878  $msg->$format() . "\n</div>";
3879  }
3880 
3888  public static function getPreviewLimitReport( ParserOutput $output = null ) {
3889  global $wgLang;
3890 
3891  if ( !$output || !$output->getLimitReportData() ) {
3892  return '';
3893  }
3894 
3895  $limitReport = Html::rawElement( 'div', [ 'class' => 'mw-limitReportExplanation' ],
3896  wfMessage( 'limitreport-title' )->parseAsBlock()
3897  );
3898 
3899  // Show/hide animation doesn't work correctly on a table, so wrap it in a div.
3900  $limitReport .= Html::openElement( 'div', [ 'class' => 'preview-limit-report-wrapper' ] );
3901 
3902  $limitReport .= Html::openElement( 'table', [
3903  'class' => 'preview-limit-report wikitable'
3904  ] ) .
3905  Html::openElement( 'tbody' );
3906 
3907  foreach ( $output->getLimitReportData() as $key => $value ) {
3908  if ( Hooks::run( 'ParserLimitReportFormat',
3909  [ $key, &$value, &$limitReport, true, true ]
3910  ) ) {
3911  $keyMsg = wfMessage( $key );
3912  $valueMsg = wfMessage( [ "$key-value-html", "$key-value" ] );
3913  if ( !$valueMsg->exists() ) {
3914  $valueMsg = new RawMessage( '$1' );
3915  }
3916  if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) {
3917  $limitReport .= Html::openElement( 'tr' ) .
3918  Html::rawElement( 'th', null, $keyMsg->parse() ) .
3919  Html::rawElement( 'td', null,
3920  $wgLang->formatNum( $valueMsg->params( $value )->parse() )
3921  ) .
3922  Html::closeElement( 'tr' );
3923  }
3924  }
3925  }
3926 
3927  $limitReport .= Html::closeElement( 'tbody' ) .
3928  Html::closeElement( 'table' ) .
3929  Html::closeElement( 'div' );
3930 
3931  return $limitReport;
3932  }
3933 
3934  protected function showStandardInputs( &$tabindex = 2 ) {
3935  $out = $this->context->getOutput();
3936  $out->addHTML( "<div class='editOptions'>\n" );
3937 
3938  if ( $this->section != 'new' ) {
3939  $this->showSummaryInput( false, $this->summary );
3940  $out->addHTML( $this->getSummaryPreview( false, $this->summary ) );
3941  }
3942 
3943  $checkboxes = $this->getCheckboxesWidget(
3944  $tabindex,
3945  [ 'minor' => $this->minoredit, 'watch' => $this->watchthis ]
3946  );
3947  $checkboxesHTML = new OOUI\HorizontalLayout( [ 'items' => $checkboxes ] );
3948 
3949  $out->addHTML( "<div class='editCheckboxes'>" . $checkboxesHTML . "</div>\n" );
3950 
3951  // Show copyright warning.
3952  $out->addWikiTextAsInterface( $this->getCopywarn() );
3953  $out->addHTML( $this->editFormTextAfterWarn );
3954 
3955  $out->addHTML( "<div class='editButtons'>\n" );
3956  $out->addHTML( implode( "\n", $this->getEditButtons( $tabindex ) ) . "\n" );
3957 
3958  $cancel = $this->getCancelLink( $tabindex++ );
3959 
3960  $message = $this->context->msg( 'edithelppage' )->inContentLanguage()->text();
3961  $edithelpurl = Skin::makeInternalOrExternalUrl( $message );
3962  $edithelp =
3964  $this->context->msg( 'edithelp' )->text(),
3965  [ 'target' => 'helpwindow', 'href' => $edithelpurl ],
3966  [ 'mw-ui-quiet' ]
3967  ) .
3968  $this->context->msg( 'word-separator' )->escaped() .
3969  $this->context->msg( 'newwindow' )->parse();
3970 
3971  $out->addHTML( " <span class='cancelLink'>{$cancel}</span>\n" );
3972  $out->addHTML( " <span class='editHelp'>{$edithelp}</span>\n" );
3973  $out->addHTML( "</div><!-- editButtons -->\n" );
3974 
3975  Hooks::run( 'EditPage::showStandardInputs:options', [ $this, $out, &$tabindex ] );
3976 
3977  $out->addHTML( "</div><!-- editOptions -->\n" );
3978  }
3979 
3984  protected function showConflict() {
3985  $out = $this->context->getOutput();
3986  // Avoid PHP 7.1 warning of passing $this by reference
3987  $editPage = $this;
3988  if ( Hooks::run( 'EditPageBeforeConflictDiff', [ &$editPage, &$out ] ) ) {
3989  $this->incrementConflictStats();
3990 
3991  $this->getEditConflictHelper()->showEditFormTextAfterFooters();
3992  }
3993  }
3994 
3995  protected function incrementConflictStats() {
3996  $this->getEditConflictHelper()->incrementConflictStats( $this->context->getUser() );
3997  }
3998 
4003  public function getCancelLink( $tabindex = 0 ) {
4004  $cancelParams = [];
4005  if ( !$this->isConflict && $this->oldid > 0 ) {
4006  $cancelParams['oldid'] = $this->oldid;
4007  } elseif ( $this->getContextTitle()->isRedirect() ) {
4008  $cancelParams['redirect'] = 'no';
4009  }
4010 
4011  return new OOUI\ButtonWidget( [
4012  'id' => 'mw-editform-cancel',
4013  'tabIndex' => $tabindex,
4014  'href' => $this->getContextTitle()->getLinkURL( $cancelParams ),
4015  'label' => new OOUI\HtmlSnippet( $this->context->msg( 'cancel' )->parse() ),
4016  'framed' => false,
4017  'infusable' => true,
4018  'flags' => 'destructive',
4019  ] );
4020  }
4021 
4031  protected function getActionURL( Title $title ) {
4032  return $title->getLocalURL( [ 'action' => $this->action ] );
4033  }
4034 
4042  protected function wasDeletedSinceLastEdit() {
4043  if ( $this->deletedSinceEdit !== null ) {
4044  return $this->deletedSinceEdit;
4045  }
4046 
4047  $this->deletedSinceEdit = false;
4048 
4049  if ( !$this->mTitle->exists() && $this->mTitle->isDeletedQuick() ) {
4050  $this->lastDelete = $this->getLastDelete();
4051  if ( $this->lastDelete ) {
4052  $deleteTime = wfTimestamp( TS_MW, $this->lastDelete->log_timestamp );
4053  if ( $deleteTime > $this->starttime ) {
4054  $this->deletedSinceEdit = true;
4055  }
4056  }
4057  }
4058 
4059  return $this->deletedSinceEdit;
4060  }
4061 
4067  protected function getLastDelete() {
4068  $dbr = wfGetDB( DB_REPLICA );
4069  $commentQuery = CommentStore::getStore()->getJoin( 'log_comment' );
4070  $actorQuery = ActorMigration::newMigration()->getJoin( 'log_user' );
4071  $data = $dbr->selectRow(
4072  array_merge( [ 'logging' ], $commentQuery['tables'], $actorQuery['tables'], [ 'user' ] ),
4073  [
4074  'log_type',
4075  'log_action',
4076  'log_timestamp',
4077  'log_namespace',
4078  'log_title',
4079  'log_params',
4080  'log_deleted',
4081  'user_name'
4082  ] + $commentQuery['fields'] + $actorQuery['fields'],
4083  [
4084  'log_namespace' => $this->mTitle->getNamespace(),
4085  'log_title' => $this->mTitle->getDBkey(),
4086  'log_type' => 'delete',
4087  'log_action' => 'delete',
4088  ],
4089  __METHOD__,
4090  [ 'LIMIT' => 1, 'ORDER BY' => 'log_timestamp DESC' ],
4091  [
4092  'user' => [ 'JOIN', 'user_id=' . $actorQuery['fields']['log_user'] ],
4093  ] + $commentQuery['joins'] + $actorQuery['joins']
4094  );
4095  // Quick paranoid permission checks...
4096  if ( is_object( $data ) ) {
4097  if ( $data->log_deleted & LogPage::DELETED_USER ) {
4098  $data->user_name = $this->context->msg( 'rev-deleted-user' )->escaped();
4099  }
4100 
4101  if ( $data->log_deleted & LogPage::DELETED_COMMENT ) {
4102  $data->log_comment_text = $this->context->msg( 'rev-deleted-comment' )->escaped();
4103  $data->log_comment_data = null;
4104  }
4105  }
4106 
4107  return $data;
4108  }
4109 
4115  public function getPreviewText() {
4116  $out = $this->context->getOutput();
4117  $config = $this->context->getConfig();
4118 
4119  if ( $config->get( 'RawHtml' ) && !$this->mTokenOk ) {
4120  // Could be an offsite preview attempt. This is very unsafe if
4121  // HTML is enabled, as it could be an attack.
4122  $parsedNote = '';
4123  if ( $this->textbox1 !== '' ) {
4124  // Do not put big scary notice, if previewing the empty
4125  // string, which happens when you initially edit
4126  // a category page, due to automatic preview-on-open.
4127  $parsedNote = Html::rawElement( 'div', [ 'class' => 'previewnote' ],
4128  $out->parseAsInterface(
4129  $this->context->msg( 'session_fail_preview_html' )->plain()
4130  ) );
4131  }
4132  $this->incrementEditFailureStats( 'session_loss' );
4133  return $parsedNote;
4134  }
4135 
4136  $note = '';
4137 
4138  try {
4139  $content = $this->toEditContent( $this->textbox1 );
4140 
4141  $previewHTML = '';
4142  if ( !Hooks::run(
4143  'AlternateEditPreview',
4144  [ $this, &$content, &$previewHTML, &$this->mParserOutput ] )
4145  ) {
4146  return $previewHTML;
4147  }
4148 
4149  # provide a anchor link to the editform
4150  $continueEditing = '<span class="mw-continue-editing">' .
4151  '[[#' . self::EDITFORM_ID . '|' .
4152  $this->context->getLanguage()->getArrow() . ' ' .
4153  $this->context->msg( 'continue-editing' )->text() . ']]</span>';
4154  if ( $this->mTriedSave && !$this->mTokenOk ) {
4155  if ( $this->mTokenOkExceptSuffix ) {
4156  $note = $this->context->msg( 'token_suffix_mismatch' )->plain();
4157  $this->incrementEditFailureStats( 'bad_token' );
4158  } else {
4159  $note = $this->context->msg( 'session_fail_preview' )->plain();
4160  $this->incrementEditFailureStats( 'session_loss' );
4161  }
4162  } elseif ( $this->incompleteForm ) {
4163  $note = $this->context->msg( 'edit_form_incomplete' )->plain();
4164  if ( $this->mTriedSave ) {
4165  $this->incrementEditFailureStats( 'incomplete_form' );
4166  }
4167  } else {
4168  $note = $this->context->msg( 'previewnote' )->plain() . ' ' . $continueEditing;
4169  }
4170 
4171  # don't parse non-wikitext pages, show message about preview
4172  if ( $this->mTitle->isUserConfigPage() || $this->mTitle->isSiteConfigPage() ) {
4173  if ( $this->mTitle->isUserConfigPage() ) {
4174  $level = 'user';
4175  } elseif ( $this->mTitle->isSiteConfigPage() ) {
4176  $level = 'site';
4177  } else {
4178  $level = false;
4179  }
4180 
4181  if ( $content->getModel() == CONTENT_MODEL_CSS ) {
4182  $format = 'css';
4183  if ( $level === 'user' && !$config->get( 'AllowUserCss' ) ) {
4184  $format = false;
4185  }
4186  } elseif ( $content->getModel() == CONTENT_MODEL_JSON ) {
4187  $format = 'json';
4188  if ( $level === 'user' /* No comparable 'AllowUserJson' */ ) {
4189  $format = false;
4190  }
4191  } elseif ( $content->getModel() == CONTENT_MODEL_JAVASCRIPT ) {
4192  $format = 'js';
4193  if ( $level === 'user' && !$config->get( 'AllowUserJs' ) ) {
4194  $format = false;
4195  }
4196  } else {
4197  $format = false;
4198  }
4199 
4200  # Used messages to make sure grep find them:
4201  # Messages: usercsspreview, userjsonpreview, userjspreview,
4202  # sitecsspreview, sitejsonpreview, sitejspreview
4203  if ( $level && $format ) {
4204  $note = "<div id='mw-{$level}{$format}preview'>" .
4205  $this->context->msg( "{$level}{$format}preview" )->plain() .
4206  ' ' . $continueEditing . "</div>";
4207  }
4208  }
4209 
4210  # If we're adding a comment, we need to show the
4211  # summary as the headline
4212  if ( $this->section === "new" && $this->summary !== "" ) {
4213  $content = $content->addSectionHeader( $this->summary );
4214  }
4215 
4216  Hooks::run( 'EditPageGetPreviewContent', [ $this, &$content ] );
4217 
4218  $parserResult = $this->doPreviewParse( $content );
4219  $parserOutput = $parserResult['parserOutput'];
4220  $previewHTML = $parserResult['html'];
4221  $this->mParserOutput = $parserOutput;
4222  $out->addParserOutputMetadata( $parserOutput );
4223  if ( $out->userCanPreview() ) {
4224  $out->addContentOverride( $this->getTitle(), $content );
4225  }
4226 
4227  if ( count( $parserOutput->getWarnings() ) ) {
4228  $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
4229  }
4230 
4231  } catch ( MWContentSerializationException $ex ) {
4232  $m = $this->context->msg(
4233  'content-failed-to-parse',
4234  $this->contentModel,
4235  $this->contentFormat,
4236  $ex->getMessage()
4237  );
4238  $note .= "\n\n" . $m->plain(); # gets parsed down below
4239  $previewHTML = '';
4240  }
4241 
4242  if ( $this->isConflict ) {
4243  $conflict = Html::rawElement(
4244  'div', [ 'id' => 'mw-previewconflict', 'class' => 'warningbox' ],
4245  $this->context->msg( 'previewconflict' )->escaped()
4246  );
4247  } else {
4248  $conflict = '';
4249  }
4250 
4251  $previewhead = Html::rawElement(
4252  'div', [ 'class' => 'previewnote' ],
4254  'h2', [ 'id' => 'mw-previewheader' ],
4255  $this->context->msg( 'preview' )->escaped()
4256  ) .
4257  Html::rawElement( 'div', [ 'class' => 'warningbox' ],
4258  $out->parseAsInterface( $note )
4259  ) . $conflict
4260  );
4261 
4262  $pageViewLang = $this->mTitle->getPageViewLanguage();
4263  $attribs = [ 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(),
4264  'class' => 'mw-content-' . $pageViewLang->getDir() ];
4265  $previewHTML = Html::rawElement( 'div', $attribs, $previewHTML );
4266 
4267  return $previewhead . $previewHTML . $this->previewTextAfterContent;
4268  }
4269 
4270  private function incrementEditFailureStats( $failureType ) {
4271  $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
4272  $stats->increment( 'edit.failures.' . $failureType );
4273  }
4274 
4279  protected function getPreviewParserOptions() {
4280  $parserOptions = $this->page->makeParserOptions( $this->context );
4281  $parserOptions->setIsPreview( true );
4282  $parserOptions->setIsSectionPreview( $this->section !== null && $this->section !== '' );
4283  $parserOptions->enableLimitReport();
4284 
4285  // XXX: we could call $parserOptions->setCurrentRevisionCallback here to force the
4286  // current revision to be null during PST, until setupFakeRevision is called on
4287  // the ParserOptions. Currently, we rely on Parser::getRevisionObject() to ignore
4288  // existing revisions in preview mode.
4289 
4290  return $parserOptions;
4291  }
4292 
4302  protected function doPreviewParse( Content $content ) {
4303  $user = $this->context->getUser();
4304  $parserOptions = $this->getPreviewParserOptions();
4305 
4306  // NOTE: preSaveTransform doesn't have a fake revision to operate on.
4307  // Parser::getRevisionObject() will return null in preview mode,
4308  // causing the context user to be used for {{subst:REVISIONUSER}}.
4309  // XXX: Alternatively, we could also call setupFakeRevision() a second time:
4310  // once before PST with $content, and then after PST with $pstContent.
4311  $pstContent = $content->preSaveTransform( $this->mTitle, $user, $parserOptions );
4312  $scopedCallback = $parserOptions->setupFakeRevision( $this->mTitle, $pstContent, $user );
4313  $parserOutput = $pstContent->getParserOutput( $this->mTitle, null, $parserOptions );
4314  ScopedCallback::consume( $scopedCallback );
4315  return [
4316  'parserOutput' => $parserOutput,
4317  'html' => $parserOutput->getText( [
4318  'enableSectionEditLinks' => false
4319  ] )
4320  ];
4321  }
4322 
4326  public function getTemplates() {
4327  if ( $this->preview || $this->section != '' ) {
4328  $templates = [];
4329  if ( !isset( $this->mParserOutput ) ) {
4330  return $templates;
4331  }
4332  foreach ( $this->mParserOutput->getTemplates() as $ns => $template ) {
4333  foreach ( array_keys( $template ) as $dbk ) {
4334  $templates[] = Title::makeTitle( $ns, $dbk );
4335  }
4336  }
4337  return $templates;
4338  } else {
4339  return $this->mTitle->getTemplateLinksFrom();
4340  }
4341  }
4342 
4348  public static function getEditToolbar() {
4349  $startingToolbar = '<div id="toolbar"></div>';
4350  $toolbar = $startingToolbar;
4351 
4352  if ( !Hooks::run( 'EditPageBeforeEditToolbar', [ &$toolbar ] ) ) {
4353  return null;
4354  }
4355  // Don't add a pointless `<div>` to the page unless a hook caller populated it
4356  return ( $toolbar === $startingToolbar ) ? null : $toolbar;
4357  }
4358 
4377  public function getCheckboxesDefinition( $checked ) {
4378  $checkboxes = [];
4379 
4380  $user = $this->context->getUser();
4381  // don't show the minor edit checkbox if it's a new page or section
4382  if ( !$this->isNew && $this->permManager->userHasRight( $user, 'minoredit' ) ) {
4383  $checkboxes['wpMinoredit'] = [
4384  'id' => 'wpMinoredit',
4385  'label-message' => 'minoredit',
4386  // Uses messages: tooltip-minoredit, accesskey-minoredit
4387  'tooltip' => 'minoredit',
4388  'label-id' => 'mw-editpage-minoredit',
4389  'legacy-name' => 'minor',
4390  'default' => $checked['minor'],
4391  ];
4392  }
4393 
4394  if ( $user->isLoggedIn() ) {
4395  $checkboxes['wpWatchthis'] = [
4396  'id' => 'wpWatchthis',
4397  'label-message' => 'watchthis',
4398  // Uses messages: tooltip-watch, accesskey-watch
4399  'tooltip' => 'watch',
4400  'label-id' => 'mw-editpage-watch',
4401  'legacy-name' => 'watch',
4402  'default' => $checked['watch'],
4403  ];
4404  }
4405 
4406  $editPage = $this;
4407  Hooks::run( 'EditPageGetCheckboxesDefinition', [ $editPage, &$checkboxes ] );
4408 
4409  return $checkboxes;
4410  }
4411 
4423  public function getCheckboxesWidget( &$tabindex, $checked ) {
4424  $checkboxes = [];
4425  $checkboxesDef = $this->getCheckboxesDefinition( $checked );
4426 
4427  foreach ( $checkboxesDef as $name => $options ) {
4428  $legacyName = $options['legacy-name'] ?? $name;
4429 
4430  $title = null;
4431  $accesskey = null;
4432  if ( isset( $options['tooltip'] ) ) {
4433  $accesskey = $this->context->msg( "accesskey-{$options['tooltip']}" )->text();
4434  $title = Linker::titleAttrib( $options['tooltip'] );
4435  }
4436  if ( isset( $options['title-message'] ) ) {
4437  $title = $this->context->msg( $options['title-message'] )->text();
4438  }
4439 
4440  $checkboxes[ $legacyName ] = new OOUI\FieldLayout(
4441  new OOUI\CheckboxInputWidget( [
4442  'tabIndex' => ++$tabindex,
4443  'accessKey' => $accesskey,
4444  'id' => $options['id'] . 'Widget',
4445  'inputId' => $options['id'],
4446  'name' => $name,
4447  'selected' => $options['default'],
4448  'infusable' => true,
4449  ] ),
4450  [
4451  'align' => 'inline',
4452  'label' => new OOUI\HtmlSnippet( $this->context->msg( $options['label-message'] )->parse() ),
4453  'title' => $title,
4454  'id' => $options['label-id'] ?? null,
4455  ]
4456  );
4457  }
4458 
4459  return $checkboxes;
4460  }
4461 
4468  protected function getSubmitButtonLabel() {
4469  $labelAsPublish =
4470  $this->context->getConfig()->get( 'EditSubmitButtonLabelPublish' );
4471 
4472  // Can't use $this->isNew as that's also true if we're adding a new section to an extant page
4473  $newPage = !$this->mTitle->exists();
4474 
4475  if ( $labelAsPublish ) {
4476  $buttonLabelKey = $newPage ? 'publishpage' : 'publishchanges';
4477  } else {
4478  $buttonLabelKey = $newPage ? 'savearticle' : 'savechanges';
4479  }
4480 
4481  return $buttonLabelKey;
4482  }
4483 
4492  public function getEditButtons( &$tabindex ) {
4493  $buttons = [];
4494 
4495  $labelAsPublish =
4496  $this->context->getConfig()->get( 'EditSubmitButtonLabelPublish' );
4497 
4498  $buttonLabel = $this->context->msg( $this->getSubmitButtonLabel() )->text();
4499  $buttonTooltip = $labelAsPublish ? 'publish' : 'save';
4500 
4501  $buttons['save'] = new OOUI\ButtonInputWidget( [
4502  'name' => 'wpSave',
4503  'tabIndex' => ++$tabindex,
4504  'id' => 'wpSaveWidget',
4505  'inputId' => 'wpSave',
4506  // Support: IE 6 – Use <input>, otherwise it can't distinguish which button was clicked
4507  'useInputTag' => true,
4508  'flags' => [ 'progressive', 'primary' ],
4509  'label' => $buttonLabel,
4510  'infusable' => true,
4511  'type' => 'submit',
4512  // Messages used: tooltip-save, tooltip-publish
4513  'title' => Linker::titleAttrib( $buttonTooltip ),
4514  // Messages used: accesskey-save, accesskey-publish
4515  'accessKey' => Linker::accesskey( $buttonTooltip ),
4516  ] );
4517 
4518  $buttons['preview'] = new OOUI\ButtonInputWidget( [
4519  'name' => 'wpPreview',
4520  'tabIndex' => ++$tabindex,
4521  'id' => 'wpPreviewWidget',
4522  'inputId' => 'wpPreview',
4523  // Support: IE 6 – Use <input>, otherwise it can't distinguish which button was clicked
4524  'useInputTag' => true,
4525  'label' => $this->context->msg( 'showpreview' )->text(),
4526  'infusable' => true,
4527  'type' => 'submit',
4528  // Message used: tooltip-preview
4529  'title' => Linker::titleAttrib( 'preview' ),
4530  // Message used: accesskey-preview
4531  'accessKey' => Linker::accesskey( 'preview' ),
4532  ] );
4533 
4534  $buttons['diff'] = new OOUI\ButtonInputWidget( [
4535  'name' => 'wpDiff',
4536  'tabIndex' => ++$tabindex,
4537  'id' => 'wpDiffWidget',
4538  'inputId' => 'wpDiff',
4539  // Support: IE 6 – Use <input>, otherwise it can't distinguish which button was clicked
4540  'useInputTag' => true,
4541  'label' => $this->context->msg( 'showdiff' )->text(),
4542  'infusable' => true,
4543  'type' => 'submit',
4544  // Message used: tooltip-diff
4545  'title' => Linker::titleAttrib( 'diff' ),
4546  // Message used: accesskey-diff
4547  'accessKey' => Linker::accesskey( 'diff' ),
4548  ] );
4549 
4550  // Avoid PHP 7.1 warning of passing $this by reference
4551  $editPage = $this;
4552  Hooks::run( 'EditPageBeforeEditButtons', [ &$editPage, &$buttons, &$tabindex ] );
4553 
4554  return $buttons;
4555  }
4556 
4561  public function noSuchSectionPage() {
4562  $out = $this->context->getOutput();
4563  $out->prepareErrorPage( $this->context->msg( 'nosuchsectiontitle' ) );
4564 
4565  $res = $this->context->msg( 'nosuchsectiontext', $this->section )->parseAsBlock();
4566 
4567  // Avoid PHP 7.1 warning of passing $this by reference
4568  $editPage = $this;
4569  Hooks::run( 'EditPageNoSuchSection', [ &$editPage, &$res ] );
4570  $out->addHTML( $res );
4571 
4572  $out->returnToMain( false, $this->mTitle );
4573  }
4574 
4580  public function spamPageWithContent( $match = false ) {
4581  $this->textbox2 = $this->textbox1;
4582 
4583  if ( is_array( $match ) ) {
4584  $match = $this->context->getLanguage()->listToText( $match );
4585  }
4586  $out = $this->context->getOutput();
4587  $out->prepareErrorPage( $this->context->msg( 'spamprotectiontitle' ) );
4588 
4589  $out->addHTML( '<div id="spamprotected">' );
4590  $out->addWikiMsg( 'spamprotectiontext' );
4591  if ( $match ) {
4592  $out->addWikiMsg( 'spamprotectionmatch', wfEscapeWikiText( $match ) );
4593  }
4594  $out->addHTML( '</div>' );
4595 
4596  $out->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
4597  $this->showDiff();
4598 
4599  $out->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
4600  $this->showTextbox2();
4601 
4602  $out->addReturnTo( $this->getContextTitle(), [ 'action' => 'edit' ] );
4603  }
4604 
4608  protected function addEditNotices() {
4609  $out = $this->context->getOutput();
4610  $editNotices = $this->mTitle->getEditNotices( $this->oldid );
4611  if ( count( $editNotices ) ) {
4612  $out->addHTML( implode( "\n", $editNotices ) );
4613  } else {
4614  $msg = $this->context->msg( 'editnotice-notext' );
4615  if ( !$msg->isDisabled() ) {
4616  $out->addHTML(
4617  '<div class="mw-editnotice-notext">'
4618  . $msg->parseAsBlock()
4619  . '</div>'
4620  );
4621  }
4622  }
4623  }
4624 
4628  protected function addTalkPageText() {
4629  if ( $this->mTitle->isTalkPage() ) {
4630  $this->context->getOutput()->addWikiMsg( 'talkpagetext' );
4631  }
4632  }
4633 
4637  protected function addLongPageWarningHeader() {
4638  if ( $this->contentLength === false ) {
4639  $this->contentLength = strlen( $this->textbox1 );
4640  }
4641 
4642  $out = $this->context->getOutput();
4643  $lang = $this->context->getLanguage();
4644  $maxArticleSize = $this->context->getConfig()->get( 'MaxArticleSize' );
4645  if ( $this->tooBig || $this->contentLength > $maxArticleSize * 1024 ) {
4646  $out->wrapWikiMsg( "<div class='error' id='mw-edit-longpageerror'>\n$1\n</div>",
4647  [
4648  'longpageerror',
4649  $lang->formatNum( round( $this->contentLength / 1024, 3 ) ),
4650  $lang->formatNum( $maxArticleSize )
4651  ]
4652  );
4653  } elseif ( !$this->context->msg( 'longpage-hint' )->isDisabled() ) {
4654  $out->wrapWikiMsg( "<div id='mw-edit-longpage-hint'>\n$1\n</div>",
4655  [
4656  'longpage-hint',
4657  $lang->formatSize( strlen( $this->textbox1 ) ),
4658  strlen( $this->textbox1 )
4659  ]
4660  );
4661  }
4662  }
4663 
4667  protected function addPageProtectionWarningHeaders() {
4668  $out = $this->context->getOutput();
4669  if ( $this->mTitle->isProtected( 'edit' ) &&
4670  $this->permManager->getNamespaceRestrictionLevels(
4671  $this->getTitle()->getNamespace()
4672  ) !== [ '' ]
4673  ) {
4674  # Is the title semi-protected?
4675  if ( $this->mTitle->isSemiProtected() ) {
4676  $noticeMsg = 'semiprotectedpagewarning';
4677  } else {
4678  # Then it must be protected based on static groups (regular)
4679  $noticeMsg = 'protectedpagewarning';
4680  }
4681  LogEventsList::showLogExtract( $out, 'protect', $this->mTitle, '',
4682  [ 'lim' => 1, 'msgKey' => [ $noticeMsg ] ] );
4683  }
4684  if ( $this->mTitle->isCascadeProtected() ) {
4685  # Is this page under cascading protection from some source pages?
4686 
4687  list( $cascadeSources, /* $restrictions */ ) = $this->mTitle->getCascadeProtectionSources();
4688  $notice = "<div class='warningbox mw-cascadeprotectedwarning'>\n$1\n";
4689  $cascadeSourcesCount = count( $cascadeSources );
4690  if ( $cascadeSourcesCount > 0 ) {
4691  # Explain, and list the titles responsible
4692  foreach ( $cascadeSources as $page ) {
4693  $notice .= '* [[:' . $page->getPrefixedText() . "]]\n";
4694  }
4695  }
4696  $notice .= '</div>';
4697  $out->wrapWikiMsg( $notice, [ 'cascadeprotectedwarning', $cascadeSourcesCount ] );
4698  }
4699  if ( !$this->mTitle->exists() && $this->mTitle->getRestrictions( 'create' ) ) {
4700  LogEventsList::showLogExtract( $out, 'protect', $this->mTitle, '',
4701  [ 'lim' => 1,
4702  'showIfEmpty' => false,
4703  'msgKey' => [ 'titleprotectedwarning' ],
4704  'wrap' => "<div class=\"mw-titleprotectedwarning\">\n$1</div>" ] );
4705  }
4706  }
4707 
4712  protected function addExplainConflictHeader( OutputPage $out ) {
4713  $out->addHTML(
4714  $this->getEditConflictHelper()->getExplainHeader()
4715  );
4716  }
4717 
4725  protected function buildTextboxAttribs( $name, array $customAttribs, User $user ) {
4726  return ( new TextboxBuilder() )->buildTextboxAttribs(
4727  $name, $customAttribs, $user, $this->mTitle
4728  );
4729  }
4730 
4736  protected function addNewLineAtEnd( $wikitext ) {
4737  return ( new TextboxBuilder() )->addNewLineAtEnd( $wikitext );
4738  }
4739 
4750  private function guessSectionName( $text ) {
4751  // Detect Microsoft browsers
4752  $userAgent = $this->context->getRequest()->getHeader( 'User-Agent' );
4753  $parser = MediaWikiServices::getInstance()->getParser();
4754  if ( $userAgent && preg_match( '/MSIE|Edge/', $userAgent ) ) {
4755  // ...and redirect them to legacy encoding, if available
4756  return $parser->guessLegacySectionNameFromWikiText( $text );
4757  }
4758  // Meanwhile, real browsers get real anchors
4759  $name = $parser->guessSectionNameFromWikiText( $text );
4760  // With one little caveat: per T216029, fragments in HTTP redirects need to be urlencoded,
4761  // otherwise Chrome double-escapes the rest of the URL.
4762  return '#' . urlencode( mb_substr( $name, 1 ) );
4763  }
4764 
4771  public function setEditConflictHelperFactory( callable $factory ) {
4772  $this->editConflictHelperFactory = $factory;
4773  $this->editConflictHelper = null;
4774  }
4775 
4779  private function getEditConflictHelper() {
4780  if ( !$this->editConflictHelper ) {
4781  $this->editConflictHelper = call_user_func(
4782  $this->editConflictHelperFactory,
4783  $this->getSubmitButtonLabel()
4784  );
4785  }
4786 
4788  }
4789 
4795  private function newTextConflictHelper( $submitButtonLabel ) {
4796  return new TextConflictHelper(
4797  $this->getTitle(),
4798  $this->getContext()->getOutput(),
4799  MediaWikiServices::getInstance()->getStatsdDataFactory(),
4800  $submitButtonLabel,
4801  MediaWikiServices::getInstance()->getContentHandlerFactory()
4802  );
4803  }
4804 }
ReadOnlyError
Show an error when the wiki is locked/read-only and the user tries to do something that requires writ...
Definition: ReadOnlyError.php:28
WikiPage\hasDifferencesOutsideMainSlot
static hasDifferencesOutsideMainSlot( $a, $b)
Helper method for checking whether two revisions have differences that go beyond the main slot.
Definition: WikiPage.php:1551
EditPage\__construct
__construct(Article $article)
Definition: EditPage.php:517
EditPage\$editFormTextBeforeContent
$editFormTextBeforeContent
Definition: EditPage.php:447
EditPage\$mTriedSave
bool $mTriedSave
Definition: EditPage.php:281
CONTENT_MODEL_JSON
const CONTENT_MODEL_JSON
Definition: Defines.php:228
EditPage\AS_SELF_REDIRECT
const AS_SELF_REDIRECT
Status: user tried to create self-redirect (redirect to the same article) and wpIgnoreSelfRedirect ==...
Definition: EditPage.php:178
EditPage\$contentModel
string $contentModel
Definition: EditPage.php:433
EditPage\showFormBeforeText
showFormBeforeText()
Definition: EditPage.php:3576
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:332
EditPage\$lastDelete
bool stdClass $lastDelete
Definition: EditPage.php:272
EditPage\tokenOk
tokenOk(&$request)
Make sure the form isn't faking a user's credentials.
Definition: EditPage.php:1706
EditPage\$editFormPageTop
string $editFormPageTop
Before even the preview.
Definition: EditPage.php:445
EditPage\AS_BLANK_ARTICLE
const AS_BLANK_ARTICLE
Status: user tried to create a blank page and wpIgnoreBlankArticle == false.
Definition: EditPage.php:125
EditPage\showContentForm
showContentForm()
Subpage overridable method for printing the form for page content editing By default this simply outp...
Definition: EditPage.php:3613
EditPage\$mTitle
Title $mTitle
Definition: EditPage.php:242
Revision\RevisionRecord
Page revision base class.
Definition: RevisionRecord.php:46
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:165
Html\textarea
static textarea( $name, $value='', array $attribs=[])
Convenience function to produce a <textarea> element.
Definition: Html.php:818
EditPage\spamPageWithContent
spamPageWithContent( $match=false)
Show "your edit contains spam" page with your diff and text.
Definition: EditPage.php:4580
EditPage\$section
string $section
Definition: EditPage.php:399
ParserOutput
Definition: ParserOutput.php:25
WikiPage\getRedirectTarget
getRedirectTarget()
If this page is a redirect, get its target.
Definition: WikiPage.php:1008
UserBlockedError
Show an error when the user tries to do something whilst blocked.
Definition: UserBlockedError.php:30
EditPage\mergeChangesIntoContent
mergeChangesIntoContent(&$editContent)
Attempts to do 3-way merge of edit content with a base revision and current content,...
Definition: EditPage.php:2593
EditPage\displayPermissionsError
displayPermissionsError(array $permErrors)
Display a permissions error page, like OutputPage::showPermissionsErrorPage(), but with the following...
Definition: EditPage.php:819
EditPage\$editFormTextAfterContent
$editFormTextAfterContent
Definition: EditPage.php:451
EditPage\displayPreviewArea
displayPreviewArea( $previewOutput, $isOnTop=false)
Definition: EditPage.php:3674
EditPage\$blankArticle
bool $blankArticle
Definition: EditPage.php:299
EditPage\$allowBlankSummary
bool $allowBlankSummary
Definition: EditPage.php:296
$response
$response
Definition: opensearch_desc.php:44
$wgRightsText
$wgRightsText
If either $wgRightsUrl or $wgRightsPage is specified then this variable gives the text for the link.
Definition: DefaultSettings.php:7501
EditPage\$editFormTextBottom
$editFormTextBottom
Definition: EditPage.php:450
EditPage\getSummaryInputAttributes
getSummaryInputAttributes(array $inputAttrs=null)
Helper function for summary input functions, which returns the necessary attributes for the input.
Definition: EditPage.php:3473
EditPage\$editFormTextTop
$editFormTextTop
Definition: EditPage.php:446
EditPage\$editintro
string $editintro
Definition: EditPage.php:424
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:144
EditPage\showTextbox2
showTextbox2()
Definition: EditPage.php:3656
EDIT_FORCE_BOT
const EDIT_FORCE_BOT
Definition: Defines.php:145
$lang
if(!isset( $args[0])) $lang
Definition: testCompression.php:37
EditPage\$summary
string $summary
Definition: EditPage.php:372
EditPage\AS_READ_ONLY_PAGE_ANON
const AS_READ_ONLY_PAGE_ANON
Status: this anonymous user is not allowed to edit this page.
Definition: EditPage.php:93
EditPage\buildTextboxAttribs
buildTextboxAttribs( $name, array $customAttribs, User $user)
Definition: EditPage.php:4725
Revision\RevisionStore
Service for looking up page revisions.
Definition: RevisionStore.php:78
EditPage\$textbox2
string $textbox2
Definition: EditPage.php:369
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:1629
EditPage\$mTokenOk
bool $mTokenOk
Definition: EditPage.php:275
EditPage\$contentHandlerFactory
IContentHandlerFactory $contentHandlerFactory
Definition: EditPage.php:502
CONTENT_MODEL_CSS
const CONTENT_MODEL_CSS
Definition: Defines.php:226
wfTimestamp
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Definition: GlobalFunctions.php:1806
EditPage\$oldid
int $oldid
Revision ID the edit is based on, or 0 if it's the current revision.
Definition: EditPage.php:414
MediaWiki\EditPage\TextboxBuilder
Helps EditPage build textboxes.
Definition: TextboxBuilder.php:37
EditPage\getContextTitle
getContextTitle()
Get the context title object.
Definition: EditPage.php:585
EditPage\getEditToolbar
static getEditToolbar()
Allow extensions to provide a toolbar.
Definition: EditPage.php:4348
EditPage\showTosSummary
showTosSummary()
Give a chance for site and per-namespace customizations of terms of service summary link that might e...
Definition: EditPage.php:3821
EditPage\AS_UNICODE_NOT_SUPPORTED
const AS_UNICODE_NOT_SUPPORTED
Status: edit rejected because browser doesn't support Unicode.
Definition: EditPage.php:202
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:236
EditPage\$save
bool $save
Definition: EditPage.php:346
EditPage\addPageProtectionWarningHeaders
addPageProtectionWarningHeaders()
Definition: EditPage.php:4667
EditPage\getExpectedParentRevision
getExpectedParentRevision()
Returns the RevisionRecord corresponding to the revision that was current at the time editing was ini...
Definition: EditPage.php:2663
EditPage\setContextTitle
setContextTitle( $title)
Set the context Title object.
Definition: EditPage.php:573
WikiPage
Class representing a MediaWiki article and history.
Definition: WikiPage.php:48
EditPage\edit
edit()
This is the function that gets called for "action=edit".
Definition: EditPage.php:628
EditPage\$autoSumm
string $autoSumm
Definition: EditPage.php:311
NS_FILE
const NS_FILE
Definition: Defines.php:75
$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:4067
wfReadOnly
wfReadOnly()
Check whether the wiki is in read-only mode.
Definition: GlobalFunctions.php:1104
EditPage\incrementConflictStats
incrementConflictStats()
Definition: EditPage.php:3995
EditPage\addEditNotices
addEditNotices()
Definition: EditPage.php:4608
StatusValue\fatal
fatal( $message,... $parameters)
Add an error and set OK to false, indicating that the operation as a whole was fatal.
Definition: StatusValue.php:208
User\newFromName
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition: User.php:533
wfMessage
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Definition: GlobalFunctions.php:1198
EditPage\$editFormTextAfterTools
$editFormTextAfterTools
Definition: EditPage.php:449
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:83
EditPage\AS_HOOK_ERROR
const AS_HOOK_ERROR
Status: Article update aborted by a hook function.
Definition: EditPage.php:73
EditPage\getEditPermissionErrors
getEditPermissionErrors( $rigor=PermissionManager::RIGOR_SECURE)
Definition: EditPage.php:766
EditPage\$editFormTextAfterWarn
$editFormTextAfterWarn
Definition: EditPage.php:448
EditPage\setPreloadedContent
setPreloadedContent(Content $content)
Use this method before edit() to preload some content into the edit box.
Definition: EditPage.php:1614
EditPage\AS_CONTENT_TOO_BIG
const AS_CONTENT_TOO_BIG
Status: Content too big (> $wgMaxArticleSize)
Definition: EditPage.php:88
$wgTitle
if(isset( $_SERVER['PATH_INFO']) && $_SERVER['PATH_INFO'] !='') $wgTitle
Definition: api.php:59
PermissionsError
Show an error when a user tries to do something they do not have the necessary permissions for.
Definition: PermissionsError.php:30
MediaWiki\EditPage\TextConflictHelper\setContentFormat
setContentFormat( $contentFormat)
Definition: TextConflictHelper.php:139
EditPage\showHeaderCopyrightWarning
showHeaderCopyrightWarning()
Show the header copyright warning.
Definition: EditPage.php:3805
Html\warningBox
static warningBox( $html, $className='')
Return a warning box.
Definition: Html.php:726
EditPage\$mExpectedParentRevision
RevisionRecord bool null $mExpectedParentRevision
A RevisionRecord corresponding to $this->editRevId or $this->edittime Replaced $mBaseRevision.
Definition: EditPage.php:338
EditPage\addLongPageWarningHeader
addLongPageWarningHeader()
Definition: EditPage.php:4637
EditPage\$context
IContextSource $context
Definition: EditPage.php:475
$res
$res
Definition: testCompression.php:57
EditPage\$didSave
$didSave
Definition: EditPage.php:456
EditPage\AS_BLOCKED_PAGE_FOR_USER
const AS_BLOCKED_PAGE_FOR_USER
Status: User is blocked from editing this page.
Definition: EditPage.php:83
EditPage\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: EditPage.php:136
Xml\openElement
static openElement( $element, $attribs=null)
This opens an XML element.
Definition: Xml.php:108
EditPage\AS_NO_CREATE_PERMISSION
const AS_NO_CREATE_PERMISSION
Status: user tried to create this page, but is not allowed to do that ( Title->userCan('create') == f...
Definition: EditPage.php:120
wfDebugLog
wfDebugLog( $logGroup, $text, $dest='all', array $context=[])
Send a line to a supplementary debug log file, if configured, or main debug log if not.
Definition: GlobalFunctions.php:992
OutputPage\addHTML
addHTML( $text)
Append $text to the body HTML.
Definition: OutputPage.php:1615
ActorMigration\newMigration
static newMigration()
Static constructor.
Definition: ActorMigration.php:139
Linker\formatHiddenCategories
static formatHiddenCategories( $hiddencats)
Returns HTML for the "hidden categories on this page" list.
Definition: Linker.php:2077
EditPage\$mArticle
Article $mArticle
Definition: EditPage.php:234
EditPage\$contentFormat
null string $contentFormat
Definition: EditPage.php:436
Skin\getSkinNames
static getSkinNames()
Fetch the set of available skins.
Definition: Skin.php:58
EditPage\POST_EDIT_COOKIE_DURATION
const POST_EDIT_COOKIE_DURATION
Duration of PostEdit cookie, in seconds.
Definition: EditPage.php:228
$dbr
$dbr
Definition: testCompression.php:54
Status
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: Status.php:42
EditPage\$editConflictHelper
TextConflictHelper null $editConflictHelper
Definition: EditPage.php:497
Revision
Definition: Revision.php:39
EditPage\$watchthis
bool $watchthis
Definition: EditPage.php:358
EditPage\$previewTextAfterContent
$previewTextAfterContent
Definition: EditPage.php:452
Html\closeElement
static closeElement( $element)
Returns "</$element>".
Definition: Html.php:315
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:3743
EditPage\$tooBig
bool $tooBig
Definition: EditPage.php:287
Status\getWikiText
getWikiText( $shortContext=false, $longContext=false, $lang=null)
Get the error list as a wikitext formatted list.
Definition: Status.php:187
MediaWiki\Block\DatabaseBlock
A DatabaseBlock (unlike a SystemBlock) is stored in the database, may give rise to autoblocks and may...
Definition: DatabaseBlock.php:54
StatusValue\isGood
isGood()
Returns whether the operation completed and didn't have any error or warnings.
Definition: StatusValue.php:121
DerivativeContext
An IContextSource implementation which will inherit context from another source but allow individual ...
Definition: DerivativeContext.php:30
EditPage\AS_SUCCESS_NEW_ARTICLE
const AS_SUCCESS_NEW_ARTICLE
Status: Article successfully created.
Definition: EditPage.php:68
EditPage\UNICODE_CHECK
const UNICODE_CHECK
Used for Unicode support checks.
Definition: EditPage.php:58
MWException
MediaWiki exception.
Definition: MWException.php:26
EditPage\addContentModelChangeLogEntry
addContentModelChangeLogEntry(User $user, $oldModel, $newModel, $reason)
Definition: EditPage.php:2539
WikiPage\factory
static factory(Title $title)
Create a WikiPage object of the appropriate class for the given title.
Definition: WikiPage.php:143
EditPage\toEditContent
toEditContent( $text)
Turns the given text into a Content object by unserializing it.
Definition: EditPage.php:2962
Article\getTitle
getTitle()
Get the title object of the article.
Definition: Article.php:253
EditPage\getEditButtons
getEditButtons(&$tabindex)
Returns an array of html code of the following buttons: save, diff and preview.
Definition: EditPage.php:4492
wfDeprecated
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that $function is deprecated.
Definition: GlobalFunctions.php:1030
MediaWiki\Logger\LoggerFactory
PSR-3 logger instance factory.
Definition: LoggerFactory.php:45
EditPage\AS_END
const AS_END
Status: WikiPage::doEdit() was unsuccessful.
Definition: EditPage.php:151
LogPage\DELETED_COMMENT
const DELETED_COMMENT
Definition: LogPage.php:35
EditPage\$editRevId
int $editRevId
Revision ID of the latest revision of the page when editing was initiated on the client.
Definition: EditPage.php:396
EditPage\showSummaryInput
showSummaryInput( $isSubjectPreview, $summary="")
Definition: EditPage.php:3529
EditPage\getParentRevId
getParentRevId()
Get the edit's parent revision ID.
Definition: EditPage.php:1540
ParserOptions\newFromUserAndLang
static newFromUserAndLang(User $user, Language $lang)
Get a ParserOptions object from a given user and language.
Definition: ParserOptions.php:1090
wfArrayDiff2
wfArrayDiff2( $a, $b)
Like array_diff( $a, $b ) except that it works with two-dimensional arrays.
Definition: GlobalFunctions.php:113
EditPage\isSectionEditSupported
isSectionEditSupported()
Returns whether section editing is supported for the current page.
Definition: EditPage.php:964
EditPage\importFormData
importFormData(&$request)
This function collects the form data and uses it to populate various member variables.
Definition: EditPage.php:975
EditPage\getActionURL
getActionURL(Title $title)
Returns the URL to use in the form's action attribute.
Definition: EditPage.php:4031
EditPage\addExplainConflictHeader
addExplainConflictHeader(OutputPage $out)
Definition: EditPage.php:4712
LogPage\DELETED_USER
const DELETED_USER
Definition: LogPage.php:36
wfGetDB
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
Definition: GlobalFunctions.php:2463
EditPage\showIntro
showIntro()
Show all applicable editing introductions.
Definition: EditPage.php:2776
EditPage\$firsttime
bool $firsttime
True the first time the edit form is rendered, false after re-rendering with diff,...
Definition: EditPage.php:269
$matches
$matches
Definition: NoLocalSettings.php:24
StatusValue\isOK
isOK()
Returns whether the operation completed.
Definition: StatusValue.php:130
EditPage\$missingComment
bool $missingComment
Definition: EditPage.php:290
EditPage\getPreviewLimitReport
static getPreviewLimitReport(ParserOutput $output=null)
Get the Limit report for page previews.
Definition: EditPage.php:3888
EditPage\$editConflictHelperFactory
callable $editConflictHelperFactory
Factory function to create an edit conflict helper.
Definition: EditPage.php:492
$wgLang
$wgLang
Definition: Setup.php:774
MWContentSerializationException
Exception representing a failure to serialize or unserialize a content object.
Definition: MWContentSerializationException.php:7
EditPage\attemptSave
attemptSave(&$resultDetails=false)
Attempt submission.
Definition: EditPage.php:1749
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:263
Article\getContext
getContext()
Gets the context this Article is executed in.
Definition: Article.php:2317
User\isIP
static isIP( $name)
Does the string match an anonymous IP address?
Definition: User.php:946
EditPage\getArticle
getArticle()
Definition: EditPage.php:548
EditPage\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: EditPage.php:197
ThrottledError
Show an error when the user hits a rate limit.
Definition: ThrottledError.php:27
EditPage\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: EditPage.php:172
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:4423
EditPage\previewOnOpen
previewOnOpen()
Should we show a preview when the edit form is first shown?
Definition: EditPage.php:902
WatchAction\doWatchOrUnwatch
static doWatchOrUnwatch( $watch, Title $title, User $user)
Watch or unwatch a page.
Definition: WatchAction.php:174
EditPage\incrementEditFailureStats
incrementEditFailureStats( $failureType)
Definition: EditPage.php:4270
EditPage\AS_READ_ONLY_PAGE
const AS_READ_ONLY_PAGE
Status: wiki is in readonly mode (wfReadOnly() == true)
Definition: EditPage.php:103
EditPage\AS_SPAM_ERROR
const AS_SPAM_ERROR
Status: summary contained spam according to one of the regexes in $wgSummarySpamRegex.
Definition: EditPage.php:156
$title
$title
Definition: testCompression.php:38
EditPage\$allowSelfRedirect
bool $allowSelfRedirect
Definition: EditPage.php:308
EditPage\showEditForm
showEditForm( $formCallback=null)
Send the edit form and related headers to OutputPage.
Definition: EditPage.php:2985
Title\makeTitle
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:595
LogEventsList\showLogExtract
static showLogExtract(&$out, $types=[], $page='', $user='', $param=[])
Show log extract.
Definition: LogEventsList.php:634
EditPage\wasDeletedSinceLastEdit
wasDeletedSinceLastEdit()
Check if a page was deleted while the user was editing it, before submit.
Definition: EditPage.php:4042
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
EditPage\getTemplates
getTemplates()
Definition: EditPage.php:4326
wfTimestampNow
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
Definition: GlobalFunctions.php:1835
EditPage\getPreviewParserOptions
getPreviewParserOptions()
Get parser options for a preview.
Definition: EditPage.php:4279
EditPage\runPostMergeFilters
runPostMergeFilters(Content $content, Status $status, User $user)
Run hooks that can filter edits just before they get saved.
Definition: EditPage.php:1929
EditPage\AS_MAX_ARTICLE_SIZE_EXCEEDED
const AS_MAX_ARTICLE_SIZE_EXCEEDED
Status: article is too big (> $wgMaxArticleSize), after merging in the new section.
Definition: EditPage.php:146
DB_MASTER
const DB_MASTER
Definition: defines.php:26
EditPage\$mContextTitle
null Title $mContextTitle
Definition: EditPage.php:245
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:913
EditPage\showFormAfterText
showFormAfterText()
Definition: EditPage.php:3585
EditPage\getCancelLink
getCancelLink( $tabindex=0)
Definition: EditPage.php:4003
OutputPage
This is one of the Core classes and should be read at least once by any new developers.
Definition: OutputPage.php:46
EditPage\showPreview
showPreview( $text)
Append preview output to OutputPage.
Definition: EditPage.php:3722
EditPage\initialiseForm
initialiseForm()
Initialise form fields in the object Called on the first invocation, e.g.
Definition: EditPage.php:1204
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:137
deprecatePublicProperty
deprecatePublicProperty( $property, $version, $class=null, $component=null)
Mark a property as deprecated.
Definition: DeprecationHelper.php:68
MediaWiki\EditPage\TextConflictHelper\setTextboxes
setTextboxes( $yourtext, $storedversion)
Definition: TextConflictHelper.php:124
MediaWiki\EditPage\TextConflictHelper
Helper for displaying edit conflicts in text content models to users.
Definition: TextConflictHelper.php:44
EditPage\isPageExistingAndViewable
isPageExistingAndViewable( $title, User $user)
Verify if a given title exists and the given user is allowed to view it.
Definition: EditPage.php:1695
EditPage\matchSpamRegex
static matchSpamRegex( $text)
Check given input text against $wgSpamRegex, and return the text of the first match.
Definition: EditPage.php:2692
Html\hidden
static hidden( $name, $value, array $attribs=[])
Convenience function to produce an input element with type=hidden.
Definition: Html.php:802
EditPage\$recreate
bool $recreate
Definition: EditPage.php:361
EditPage\$contentLength
bool int $contentLength
Definition: EditPage.php:465
EditPage\AS_SUCCESS_UPDATE
const AS_SUCCESS_UPDATE
Status: Article successfully updated.
Definition: EditPage.php:63
EditPage\showTextbox1
showTextbox1( $customAttribs=null, $textoverride=null)
Method to output wpTextbox1 The $textoverride method can be used by subclasses overriding showContent...
Definition: EditPage.php:3625
EditPage\addTalkPageText
addTalkPageText()
Definition: EditPage.php:4628
MediaWiki\Permissions\PermissionManager
A service class for checking permissions To obtain an instance, use MediaWikiServices::getInstance()-...
Definition: PermissionManager.php:48
$content
$content
Definition: router.php:76
NS_USER_TALK
const NS_USER_TALK
Definition: Defines.php:72
EditPage\getSummaryPreview
getSummaryPreview( $isSubjectPreview, $summary="")
Definition: EditPage.php:3555
EditPage\importContentFormData
importContentFormData(&$request)
Subpage overridable method for extracting the page content data from the posted form to be placed in ...
Definition: EditPage.php:1195
EditPage\$minoredit
bool $minoredit
Definition: EditPage.php:355
EditPage\$isOldRev
bool $isOldRev
Whether an old revision is edited.
Definition: EditPage.php:480
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:470
EDIT_UPDATE
const EDIT_UPDATE
Definition: Defines.php:142
EditPage\showHeader
showHeader()
Definition: EditPage.php:3290
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:2647
ContentHandler\getLocalizedName
static getLocalizedName( $name, Language $lang=null)
Returns the localized name for a given content model.
Definition: ContentHandler.php:294
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:1576
EditPage\addNewLineAtEnd
addNewLineAtEnd( $wikitext)
Definition: EditPage.php:4736
MediaWiki\Content\IContentHandlerFactory
Definition: IContentHandlerFactory.php:10
StatusValue\newGood
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:81
Message\plaintextParam
static plaintextParam( $plaintext)
Definition: Message.php:1112
EditPage\incrementResolvedConflicts
incrementResolvedConflicts()
Log when a page was successfully saved after the edit conflict view.
Definition: EditPage.php:1768
StatusValue\getErrors
getErrors()
Get the list of errors.
Definition: StatusValue.php:148
Xml\tags
static tags( $element, $attribs, $contents)
Same as Xml::element(), but does not escape contents.
Definition: Xml.php:130
EditPage\$edittime
string $edittime
Timestamp of the latest revision of the page when editing was initiated on the client.
Definition: EditPage.php:383
EditPage\matchSummarySpamRegex
static matchSummarySpamRegex( $text)
Check given input text against $wgSummarySpamRegex, and return the text of the first match.
Definition: EditPage.php:2706
EditPage\$mTokenOkExceptSuffix
bool $mTokenOkExceptSuffix
Definition: EditPage.php:278
EditPage\showEditTools
showEditTools()
Inserts optional text shown below edit and upload forms.
Definition: EditPage.php:3836
EditPage\$preview
bool $preview
Definition: EditPage.php:349
EditPage\$isNew
bool $isNew
New page or new section.
Definition: EditPage.php:257
EditPage\getCheckboxesDefinition
getCheckboxesDefinition( $checked)
Return an array of checkbox definitions.
Definition: EditPage.php:4377
EditPage\$mBaseRevision
Revision bool null $mBaseRevision
A revision object corresponding to $this->editRevId.
Definition: EditPage.php:330
EditPage\internalAttemptSave
internalAttemptSave(&$result, $markAsBot=false)
Attempt submission (no UI)
Definition: EditPage.php:2048
EditPage\getCopywarn
getCopywarn()
Get the copyright warning.
Definition: EditPage.php:3848
EditPage\setApiEditOverride
setApiEditOverride( $enableOverride)
Allow editing of content that supports API direct editing, but not general direct editing.
Definition: EditPage.php:613
wfEscapeWikiText
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
Definition: GlobalFunctions.php:1485
EditPage\AS_IMAGE_REDIRECT_LOGGED
const AS_IMAGE_REDIRECT_LOGGED
Status: logged in user is not allowed to upload (User::isAllowed('upload') == false)
Definition: EditPage.php:166
EditPage\newTextConflictHelper
newTextConflictHelper( $submitButtonLabel)
Definition: EditPage.php:4795
EditPage\showCustomIntro
showCustomIntro()
Attempt to show a custom editing introduction, if supplied.
Definition: EditPage.php:2900
EditPage\getContext
getContext()
Definition: EditPage.php:556
Revision\RevisionStoreRecord
A RevisionRecord representing an existing revision persisted in the revision table.
Definition: RevisionStoreRecord.php:40
EditPage\AS_PARSE_ERROR
const AS_PARSE_ERROR
Status: can't parse content.
Definition: EditPage.php:189
EditPage\EDITFORM_ID
const EDITFORM_ID
HTML id and name for the beginning of the edit form.
Definition: EditPage.php:207
EditPage\extractSectionTitle
static extractSectionTitle( $text)
Extract the section title from current section text, if any.
Definition: EditPage.php:3280
EditPage\makeTemplatesOnThisPageList
makeTemplatesOnThisPageList(array $templates)
Wrapper around TemplatesOnThisPageFormatter to make a "templates on this page" list.
Definition: EditPage.php:3256
EditPage\$textbox1
string $textbox1
Page content input field.
Definition: EditPage.php:366
EditPage\$parentRevId
int $parentRevId
Revision ID the edit is based on, adjusted when an edit conflict is resolved.
Definition: EditPage.php:421
MediaWiki\EditPage\TextConflictHelper\setContentModel
setContentModel( $contentModel)
Definition: TextConflictHelper.php:132
EditPage\$undidRev
$undidRev
Definition: EditPage.php:457
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:2112
EditPage\$changeTags
null array $changeTags
Definition: EditPage.php:439
EditPage
The edit page/HTML interface (split from Article) The actual database and text munging is still in Ar...
Definition: EditPage.php:51
EditPage\noSuchSectionPage
noSuchSectionPage()
Creates a basic error page which informs the user that they have attempted to edit a nonexistent sect...
Definition: EditPage.php:4561
IContextSource
Interface for objects which can provide a MediaWiki context on request.
Definition: IContextSource.php:53
EditPage\$formtype
string $formtype
Definition: EditPage.php:263
Content
Base interface for content objects.
Definition: Content.php:34
EditPage\getSummaryInputWidget
getSummaryInputWidget( $summary="", $labelText=null, $inputAttrs=null)
Builds a standard summary input with a label.
Definition: EditPage.php:3496
CommentStore\COMMENT_CHARACTER_LIMIT
const COMMENT_CHARACTER_LIMIT
Maximum length of a comment in UTF-8 characters.
Definition: CommentStore.php:37
EDIT_NEW
const EDIT_NEW
Definition: Defines.php:141
EditPage\$hasPresetSummary
bool $hasPresetSummary
Has a summary been preset using GET parameter &summary= ?
Definition: EditPage.php:320
ChangeTags\canAddTagsAccompanyingChange
static canAddTagsAccompanyingChange(array $tags, User $user=null)
Is it OK to allow the user to apply all the specified tags at the same time as they edit/make the cha...
Definition: ChangeTags.php:528
EditPage\$mParserOutput
ParserOutput $mParserOutput
Definition: EditPage.php:317
Title
Represents a title within MediaWiki.
Definition: Title.php:42
EDIT_AUTOSUMMARY
const EDIT_AUTOSUMMARY
Definition: Defines.php:147
EditPage\$mShowSummaryField
bool $mShowSummaryField
Definition: EditPage.php:341
EditPage\$sectiontitle
string $sectiontitle
Definition: EditPage.php:402
EditPage\$starttime
string $starttime
Timestamp from the first time the edit form was rendered.
Definition: EditPage.php:407
EditPage\$suppressIntro
$suppressIntro
Definition: EditPage.php:459
ContentHandler\getContentText
static getContentText(Content $content=null)
Convenience function for getting flat text from a Content object.
Definition: ContentHandler.php:86
EditPage\$permManager
PermissionManager $permManager
Definition: EditPage.php:507
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:117
wfReadOnlyReason
wfReadOnlyReason()
Check if the site is in read-only mode and return the message if so.
Definition: GlobalFunctions.php:1117
EditPage\$scrolltop
int null $scrolltop
Definition: EditPage.php:427
EditPage\formatStatusErrors
formatStatusErrors(Status $status)
Wrap status errors in an errorbox for increased visibility.
Definition: EditPage.php:1979
EditPage\$deletedSinceEdit
bool $deletedSinceEdit
Definition: EditPage.php:260
EditPage\$selfRedirect
bool $selfRedirect
Definition: EditPage.php:305
EditPage\$edit
bool $edit
Definition: EditPage.php:462
EditPage\isSupportedContentModel
isSupportedContentModel( $modelId)
Returns if the given content model is editable.
Definition: EditPage.php:602
EditPage\$mPreloadContent
$mPreloadContent
Definition: EditPage.php:453
EditPage\showConflict
showConflict()
Show an edit conflict.
Definition: EditPage.php:3984
EditPage\getSubmitButtonLabel
getSubmitButtonLabel()
Get the message key of the label for the button to save the page.
Definition: EditPage.php:4468
EditPage\$unicodeCheck
string null $unicodeCheck
What the user submitted in the 'wpUnicodeCheck' field.
Definition: EditPage.php:485
EditPage\$diff
bool $diff
Definition: EditPage.php:352
EditPage\doPreviewParse
doPreviewParse(Content $content)
Parse the page for a preview.
Definition: EditPage.php:4302
EditPage\newSectionSummary
newSectionSummary(&$sectionanchor=null)
Return the summary to be used for a new section.
Definition: EditPage.php:1999
EditPage\$action
string $action
Definition: EditPage.php:248
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:251
Html\rawElement
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:209
EditPage\setEditConflictHelperFactory
setEditConflictHelperFactory(callable $factory)
Set a factory function to create an EditConflictHelper.
Definition: EditPage.php:4771
EditPage\AS_READ_ONLY_PAGE_LOGGED
const AS_READ_ONLY_PAGE_LOGGED
Status: this logged in user is not allowed to edit this page.
Definition: EditPage.php:98
NS_USER
const NS_USER
Definition: Defines.php:71
EditPage\showTextbox
showTextbox( $text, $name, $customAttribs=[])
Definition: EditPage.php:3660
EditPage\getTitle
getTitle()
Definition: EditPage.php:564
ManualLogEntry
Class for creating new log entries and inserting them into the database.
Definition: ManualLogEntry.php:38
EditPage\AS_CONFLICT_DETECTED
const AS_CONFLICT_DETECTED
Status: (non-resolvable) edit conflict.
Definition: EditPage.php:130
EditPage\AS_RATE_LIMITED
const AS_RATE_LIMITED
Status: rate limiter for action 'edit' was tripped.
Definition: EditPage.php:108
MWUnknownContentModelException
Exception thrown when an unregistered content model is requested.
Definition: MWUnknownContentModelException.php:10
EditPage\getCurrentContent
getCurrentContent()
Get the current content of the page.
Definition: EditPage.php:1556
Title\setContentModel
setContentModel( $model)
Set a proposed content model for the page for permissions checking.
Definition: Title.php:1096
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:1051
EditPage\$isConflict
bool $isConflict
Whether an edit conflict needs to be resolved.
Definition: EditPage.php:254
NS_MEDIAWIKI
const NS_MEDIAWIKI
Definition: Defines.php:77
CONTENT_MODEL_JAVASCRIPT
const CONTENT_MODEL_JAVASCRIPT
Definition: Defines.php:225
EditPage\displayViewSourcePage
displayViewSourcePage(Content $content, $errorMessage='')
Display a read-only View Source page.
Definition: EditPage.php:849
EDIT_MINOR
const EDIT_MINOR
Definition: Defines.php:143
Article
Class for viewing MediaWiki article and history.
Definition: Article.php:45
EditPage\getContentObject
getContentObject( $def_content=null)
Definition: EditPage.php:1270
User\IGNORE_USER_RIGHTS
const IGNORE_USER_RIGHTS
Definition: User.php:87
EditPage\AS_IMAGE_REDIRECT_ANON
const AS_IMAGE_REDIRECT_ANON
Status: anonymous user is not allowed to upload (User::isAllowed('upload') == false)
Definition: EditPage.php:161
EditPage\showStandardInputs
showStandardInputs(&$tabindex=2)
Definition: EditPage.php:3934
MediaWiki\EditPage\TextConflictHelper\getEditConflictMainTextBox
getEditConflictMainTextBox(array $customAttribs=[])
HTML to build the textbox1 on edit conflicts.
Definition: TextConflictHelper.php:222
RawMessage
Variant of the Message class.
Definition: RawMessage.php:34
ErrorPageError
An error page which can definitely be safely rendered using the OutputPage.
Definition: ErrorPageError.php:27
WikiPage\isRedirect
isRedirect()
Tests if the article content represents a redirect.
Definition: WikiPage.php:605
EditPage\$revisionStore
RevisionStore $revisionStore
Definition: EditPage.php:512
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:213
DeprecationHelper
trait DeprecationHelper
Use this trait in classes which have properties for which public access is deprecated.
Definition: DeprecationHelper.php:45
EditPage\getOriginalContent
getOriginalContent(User $user)
Get the content of the wanted revision, without section extraction.
Definition: EditPage.php:1515
EditPage\AS_ARTICLE_WAS_DELETED
const AS_ARTICLE_WAS_DELETED
Status: article was deleted while editing and param wpRecreate == false or form was not posted.
Definition: EditPage.php:114
EditPage\setPostEditCookie
setPostEditCookie( $statusValue)
Sets post-edit cookie indicating the user just saved a particular revision.
Definition: EditPage.php:1728
Linker\accesskey
static accesskey( $name)
Given the id of an interface element, constructs the appropriate accesskey attribute from the system ...
Definition: Linker.php:2160
CommentStore\getStore
static getStore()
Definition: CommentStore.php:109
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:54
DeferredUpdates\addCallableUpdate
static addCallableUpdate( $callable, $stage=self::POSTSEND, $dbw=null)
Add a callable update.
Definition: DeferredUpdates.php:145
Hooks\run
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:133
EditPage\isWrongCaseUserConfigPage
isWrongCaseUserConfigPage()
Checks whether the user entered a skin name in uppercase, e.g.
Definition: EditPage.php:943
EditPage\$incompleteForm
bool $incompleteForm
Definition: EditPage.php:284
EditPage\$missingSummary
bool $missingSummary
Definition: EditPage.php:293
EditPage\getEditConflictHelper
getEditConflictHelper()
Definition: EditPage.php:4779
WikiPage\getContent
getContent( $audience=RevisionRecord::FOR_PUBLIC, User $user=null)
Get the content of the current revision.
Definition: WikiPage.php:795
ExternalUserNames\isExternal
static isExternal( $username)
Tells whether the username is external or not.
Definition: ExternalUserNames.php:137
EditPage\$markAsBot
bool $markAsBot
Definition: EditPage.php:430
EditPage\getCopyrightWarning
static getCopyrightWarning( $title, $format='plain', $langcode=null)
Get the copyright warning, by default returns wikitext.
Definition: EditPage.php:3860
EditPage\AS_HOOK_ERROR_EXPECTED
const AS_HOOK_ERROR_EXPECTED
Status: A hook function returned an error.
Definition: EditPage.php:78
$wgDisableAnonTalk
$wgDisableAnonTalk
Disable links to talk pages of anonymous users (IPs) in listings on special pages like page history,...
Definition: DefaultSettings.php:7377
Skin\makeInternalOrExternalUrl
static makeInternalOrExternalUrl( $name)
If url string starts with http, consider as external URL, else internal.
Definition: Skin.php:1242
EditPage\updateWatchlist
updateWatchlist()
Register the change of watch status.
Definition: EditPage.php:2556
Revision\SlotRecord
Value object representing a content slot associated with a page revision.
Definition: SlotRecord.php:39
wfExpandUrl
wfExpandUrl( $url, $defaultProto=PROTO_CURRENT)
Expand a potentially local URL to a fully-qualified URL.
Definition: GlobalFunctions.php:491
EditPage\handleStatus
handleStatus(Status $status, $resultDetails)
Handle status, such as after attempt save.
Definition: EditPage.php:1785
ParserOptions\newFromUser
static newFromUser( $user)
Get a ParserOptions object from a given user.
Definition: ParserOptions.php:1077
EditPage\$hookError
string $hookError
Definition: EditPage.php:314
EditPage\$allowBlankArticle
bool $allowBlankArticle
Definition: EditPage.php:302
EditPage\toEditText
toEditText( $content)
Gets an editable textual representation of $content.
Definition: EditPage.php:2934
Xml\checkLabel
static checkLabel( $label, $name, $id, $checked=false, $attribs=[])
Convenience function to build an HTML checkbox with a label.
Definition: Xml.php:423
EditPage\setHeaders
setHeaders()
Definition: EditPage.php:2711
EditPage\AS_TEXTBOX_EMPTY
const AS_TEXTBOX_EMPTY
Status: user tried to create a new section without content.
Definition: EditPage.php:141
EditPage\AS_CHANGE_TAG_ERROR
const AS_CHANGE_TAG_ERROR
Status: an error relating to change tagging.
Definition: EditPage.php:184
EditPage\guessSectionName
guessSectionName( $text)
Turns section name wikitext into anchors for use in HTTP redirects.
Definition: EditPage.php:4750
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:347
$type
$type
Definition: testCompression.php:52
EditPage\$nosummary
bool $nosummary
If true, hide the summary field.
Definition: EditPage.php:377
EditPage\getPreviewText
getPreviewText()
Get the rendered text for previewing.
Definition: EditPage.php:4115