MediaWiki  master
EditPage.php
Go to the documentation of this file.
1 <?php
31 
47 class EditPage {
51  public const UNICODE_CHECK = 'ℳ𝒲♥𝓊𝓃𝒾𝒸ℴ𝒹ℯ';
52 
56  public const AS_SUCCESS_UPDATE = 200;
57 
61  public const AS_SUCCESS_NEW_ARTICLE = 201;
62 
66  public const AS_HOOK_ERROR = 210;
67 
71  public const AS_HOOK_ERROR_EXPECTED = 212;
72 
76  public const AS_BLOCKED_PAGE_FOR_USER = 215;
77 
81  public const AS_CONTENT_TOO_BIG = 216;
82 
86  public const AS_READ_ONLY_PAGE_ANON = 218;
87 
91  public const AS_READ_ONLY_PAGE_LOGGED = 219;
92 
96  public const AS_READ_ONLY_PAGE = 220;
97 
101  public const AS_RATE_LIMITED = 221;
102 
107  public const AS_ARTICLE_WAS_DELETED = 222;
108 
113  public const AS_NO_CREATE_PERMISSION = 223;
114 
118  public const AS_BLANK_ARTICLE = 224;
119 
123  public const AS_CONFLICT_DETECTED = 225;
124 
129  public const AS_SUMMARY_NEEDED = 226;
130 
134  public const AS_TEXTBOX_EMPTY = 228;
135 
139  public const AS_MAX_ARTICLE_SIZE_EXCEEDED = 229;
140 
144  public const AS_END = 231;
145 
149  public const AS_SPAM_ERROR = 232;
150 
154  public const AS_IMAGE_REDIRECT_ANON = 233;
155 
159  public const AS_IMAGE_REDIRECT_LOGGED = 234;
160 
165  public const AS_NO_CHANGE_CONTENT_MODEL = 235;
166 
171  public const AS_SELF_REDIRECT = 236;
172 
177  public const AS_CHANGE_TAG_ERROR = 237;
178 
182  public const AS_PARSE_ERROR = 240;
183 
188  public const AS_CANNOT_USE_CUSTOM_MODEL = 241;
189 
193  public const AS_UNICODE_NOT_SUPPORTED = 242;
194 
198  public const EDITFORM_ID = 'editform';
199 
204  public const POST_EDIT_COOKIE_KEY_PREFIX = 'PostEditRevision';
205 
219  public const POST_EDIT_COOKIE_DURATION = 1200;
220 
225  public $mArticle;
227  private $page;
228 
233  public $mTitle;
234 
236  private $mContextTitle = null;
237 
239  public $action = 'submit';
240 
245  public $isConflict = false;
246 
248  public $isNew = false;
249 
252 
254  public $formtype;
255 
260  public $firsttime;
261 
263  public $lastDelete;
264 
266  public $mTokenOk = false;
267 
269  public $mTokenOkExceptSuffix = false;
270 
272  public $mTriedSave = false;
273 
275  public $incompleteForm = false;
276 
278  public $tooBig = false;
279 
281  public $missingComment = false;
282 
284  public $missingSummary = false;
285 
287  public $allowBlankSummary = false;
288 
290  protected $blankArticle = false;
291 
293  protected $allowBlankArticle = false;
294 
296  protected $selfRedirect = false;
297 
299  protected $allowSelfRedirect = false;
300 
302  public $autoSumm = '';
303 
305  public $hookError = '';
306 
309 
311  public $hasPresetSummary = false;
312 
314  public $mBaseRevision = false;
315 
317  public $mShowSummaryField = true;
318 
319  # Form values
320 
322  public $save = false;
323 
325  public $preview = false;
326 
328  public $diff = false;
329 
331  public $minoredit = false;
332 
334  public $watchthis = false;
335 
337  public $recreate = false;
338 
342  public $textbox1 = '';
343 
345  public $textbox2 = '';
346 
348  public $summary = '';
349 
353  public $nosummary = false;
354 
359  public $edittime = '';
360 
372  private $editRevId = null;
373 
375  public $section = '';
376 
378  public $sectiontitle = '';
379 
383  public $starttime = '';
384 
390  public $oldid = 0;
391 
397  public $parentRevId = 0;
398 
400  public $editintro = '';
401 
403  public $scrolltop = null;
404 
406  public $bot = true;
407 
410 
412  public $contentFormat = null;
413 
415  private $changeTags = null;
416 
417  # Placeholders for text injection by hooks (must be HTML)
418  # extensions should take care to _append_ to the present value
419 
421  public $editFormPageTop = '';
422  public $editFormTextTop = '';
426  public $editFormTextBottom = '';
429  public $mPreloadContent = null;
430 
431  /* $didSave should be set to true whenever an article was successfully altered. */
432  public $didSave = false;
433  public $undidRev = 0;
434 
435  public $suppressIntro = false;
436 
438  protected $edit;
439 
441  protected $contentLength = false;
442 
446  private $enableApiEditOverride = false;
447 
451  protected $context;
452 
456  private $isOldRev = false;
457 
461  private $unicodeCheck;
462 
469 
474 
478  public function __construct( Article $article ) {
479  $this->mArticle = $article;
480  $this->page = $article->getPage(); // model object
481  $this->mTitle = $article->getTitle();
482 
483  // Make sure the local context is in sync with other member variables.
484  // Particularly make sure everything is using the same WikiPage instance.
485  // This should probably be the case in Article as well, but it's
486  // particularly important for EditPage, to make use of the in-place caching
487  // facility in WikiPage::prepareContentForEdit.
488  $this->context = new DerivativeContext( $article->getContext() );
489  $this->context->setWikiPage( $this->page );
490  $this->context->setTitle( $this->mTitle );
491 
492  $this->contentModel = $this->mTitle->getContentModel();
493 
494  $handler = ContentHandler::getForModelID( $this->contentModel );
495  $this->contentFormat = $handler->getDefaultFormat();
496  $this->editConflictHelperFactory = [ $this, 'newTextConflictHelper' ];
497  }
498 
502  public function getArticle() {
503  return $this->mArticle;
504  }
505 
510  public function getContext() {
511  return $this->context;
512  }
513 
518  public function getTitle() {
519  return $this->mTitle;
520  }
521 
527  public function setContextTitle( $title ) {
528  $this->mContextTitle = $title;
529  }
530 
539  public function getContextTitle() {
540  if ( is_null( $this->mContextTitle ) ) {
541  wfDeprecated( __METHOD__ . ' called with no title set', '1.32' );
542  global $wgTitle;
543  return $wgTitle;
544  } else {
545  return $this->mContextTitle;
546  }
547  }
548 
556  public function isSupportedContentModel( $modelId ) {
557  return $this->enableApiEditOverride === true ||
558  ContentHandler::getForModelID( $modelId )->supportsDirectEditing();
559  }
560 
567  public function setApiEditOverride( $enableOverride ) {
568  $this->enableApiEditOverride = $enableOverride;
569  }
570 
582  public function edit() {
583  // Allow extensions to modify/prevent this form or submission
584  if ( !Hooks::run( 'AlternateEdit', [ $this ] ) ) {
585  return;
586  }
587 
588  wfDebug( __METHOD__ . ": enter\n" );
589 
590  $request = $this->context->getRequest();
591  // If they used redlink=1 and the page exists, redirect to the main article
592  if ( $request->getBool( 'redlink' ) && $this->mTitle->exists() ) {
593  $this->context->getOutput()->redirect( $this->mTitle->getFullURL() );
594  return;
595  }
596 
597  $this->importFormData( $request );
598  $this->firsttime = false;
599 
600  if ( wfReadOnly() && $this->save ) {
601  // Force preview
602  $this->save = false;
603  $this->preview = true;
604  }
605 
606  if ( $this->save ) {
607  $this->formtype = 'save';
608  } elseif ( $this->preview ) {
609  $this->formtype = 'preview';
610  } elseif ( $this->diff ) {
611  $this->formtype = 'diff';
612  } else { # First time through
613  $this->firsttime = true;
614  if ( $this->previewOnOpen() ) {
615  $this->formtype = 'preview';
616  } else {
617  $this->formtype = 'initial';
618  }
619  }
620 
621  $permErrors = $this->getEditPermissionErrors(
622  $this->save ? PermissionManager::RIGOR_SECURE : PermissionManager::RIGOR_FULL
623  );
624  if ( $permErrors ) {
625  wfDebug( __METHOD__ . ": User can't edit\n" );
626 
627  if ( $this->context->getUser()->getBlock() ) {
628  // Auto-block user's IP if the account was "hard" blocked
629  if ( !wfReadOnly() ) {
631  $this->context->getUser()->spreadAnyEditBlock();
632  } );
633  }
634  }
635  $this->displayPermissionsError( $permErrors );
636 
637  return;
638  }
639 
640  $revision = $this->mArticle->getRevisionFetched();
641  // Disallow editing revisions with content models different from the current one
642  // Undo edits being an exception in order to allow reverting content model changes.
643  if ( $revision
644  && $revision->getContentModel() !== $this->contentModel
645  ) {
646  $prevRev = null;
647  if ( $this->undidRev ) {
648  $undidRevObj = Revision::newFromId( $this->undidRev );
649  $prevRev = $undidRevObj ? $undidRevObj->getPrevious() : null;
650  }
651  if ( !$this->undidRev
652  || !$prevRev
653  || $prevRev->getContentModel() !== $this->contentModel
654  ) {
655  $this->displayViewSourcePage(
656  $this->getContentObject(),
657  $this->context->msg(
658  'contentmodelediterror',
659  $revision->getContentModel(),
661  )->plain()
662  );
663  return;
664  }
665  }
666 
667  $this->isConflict = false;
668 
669  # Show applicable editing introductions
670  if ( $this->formtype == 'initial' || $this->firsttime ) {
671  $this->showIntro();
672  }
673 
674  # Attempt submission here. This will check for edit conflicts,
675  # and redundantly check for locked database, blocked IPs, etc.
676  # that edit() already checked just in case someone tries to sneak
677  # in the back door with a hand-edited submission URL.
678 
679  if ( $this->formtype == 'save' ) {
680  $resultDetails = null;
681  $status = $this->attemptSave( $resultDetails );
682  if ( !$this->handleStatus( $status, $resultDetails ) ) {
683  return;
684  }
685  }
686 
687  # First time through: get contents, set time for conflict
688  # checking, etc.
689  if ( $this->formtype == 'initial' || $this->firsttime ) {
690  if ( $this->initialiseForm() === false ) {
691  return;
692  }
693 
694  if ( !$this->mTitle->getArticleID() ) {
695  Hooks::run( 'EditFormPreloadText', [ &$this->textbox1, &$this->mTitle ] );
696  } else {
697  Hooks::run( 'EditFormInitialText', [ $this ] );
698  }
699 
700  }
701 
702  $this->showEditForm();
703  }
704 
709  protected function getEditPermissionErrors( $rigor = PermissionManager::RIGOR_SECURE ) {
710  $user = $this->context->getUser();
711  $permErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $user, $rigor );
712  # Can this title be created?
713  if ( !$this->mTitle->exists() ) {
714  $permErrors = array_merge(
715  $permErrors,
716  wfArrayDiff2(
717  $this->mTitle->getUserPermissionsErrors( 'create', $user, $rigor ),
718  $permErrors
719  )
720  );
721  }
722  # Ignore some permissions errors when a user is just previewing/viewing diffs
723  $remove = [];
724  foreach ( $permErrors as $error ) {
725  if ( ( $this->preview || $this->diff )
726  && (
727  $error[0] == 'blockedtext' ||
728  $error[0] == 'autoblockedtext' ||
729  $error[0] == 'systemblockedtext'
730  )
731  ) {
732  $remove[] = $error;
733  }
734  }
735  $permErrors = wfArrayDiff2( $permErrors, $remove );
736 
737  return $permErrors;
738  }
739 
753  protected function displayPermissionsError( array $permErrors ) {
754  $out = $this->context->getOutput();
755  if ( $this->context->getRequest()->getBool( 'redlink' ) ) {
756  // The edit page was reached via a red link.
757  // Redirect to the article page and let them click the edit tab if
758  // they really want a permission error.
759  $out->redirect( $this->mTitle->getFullURL() );
760  return;
761  }
762 
763  $content = $this->getContentObject();
764 
765  # Use the normal message if there's nothing to display
766  if ( $this->firsttime && ( !$content || $content->isEmpty() ) ) {
767  $action = $this->mTitle->exists() ? 'edit' :
768  ( $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage' );
769  throw new PermissionsError( $action, $permErrors );
770  }
771 
772  $this->displayViewSourcePage(
773  $content,
774  $out->formatPermissionsErrorMessage( $permErrors, 'edit' )
775  );
776  }
777 
783  protected function displayViewSourcePage( Content $content, $errorMessage = '' ) {
784  $out = $this->context->getOutput();
785  Hooks::run( 'EditPage::showReadOnlyForm:initial', [ $this, &$out ] );
786 
787  $out->setRobotPolicy( 'noindex,nofollow' );
788  $out->setPageTitle( $this->context->msg(
789  'viewsource-title',
790  $this->getContextTitle()->getPrefixedText()
791  ) );
792  $out->addBacklinkSubtitle( $this->getContextTitle() );
793  $out->addHTML( $this->editFormPageTop );
794  $out->addHTML( $this->editFormTextTop );
795 
796  if ( $errorMessage !== '' ) {
797  $out->addWikiTextAsInterface( $errorMessage );
798  $out->addHTML( "<hr />\n" );
799  }
800 
801  # If the user made changes, preserve them when showing the markup
802  # (This happens when a user is blocked during edit, for instance)
803  if ( !$this->firsttime ) {
804  $text = $this->textbox1;
805  $out->addWikiMsg( 'viewyourtext' );
806  } else {
807  try {
808  $text = $this->toEditText( $content );
809  } catch ( MWException $e ) {
810  # Serialize using the default format if the content model is not supported
811  # (e.g. for an old revision with a different model)
812  $text = $content->serialize();
813  }
814  $out->addWikiMsg( 'viewsourcetext' );
815  }
816 
817  $out->addHTML( $this->editFormTextBeforeContent );
818  $this->showTextbox( $text, 'wpTextbox1', [ 'readonly' ] );
819  $out->addHTML( $this->editFormTextAfterContent );
820 
821  $out->addHTML( $this->makeTemplatesOnThisPageList( $this->getTemplates() ) );
822 
823  $out->addModules( 'mediawiki.action.edit.collapsibleFooter' );
824 
825  $out->addHTML( $this->editFormTextBottom );
826  if ( $this->mTitle->exists() ) {
827  $out->returnToMain( null, $this->mTitle );
828  }
829  }
830 
836  protected function previewOnOpen() {
837  $config = $this->context->getConfig();
838  $previewOnOpenNamespaces = $config->get( 'PreviewOnOpenNamespaces' );
839  $request = $this->context->getRequest();
840  if ( $config->get( 'RawHtml' ) ) {
841  // If raw HTML is enabled, disable preview on open
842  // since it has to be posted with a token for
843  // security reasons
844  return false;
845  }
846  if ( $request->getVal( 'preview' ) == 'yes' ) {
847  // Explicit override from request
848  return true;
849  } elseif ( $request->getVal( 'preview' ) == 'no' ) {
850  // Explicit override from request
851  return false;
852  } elseif ( $this->section == 'new' ) {
853  // Nothing *to* preview for new sections
854  return false;
855  } elseif ( ( $request->getCheck( 'preload' ) || $this->mTitle->exists() )
856  && $this->context->getUser()->getOption( 'previewonfirst' )
857  ) {
858  // Standard preference behavior
859  return true;
860  } elseif ( !$this->mTitle->exists()
861  && isset( $previewOnOpenNamespaces[$this->mTitle->getNamespace()] )
862  && $previewOnOpenNamespaces[$this->mTitle->getNamespace()]
863  ) {
864  // Categories are special
865  return true;
866  } else {
867  return false;
868  }
869  }
870 
877  protected function isWrongCaseUserConfigPage() {
878  if ( $this->mTitle->isUserConfigPage() ) {
879  $name = $this->mTitle->getSkinFromConfigSubpage();
880  $skins = array_merge(
881  array_keys( Skin::getSkinNames() ),
882  [ 'common' ]
883  );
884  return !in_array( $name, $skins )
885  && in_array( strtolower( $name ), $skins );
886  } else {
887  return false;
888  }
889  }
890 
898  protected function isSectionEditSupported() {
899  $contentHandler = ContentHandler::getForTitle( $this->mTitle );
900  return $contentHandler->supportsSections();
901  }
902 
908  public function importFormData( &$request ) {
909  # Section edit can come from either the form or a link
910  $this->section = $request->getVal( 'wpSection', $request->getVal( 'section' ) );
911 
912  if ( $this->section !== null && $this->section !== '' && !$this->isSectionEditSupported() ) {
913  throw new ErrorPageError( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
914  }
915 
916  $this->isNew = !$this->mTitle->exists() || $this->section == 'new';
917 
918  if ( $request->wasPosted() ) {
919  # These fields need to be checked for encoding.
920  # Also remove trailing whitespace, but don't remove _initial_
921  # whitespace from the text boxes. This may be significant formatting.
922  $this->textbox1 = rtrim( $request->getText( 'wpTextbox1' ) );
923  if ( !$request->getCheck( 'wpTextbox2' ) ) {
924  // Skip this if wpTextbox2 has input, it indicates that we came
925  // from a conflict page with raw page text, not a custom form
926  // modified by subclasses
927  $textbox1 = $this->importContentFormData( $request );
928  if ( $textbox1 !== null ) {
929  $this->textbox1 = $textbox1;
930  }
931  }
932 
933  $this->unicodeCheck = $request->getText( 'wpUnicodeCheck' );
934 
935  $this->summary = $request->getText( 'wpSummary' );
936 
937  # If the summary consists of a heading, e.g. '==Foobar==', extract the title from the
938  # header syntax, e.g. 'Foobar'. This is mainly an issue when we are using wpSummary for
939  # section titles.
940  $this->summary = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->summary );
941 
942  # Treat sectiontitle the same way as summary.
943  # Note that wpSectionTitle is not yet a part of the actual edit form, as wpSummary is
944  # currently doing double duty as both edit summary and section title. Right now this
945  # is just to allow API edits to work around this limitation, but this should be
946  # incorporated into the actual edit form when EditPage is rewritten (T20654, T28312).
947  $this->sectiontitle = $request->getText( 'wpSectionTitle' );
948  $this->sectiontitle = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->sectiontitle );
949 
950  $this->edittime = $request->getVal( 'wpEdittime' );
951  $this->editRevId = $request->getIntOrNull( 'editRevId' );
952  $this->starttime = $request->getVal( 'wpStarttime' );
953 
954  $undidRev = $request->getInt( 'wpUndidRevision' );
955  if ( $undidRev ) {
956  $this->undidRev = $undidRev;
957  }
958 
959  $this->scrolltop = $request->getIntOrNull( 'wpScrolltop' );
960 
961  if ( $this->textbox1 === '' && !$request->getCheck( 'wpTextbox1' ) ) {
962  // wpTextbox1 field is missing, possibly due to being "too big"
963  // according to some filter rules such as Suhosin's setting for
964  // suhosin.request.max_value_length (d'oh)
965  $this->incompleteForm = true;
966  } else {
967  // If we receive the last parameter of the request, we can fairly
968  // claim the POST request has not been truncated.
969  $this->incompleteForm = !$request->getVal( 'wpUltimateParam' );
970  }
971  if ( $this->incompleteForm ) {
972  # If the form is incomplete, force to preview.
973  wfDebug( __METHOD__ . ": Form data appears to be incomplete\n" );
974  wfDebug( "POST DATA: " . var_export( $request->getPostValues(), true ) . "\n" );
975  $this->preview = true;
976  } else {
977  $this->preview = $request->getCheck( 'wpPreview' );
978  $this->diff = $request->getCheck( 'wpDiff' );
979 
980  // Remember whether a save was requested, so we can indicate
981  // if we forced preview due to session failure.
982  $this->mTriedSave = !$this->preview;
983 
984  if ( $this->tokenOk( $request ) ) {
985  # Some browsers will not report any submit button
986  # if the user hits enter in the comment box.
987  # The unmarked state will be assumed to be a save,
988  # if the form seems otherwise complete.
989  wfDebug( __METHOD__ . ": Passed token check.\n" );
990  } elseif ( $this->diff ) {
991  # Failed token check, but only requested "Show Changes".
992  wfDebug( __METHOD__ . ": Failed token check; Show Changes requested.\n" );
993  } else {
994  # Page might be a hack attempt posted from
995  # an external site. Preview instead of saving.
996  wfDebug( __METHOD__ . ": Failed token check; forcing preview\n" );
997  $this->preview = true;
998  }
999  }
1000  $this->save = !$this->preview && !$this->diff;
1001  if ( !preg_match( '/^\d{14}$/', $this->edittime ) ) {
1002  $this->edittime = null;
1003  }
1004 
1005  if ( !preg_match( '/^\d{14}$/', $this->starttime ) ) {
1006  $this->starttime = null;
1007  }
1008 
1009  $this->recreate = $request->getCheck( 'wpRecreate' );
1010 
1011  $this->minoredit = $request->getCheck( 'wpMinoredit' );
1012  $this->watchthis = $request->getCheck( 'wpWatchthis' );
1013 
1014  $user = $this->context->getUser();
1015  # Don't force edit summaries when a user is editing their own user or talk page
1016  if ( ( $this->mTitle->mNamespace == NS_USER || $this->mTitle->mNamespace == NS_USER_TALK )
1017  && $this->mTitle->getText() == $user->getName()
1018  ) {
1019  $this->allowBlankSummary = true;
1020  } else {
1021  $this->allowBlankSummary = $request->getBool( 'wpIgnoreBlankSummary' )
1022  || !$user->getOption( 'forceeditsummary' );
1023  }
1024 
1025  $this->autoSumm = $request->getText( 'wpAutoSummary' );
1026 
1027  $this->allowBlankArticle = $request->getBool( 'wpIgnoreBlankArticle' );
1028  $this->allowSelfRedirect = $request->getBool( 'wpIgnoreSelfRedirect' );
1029 
1030  $changeTags = $request->getVal( 'wpChangeTags' );
1031  if ( is_null( $changeTags ) || $changeTags === '' ) {
1032  $this->changeTags = [];
1033  } else {
1034  $this->changeTags = array_filter( array_map( 'trim', explode( ',',
1035  $changeTags ) ) );
1036  }
1037  } else {
1038  # Not a posted form? Start with nothing.
1039  wfDebug( __METHOD__ . ": Not a posted form.\n" );
1040  $this->textbox1 = '';
1041  $this->summary = '';
1042  $this->sectiontitle = '';
1043  $this->edittime = '';
1044  $this->editRevId = null;
1045  $this->starttime = wfTimestampNow();
1046  $this->edit = false;
1047  $this->preview = false;
1048  $this->save = false;
1049  $this->diff = false;
1050  $this->minoredit = false;
1051  // Watch may be overridden by request parameters
1052  $this->watchthis = $request->getBool( 'watchthis', false );
1053  $this->recreate = false;
1054 
1055  // When creating a new section, we can preload a section title by passing it as the
1056  // preloadtitle parameter in the URL (T15100)
1057  if ( $this->section == 'new' && $request->getVal( 'preloadtitle' ) ) {
1058  $this->sectiontitle = $request->getVal( 'preloadtitle' );
1059  // Once wpSummary isn't being use for setting section titles, we should delete this.
1060  $this->summary = $request->getVal( 'preloadtitle' );
1061  } elseif ( $this->section != 'new' && $request->getVal( 'summary' ) !== '' ) {
1062  $this->summary = $request->getText( 'summary' );
1063  if ( $this->summary !== '' ) {
1064  $this->hasPresetSummary = true;
1065  }
1066  }
1067 
1068  if ( $request->getVal( 'minor' ) ) {
1069  $this->minoredit = true;
1070  }
1071  }
1072 
1073  $this->oldid = $request->getInt( 'oldid' );
1074  $this->parentRevId = $request->getInt( 'parentRevId' );
1075 
1076  $this->bot = $request->getBool( 'bot', true );
1077  $this->nosummary = $request->getBool( 'nosummary' );
1078 
1079  // May be overridden by revision.
1080  $this->contentModel = $request->getText( 'model', $this->contentModel );
1081  // May be overridden by revision.
1082  $this->contentFormat = $request->getText( 'format', $this->contentFormat );
1083 
1084  try {
1085  $handler = ContentHandler::getForModelID( $this->contentModel );
1086  } catch ( MWUnknownContentModelException $e ) {
1087  throw new ErrorPageError(
1088  'editpage-invalidcontentmodel-title',
1089  'editpage-invalidcontentmodel-text',
1090  [ wfEscapeWikiText( $this->contentModel ) ]
1091  );
1092  }
1093 
1094  if ( !$handler->isSupportedFormat( $this->contentFormat ) ) {
1095  throw new ErrorPageError(
1096  'editpage-notsupportedcontentformat-title',
1097  'editpage-notsupportedcontentformat-text',
1098  [
1099  wfEscapeWikiText( $this->contentFormat ),
1100  wfEscapeWikiText( ContentHandler::getLocalizedName( $this->contentModel ) )
1101  ]
1102  );
1103  }
1104 
1111  $this->editintro = $request->getText( 'editintro',
1112  // Custom edit intro for new sections
1113  $this->section === 'new' ? 'MediaWiki:addsection-editintro' : '' );
1114 
1115  // Allow extensions to modify form data
1116  Hooks::run( 'EditPage::importFormData', [ $this, $request ] );
1117  }
1118 
1128  protected function importContentFormData( &$request ) {
1129  return null; // Don't do anything, EditPage already extracted wpTextbox1
1130  }
1131 
1137  public function initialiseForm() {
1138  $this->edittime = $this->page->getTimestamp();
1139  $this->editRevId = $this->page->getLatest();
1140 
1141  $dummy = ContentHandler::getForModelID( $this->contentModel )->makeEmptyContent();
1142  $content = $this->getContentObject( $dummy ); # TODO: track content object?!
1143  if ( $content === $dummy ) { // Invalid section
1144  $this->noSuchSectionPage();
1145  return false;
1146  }
1147 
1148  if ( !$content ) {
1149  $out = $this->context->getOutput();
1150  $this->editFormPageTop .= Html::rawElement(
1151  'div', [ 'class' => 'errorbox' ],
1152  $out->parseAsInterface( $this->context->msg( 'missing-revision-content',
1153  $this->oldid,
1154  Message::plaintextParam( $this->mTitle->getPrefixedText() )
1155  ) )
1156  );
1157  } elseif ( !$this->isSupportedContentModel( $content->getModel() ) ) {
1158  $modelMsg = $this->getContext()->msg( 'content-model-' . $content->getModel() );
1159  $modelName = $modelMsg->exists() ? $modelMsg->text() : $content->getModel();
1160 
1161  $out = $this->context->getOutput();
1162  $out->showErrorPage(
1163  'modeleditnotsupported-title',
1164  'modeleditnotsupported-text',
1165  [ $modelName ]
1166  );
1167  return false;
1168  }
1169 
1170  $this->textbox1 = $this->toEditText( $content );
1171 
1172  $user = $this->context->getUser();
1173  // activate checkboxes if user wants them to be always active
1174  # Sort out the "watch" checkbox
1175  if ( $user->getOption( 'watchdefault' ) ) {
1176  # Watch all edits
1177  $this->watchthis = true;
1178  } elseif ( $user->getOption( 'watchcreations' ) && !$this->mTitle->exists() ) {
1179  # Watch creations
1180  $this->watchthis = true;
1181  } elseif ( $user->isWatched( $this->mTitle ) ) {
1182  # Already watched
1183  $this->watchthis = true;
1184  }
1185  if ( $user->getOption( 'minordefault' ) && !$this->isNew ) {
1186  $this->minoredit = true;
1187  }
1188  if ( $this->textbox1 === false ) {
1189  return false;
1190  }
1191  return true;
1192  }
1193 
1201  protected function getContentObject( $def_content = null ) {
1202  global $wgDisableAnonTalk;
1203 
1204  $content = false;
1205 
1206  $user = $this->context->getUser();
1207  $request = $this->context->getRequest();
1208  // For message page not locally set, use the i18n message.
1209  // For other non-existent articles, use preload text if any.
1210  if ( !$this->mTitle->exists() || $this->section == 'new' ) {
1211  if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && $this->section != 'new' ) {
1212  # If this is a system message, get the default text.
1213  $msg = $this->mTitle->getDefaultMessageText();
1214 
1215  $content = $this->toEditContent( $msg );
1216  }
1217  if ( $content === false ) {
1218  # If requested, preload some text.
1219  $preload = $request->getVal( 'preload',
1220  // Custom preload text for new sections
1221  $this->section === 'new' ? 'MediaWiki:addsection-preload' : '' );
1222  $params = $request->getArray( 'preloadparams', [] );
1223 
1224  $content = $this->getPreloadedContent( $preload, $params );
1225  }
1226  // For existing pages, get text based on "undo" or section parameters.
1227  } elseif ( $this->section != '' ) {
1228  // Get section edit text (returns $def_text for invalid sections)
1229  $orig = $this->getOriginalContent( $user );
1230  $content = $orig ? $orig->getSection( $this->section ) : null;
1231 
1232  if ( !$content ) {
1233  $content = $def_content;
1234  }
1235  } else {
1236  $undoafter = $request->getInt( 'undoafter' );
1237  $undo = $request->getInt( 'undo' );
1238 
1239  if ( $undo > 0 && $undoafter > 0 ) {
1240  $undorev = Revision::newFromId( $undo );
1241  $oldrev = Revision::newFromId( $undoafter );
1242  $undoMsg = null;
1243 
1244  # Sanity check, make sure it's the right page,
1245  # the revisions exist and they were not deleted.
1246  # Otherwise, $content will be left as-is.
1247  if ( !is_null( $undorev ) && !is_null( $oldrev ) &&
1248  !$undorev->isDeleted( RevisionRecord::DELETED_TEXT ) &&
1249  !$oldrev->isDeleted( RevisionRecord::DELETED_TEXT )
1250  ) {
1251  if ( WikiPage::hasDifferencesOutsideMainSlot( $undorev, $oldrev )
1252  || !$this->isSupportedContentModel( $oldrev->getContentModel() )
1253  ) {
1254  // Hack for undo while EditPage can't handle multi-slot editing
1255  $this->context->getOutput()->redirect( $this->mTitle->getFullURL( [
1256  'action' => 'mcrundo',
1257  'undo' => $undo,
1258  'undoafter' => $undoafter,
1259  ] ) );
1260  return false;
1261  } else {
1262  $content = $this->page->getUndoContent( $undorev, $oldrev );
1263 
1264  if ( $content === false ) {
1265  # Warn the user that something went wrong
1266  $undoMsg = 'failure';
1267  }
1268  }
1269 
1270  if ( $undoMsg === null ) {
1271  $oldContent = $this->page->getContent( RevisionRecord::RAW );
1273  $user, MediaWikiServices::getInstance()->getContentLanguage() );
1274  $newContent = $content->preSaveTransform( $this->mTitle, $user, $popts );
1275  if ( $newContent->getModel() !== $oldContent->getModel() ) {
1276  // The undo may change content
1277  // model if its reverting the top
1278  // edit. This can result in
1279  // mismatched content model/format.
1280  $this->contentModel = $newContent->getModel();
1281  $this->contentFormat = $oldrev->getContentFormat();
1282  }
1283 
1284  if ( $newContent->equals( $oldContent ) ) {
1285  # Tell the user that the undo results in no change,
1286  # i.e. the revisions were already undone.
1287  $undoMsg = 'nochange';
1288  $content = false;
1289  } else {
1290  # Inform the user of our success and set an automatic edit summary
1291  $undoMsg = 'success';
1292 
1293  # If we just undid one rev, use an autosummary
1294  $firstrev = $oldrev->getNext();
1295  if ( $firstrev && $firstrev->getId() == $undo ) {
1296  $userText = $undorev->getUserText();
1297  if ( $userText === '' ) {
1298  $undoSummary = $this->context->msg(
1299  'undo-summary-username-hidden',
1300  $undo
1301  )->inContentLanguage()->text();
1302  } else {
1303  $undoMessage = ( $undorev->getUser() === 0 && $wgDisableAnonTalk ) ?
1304  'undo-summary-anon' :
1305  'undo-summary';
1306  $undoSummary = $this->context->msg(
1307  $undoMessage,
1308  $undo,
1309  $userText
1310  )->inContentLanguage()->text();
1311  }
1312  if ( $this->summary === '' ) {
1313  $this->summary = $undoSummary;
1314  } else {
1315  $this->summary = $undoSummary . $this->context->msg( 'colon-separator' )
1316  ->inContentLanguage()->text() . $this->summary;
1317  }
1318  $this->undidRev = $undo;
1319  }
1320  $this->formtype = 'diff';
1321  }
1322  }
1323  } else {
1324  // Failed basic sanity checks.
1325  // Older revisions may have been removed since the link
1326  // was created, or we may simply have got bogus input.
1327  $undoMsg = 'norev';
1328  }
1329 
1330  $out = $this->context->getOutput();
1331  // Messages: undo-success, undo-failure, undo-main-slot-only, undo-norev,
1332  // undo-nochange.
1333  $class = ( $undoMsg == 'success' ? '' : 'error ' ) . "mw-undo-{$undoMsg}";
1334  $this->editFormPageTop .= Html::rawElement(
1335  'div', [ 'class' => $class ],
1336  $out->parseAsInterface(
1337  $this->context->msg( 'undo-' . $undoMsg )->plain()
1338  )
1339  );
1340  }
1341 
1342  if ( $content === false ) {
1343  // Hack for restoring old revisions while EditPage
1344  // can't handle multi-slot editing.
1345 
1346  $curRevision = $this->page->getRevision();
1347  $oldRevision = $this->mArticle->getRevisionFetched();
1348 
1349  if ( $curRevision
1350  && $oldRevision
1351  && $curRevision->getId() !== $oldRevision->getId()
1352  && ( WikiPage::hasDifferencesOutsideMainSlot( $oldRevision, $curRevision )
1353  || !$this->isSupportedContentModel( $oldRevision->getContentModel() ) )
1354  ) {
1355  $this->context->getOutput()->redirect(
1356  $this->mTitle->getFullURL(
1357  [
1358  'action' => 'mcrrestore',
1359  'restore' => $oldRevision->getId(),
1360  ]
1361  )
1362  );
1363 
1364  return false;
1365  }
1366  }
1367 
1368  if ( $content === false ) {
1369  $content = $this->getOriginalContent( $user );
1370  }
1371  }
1372 
1373  return $content;
1374  }
1375 
1391  private function getOriginalContent( User $user ) {
1392  if ( $this->section == 'new' ) {
1393  return $this->getCurrentContent();
1394  }
1395  $revision = $this->mArticle->getRevisionFetched();
1396  if ( $revision === null ) {
1397  $handler = ContentHandler::getForModelID( $this->contentModel );
1398  return $handler->makeEmptyContent();
1399  }
1400  $content = $revision->getContent( RevisionRecord::FOR_THIS_USER, $user );
1401  return $content;
1402  }
1403 
1416  public function getParentRevId() {
1417  if ( $this->parentRevId ) {
1418  return $this->parentRevId;
1419  } else {
1420  return $this->mArticle->getRevIdFetched();
1421  }
1422  }
1423 
1432  protected function getCurrentContent() {
1433  $rev = $this->page->getRevision();
1434  $content = $rev ? $rev->getContent( RevisionRecord::RAW ) : null;
1435 
1436  if ( $content === false || $content === null ) {
1437  $handler = ContentHandler::getForModelID( $this->contentModel );
1438  return $handler->makeEmptyContent();
1439  } elseif ( !$this->undidRev ) {
1440  // Content models should always be the same since we error
1441  // out if they are different before this point (in ->edit()).
1442  // The exception being, during an undo, the current revision might
1443  // differ from the prior revision.
1444  $logger = LoggerFactory::getInstance( 'editpage' );
1445  if ( $this->contentModel !== $rev->getContentModel() ) {
1446  $logger->warning( "Overriding content model from current edit {prev} to {new}", [
1447  'prev' => $this->contentModel,
1448  'new' => $rev->getContentModel(),
1449  'title' => $this->getTitle()->getPrefixedDBkey(),
1450  'method' => __METHOD__
1451  ] );
1452  $this->contentModel = $rev->getContentModel();
1453  }
1454 
1455  // Given that the content models should match, the current selected
1456  // format should be supported.
1457  if ( !$content->isSupportedFormat( $this->contentFormat ) ) {
1458  $logger->warning( "Current revision content format unsupported. Overriding {prev} to {new}", [
1459 
1460  'prev' => $this->contentFormat,
1461  'new' => $rev->getContentFormat(),
1462  'title' => $this->getTitle()->getPrefixedDBkey(),
1463  'method' => __METHOD__
1464  ] );
1465  $this->contentFormat = $rev->getContentFormat();
1466  }
1467  }
1468  return $content;
1469  }
1470 
1478  public function setPreloadedContent( Content $content ) {
1479  $this->mPreloadContent = $content;
1480  }
1481 
1493  protected function getPreloadedContent( $preload, $params = [] ) {
1494  if ( !empty( $this->mPreloadContent ) ) {
1495  return $this->mPreloadContent;
1496  }
1497 
1498  $handler = ContentHandler::getForModelID( $this->contentModel );
1499 
1500  if ( $preload === '' ) {
1501  return $handler->makeEmptyContent();
1502  }
1503 
1504  $user = $this->context->getUser();
1505  $title = Title::newFromText( $preload );
1506 
1507  # Check for existence to avoid getting MediaWiki:Noarticletext
1508  if ( !$this->isPageExistingAndViewable( $title, $user ) ) {
1509  // TODO: somehow show a warning to the user!
1510  return $handler->makeEmptyContent();
1511  }
1512 
1514  if ( $page->isRedirect() ) {
1516  # Same as before
1517  if ( !$this->isPageExistingAndViewable( $title, $user ) ) {
1518  // TODO: somehow show a warning to the user!
1519  return $handler->makeEmptyContent();
1520  }
1522  }
1523 
1524  $parserOptions = ParserOptions::newFromUser( $user );
1525  $content = $page->getContent( RevisionRecord::RAW );
1526 
1527  if ( !$content ) {
1528  // TODO: somehow show a warning to the user!
1529  return $handler->makeEmptyContent();
1530  }
1531 
1532  if ( $content->getModel() !== $handler->getModelID() ) {
1533  $converted = $content->convert( $handler->getModelID() );
1534 
1535  if ( !$converted ) {
1536  // TODO: somehow show a warning to the user!
1537  wfDebug( "Attempt to preload incompatible content: " .
1538  "can't convert " . $content->getModel() .
1539  " to " . $handler->getModelID() );
1540 
1541  return $handler->makeEmptyContent();
1542  }
1543 
1544  $content = $converted;
1545  }
1546 
1547  return $content->preloadTransform( $title, $parserOptions, $params );
1548  }
1549 
1559  private function isPageExistingAndViewable( $title, User $user ) {
1560  $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
1561 
1562  return $title && $title->exists() && $permissionManager->userCan( 'read', $user, $title );
1563  }
1564 
1572  public function tokenOk( &$request ) {
1573  $token = $request->getVal( 'wpEditToken' );
1574  $user = $this->context->getUser();
1575  $this->mTokenOk = $user->matchEditToken( $token );
1576  $this->mTokenOkExceptSuffix = $user->matchEditTokenNoSuffix( $token );
1577  return $this->mTokenOk;
1578  }
1579 
1594  protected function setPostEditCookie( $statusValue ) {
1595  $revisionId = $this->page->getLatest();
1596  $postEditKey = self::POST_EDIT_COOKIE_KEY_PREFIX . $revisionId;
1597 
1598  $val = 'saved';
1599  if ( $statusValue == self::AS_SUCCESS_NEW_ARTICLE ) {
1600  $val = 'created';
1601  } elseif ( $this->oldid ) {
1602  $val = 'restored';
1603  }
1604 
1605  $response = $this->context->getRequest()->response();
1606  $response->setCookie( $postEditKey, $val, time() + self::POST_EDIT_COOKIE_DURATION );
1607  }
1608 
1615  public function attemptSave( &$resultDetails = false ) {
1616  // TODO: MCR: treat $this->minoredit like $this->bot and check isAllowed( 'minoredit' )!
1617  // Also, add $this->autopatrol like $this->bot and check isAllowed( 'autopatrol' )!
1618  // This is needed since PageUpdater no longer checks these rights!
1619 
1620  // Allow bots to exempt some edits from bot flagging
1621  $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
1622  $bot = $permissionManager->userHasRight( $this->context->getUser(), 'bot' ) && $this->bot;
1623  $status = $this->internalAttemptSave( $resultDetails, $bot );
1624 
1625  Hooks::run( 'EditPage::attemptSave:after', [ $this, $status, $resultDetails ] );
1626 
1627  return $status;
1628  }
1629 
1633  private function incrementResolvedConflicts() {
1634  if ( $this->context->getRequest()->getText( 'mode' ) !== 'conflict' ) {
1635  return;
1636  }
1637 
1638  $this->getEditConflictHelper()->incrementResolvedStats( $this->context->getUser() );
1639  }
1640 
1650  private function handleStatus( Status $status, $resultDetails ) {
1655  if ( $status->value == self::AS_SUCCESS_UPDATE
1656  || $status->value == self::AS_SUCCESS_NEW_ARTICLE
1657  ) {
1658  $this->incrementResolvedConflicts();
1659 
1660  $this->didSave = true;
1661  if ( !$resultDetails['nullEdit'] ) {
1662  $this->setPostEditCookie( $status->value );
1663  }
1664  }
1665 
1666  $out = $this->context->getOutput();
1667 
1668  // "wpExtraQueryRedirect" is a hidden input to modify
1669  // after save URL and is not used by actual edit form
1670  $request = $this->context->getRequest();
1671  $extraQueryRedirect = $request->getVal( 'wpExtraQueryRedirect' );
1672 
1673  switch ( $status->value ) {
1674  case self::AS_HOOK_ERROR_EXPECTED:
1675  case self::AS_CONTENT_TOO_BIG:
1676  case self::AS_ARTICLE_WAS_DELETED:
1677  case self::AS_CONFLICT_DETECTED:
1678  case self::AS_SUMMARY_NEEDED:
1679  case self::AS_TEXTBOX_EMPTY:
1680  case self::AS_MAX_ARTICLE_SIZE_EXCEEDED:
1681  case self::AS_END:
1682  case self::AS_BLANK_ARTICLE:
1683  case self::AS_SELF_REDIRECT:
1684  return true;
1685 
1686  case self::AS_HOOK_ERROR:
1687  return false;
1688 
1689  case self::AS_CANNOT_USE_CUSTOM_MODEL:
1690  case self::AS_PARSE_ERROR:
1691  case self::AS_UNICODE_NOT_SUPPORTED:
1692  $out->wrapWikiTextAsInterface( 'error',
1693  $status->getWikiText( false, false, $this->context->getLanguage() )
1694  );
1695  return true;
1696 
1697  case self::AS_SUCCESS_NEW_ARTICLE:
1698  $query = $resultDetails['redirect'] ? 'redirect=no' : '';
1699  if ( $extraQueryRedirect ) {
1700  if ( $query !== '' ) {
1701  $query .= '&';
1702  }
1703  $query .= $extraQueryRedirect;
1704  }
1705  $anchor = $resultDetails['sectionanchor'] ?? '';
1706  $out->redirect( $this->mTitle->getFullURL( $query ) . $anchor );
1707  return false;
1708 
1709  case self::AS_SUCCESS_UPDATE:
1710  $extraQuery = '';
1711  $sectionanchor = $resultDetails['sectionanchor'];
1712 
1713  // Give extensions a chance to modify URL query on update
1714  Hooks::run(
1715  'ArticleUpdateBeforeRedirect',
1716  [ $this->mArticle, &$sectionanchor, &$extraQuery ]
1717  );
1718 
1719  if ( $resultDetails['redirect'] ) {
1720  if ( $extraQuery !== '' ) {
1721  $extraQuery = '&' . $extraQuery;
1722  }
1723  $extraQuery = 'redirect=no' . $extraQuery;
1724  }
1725  if ( $extraQueryRedirect ) {
1726  if ( $extraQuery !== '' ) {
1727  $extraQuery .= '&';
1728  }
1729  $extraQuery .= $extraQueryRedirect;
1730  }
1731 
1732  $out->redirect( $this->mTitle->getFullURL( $extraQuery ) . $sectionanchor );
1733  return false;
1734 
1735  case self::AS_SPAM_ERROR:
1736  $this->spamPageWithContent( $resultDetails['spam'] );
1737  return false;
1738 
1739  case self::AS_BLOCKED_PAGE_FOR_USER:
1740  throw new UserBlockedError(
1741  $this->context->getUser()->getBlock(),
1742  $this->context->getUser(),
1743  $this->context->getLanguage(),
1744  $request->getIP()
1745  );
1746 
1747  case self::AS_IMAGE_REDIRECT_ANON:
1748  case self::AS_IMAGE_REDIRECT_LOGGED:
1749  throw new PermissionsError( 'upload' );
1750 
1751  case self::AS_READ_ONLY_PAGE_ANON:
1752  case self::AS_READ_ONLY_PAGE_LOGGED:
1753  throw new PermissionsError( 'edit' );
1754 
1755  case self::AS_READ_ONLY_PAGE:
1756  throw new ReadOnlyError;
1757 
1758  case self::AS_RATE_LIMITED:
1759  throw new ThrottledError();
1760 
1761  case self::AS_NO_CREATE_PERMISSION:
1762  $permission = $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage';
1763  throw new PermissionsError( $permission );
1764 
1765  case self::AS_NO_CHANGE_CONTENT_MODEL:
1766  throw new PermissionsError( 'editcontentmodel' );
1767 
1768  default:
1769  // We don't recognize $status->value. The only way that can happen
1770  // is if an extension hook aborted from inside ArticleSave.
1771  // Render the status object into $this->hookError
1772  // FIXME this sucks, we should just use the Status object throughout
1773  $this->hookError = '<div class="error">' . "\n" .
1774  $status->getWikiText( false, false, $this->context->getLanguage() ) .
1775  '</div>';
1776  return true;
1777  }
1778  }
1779 
1789  protected function runPostMergeFilters( Content $content, Status $status, User $user ) {
1790  // Run old style post-section-merge edit filter
1791  if ( $this->hookError != '' ) {
1792  # ...or the hook could be expecting us to produce an error
1793  $status->fatal( 'hookaborted' );
1794  $status->value = self::AS_HOOK_ERROR_EXPECTED;
1795  return false;
1796  }
1797 
1798  // Run new style post-section-merge edit filter
1799  if ( !Hooks::run( 'EditFilterMergedContent',
1800  [ $this->context, $content, $status, $this->summary,
1801  $user, $this->minoredit ] )
1802  ) {
1803  # Error messages etc. could be handled within the hook...
1804  if ( $status->isGood() ) {
1805  $status->fatal( 'hookaborted' );
1806  // Not setting $this->hookError here is a hack to allow the hook
1807  // to cause a return to the edit page without $this->hookError
1808  // being set. This is used by ConfirmEdit to display a captcha
1809  // without any error message cruft.
1810  } else {
1811  $this->hookError = $this->formatStatusErrors( $status );
1812  }
1813  // Use the existing $status->value if the hook set it
1814  if ( !$status->value ) {
1815  $status->value = self::AS_HOOK_ERROR;
1816  }
1817  return false;
1818  } elseif ( !$status->isOK() ) {
1819  # ...or the hook could be expecting us to produce an error
1820  // FIXME this sucks, we should just use the Status object throughout
1821  if ( !$status->getErrors() ) {
1822  // Provide a fallback error message if none was set
1823  $status->fatal( 'hookaborted' );
1824  }
1825  $this->hookError = $this->formatStatusErrors( $status );
1826  $status->value = self::AS_HOOK_ERROR_EXPECTED;
1827  return false;
1828  }
1829 
1830  return true;
1831  }
1832 
1839  private function formatStatusErrors( Status $status ) {
1840  $errmsg = $status->getWikiText(
1841  'edit-error-short',
1842  'edit-error-long',
1843  $this->context->getLanguage()
1844  );
1845  return <<<ERROR
1846 <div class="errorbox">
1847 {$errmsg}
1848 </div>
1849 <br clear="all" />
1850 ERROR;
1851  }
1852 
1859  private function newSectionSummary( &$sectionanchor = null ) {
1860  if ( $this->sectiontitle !== '' ) {
1861  $sectionanchor = $this->guessSectionName( $this->sectiontitle );
1862  // If no edit summary was specified, create one automatically from the section
1863  // title and have it link to the new section. Otherwise, respect the summary as
1864  // passed.
1865  if ( $this->summary === '' ) {
1866  $cleanSectionTitle = MediaWikiServices::getInstance()->getParser()
1867  ->stripSectionName( $this->sectiontitle );
1868  return $this->context->msg( 'newsectionsummary' )
1869  ->plaintextParams( $cleanSectionTitle )->inContentLanguage()->text();
1870  }
1871  } elseif ( $this->summary !== '' ) {
1872  $sectionanchor = $this->guessSectionName( $this->summary );
1873  # This is a new section, so create a link to the new section
1874  # in the revision summary.
1875  $cleanSummary = MediaWikiServices::getInstance()->getParser()
1876  ->stripSectionName( $this->summary );
1877  return $this->context->msg( 'newsectionsummary' )
1878  ->plaintextParams( $cleanSummary )->inContentLanguage()->text();
1879  }
1880  return $this->summary;
1881  }
1882 
1907  public function internalAttemptSave( &$result, $bot = false ) {
1908  $status = Status::newGood();
1909  $user = $this->context->getUser();
1910  $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
1911 
1912  if ( !Hooks::run( 'EditPage::attemptSave', [ $this ] ) ) {
1913  wfDebug( "Hook 'EditPage::attemptSave' aborted article saving\n" );
1914  $status->fatal( 'hookaborted' );
1915  $status->value = self::AS_HOOK_ERROR;
1916  return $status;
1917  }
1918 
1919  if ( $this->unicodeCheck !== self::UNICODE_CHECK ) {
1920  $status->fatal( 'unicode-support-fail' );
1921  $status->value = self::AS_UNICODE_NOT_SUPPORTED;
1922  return $status;
1923  }
1924 
1925  $request = $this->context->getRequest();
1926  $spam = $request->getText( 'wpAntispam' );
1927  if ( $spam !== '' ) {
1928  wfDebugLog(
1929  'SimpleAntiSpam',
1930  $user->getName() .
1931  ' editing "' .
1932  $this->mTitle->getPrefixedText() .
1933  '" submitted bogus field "' .
1934  $spam .
1935  '"'
1936  );
1937  $status->fatal( 'spamprotectionmatch', false );
1938  $status->value = self::AS_SPAM_ERROR;
1939  return $status;
1940  }
1941 
1942  try {
1943  # Construct Content object
1944  $textbox_content = $this->toEditContent( $this->textbox1 );
1945  } catch ( MWContentSerializationException $ex ) {
1946  $status->fatal(
1947  'content-failed-to-parse',
1948  $this->contentModel,
1949  $this->contentFormat,
1950  $ex->getMessage()
1951  );
1952  $status->value = self::AS_PARSE_ERROR;
1953  return $status;
1954  }
1955 
1956  # Check image redirect
1957  if ( $this->mTitle->getNamespace() == NS_FILE &&
1958  $textbox_content->isRedirect() &&
1959  !$permissionManager->userHasRight( $user, 'upload' )
1960  ) {
1961  $code = $user->isAnon() ? self::AS_IMAGE_REDIRECT_ANON : self::AS_IMAGE_REDIRECT_LOGGED;
1962  $status->setResult( false, $code );
1963 
1964  return $status;
1965  }
1966 
1967  # Check for spam
1968  $match = self::matchSummarySpamRegex( $this->summary );
1969  if ( $match === false && $this->section == 'new' ) {
1970  # $wgSpamRegex is enforced on this new heading/summary because, unlike
1971  # regular summaries, it is added to the actual wikitext.
1972  if ( $this->sectiontitle !== '' ) {
1973  # This branch is taken when the API is used with the 'sectiontitle' parameter.
1974  $match = self::matchSpamRegex( $this->sectiontitle );
1975  } else {
1976  # This branch is taken when the "Add Topic" user interface is used, or the API
1977  # is used with the 'summary' parameter.
1978  $match = self::matchSpamRegex( $this->summary );
1979  }
1980  }
1981  if ( $match === false ) {
1982  $match = self::matchSpamRegex( $this->textbox1 );
1983  }
1984  if ( $match !== false ) {
1985  $result['spam'] = $match;
1986  $ip = $request->getIP();
1987  $pdbk = $this->mTitle->getPrefixedDBkey();
1988  $match = str_replace( "\n", '', $match );
1989  wfDebugLog( 'SpamRegex', "$ip spam regex hit [[$pdbk]]: \"$match\"" );
1990  $status->fatal( 'spamprotectionmatch', $match );
1991  $status->value = self::AS_SPAM_ERROR;
1992  return $status;
1993  }
1994  if ( !Hooks::run(
1995  'EditFilter',
1996  [ $this, $this->textbox1, $this->section, &$this->hookError, $this->summary ] )
1997  ) {
1998  # Error messages etc. could be handled within the hook...
1999  $status->fatal( 'hookaborted' );
2000  $status->value = self::AS_HOOK_ERROR;
2001  return $status;
2002  } elseif ( $this->hookError != '' ) {
2003  # ...or the hook could be expecting us to produce an error
2004  $status->fatal( 'hookaborted' );
2005  $status->value = self::AS_HOOK_ERROR_EXPECTED;
2006  return $status;
2007  }
2008 
2009  if ( $permissionManager->isBlockedFrom( $user, $this->mTitle ) ) {
2010  // Auto-block user's IP if the account was "hard" blocked
2011  if ( !wfReadOnly() ) {
2012  $user->spreadAnyEditBlock();
2013  }
2014  # Check block state against master, thus 'false'.
2015  $status->setResult( false, self::AS_BLOCKED_PAGE_FOR_USER );
2016  return $status;
2017  }
2018 
2019  $this->contentLength = strlen( $this->textbox1 );
2020  $config = $this->context->getConfig();
2021  $maxArticleSize = $config->get( 'MaxArticleSize' );
2022  if ( $this->contentLength > $maxArticleSize * 1024 ) {
2023  // Error will be displayed by showEditForm()
2024  $this->tooBig = true;
2025  $status->setResult( false, self::AS_CONTENT_TOO_BIG );
2026  return $status;
2027  }
2028 
2029  if ( !$permissionManager->userHasRight( $user, 'edit' ) ) {
2030  if ( $user->isAnon() ) {
2031  $status->setResult( false, self::AS_READ_ONLY_PAGE_ANON );
2032  return $status;
2033  } else {
2034  $status->fatal( 'readonlytext' );
2035  $status->value = self::AS_READ_ONLY_PAGE_LOGGED;
2036  return $status;
2037  }
2038  }
2039 
2040  $changingContentModel = false;
2041  if ( $this->contentModel !== $this->mTitle->getContentModel() ) {
2042  if ( !$config->get( 'ContentHandlerUseDB' ) ) {
2043  $status->fatal( 'editpage-cannot-use-custom-model' );
2044  $status->value = self::AS_CANNOT_USE_CUSTOM_MODEL;
2045  return $status;
2046  } elseif ( !$permissionManager->userHasRight( $user, 'editcontentmodel' ) ) {
2047  $status->setResult( false, self::AS_NO_CHANGE_CONTENT_MODEL );
2048  return $status;
2049  }
2050  // Make sure the user can edit the page under the new content model too
2051  $titleWithNewContentModel = clone $this->mTitle;
2052  $titleWithNewContentModel->setContentModel( $this->contentModel );
2053 
2054  $canEditModel = $permissionManager->userCan(
2055  'editcontentmodel',
2056  $user,
2057  $titleWithNewContentModel
2058  );
2059 
2060  if (
2061  !$canEditModel
2062  || !$permissionManager->userCan( 'edit', $user, $titleWithNewContentModel )
2063  ) {
2064  $status->setResult( false, self::AS_NO_CHANGE_CONTENT_MODEL );
2065 
2066  return $status;
2067  }
2068 
2069  $changingContentModel = true;
2070  $oldContentModel = $this->mTitle->getContentModel();
2071  }
2072 
2073  if ( $this->changeTags ) {
2074  $changeTagsStatus = ChangeTags::canAddTagsAccompanyingChange(
2075  $this->changeTags, $user );
2076  if ( !$changeTagsStatus->isOK() ) {
2077  $changeTagsStatus->value = self::AS_CHANGE_TAG_ERROR;
2078  return $changeTagsStatus;
2079  }
2080  }
2081 
2082  if ( wfReadOnly() ) {
2083  $status->fatal( 'readonlytext' );
2084  $status->value = self::AS_READ_ONLY_PAGE;
2085  return $status;
2086  }
2087  if ( $user->pingLimiter() || $user->pingLimiter( 'linkpurge', 0 )
2088  || ( $changingContentModel && $user->pingLimiter( 'editcontentmodel' ) )
2089  ) {
2090  $status->fatal( 'actionthrottledtext' );
2091  $status->value = self::AS_RATE_LIMITED;
2092  return $status;
2093  }
2094 
2095  # If the article has been deleted while editing, don't save it without
2096  # confirmation
2097  if ( $this->wasDeletedSinceLastEdit() && !$this->recreate ) {
2098  $status->setResult( false, self::AS_ARTICLE_WAS_DELETED );
2099  return $status;
2100  }
2101 
2102  # Load the page data from the master. If anything changes in the meantime,
2103  # we detect it by using page_latest like a token in a 1 try compare-and-swap.
2104  $this->page->loadPageData( 'fromdbmaster' );
2105  $new = !$this->page->exists();
2106 
2107  if ( $new ) {
2108  // Late check for create permission, just in case *PARANOIA*
2109  if ( !$permissionManager->userCan( 'create', $user, $this->mTitle ) ) {
2110  $status->fatal( 'nocreatetext' );
2111  $status->value = self::AS_NO_CREATE_PERMISSION;
2112  wfDebug( __METHOD__ . ": no create permission\n" );
2113  return $status;
2114  }
2115 
2116  // Don't save a new page if it's blank or if it's a MediaWiki:
2117  // message with content equivalent to default (allow empty pages
2118  // in this case to disable messages, see T52124)
2119  $defaultMessageText = $this->mTitle->getDefaultMessageText();
2120  if ( $this->mTitle->getNamespace() === NS_MEDIAWIKI && $defaultMessageText !== false ) {
2121  $defaultText = $defaultMessageText;
2122  } else {
2123  $defaultText = '';
2124  }
2125 
2126  if ( !$this->allowBlankArticle && $this->textbox1 === $defaultText ) {
2127  $this->blankArticle = true;
2128  $status->fatal( 'blankarticle' );
2129  $status->setResult( false, self::AS_BLANK_ARTICLE );
2130  return $status;
2131  }
2132 
2133  if ( !$this->runPostMergeFilters( $textbox_content, $status, $user ) ) {
2134  return $status;
2135  }
2136 
2137  $content = $textbox_content;
2138 
2139  $result['sectionanchor'] = '';
2140  if ( $this->section == 'new' ) {
2141  if ( $this->sectiontitle !== '' ) {
2142  // Insert the section title above the content.
2143  $content = $content->addSectionHeader( $this->sectiontitle );
2144  } elseif ( $this->summary !== '' ) {
2145  // Insert the section title above the content.
2146  $content = $content->addSectionHeader( $this->summary );
2147  }
2148  $this->summary = $this->newSectionSummary( $result['sectionanchor'] );
2149  }
2150 
2151  $status->value = self::AS_SUCCESS_NEW_ARTICLE;
2152 
2153  } else { # not $new
2154 
2155  # Article exists. Check for edit conflict.
2156 
2157  $this->page->clear(); # Force reload of dates, etc.
2158  $timestamp = $this->page->getTimestamp();
2159  $latest = $this->page->getLatest();
2160 
2161  wfDebug( "timestamp: {$timestamp}, edittime: {$this->edittime}\n" );
2162 
2163  // An edit conflict is detected if the current revision is different from the
2164  // revision that was current when editing was initiated on the client.
2165  // This is checked based on the timestamp and revision ID.
2166  // TODO: the timestamp based check can probably go away now.
2167  if ( $timestamp != $this->edittime
2168  || ( $this->editRevId !== null && $this->editRevId != $latest )
2169  ) {
2170  $this->isConflict = true;
2171  if ( $this->section == 'new' ) {
2172  if ( $this->page->getUserText() == $user->getName() &&
2173  $this->page->getComment() == $this->newSectionSummary()
2174  ) {
2175  // Probably a duplicate submission of a new comment.
2176  // This can happen when CDN resends a request after
2177  // a timeout but the first one actually went through.
2178  wfDebug( __METHOD__
2179  . ": duplicate new section submission; trigger edit conflict!\n" );
2180  } else {
2181  // New comment; suppress conflict.
2182  $this->isConflict = false;
2183  wfDebug( __METHOD__ . ": conflict suppressed; new section\n" );
2184  }
2185  } elseif ( $this->section == ''
2187  DB_MASTER, $this->mTitle->getArticleID(),
2188  $user->getId(), $this->edittime
2189  )
2190  ) {
2191  # Suppress edit conflict with self, except for section edits where merging is required.
2192  wfDebug( __METHOD__ . ": Suppressing edit conflict, same user.\n" );
2193  $this->isConflict = false;
2194  }
2195  }
2196 
2197  // If sectiontitle is set, use it, otherwise use the summary as the section title.
2198  if ( $this->sectiontitle !== '' ) {
2199  $sectionTitle = $this->sectiontitle;
2200  } else {
2201  $sectionTitle = $this->summary;
2202  }
2203 
2204  $content = null;
2205 
2206  if ( $this->isConflict ) {
2207  wfDebug( __METHOD__
2208  . ": conflict! getting section '{$this->section}' for time '{$this->edittime}'"
2209  . " (id '{$this->editRevId}') (article time '{$timestamp}')\n" );
2210  // @TODO: replaceSectionAtRev() with base ID (not prior current) for ?oldid=X case
2211  // ...or disable section editing for non-current revisions (not exposed anyway).
2212  if ( $this->editRevId !== null ) {
2213  $content = $this->page->replaceSectionAtRev(
2214  $this->section,
2215  $textbox_content,
2216  $sectionTitle,
2217  $this->editRevId
2218  );
2219  } else {
2220  $content = $this->page->replaceSectionContent(
2221  $this->section,
2222  $textbox_content,
2223  $sectionTitle,
2224  $this->edittime
2225  );
2226  }
2227  } else {
2228  wfDebug( __METHOD__ . ": getting section '{$this->section}'\n" );
2229  $content = $this->page->replaceSectionContent(
2230  $this->section,
2231  $textbox_content,
2232  $sectionTitle
2233  );
2234  }
2235 
2236  if ( is_null( $content ) ) {
2237  wfDebug( __METHOD__ . ": activating conflict; section replace failed.\n" );
2238  $this->isConflict = true;
2239  $content = $textbox_content; // do not try to merge here!
2240  } elseif ( $this->isConflict ) {
2241  # Attempt merge
2242  if ( $this->mergeChangesIntoContent( $content ) ) {
2243  // Successful merge! Maybe we should tell the user the good news?
2244  $this->isConflict = false;
2245  wfDebug( __METHOD__ . ": Suppressing edit conflict, successful merge.\n" );
2246  } else {
2247  $this->section = '';
2248  $this->textbox1 = ContentHandler::getContentText( $content );
2249  wfDebug( __METHOD__ . ": Keeping edit conflict, failed merge.\n" );
2250  }
2251  }
2252 
2253  if ( $this->isConflict ) {
2254  $status->setResult( false, self::AS_CONFLICT_DETECTED );
2255  return $status;
2256  }
2257 
2258  if ( !$this->runPostMergeFilters( $content, $status, $user ) ) {
2259  return $status;
2260  }
2261 
2262  if ( $this->section == 'new' ) {
2263  // Handle the user preference to force summaries here
2264  if ( !$this->allowBlankSummary && trim( $this->summary ) == '' ) {
2265  $this->missingSummary = true;
2266  $status->fatal( 'missingsummary' ); // or 'missingcommentheader' if $section == 'new'. Blegh
2267  $status->value = self::AS_SUMMARY_NEEDED;
2268  return $status;
2269  }
2270 
2271  // Do not allow the user to post an empty comment
2272  if ( $this->textbox1 == '' ) {
2273  $this->missingComment = true;
2274  $status->fatal( 'missingcommenttext' );
2275  $status->value = self::AS_TEXTBOX_EMPTY;
2276  return $status;
2277  }
2278  } elseif ( !$this->allowBlankSummary
2279  && !$content->equals( $this->getOriginalContent( $user ) )
2280  && !$content->isRedirect()
2281  && md5( $this->summary ) == $this->autoSumm
2282  ) {
2283  $this->missingSummary = true;
2284  $status->fatal( 'missingsummary' );
2285  $status->value = self::AS_SUMMARY_NEEDED;
2286  return $status;
2287  }
2288 
2289  # All's well
2290  $sectionanchor = '';
2291  if ( $this->section == 'new' ) {
2292  $this->summary = $this->newSectionSummary( $sectionanchor );
2293  } elseif ( $this->section != '' ) {
2294  # Try to get a section anchor from the section source, redirect
2295  # to edited section if header found.
2296  # XXX: Might be better to integrate this into Article::replaceSectionAtRev
2297  # for duplicate heading checking and maybe parsing.
2298  $hasmatch = preg_match( "/^ *([=]{1,6})(.*?)(\\1) *\\n/i", $this->textbox1, $matches );
2299  # We can't deal with anchors, includes, html etc in the header for now,
2300  # headline would need to be parsed to improve this.
2301  if ( $hasmatch && strlen( $matches[2] ) > 0 ) {
2302  $sectionanchor = $this->guessSectionName( $matches[2] );
2303  }
2304  }
2305  $result['sectionanchor'] = $sectionanchor;
2306 
2307  // Save errors may fall down to the edit form, but we've now
2308  // merged the section into full text. Clear the section field
2309  // so that later submission of conflict forms won't try to
2310  // replace that into a duplicated mess.
2311  $this->textbox1 = $this->toEditText( $content );
2312  $this->section = '';
2313 
2314  $status->value = self::AS_SUCCESS_UPDATE;
2315  }
2316 
2317  if ( !$this->allowSelfRedirect
2318  && $content->isRedirect()
2319  && $content->getRedirectTarget()->equals( $this->getTitle() )
2320  ) {
2321  // If the page already redirects to itself, don't warn.
2322  $currentTarget = $this->getCurrentContent()->getRedirectTarget();
2323  if ( !$currentTarget || !$currentTarget->equals( $this->getTitle() ) ) {
2324  $this->selfRedirect = true;
2325  $status->fatal( 'selfredirect' );
2326  $status->value = self::AS_SELF_REDIRECT;
2327  return $status;
2328  }
2329  }
2330 
2331  // Check for length errors again now that the section is merged in
2332  $this->contentLength = strlen( $this->toEditText( $content ) );
2333  if ( $this->contentLength > $maxArticleSize * 1024 ) {
2334  $this->tooBig = true;
2335  $status->setResult( false, self::AS_MAX_ARTICLE_SIZE_EXCEEDED );
2336  return $status;
2337  }
2338 
2339  $flags = EDIT_AUTOSUMMARY |
2340  ( $new ? EDIT_NEW : EDIT_UPDATE ) |
2341  ( ( $this->minoredit && !$this->isNew ) ? EDIT_MINOR : 0 ) |
2342  ( $bot ? EDIT_FORCE_BOT : 0 );
2343 
2344  $doEditStatus = $this->page->doEditContent(
2345  $content,
2346  $this->summary,
2347  $flags,
2348  false,
2349  $user,
2350  $content->getDefaultFormat(),
2353  );
2354 
2355  if ( !$doEditStatus->isOK() ) {
2356  // Failure from doEdit()
2357  // Show the edit conflict page for certain recognized errors from doEdit(),
2358  // but don't show it for errors from extension hooks
2359  $errors = $doEditStatus->getErrorsArray();
2360  if ( in_array( $errors[0][0],
2361  [ 'edit-gone-missing', 'edit-conflict', 'edit-already-exists' ] )
2362  ) {
2363  $this->isConflict = true;
2364  // Destroys data doEdit() put in $status->value but who cares
2365  $doEditStatus->value = self::AS_END;
2366  }
2367  return $doEditStatus;
2368  }
2369 
2370  $result['nullEdit'] = $doEditStatus->hasMessage( 'edit-no-change' );
2371  if ( $result['nullEdit'] ) {
2372  // We don't know if it was a null edit until now, so increment here
2373  $user->pingLimiter( 'linkpurge' );
2374  }
2375  $result['redirect'] = $content->isRedirect();
2376 
2377  $this->updateWatchlist();
2378 
2379  // If the content model changed, add a log entry
2380  if ( $changingContentModel ) {
2382  $user,
2383  $new ? false : $oldContentModel,
2384  $this->contentModel,
2385  $this->summary
2386  );
2387  }
2388 
2389  return $status;
2390  }
2391 
2398  protected function addContentModelChangeLogEntry( User $user, $oldModel, $newModel, $reason ) {
2399  $new = $oldModel === false;
2400  $log = new ManualLogEntry( 'contentmodel', $new ? 'new' : 'change' );
2401  $log->setPerformer( $user );
2402  $log->setTarget( $this->mTitle );
2403  $log->setComment( $reason );
2404  $log->setParameters( [
2405  '4::oldmodel' => $oldModel,
2406  '5::newmodel' => $newModel
2407  ] );
2408  $logid = $log->insert();
2409  $log->publish( $logid );
2410  }
2411 
2415  protected function updateWatchlist() {
2416  $user = $this->context->getUser();
2417  if ( !$user->isLoggedIn() ) {
2418  return;
2419  }
2420 
2422  $watch = $this->watchthis;
2423  // Do this in its own transaction to reduce contention...
2424  DeferredUpdates::addCallableUpdate( function () use ( $user, $title, $watch ) {
2425  if ( $watch == $user->isWatched( $title, User::IGNORE_USER_RIGHTS ) ) {
2426  return; // nothing to change
2427  }
2428  WatchAction::doWatchOrUnwatch( $watch, $title, $user );
2429  } );
2430  }
2431 
2443  private function mergeChangesIntoContent( &$editContent ) {
2444  $db = wfGetDB( DB_MASTER );
2445 
2446  // This is the revision that was current at the time editing was initiated on the client,
2447  // even if the edit was based on an old revision.
2448  $baseRevision = $this->getBaseRevision();
2449  $baseContent = $baseRevision ? $baseRevision->getContent() : null;
2450 
2451  if ( is_null( $baseContent ) ) {
2452  return false;
2453  }
2454 
2455  // The current state, we want to merge updates into it
2456  $currentRevision = Revision::loadFromTitle( $db, $this->mTitle );
2457  $currentContent = $currentRevision ? $currentRevision->getContent() : null;
2458 
2459  if ( is_null( $currentContent ) ) {
2460  return false;
2461  }
2462 
2463  $handler = ContentHandler::getForModelID( $baseContent->getModel() );
2464 
2465  $result = $handler->merge3( $baseContent, $editContent, $currentContent );
2466 
2467  if ( $result ) {
2468  $editContent = $result;
2469  // Update parentRevId to what we just merged.
2470  $this->parentRevId = $currentRevision->getId();
2471  return true;
2472  }
2473 
2474  return false;
2475  }
2476 
2489  public function getBaseRevision() {
2490  if ( !$this->mBaseRevision ) {
2491  $db = wfGetDB( DB_MASTER );
2492  $this->mBaseRevision = $this->editRevId
2493  ? Revision::newFromId( $this->editRevId, Revision::READ_LATEST )
2494  : Revision::loadFromTimestamp( $db, $this->mTitle, $this->edittime );
2495  }
2496  return $this->mBaseRevision;
2497  }
2498 
2506  public static function matchSpamRegex( $text ) {
2507  global $wgSpamRegex;
2508  // For back compatibility, $wgSpamRegex may be a single string or an array of regexes.
2509  $regexes = (array)$wgSpamRegex;
2510  return self::matchSpamRegexInternal( $text, $regexes );
2511  }
2512 
2520  public static function matchSummarySpamRegex( $text ) {
2521  global $wgSummarySpamRegex;
2522  $regexes = (array)$wgSummarySpamRegex;
2523  return self::matchSpamRegexInternal( $text, $regexes );
2524  }
2525 
2531  protected static function matchSpamRegexInternal( $text, $regexes ) {
2532  foreach ( $regexes as $regex ) {
2533  $matches = [];
2534  if ( preg_match( $regex, $text, $matches ) ) {
2535  return $matches[0];
2536  }
2537  }
2538  return false;
2539  }
2540 
2541  public function setHeaders() {
2542  $out = $this->context->getOutput();
2543 
2544  $out->addModules( 'mediawiki.action.edit' );
2545  $out->addModuleStyles( 'mediawiki.action.edit.styles' );
2546  $out->addModuleStyles( 'mediawiki.editfont.styles' );
2547 
2548  $user = $this->context->getUser();
2549 
2550  if ( $user->getOption( 'uselivepreview' ) ) {
2551  $out->addModules( 'mediawiki.action.edit.preview' );
2552  }
2553 
2554  if ( $user->getOption( 'useeditwarning' ) ) {
2555  $out->addModules( 'mediawiki.action.edit.editWarning' );
2556  }
2557 
2558  # Enabled article-related sidebar, toplinks, etc.
2559  $out->setArticleRelated( true );
2560 
2561  $contextTitle = $this->getContextTitle();
2562  if ( $this->isConflict ) {
2563  $msg = 'editconflict';
2564  } elseif ( $contextTitle->exists() && $this->section != '' ) {
2565  $msg = $this->section == 'new' ? 'editingcomment' : 'editingsection';
2566  } else {
2567  $msg = $contextTitle->exists()
2568  || ( $contextTitle->getNamespace() == NS_MEDIAWIKI
2569  && $contextTitle->getDefaultMessageText() !== false
2570  )
2571  ? 'editing'
2572  : 'creating';
2573  }
2574 
2575  # Use the title defined by DISPLAYTITLE magic word when present
2576  # NOTE: getDisplayTitle() returns HTML while getPrefixedText() returns plain text.
2577  # setPageTitle() treats the input as wikitext, which should be safe in either case.
2578  $displayTitle = isset( $this->mParserOutput ) ? $this->mParserOutput->getDisplayTitle() : false;
2579  if ( $displayTitle === false ) {
2580  $displayTitle = $contextTitle->getPrefixedText();
2581  } else {
2582  $out->setDisplayTitle( $displayTitle );
2583  }
2584  $out->setPageTitle( $this->context->msg( $msg, $displayTitle ) );
2585 
2586  $config = $this->context->getConfig();
2587 
2588  # Transmit the name of the message to JavaScript for live preview
2589  # Keep Resources.php/mediawiki.action.edit.preview in sync with the possible keys
2590  $out->addJsConfigVars( [
2591  'wgEditMessage' => $msg,
2592  'wgAjaxEditStash' => $config->get( 'AjaxEditStash' ),
2593  ] );
2594 
2595  // Add whether to use 'save' or 'publish' messages to JavaScript for post-edit, other
2596  // editors, etc.
2597  $out->addJsConfigVars(
2598  'wgEditSubmitButtonLabelPublish',
2599  $config->get( 'EditSubmitButtonLabelPublish' )
2600  );
2601  }
2602 
2606  protected function showIntro() {
2607  if ( $this->suppressIntro ) {
2608  return;
2609  }
2610 
2611  $out = $this->context->getOutput();
2612  $namespace = $this->mTitle->getNamespace();
2613 
2614  if ( $namespace == NS_MEDIAWIKI ) {
2615  # Show a warning if editing an interface message
2616  $out->wrapWikiMsg( "<div class='mw-editinginterface'>\n$1\n</div>", 'editinginterface' );
2617  # If this is a default message (but not css, json, or js),
2618  # show a hint that it is translatable on translatewiki.net
2619  if (
2620  !$this->mTitle->hasContentModel( CONTENT_MODEL_CSS )
2621  && !$this->mTitle->hasContentModel( CONTENT_MODEL_JSON )
2622  && !$this->mTitle->hasContentModel( CONTENT_MODEL_JAVASCRIPT )
2623  ) {
2624  $defaultMessageText = $this->mTitle->getDefaultMessageText();
2625  if ( $defaultMessageText !== false ) {
2626  $out->wrapWikiMsg( "<div class='mw-translateinterface'>\n$1\n</div>",
2627  'translateinterface' );
2628  }
2629  }
2630  } elseif ( $namespace == NS_FILE ) {
2631  # Show a hint to shared repo
2632  $file = MediaWikiServices::getInstance()->getRepoGroup()->findFile( $this->mTitle );
2633  if ( $file && !$file->isLocal() ) {
2634  $descUrl = $file->getDescriptionUrl();
2635  # there must be a description url to show a hint to shared repo
2636  if ( $descUrl ) {
2637  if ( !$this->mTitle->exists() ) {
2638  $out->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-create\">\n$1\n</div>", [
2639  'sharedupload-desc-create', $file->getRepo()->getDisplayName(), $descUrl
2640  ] );
2641  } else {
2642  $out->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-edit\">\n$1\n</div>", [
2643  'sharedupload-desc-edit', $file->getRepo()->getDisplayName(), $descUrl
2644  ] );
2645  }
2646  }
2647  }
2648  }
2649 
2650  # Show a warning message when someone creates/edits a user (talk) page but the user does not exist
2651  # Show log extract when the user is currently blocked
2652  if ( $namespace == NS_USER || $namespace == NS_USER_TALK ) {
2653  $username = explode( '/', $this->mTitle->getText(), 2 )[0];
2654  $user = User::newFromName( $username, false /* allow IP users */ );
2655  $ip = User::isIP( $username );
2656  $block = DatabaseBlock::newFromTarget( $user, $user );
2657  if ( !( $user && $user->isLoggedIn() ) && !$ip ) { # User does not exist
2658  $out->wrapWikiMsg( "<div class=\"mw-userpage-userdoesnotexist error\">\n$1\n</div>",
2659  [ 'userpage-userdoesnotexist', wfEscapeWikiText( $username ) ] );
2660  } elseif (
2661  !is_null( $block ) &&
2662  $block->getType() != DatabaseBlock::TYPE_AUTO &&
2663  ( $block->isSitewide() || $user->isBlockedFrom( $this->mTitle ) )
2664  ) {
2665  // Show log extract if the user is sitewide blocked or is partially
2666  // blocked and not allowed to edit their user page or user talk page
2668  $out,
2669  'block',
2670  MediaWikiServices::getInstance()->getNamespaceInfo()->
2671  getCanonicalName( NS_USER ) . ':' . $block->getTarget(),
2672  '',
2673  [
2674  'lim' => 1,
2675  'showIfEmpty' => false,
2676  'msgKey' => [
2677  'blocked-notice-logextract',
2678  $user->getName() # Support GENDER in notice
2679  ]
2680  ]
2681  );
2682  }
2683  }
2684  # Try to add a custom edit intro, or use the standard one if this is not possible.
2685  if ( !$this->showCustomIntro() && !$this->mTitle->exists() ) {
2687  $this->context->msg( 'helppage' )->inContentLanguage()->text()
2688  ) );
2689  if ( $this->context->getUser()->isLoggedIn() ) {
2690  $out->wrapWikiMsg(
2691  // Suppress the external link icon, consider the help url an internal one
2692  "<div class=\"mw-newarticletext plainlinks\">\n$1\n</div>",
2693  [
2694  'newarticletext',
2695  $helpLink
2696  ]
2697  );
2698  } else {
2699  $out->wrapWikiMsg(
2700  // Suppress the external link icon, consider the help url an internal one
2701  "<div class=\"mw-newarticletextanon plainlinks\">\n$1\n</div>",
2702  [
2703  'newarticletextanon',
2704  $helpLink
2705  ]
2706  );
2707  }
2708  }
2709  # Give a notice if the user is editing a deleted/moved page...
2710  if ( !$this->mTitle->exists() ) {
2711  $dbr = wfGetDB( DB_REPLICA );
2712 
2713  LogEventsList::showLogExtract( $out, [ 'delete', 'move' ], $this->mTitle,
2714  '',
2715  [
2716  'lim' => 10,
2717  'conds' => [ 'log_action != ' . $dbr->addQuotes( 'revision' ) ],
2718  'showIfEmpty' => false,
2719  'msgKey' => [ 'recreate-moveddeleted-warn' ]
2720  ]
2721  );
2722  }
2723  }
2724 
2730  protected function showCustomIntro() {
2731  if ( $this->editintro ) {
2732  $title = Title::newFromText( $this->editintro );
2733  if ( $this->isPageExistingAndViewable( $title, $this->context->getUser() ) ) {
2734  // Added using template syntax, to take <noinclude>'s into account.
2735  $this->context->getOutput()->addWikiTextAsContent(
2736  '<div class="mw-editintro">{{:' . $title->getFullText() . '}}</div>',
2737  /*linestart*/true,
2739  );
2740  return true;
2741  }
2742  }
2743  return false;
2744  }
2745 
2764  protected function toEditText( $content ) {
2765  if ( $content === null || $content === false || is_string( $content ) ) {
2766  return $content;
2767  }
2768 
2769  if ( !$this->isSupportedContentModel( $content->getModel() ) ) {
2770  throw new MWException( 'This content model is not supported: ' . $content->getModel() );
2771  }
2772 
2773  return $content->serialize( $this->contentFormat );
2774  }
2775 
2792  protected function toEditContent( $text ) {
2793  if ( $text === false || $text === null ) {
2794  return $text;
2795  }
2796 
2797  $content = ContentHandler::makeContent( $text, $this->getTitle(),
2798  $this->contentModel, $this->contentFormat );
2799 
2800  if ( !$this->isSupportedContentModel( $content->getModel() ) ) {
2801  throw new MWException( 'This content model is not supported: ' . $content->getModel() );
2802  }
2803 
2804  return $content;
2805  }
2806 
2815  public function showEditForm( $formCallback = null ) {
2816  # need to parse the preview early so that we know which templates are used,
2817  # otherwise users with "show preview after edit box" will get a blank list
2818  # we parse this near the beginning so that setHeaders can do the title
2819  # setting work instead of leaving it in getPreviewText
2820  $previewOutput = '';
2821  if ( $this->formtype == 'preview' ) {
2822  $previewOutput = $this->getPreviewText();
2823  }
2824 
2825  $out = $this->context->getOutput();
2826 
2827  // Avoid PHP 7.1 warning of passing $this by reference
2828  $editPage = $this;
2829  Hooks::run( 'EditPage::showEditForm:initial', [ &$editPage, &$out ] );
2830 
2831  $this->setHeaders();
2832 
2833  $this->addTalkPageText();
2834  $this->addEditNotices();
2835 
2836  if ( !$this->isConflict &&
2837  $this->section != '' &&
2838  !$this->isSectionEditSupported() ) {
2839  // We use $this->section to much before this and getVal('wgSection') directly in other places
2840  // at this point we can't reset $this->section to '' to fallback to non-section editing.
2841  // Someone is welcome to try refactoring though
2842  $out->showErrorPage( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
2843  return;
2844  }
2845 
2846  $this->showHeader();
2847 
2848  $out->addHTML( $this->editFormPageTop );
2849 
2850  $user = $this->context->getUser();
2851  if ( $user->getOption( 'previewontop' ) ) {
2852  $this->displayPreviewArea( $previewOutput, true );
2853  }
2854 
2855  $out->addHTML( $this->editFormTextTop );
2856 
2857  if ( $this->wasDeletedSinceLastEdit() && $this->formtype !== 'save' ) {
2858  $out->wrapWikiMsg( "<div class='error mw-deleted-while-editing'>\n$1\n</div>",
2859  'deletedwhileediting' );
2860  }
2861 
2862  // @todo add EditForm plugin interface and use it here!
2863  // search for textarea1 and textarea2, and allow EditForm to override all uses.
2864  $out->addHTML( Html::openElement(
2865  'form',
2866  [
2867  'class' => 'mw-editform',
2868  'id' => self::EDITFORM_ID,
2869  'name' => self::EDITFORM_ID,
2870  'method' => 'post',
2871  'action' => $this->getActionURL( $this->getContextTitle() ),
2872  'enctype' => 'multipart/form-data'
2873  ]
2874  ) );
2875 
2876  if ( is_callable( $formCallback ) ) {
2877  wfWarn( 'The $formCallback parameter to ' . __METHOD__ . 'is deprecated' );
2878  call_user_func_array( $formCallback, [ &$out ] );
2879  }
2880 
2881  // Add a check for Unicode support
2882  $out->addHTML( Html::hidden( 'wpUnicodeCheck', self::UNICODE_CHECK ) );
2883 
2884  // Add an empty field to trip up spambots
2885  $out->addHTML(
2886  Xml::openElement( 'div', [ 'id' => 'antispam-container', 'style' => 'display: none;' ] )
2887  . Html::rawElement(
2888  'label',
2889  [ 'for' => 'wpAntispam' ],
2890  $this->context->msg( 'simpleantispam-label' )->parse()
2891  )
2892  . Xml::element(
2893  'input',
2894  [
2895  'type' => 'text',
2896  'name' => 'wpAntispam',
2897  'id' => 'wpAntispam',
2898  'value' => ''
2899  ]
2900  )
2901  . Xml::closeElement( 'div' )
2902  );
2903 
2904  // Avoid PHP 7.1 warning of passing $this by reference
2905  $editPage = $this;
2906  Hooks::run( 'EditPage::showEditForm:fields', [ &$editPage, &$out ] );
2907 
2908  // Put these up at the top to ensure they aren't lost on early form submission
2909  $this->showFormBeforeText();
2910 
2911  if ( $this->wasDeletedSinceLastEdit() && $this->formtype == 'save' ) {
2912  $username = $this->lastDelete->user_name;
2913  $comment = CommentStore::getStore()
2914  ->getComment( 'log_comment', $this->lastDelete )->text;
2915 
2916  // It is better to not parse the comment at all than to have templates expanded in the middle
2917  // TODO: can the checkLabel be moved outside of the div so that wrapWikiMsg could be used?
2918  $key = $comment === ''
2919  ? 'confirmrecreate-noreason'
2920  : 'confirmrecreate';
2921  $out->addHTML(
2922  '<div class="mw-confirm-recreate">' .
2923  $this->context->msg( $key, $username, "<nowiki>$comment</nowiki>" )->parse() .
2924  Xml::checkLabel( $this->context->msg( 'recreate' )->text(), 'wpRecreate', 'wpRecreate', false,
2925  [ 'title' => Linker::titleAttrib( 'recreate' ), 'tabindex' => 1, 'id' => 'wpRecreate' ]
2926  ) .
2927  '</div>'
2928  );
2929  }
2930 
2931  # When the summary is hidden, also hide them on preview/show changes
2932  if ( $this->nosummary ) {
2933  $out->addHTML( Html::hidden( 'nosummary', true ) );
2934  }
2935 
2936  # If a blank edit summary was previously provided, and the appropriate
2937  # user preference is active, pass a hidden tag as wpIgnoreBlankSummary. This will stop the
2938  # user being bounced back more than once in the event that a summary
2939  # is not required.
2940  # ####
2941  # For a bit more sophisticated detection of blank summaries, hash the
2942  # automatic one and pass that in the hidden field wpAutoSummary.
2943  if ( $this->missingSummary || ( $this->section == 'new' && $this->nosummary ) ) {
2944  $out->addHTML( Html::hidden( 'wpIgnoreBlankSummary', true ) );
2945  }
2946 
2947  if ( $this->undidRev ) {
2948  $out->addHTML( Html::hidden( 'wpUndidRevision', $this->undidRev ) );
2949  }
2950 
2951  if ( $this->selfRedirect ) {
2952  $out->addHTML( Html::hidden( 'wpIgnoreSelfRedirect', true ) );
2953  }
2954 
2955  if ( $this->hasPresetSummary ) {
2956  // If a summary has been preset using &summary= we don't want to prompt for
2957  // a different summary. Only prompt for a summary if the summary is blanked.
2958  // (T19416)
2959  $this->autoSumm = md5( '' );
2960  }
2961 
2962  $autosumm = $this->autoSumm !== '' ? $this->autoSumm : md5( $this->summary );
2963  $out->addHTML( Html::hidden( 'wpAutoSummary', $autosumm ) );
2964 
2965  $out->addHTML( Html::hidden( 'oldid', $this->oldid ) );
2966  $out->addHTML( Html::hidden( 'parentRevId', $this->getParentRevId() ) );
2967 
2968  $out->addHTML( Html::hidden( 'format', $this->contentFormat ) );
2969  $out->addHTML( Html::hidden( 'model', $this->contentModel ) );
2970 
2971  $out->enableOOUI();
2972 
2973  if ( $this->section == 'new' ) {
2974  $this->showSummaryInput( true, $this->summary );
2975  $out->addHTML( $this->getSummaryPreview( true, $this->summary ) );
2976  }
2977 
2978  $out->addHTML( $this->editFormTextBeforeContent );
2979  if ( $this->isConflict ) {
2980  // In an edit conflict, we turn textbox2 into the user's text,
2981  // and textbox1 into the stored version
2982  $this->textbox2 = $this->textbox1;
2983 
2984  $content = $this->getCurrentContent();
2985  $this->textbox1 = $this->toEditText( $content );
2986 
2988  $editConflictHelper->setTextboxes( $this->textbox2, $this->textbox1 );
2989  $editConflictHelper->setContentModel( $this->contentModel );
2990  $editConflictHelper->setContentFormat( $this->contentFormat );
2992  }
2993 
2994  if ( !$this->mTitle->isUserConfigPage() ) {
2995  $out->addHTML( self::getEditToolbar() );
2996  }
2997 
2998  if ( $this->blankArticle ) {
2999  $out->addHTML( Html::hidden( 'wpIgnoreBlankArticle', true ) );
3000  }
3001 
3002  if ( $this->isConflict ) {
3003  // In an edit conflict bypass the overridable content form method
3004  // and fallback to the raw wpTextbox1 since editconflicts can't be
3005  // resolved between page source edits and custom ui edits using the
3006  // custom edit ui.
3007  $conflictTextBoxAttribs = [];
3008  if ( $this->wasDeletedSinceLastEdit() ) {
3009  $conflictTextBoxAttribs['style'] = 'display:none;';
3010  } elseif ( $this->isOldRev ) {
3011  $conflictTextBoxAttribs['class'] = 'mw-textarea-oldrev';
3012  }
3013 
3014  $out->addHTML( $editConflictHelper->getEditConflictMainTextBox( $conflictTextBoxAttribs ) );
3016  } else {
3017  $this->showContentForm();
3018  }
3019 
3020  $out->addHTML( $this->editFormTextAfterContent );
3021 
3022  $this->showStandardInputs();
3023 
3024  $this->showFormAfterText();
3025 
3026  $this->showTosSummary();
3027 
3028  $this->showEditTools();
3029 
3030  $out->addHTML( $this->editFormTextAfterTools . "\n" );
3031 
3032  $out->addHTML( $this->makeTemplatesOnThisPageList( $this->getTemplates() ) );
3033 
3034  $out->addHTML( Html::rawElement( 'div', [ 'class' => 'hiddencats' ],
3035  Linker::formatHiddenCategories( $this->page->getHiddenCategories() ) ) );
3036 
3037  $out->addHTML( Html::rawElement( 'div', [ 'class' => 'limitreport' ],
3038  self::getPreviewLimitReport( $this->mParserOutput ) ) );
3039 
3040  $out->addModules( 'mediawiki.action.edit.collapsibleFooter' );
3041 
3042  if ( $this->isConflict ) {
3043  try {
3044  $this->showConflict();
3045  } catch ( MWContentSerializationException $ex ) {
3046  // this can't really happen, but be nice if it does.
3047  $msg = $this->context->msg(
3048  'content-failed-to-parse',
3049  $this->contentModel,
3050  $this->contentFormat,
3051  $ex->getMessage()
3052  );
3053  $out->wrapWikiTextAsInterface( 'error', $msg->plain() );
3054  }
3055  }
3056 
3057  // Set a hidden field so JS knows what edit form mode we are in
3058  if ( $this->isConflict ) {
3059  $mode = 'conflict';
3060  } elseif ( $this->preview ) {
3061  $mode = 'preview';
3062  } elseif ( $this->diff ) {
3063  $mode = 'diff';
3064  } else {
3065  $mode = 'text';
3066  }
3067  $out->addHTML( Html::hidden( 'mode', $mode, [ 'id' => 'mw-edit-mode' ] ) );
3068 
3069  // Marker for detecting truncated form data. This must be the last
3070  // parameter sent in order to be of use, so do not move me.
3071  $out->addHTML( Html::hidden( 'wpUltimateParam', true ) );
3072  $out->addHTML( $this->editFormTextBottom . "\n</form>\n" );
3073 
3074  if ( !$user->getOption( 'previewontop' ) ) {
3075  $this->displayPreviewArea( $previewOutput, false );
3076  }
3077  }
3078 
3086  public function makeTemplatesOnThisPageList( array $templates ) {
3087  $templateListFormatter = new TemplatesOnThisPageFormatter(
3088  $this->context, MediaWikiServices::getInstance()->getLinkRenderer()
3089  );
3090 
3091  // preview if preview, else section if section, else false
3092  $type = false;
3093  if ( $this->preview ) {
3094  $type = 'preview';
3095  } elseif ( $this->section != '' ) {
3096  $type = 'section';
3097  }
3098 
3099  return Html::rawElement( 'div', [ 'class' => 'templatesUsed' ],
3100  $templateListFormatter->format( $templates, $type )
3101  );
3102  }
3103 
3110  public static function extractSectionTitle( $text ) {
3111  preg_match( "/^(=+)(.+)\\1\\s*(\n|$)/i", $text, $matches );
3112  if ( !empty( $matches[2] ) ) {
3113  return MediaWikiServices::getInstance()->getParser()
3114  ->stripSectionName( trim( $matches[2] ) );
3115  } else {
3116  return false;
3117  }
3118  }
3119 
3120  protected function showHeader() {
3121  $out = $this->context->getOutput();
3122  $user = $this->context->getUser();
3123  if ( $this->isConflict ) {
3124  $this->addExplainConflictHeader( $out );
3125  $this->editRevId = $this->page->getLatest();
3126  } else {
3127  if ( $this->section != '' && $this->section != 'new' && !$this->summary &&
3128  !$this->preview && !$this->diff
3129  ) {
3130  $sectionTitle = self::extractSectionTitle( $this->textbox1 ); // FIXME: use Content object
3131  if ( $sectionTitle !== false ) {
3132  $this->summary = "/* $sectionTitle */ ";
3133  }
3134  }
3135 
3136  $buttonLabel = $this->context->msg( $this->getSubmitButtonLabel() )->text();
3137 
3138  if ( $this->missingComment ) {
3139  $out->wrapWikiMsg( "<div id='mw-missingcommenttext'>\n$1\n</div>", 'missingcommenttext' );
3140  }
3141 
3142  if ( $this->missingSummary && $this->section != 'new' ) {
3143  $out->wrapWikiMsg(
3144  "<div id='mw-missingsummary'>\n$1\n</div>",
3145  [ 'missingsummary', $buttonLabel ]
3146  );
3147  }
3148 
3149  if ( $this->missingSummary && $this->section == 'new' ) {
3150  $out->wrapWikiMsg(
3151  "<div id='mw-missingcommentheader'>\n$1\n</div>",
3152  [ 'missingcommentheader', $buttonLabel ]
3153  );
3154  }
3155 
3156  if ( $this->blankArticle ) {
3157  $out->wrapWikiMsg(
3158  "<div id='mw-blankarticle'>\n$1\n</div>",
3159  [ 'blankarticle', $buttonLabel ]
3160  );
3161  }
3162 
3163  if ( $this->selfRedirect ) {
3164  $out->wrapWikiMsg(
3165  "<div id='mw-selfredirect'>\n$1\n</div>",
3166  [ 'selfredirect', $buttonLabel ]
3167  );
3168  }
3169 
3170  if ( $this->hookError !== '' ) {
3171  $out->addWikiTextAsInterface( $this->hookError );
3172  }
3173 
3174  if ( $this->section != 'new' ) {
3175  $revision = $this->mArticle->getRevisionFetched();
3176  if ( $revision ) {
3177  // Let sysop know that this will make private content public if saved
3178 
3179  if ( !$revision->userCan( RevisionRecord::DELETED_TEXT, $user ) ) {
3180  $out->wrapWikiMsg(
3181  "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
3182  'rev-deleted-text-permission'
3183  );
3184  } elseif ( $revision->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
3185  $out->wrapWikiMsg(
3186  "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
3187  'rev-deleted-text-view'
3188  );
3189  }
3190 
3191  if ( !$revision->isCurrent() ) {
3192  $this->mArticle->setOldSubtitle( $revision->getId() );
3193  $out->wrapWikiMsg(
3194  Html::warningBox( "\n$1\n" ),
3195  'editingold'
3196  );
3197  $this->isOldRev = true;
3198  }
3199  } elseif ( $this->mTitle->exists() ) {
3200  // Something went wrong
3201 
3202  $out->wrapWikiMsg( "<div class='errorbox'>\n$1\n</div>\n",
3203  [ 'missing-revision', $this->oldid ] );
3204  }
3205  }
3206  }
3207 
3208  if ( wfReadOnly() ) {
3209  $out->wrapWikiMsg(
3210  "<div id=\"mw-read-only-warning\">\n$1\n</div>",
3211  [ 'readonlywarning', wfReadOnlyReason() ]
3212  );
3213  } elseif ( $user->isAnon() ) {
3214  if ( $this->formtype != 'preview' ) {
3215  $returntoquery = array_diff_key(
3216  $this->context->getRequest()->getValues(),
3217  [ 'title' => true, 'returnto' => true, 'returntoquery' => true ]
3218  );
3219  $out->wrapWikiMsg(
3220  "<div id='mw-anon-edit-warning' class='warningbox'>\n$1\n</div>",
3221  [ 'anoneditwarning',
3222  // Log-in link
3223  SpecialPage::getTitleFor( 'Userlogin' )->getFullURL( [
3224  'returnto' => $this->getTitle()->getPrefixedDBkey(),
3225  'returntoquery' => wfArrayToCgi( $returntoquery ),
3226  ] ),
3227  // Sign-up link
3228  SpecialPage::getTitleFor( 'CreateAccount' )->getFullURL( [
3229  'returnto' => $this->getTitle()->getPrefixedDBkey(),
3230  'returntoquery' => wfArrayToCgi( $returntoquery ),
3231  ] )
3232  ]
3233  );
3234  } else {
3235  $out->wrapWikiMsg( "<div id=\"mw-anon-preview-warning\" class=\"warningbox\">\n$1</div>",
3236  'anonpreviewwarning'
3237  );
3238  }
3239  } elseif ( $this->mTitle->isUserConfigPage() ) {
3240  # Check the skin exists
3241  if ( $this->isWrongCaseUserConfigPage() ) {
3242  $out->wrapWikiMsg(
3243  "<div class='error' id='mw-userinvalidconfigtitle'>\n$1\n</div>",
3244  [ 'userinvalidconfigtitle', $this->mTitle->getSkinFromConfigSubpage() ]
3245  );
3246  }
3247  if ( $this->getTitle()->isSubpageOf( $user->getUserPage() ) ) {
3248  $isUserCssConfig = $this->mTitle->isUserCssConfigPage();
3249  $isUserJsonConfig = $this->mTitle->isUserJsonConfigPage();
3250  $isUserJsConfig = $this->mTitle->isUserJsConfigPage();
3251 
3252  $warning = $isUserCssConfig
3253  ? 'usercssispublic'
3254  : ( $isUserJsonConfig ? 'userjsonispublic' : 'userjsispublic' );
3255 
3256  $out->wrapWikiMsg( '<div class="mw-userconfigpublic">$1</div>', $warning );
3257 
3258  if ( $this->formtype !== 'preview' ) {
3259  $config = $this->context->getConfig();
3260  if ( $isUserCssConfig && $config->get( 'AllowUserCss' ) ) {
3261  $out->wrapWikiMsg(
3262  "<div id='mw-usercssyoucanpreview'>\n$1\n</div>",
3263  [ 'usercssyoucanpreview' ]
3264  );
3265  } elseif ( $isUserJsonConfig /* No comparable 'AllowUserJson' */ ) {
3266  $out->wrapWikiMsg(
3267  "<div id='mw-userjsonyoucanpreview'>\n$1\n</div>",
3268  [ 'userjsonyoucanpreview' ]
3269  );
3270  } elseif ( $isUserJsConfig && $config->get( 'AllowUserJs' ) ) {
3271  $out->wrapWikiMsg(
3272  "<div id='mw-userjsyoucanpreview'>\n$1\n</div>",
3273  [ 'userjsyoucanpreview' ]
3274  );
3275  }
3276  }
3277  }
3278  }
3279 
3281 
3282  $this->addLongPageWarningHeader();
3283 
3284  # Add header copyright warning
3285  $this->showHeaderCopyrightWarning();
3286  }
3287 
3295  private function getSummaryInputAttributes( array $inputAttrs = null ) {
3296  // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
3297  // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
3298  // Unicode codepoints.
3299  return ( is_array( $inputAttrs ) ? $inputAttrs : [] ) + [
3300  'id' => 'wpSummary',
3301  'name' => 'wpSummary',
3303  'tabindex' => 1,
3304  'size' => 60,
3305  'spellcheck' => 'true',
3306  ];
3307  }
3308 
3318  function getSummaryInputWidget( $summary = "", $labelText = null, $inputAttrs = null ) {
3319  $inputAttrs = OOUI\Element::configFromHtmlAttributes(
3320  $this->getSummaryInputAttributes( $inputAttrs )
3321  );
3322  $inputAttrs += [
3323  'title' => Linker::titleAttrib( 'summary' ),
3324  'accessKey' => Linker::accesskey( 'summary' ),
3325  ];
3326 
3327  // For compatibility with old scripts and extensions, we want the legacy 'id' on the `<input>`
3328  $inputAttrs['inputId'] = $inputAttrs['id'];
3329  $inputAttrs['id'] = 'wpSummaryWidget';
3330 
3331  return new OOUI\FieldLayout(
3332  new OOUI\TextInputWidget( [
3333  'value' => $summary,
3334  'infusable' => true,
3335  ] + $inputAttrs ),
3336  [
3337  'label' => new OOUI\HtmlSnippet( $labelText ),
3338  'align' => 'top',
3339  'id' => 'wpSummaryLabel',
3340  'classes' => [ $this->missingSummary ? 'mw-summarymissed' : 'mw-summary' ],
3341  ]
3342  );
3343  }
3344 
3351  protected function showSummaryInput( $isSubjectPreview, $summary = "" ) {
3352  # Add a class if 'missingsummary' is triggered to allow styling of the summary line
3353  $summaryClass = $this->missingSummary ? 'mw-summarymissed' : 'mw-summary';
3354  if ( $isSubjectPreview ) {
3355  if ( $this->nosummary ) {
3356  return;
3357  }
3358  } elseif ( !$this->mShowSummaryField ) {
3359  return;
3360  }
3361 
3362  $labelText = $this->context->msg( $isSubjectPreview ? 'subject' : 'summary' )->parse();
3363  $this->context->getOutput()->addHTML( $this->getSummaryInputWidget(
3364  $summary,
3365  $labelText,
3366  [ 'class' => $summaryClass ]
3367  ) );
3368  }
3369 
3377  protected function getSummaryPreview( $isSubjectPreview, $summary = "" ) {
3378  // avoid spaces in preview, gets always trimmed on save
3379  $summary = trim( $summary );
3380  if ( !$summary || ( !$this->preview && !$this->diff ) ) {
3381  return "";
3382  }
3383 
3384  if ( $isSubjectPreview ) {
3385  $summary = $this->context->msg( 'newsectionsummary' )
3386  ->rawParams( MediaWikiServices::getInstance()->getParser()
3387  ->stripSectionName( $summary ) )
3388  ->inContentLanguage()->text();
3389  }
3390 
3391  $message = $isSubjectPreview ? 'subject-preview' : 'summary-preview';
3392 
3393  $summary = $this->context->msg( $message )->parse()
3394  . Linker::commentBlock( $summary, $this->mTitle, $isSubjectPreview );
3395  return Xml::tags( 'div', [ 'class' => 'mw-summary-preview' ], $summary );
3396  }
3397 
3398  protected function showFormBeforeText() {
3399  $out = $this->context->getOutput();
3400  $out->addHTML( Html::hidden( 'wpSection', $this->section ) );
3401  $out->addHTML( Html::hidden( 'wpStarttime', $this->starttime ) );
3402  $out->addHTML( Html::hidden( 'wpEdittime', $this->edittime ) );
3403  $out->addHTML( Html::hidden( 'editRevId', $this->editRevId ) );
3404  $out->addHTML( Html::hidden( 'wpScrolltop', $this->scrolltop, [ 'id' => 'wpScrolltop' ] ) );
3405  }
3406 
3407  protected function showFormAfterText() {
3420  $this->context->getOutput()->addHTML(
3421  "\n" .
3422  Html::hidden( "wpEditToken", $this->context->getUser()->getEditToken() ) .
3423  "\n"
3424  );
3425  }
3426 
3435  protected function showContentForm() {
3436  $this->showTextbox1();
3437  }
3438 
3447  protected function showTextbox1( $customAttribs = null, $textoverride = null ) {
3448  if ( $this->wasDeletedSinceLastEdit() && $this->formtype == 'save' ) {
3449  $attribs = [ 'style' => 'display:none;' ];
3450  } else {
3451  $builder = new TextboxBuilder();
3452  $classes = $builder->getTextboxProtectionCSSClasses( $this->getTitle() );
3453 
3454  # Is an old revision being edited?
3455  if ( $this->isOldRev ) {
3456  $classes[] = 'mw-textarea-oldrev';
3457  }
3458 
3459  $attribs = [ 'tabindex' => 1 ];
3460 
3461  if ( is_array( $customAttribs ) ) {
3462  $attribs += $customAttribs;
3463  }
3464 
3465  $attribs = $builder->mergeClassesIntoAttributes( $classes, $attribs );
3466  }
3467 
3468  $this->showTextbox(
3469  $textoverride ?? $this->textbox1,
3470  'wpTextbox1',
3471  $attribs
3472  );
3473  }
3474 
3475  protected function showTextbox2() {
3476  $this->showTextbox( $this->textbox2, 'wpTextbox2', [ 'tabindex' => 6, 'readonly' ] );
3477  }
3478 
3479  protected function showTextbox( $text, $name, $customAttribs = [] ) {
3480  $builder = new TextboxBuilder();
3481  $attribs = $builder->buildTextboxAttribs(
3482  $name,
3483  $customAttribs,
3484  $this->context->getUser(),
3486  );
3487 
3488  $this->context->getOutput()->addHTML(
3489  Html::textarea( $name, $builder->addNewLineAtEnd( $text ), $attribs )
3490  );
3491  }
3492 
3493  protected function displayPreviewArea( $previewOutput, $isOnTop = false ) {
3494  $classes = [];
3495  if ( $isOnTop ) {
3496  $classes[] = 'ontop';
3497  }
3498 
3499  $attribs = [ 'id' => 'wikiPreview', 'class' => implode( ' ', $classes ) ];
3500 
3501  if ( $this->formtype != 'preview' ) {
3502  $attribs['style'] = 'display: none;';
3503  }
3504 
3505  $out = $this->context->getOutput();
3506  $out->addHTML( Xml::openElement( 'div', $attribs ) );
3507 
3508  if ( $this->formtype == 'preview' ) {
3509  $this->showPreview( $previewOutput );
3510  } else {
3511  // Empty content container for LivePreview
3512  $pageViewLang = $this->mTitle->getPageViewLanguage();
3513  $attribs = [ 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(),
3514  'class' => 'mw-content-' . $pageViewLang->getDir() ];
3515  $out->addHTML( Html::rawElement( 'div', $attribs ) );
3516  }
3517 
3518  $out->addHTML( '</div>' );
3519 
3520  if ( $this->formtype == 'diff' ) {
3521  try {
3522  $this->showDiff();
3523  } catch ( MWContentSerializationException $ex ) {
3524  $msg = $this->context->msg(
3525  'content-failed-to-parse',
3526  $this->contentModel,
3527  $this->contentFormat,
3528  $ex->getMessage()
3529  );
3530  $out->wrapWikiTextAsInterface( 'error', $msg->plain() );
3531  }
3532  }
3533  }
3534 
3541  protected function showPreview( $text ) {
3542  if ( $this->mArticle instanceof CategoryPage ) {
3543  $this->mArticle->openShowCategory();
3544  }
3545  # This hook seems slightly odd here, but makes things more
3546  # consistent for extensions.
3547  $out = $this->context->getOutput();
3548  Hooks::run( 'OutputPageBeforeHTML', [ &$out, &$text ] );
3549  $out->addHTML( $text );
3550  if ( $this->mArticle instanceof CategoryPage ) {
3551  $this->mArticle->closeShowCategory();
3552  }
3553  }
3554 
3562  public function showDiff() {
3563  $oldtitlemsg = 'currentrev';
3564  # if message does not exist, show diff against the preloaded default
3565  if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && !$this->mTitle->exists() ) {
3566  $oldtext = $this->mTitle->getDefaultMessageText();
3567  if ( $oldtext !== false ) {
3568  $oldtitlemsg = 'defaultmessagetext';
3569  $oldContent = $this->toEditContent( $oldtext );
3570  } else {
3571  $oldContent = null;
3572  }
3573  } else {
3574  $oldContent = $this->getCurrentContent();
3575  }
3576 
3577  $textboxContent = $this->toEditContent( $this->textbox1 );
3578  if ( $this->editRevId !== null ) {
3579  $newContent = $this->page->replaceSectionAtRev(
3580  $this->section, $textboxContent, $this->summary, $this->editRevId
3581  );
3582  } else {
3583  $newContent = $this->page->replaceSectionContent(
3584  $this->section, $textboxContent, $this->summary, $this->edittime
3585  );
3586  }
3587 
3588  if ( $newContent ) {
3589  Hooks::run( 'EditPageGetDiffContent', [ $this, &$newContent ] );
3590 
3591  $user = $this->context->getUser();
3592  $popts = ParserOptions::newFromUserAndLang( $user,
3593  MediaWikiServices::getInstance()->getContentLanguage() );
3594  $newContent = $newContent->preSaveTransform( $this->mTitle, $user, $popts );
3595  }
3596 
3597  if ( ( $oldContent && !$oldContent->isEmpty() ) || ( $newContent && !$newContent->isEmpty() ) ) {
3598  $oldtitle = $this->context->msg( $oldtitlemsg )->parse();
3599  $newtitle = $this->context->msg( 'yourtext' )->parse();
3600 
3601  if ( !$oldContent ) {
3602  $oldContent = $newContent->getContentHandler()->makeEmptyContent();
3603  }
3604 
3605  if ( !$newContent ) {
3606  $newContent = $oldContent->getContentHandler()->makeEmptyContent();
3607  }
3608 
3609  $de = $oldContent->getContentHandler()->createDifferenceEngine( $this->context );
3610  $de->setContent( $oldContent, $newContent );
3611 
3612  $difftext = $de->getDiff( $oldtitle, $newtitle );
3613  $de->showDiffStyle();
3614  } else {
3615  $difftext = '';
3616  }
3617 
3618  $this->context->getOutput()->addHTML( '<div id="wikiDiff">' . $difftext . '</div>' );
3619  }
3620 
3624  protected function showHeaderCopyrightWarning() {
3625  $msg = 'editpage-head-copy-warn';
3626  if ( !$this->context->msg( $msg )->isDisabled() ) {
3627  $this->context->getOutput()->wrapWikiMsg( "<div class='editpage-head-copywarn'>\n$1\n</div>",
3628  'editpage-head-copy-warn' );
3629  }
3630  }
3631 
3640  protected function showTosSummary() {
3641  $msg = 'editpage-tos-summary';
3642  Hooks::run( 'EditPageTosSummary', [ $this->mTitle, &$msg ] );
3643  if ( !$this->context->msg( $msg )->isDisabled() ) {
3644  $out = $this->context->getOutput();
3645  $out->addHTML( '<div class="mw-tos-summary">' );
3646  $out->addWikiMsg( $msg );
3647  $out->addHTML( '</div>' );
3648  }
3649  }
3650 
3655  protected function showEditTools() {
3656  $this->context->getOutput()->addHTML( '<div class="mw-editTools">' .
3657  $this->context->msg( 'edittools' )->inContentLanguage()->parse() .
3658  '</div>' );
3659  }
3660 
3667  protected function getCopywarn() {
3668  return self::getCopyrightWarning( $this->mTitle );
3669  }
3670 
3679  public static function getCopyrightWarning( $title, $format = 'plain', $langcode = null ) {
3680  global $wgRightsText;
3681  if ( $wgRightsText ) {
3682  $copywarnMsg = [ 'copyrightwarning',
3683  '[[' . wfMessage( 'copyrightpage' )->inContentLanguage()->text() . ']]',
3684  $wgRightsText ];
3685  } else {
3686  $copywarnMsg = [ 'copyrightwarning2',
3687  '[[' . wfMessage( 'copyrightpage' )->inContentLanguage()->text() . ']]' ];
3688  }
3689  // Allow for site and per-namespace customization of contribution/copyright notice.
3690  Hooks::run( 'EditPageCopyrightWarning', [ $title, &$copywarnMsg ] );
3691 
3692  $msg = wfMessage( ...$copywarnMsg )->title( $title );
3693  if ( $langcode ) {
3694  $msg->inLanguage( $langcode );
3695  }
3696  return "<div id=\"editpage-copywarn\">\n" .
3697  $msg->$format() . "\n</div>";
3698  }
3699 
3707  public static function getPreviewLimitReport( ParserOutput $output = null ) {
3708  global $wgLang;
3709 
3710  if ( !$output || !$output->getLimitReportData() ) {
3711  return '';
3712  }
3713 
3714  $limitReport = Html::rawElement( 'div', [ 'class' => 'mw-limitReportExplanation' ],
3715  wfMessage( 'limitreport-title' )->parseAsBlock()
3716  );
3717 
3718  // Show/hide animation doesn't work correctly on a table, so wrap it in a div.
3719  $limitReport .= Html::openElement( 'div', [ 'class' => 'preview-limit-report-wrapper' ] );
3720 
3721  $limitReport .= Html::openElement( 'table', [
3722  'class' => 'preview-limit-report wikitable'
3723  ] ) .
3724  Html::openElement( 'tbody' );
3725 
3726  foreach ( $output->getLimitReportData() as $key => $value ) {
3727  if ( Hooks::run( 'ParserLimitReportFormat',
3728  [ $key, &$value, &$limitReport, true, true ]
3729  ) ) {
3730  $keyMsg = wfMessage( $key );
3731  $valueMsg = wfMessage( [ "$key-value-html", "$key-value" ] );
3732  if ( !$valueMsg->exists() ) {
3733  $valueMsg = new RawMessage( '$1' );
3734  }
3735  if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) {
3736  $limitReport .= Html::openElement( 'tr' ) .
3737  Html::rawElement( 'th', null, $keyMsg->parse() ) .
3738  Html::rawElement( 'td', null,
3739  $wgLang->formatNum( $valueMsg->params( $value )->parse() )
3740  ) .
3741  Html::closeElement( 'tr' );
3742  }
3743  }
3744  }
3745 
3746  $limitReport .= Html::closeElement( 'tbody' ) .
3747  Html::closeElement( 'table' ) .
3748  Html::closeElement( 'div' );
3749 
3750  return $limitReport;
3751  }
3752 
3753  protected function showStandardInputs( &$tabindex = 2 ) {
3754  $out = $this->context->getOutput();
3755  $out->addHTML( "<div class='editOptions'>\n" );
3756 
3757  if ( $this->section != 'new' ) {
3758  $this->showSummaryInput( false, $this->summary );
3759  $out->addHTML( $this->getSummaryPreview( false, $this->summary ) );
3760  }
3761 
3762  $checkboxes = $this->getCheckboxesWidget(
3763  $tabindex,
3764  [ 'minor' => $this->minoredit, 'watch' => $this->watchthis ]
3765  );
3766  $checkboxesHTML = new OOUI\HorizontalLayout( [ 'items' => $checkboxes ] );
3767 
3768  $out->addHTML( "<div class='editCheckboxes'>" . $checkboxesHTML . "</div>\n" );
3769 
3770  // Show copyright warning.
3771  $out->addWikiTextAsInterface( $this->getCopywarn() );
3772  $out->addHTML( $this->editFormTextAfterWarn );
3773 
3774  $out->addHTML( "<div class='editButtons'>\n" );
3775  $out->addHTML( implode( "\n", $this->getEditButtons( $tabindex ) ) . "\n" );
3776 
3777  $cancel = $this->getCancelLink();
3778 
3779  $message = $this->context->msg( 'edithelppage' )->inContentLanguage()->text();
3780  $edithelpurl = Skin::makeInternalOrExternalUrl( $message );
3781  $edithelp =
3783  $this->context->msg( 'edithelp' )->text(),
3784  [ 'target' => 'helpwindow', 'href' => $edithelpurl ],
3785  [ 'mw-ui-quiet' ]
3786  ) .
3787  $this->context->msg( 'word-separator' )->escaped() .
3788  $this->context->msg( 'newwindow' )->parse();
3789 
3790  $out->addHTML( " <span class='cancelLink'>{$cancel}</span>\n" );
3791  $out->addHTML( " <span class='editHelp'>{$edithelp}</span>\n" );
3792  $out->addHTML( "</div><!-- editButtons -->\n" );
3793 
3794  Hooks::run( 'EditPage::showStandardInputs:options', [ $this, $out, &$tabindex ] );
3795 
3796  $out->addHTML( "</div><!-- editOptions -->\n" );
3797  }
3798 
3803  protected function showConflict() {
3804  $out = $this->context->getOutput();
3805  // Avoid PHP 7.1 warning of passing $this by reference
3806  $editPage = $this;
3807  if ( Hooks::run( 'EditPageBeforeConflictDiff', [ &$editPage, &$out ] ) ) {
3808  $this->incrementConflictStats();
3809 
3810  $this->getEditConflictHelper()->showEditFormTextAfterFooters();
3811  }
3812  }
3813 
3814  protected function incrementConflictStats() {
3815  $this->getEditConflictHelper()->incrementConflictStats( $this->context->getUser() );
3816  }
3817 
3821  public function getCancelLink() {
3822  $cancelParams = [];
3823  if ( !$this->isConflict && $this->oldid > 0 ) {
3824  $cancelParams['oldid'] = $this->oldid;
3825  } elseif ( $this->getContextTitle()->isRedirect() ) {
3826  $cancelParams['redirect'] = 'no';
3827  }
3828 
3829  return new OOUI\ButtonWidget( [
3830  'id' => 'mw-editform-cancel',
3831  'href' => $this->getContextTitle()->getLinkURL( $cancelParams ),
3832  'label' => new OOUI\HtmlSnippet( $this->context->msg( 'cancel' )->parse() ),
3833  'framed' => false,
3834  'infusable' => true,
3835  'flags' => 'destructive',
3836  ] );
3837  }
3838 
3848  protected function getActionURL( Title $title ) {
3849  return $title->getLocalURL( [ 'action' => $this->action ] );
3850  }
3851 
3859  protected function wasDeletedSinceLastEdit() {
3860  if ( $this->deletedSinceEdit !== null ) {
3861  return $this->deletedSinceEdit;
3862  }
3863 
3864  $this->deletedSinceEdit = false;
3865 
3866  if ( !$this->mTitle->exists() && $this->mTitle->isDeletedQuick() ) {
3867  $this->lastDelete = $this->getLastDelete();
3868  if ( $this->lastDelete ) {
3869  $deleteTime = wfTimestamp( TS_MW, $this->lastDelete->log_timestamp );
3870  if ( $deleteTime > $this->starttime ) {
3871  $this->deletedSinceEdit = true;
3872  }
3873  }
3874  }
3875 
3876  return $this->deletedSinceEdit;
3877  }
3878 
3884  protected function getLastDelete() {
3885  $dbr = wfGetDB( DB_REPLICA );
3886  $commentQuery = CommentStore::getStore()->getJoin( 'log_comment' );
3887  $actorQuery = ActorMigration::newMigration()->getJoin( 'log_user' );
3888  $data = $dbr->selectRow(
3889  array_merge( [ 'logging' ], $commentQuery['tables'], $actorQuery['tables'], [ 'user' ] ),
3890  [
3891  'log_type',
3892  'log_action',
3893  'log_timestamp',
3894  'log_namespace',
3895  'log_title',
3896  'log_params',
3897  'log_deleted',
3898  'user_name'
3899  ] + $commentQuery['fields'] + $actorQuery['fields'],
3900  [
3901  'log_namespace' => $this->mTitle->getNamespace(),
3902  'log_title' => $this->mTitle->getDBkey(),
3903  'log_type' => 'delete',
3904  'log_action' => 'delete',
3905  ],
3906  __METHOD__,
3907  [ 'LIMIT' => 1, 'ORDER BY' => 'log_timestamp DESC' ],
3908  [
3909  'user' => [ 'JOIN', 'user_id=' . $actorQuery['fields']['log_user'] ],
3910  ] + $commentQuery['joins'] + $actorQuery['joins']
3911  );
3912  // Quick paranoid permission checks...
3913  if ( is_object( $data ) ) {
3914  if ( $data->log_deleted & LogPage::DELETED_USER ) {
3915  $data->user_name = $this->context->msg( 'rev-deleted-user' )->escaped();
3916  }
3917 
3918  if ( $data->log_deleted & LogPage::DELETED_COMMENT ) {
3919  $data->log_comment_text = $this->context->msg( 'rev-deleted-comment' )->escaped();
3920  $data->log_comment_data = null;
3921  }
3922  }
3923 
3924  return $data;
3925  }
3926 
3932  public function getPreviewText() {
3933  $out = $this->context->getOutput();
3934  $config = $this->context->getConfig();
3935 
3936  if ( $config->get( 'RawHtml' ) && !$this->mTokenOk ) {
3937  // Could be an offsite preview attempt. This is very unsafe if
3938  // HTML is enabled, as it could be an attack.
3939  $parsedNote = '';
3940  if ( $this->textbox1 !== '' ) {
3941  // Do not put big scary notice, if previewing the empty
3942  // string, which happens when you initially edit
3943  // a category page, due to automatic preview-on-open.
3944  $parsedNote = Html::rawElement( 'div', [ 'class' => 'previewnote' ],
3945  $out->parseAsInterface(
3946  $this->context->msg( 'session_fail_preview_html' )->plain()
3947  ) );
3948  }
3949  $this->incrementEditFailureStats( 'session_loss' );
3950  return $parsedNote;
3951  }
3952 
3953  $note = '';
3954 
3955  try {
3956  $content = $this->toEditContent( $this->textbox1 );
3957 
3958  $previewHTML = '';
3959  if ( !Hooks::run(
3960  'AlternateEditPreview',
3961  [ $this, &$content, &$previewHTML, &$this->mParserOutput ] )
3962  ) {
3963  return $previewHTML;
3964  }
3965 
3966  # provide a anchor link to the editform
3967  $continueEditing = '<span class="mw-continue-editing">' .
3968  '[[#' . self::EDITFORM_ID . '|' .
3969  $this->context->getLanguage()->getArrow() . ' ' .
3970  $this->context->msg( 'continue-editing' )->text() . ']]</span>';
3971  if ( $this->mTriedSave && !$this->mTokenOk ) {
3972  if ( $this->mTokenOkExceptSuffix ) {
3973  $note = $this->context->msg( 'token_suffix_mismatch' )->plain();
3974  $this->incrementEditFailureStats( 'bad_token' );
3975  } else {
3976  $note = $this->context->msg( 'session_fail_preview' )->plain();
3977  $this->incrementEditFailureStats( 'session_loss' );
3978  }
3979  } elseif ( $this->incompleteForm ) {
3980  $note = $this->context->msg( 'edit_form_incomplete' )->plain();
3981  if ( $this->mTriedSave ) {
3982  $this->incrementEditFailureStats( 'incomplete_form' );
3983  }
3984  } else {
3985  $note = $this->context->msg( 'previewnote' )->plain() . ' ' . $continueEditing;
3986  }
3987 
3988  # don't parse non-wikitext pages, show message about preview
3989  if ( $this->mTitle->isUserConfigPage() || $this->mTitle->isSiteConfigPage() ) {
3990  if ( $this->mTitle->isUserConfigPage() ) {
3991  $level = 'user';
3992  } elseif ( $this->mTitle->isSiteConfigPage() ) {
3993  $level = 'site';
3994  } else {
3995  $level = false;
3996  }
3997 
3998  if ( $content->getModel() == CONTENT_MODEL_CSS ) {
3999  $format = 'css';
4000  if ( $level === 'user' && !$config->get( 'AllowUserCss' ) ) {
4001  $format = false;
4002  }
4003  } elseif ( $content->getModel() == CONTENT_MODEL_JSON ) {
4004  $format = 'json';
4005  if ( $level === 'user' /* No comparable 'AllowUserJson' */ ) {
4006  $format = false;
4007  }
4008  } elseif ( $content->getModel() == CONTENT_MODEL_JAVASCRIPT ) {
4009  $format = 'js';
4010  if ( $level === 'user' && !$config->get( 'AllowUserJs' ) ) {
4011  $format = false;
4012  }
4013  } else {
4014  $format = false;
4015  }
4016 
4017  # Used messages to make sure grep find them:
4018  # Messages: usercsspreview, userjsonpreview, userjspreview,
4019  # sitecsspreview, sitejsonpreview, sitejspreview
4020  if ( $level && $format ) {
4021  $note = "<div id='mw-{$level}{$format}preview'>" .
4022  $this->context->msg( "{$level}{$format}preview" )->plain() .
4023  ' ' . $continueEditing . "</div>";
4024  }
4025  }
4026 
4027  # If we're adding a comment, we need to show the
4028  # summary as the headline
4029  if ( $this->section === "new" && $this->summary !== "" ) {
4030  $content = $content->addSectionHeader( $this->summary );
4031  }
4032 
4033  $hook_args = [ $this, &$content ];
4034  Hooks::run( 'EditPageGetPreviewContent', $hook_args );
4035 
4036  $parserResult = $this->doPreviewParse( $content );
4037  $parserOutput = $parserResult['parserOutput'];
4038  $previewHTML = $parserResult['html'];
4039  $this->mParserOutput = $parserOutput;
4040  $out->addParserOutputMetadata( $parserOutput );
4041  if ( $out->userCanPreview() ) {
4042  $out->addContentOverride( $this->getTitle(), $content );
4043  }
4044 
4045  if ( count( $parserOutput->getWarnings() ) ) {
4046  $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
4047  }
4048 
4049  } catch ( MWContentSerializationException $ex ) {
4050  $m = $this->context->msg(
4051  'content-failed-to-parse',
4052  $this->contentModel,
4053  $this->contentFormat,
4054  $ex->getMessage()
4055  );
4056  $note .= "\n\n" . $m->plain(); # gets parsed down below
4057  $previewHTML = '';
4058  }
4059 
4060  if ( $this->isConflict ) {
4061  $conflict = Html::rawElement(
4062  'div', [ 'id' => 'mw-previewconflict', 'class' => 'warningbox' ],
4063  $this->context->msg( 'previewconflict' )->escaped()
4064  );
4065  } else {
4066  $conflict = '';
4067  }
4068 
4069  $previewhead = Html::rawElement(
4070  'div', [ 'class' => 'previewnote' ],
4072  'h2', [ 'id' => 'mw-previewheader' ],
4073  $this->context->msg( 'preview' )->escaped()
4074  ) .
4075  Html::rawElement( 'div', [ 'class' => 'warningbox' ],
4076  $out->parseAsInterface( $note )
4077  ) . $conflict
4078  );
4079 
4080  $pageViewLang = $this->mTitle->getPageViewLanguage();
4081  $attribs = [ 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(),
4082  'class' => 'mw-content-' . $pageViewLang->getDir() ];
4083  $previewHTML = Html::rawElement( 'div', $attribs, $previewHTML );
4084 
4085  return $previewhead . $previewHTML . $this->previewTextAfterContent;
4086  }
4087 
4088  private function incrementEditFailureStats( $failureType ) {
4089  $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
4090  $stats->increment( 'edit.failures.' . $failureType );
4091  }
4092 
4097  protected function getPreviewParserOptions() {
4098  $parserOptions = $this->page->makeParserOptions( $this->context );
4099  $parserOptions->setIsPreview( true );
4100  $parserOptions->setIsSectionPreview( !is_null( $this->section ) && $this->section !== '' );
4101  $parserOptions->enableLimitReport();
4102 
4103  // XXX: we could call $parserOptions->setCurrentRevisionCallback here to force the
4104  // current revision to be null during PST, until setupFakeRevision is called on
4105  // the ParserOptions. Currently, we rely on Parser::getRevisionObject() to ignore
4106  // existing revisions in preview mode.
4107 
4108  return $parserOptions;
4109  }
4110 
4120  protected function doPreviewParse( Content $content ) {
4121  $user = $this->context->getUser();
4122  $parserOptions = $this->getPreviewParserOptions();
4123 
4124  // NOTE: preSaveTransform doesn't have a fake revision to operate on.
4125  // Parser::getRevisionObject() will return null in preview mode,
4126  // causing the context user to be used for {{subst:REVISIONUSER}}.
4127  // XXX: Alternatively, we could also call setupFakeRevision() a second time:
4128  // once before PST with $content, and then after PST with $pstContent.
4129  $pstContent = $content->preSaveTransform( $this->mTitle, $user, $parserOptions );
4130  $scopedCallback = $parserOptions->setupFakeRevision( $this->mTitle, $pstContent, $user );
4131  $parserOutput = $pstContent->getParserOutput( $this->mTitle, null, $parserOptions );
4132  ScopedCallback::consume( $scopedCallback );
4133  return [
4134  'parserOutput' => $parserOutput,
4135  'html' => $parserOutput->getText( [
4136  'enableSectionEditLinks' => false
4137  ] )
4138  ];
4139  }
4140 
4144  public function getTemplates() {
4145  if ( $this->preview || $this->section != '' ) {
4146  $templates = [];
4147  if ( !isset( $this->mParserOutput ) ) {
4148  return $templates;
4149  }
4150  foreach ( $this->mParserOutput->getTemplates() as $ns => $template ) {
4151  foreach ( array_keys( $template ) as $dbk ) {
4152  $templates[] = Title::makeTitle( $ns, $dbk );
4153  }
4154  }
4155  return $templates;
4156  } else {
4157  return $this->mTitle->getTemplateLinksFrom();
4158  }
4159  }
4160 
4166  public static function getEditToolbar() {
4167  $startingToolbar = '<div id="toolbar"></div>';
4168  $toolbar = $startingToolbar;
4169 
4170  if ( !Hooks::run( 'EditPageBeforeEditToolbar', [ &$toolbar ] ) ) {
4171  return null;
4172  }
4173  // Don't add a pointless `<div>` to the page unless a hook caller populated it
4174  return ( $toolbar === $startingToolbar ) ? null : $toolbar;
4175  }
4176 
4195  public function getCheckboxesDefinition( $checked ) {
4196  $checkboxes = [];
4197 
4198  $user = $this->context->getUser();
4199  // don't show the minor edit checkbox if it's a new page or section
4200  $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
4201  if ( !$this->isNew && $permissionManager->userHasRight( $user, 'minoredit' ) ) {
4202  $checkboxes['wpMinoredit'] = [
4203  'id' => 'wpMinoredit',
4204  'label-message' => 'minoredit',
4205  // Uses messages: tooltip-minoredit, accesskey-minoredit
4206  'tooltip' => 'minoredit',
4207  'label-id' => 'mw-editpage-minoredit',
4208  'legacy-name' => 'minor',
4209  'default' => $checked['minor'],
4210  ];
4211  }
4212 
4213  if ( $user->isLoggedIn() ) {
4214  $checkboxes['wpWatchthis'] = [
4215  'id' => 'wpWatchthis',
4216  'label-message' => 'watchthis',
4217  // Uses messages: tooltip-watch, accesskey-watch
4218  'tooltip' => 'watch',
4219  'label-id' => 'mw-editpage-watch',
4220  'legacy-name' => 'watch',
4221  'default' => $checked['watch'],
4222  ];
4223  }
4224 
4225  $editPage = $this;
4226  Hooks::run( 'EditPageGetCheckboxesDefinition', [ $editPage, &$checkboxes ] );
4227 
4228  return $checkboxes;
4229  }
4230 
4241  public function getCheckboxesWidget( &$tabindex, $checked ) {
4242  $checkboxes = [];
4243  $checkboxesDef = $this->getCheckboxesDefinition( $checked );
4244 
4245  foreach ( $checkboxesDef as $name => $options ) {
4246  $legacyName = $options['legacy-name'] ?? $name;
4247 
4248  $title = null;
4249  $accesskey = null;
4250  if ( isset( $options['tooltip'] ) ) {
4251  $accesskey = $this->context->msg( "accesskey-{$options['tooltip']}" )->text();
4252  $title = Linker::titleAttrib( $options['tooltip'] );
4253  }
4254  if ( isset( $options['title-message'] ) ) {
4255  $title = $this->context->msg( $options['title-message'] )->text();
4256  }
4257 
4258  $checkboxes[ $legacyName ] = new OOUI\FieldLayout(
4259  new OOUI\CheckboxInputWidget( [
4260  'tabIndex' => ++$tabindex,
4261  'accessKey' => $accesskey,
4262  'id' => $options['id'] . 'Widget',
4263  'inputId' => $options['id'],
4264  'name' => $name,
4265  'selected' => $options['default'],
4266  'infusable' => true,
4267  ] ),
4268  [
4269  'align' => 'inline',
4270  'label' => new OOUI\HtmlSnippet( $this->context->msg( $options['label-message'] )->parse() ),
4271  'title' => $title,
4272  'id' => $options['label-id'] ?? null,
4273  ]
4274  );
4275  }
4276 
4277  return $checkboxes;
4278  }
4279 
4286  protected function getSubmitButtonLabel() {
4287  $labelAsPublish =
4288  $this->context->getConfig()->get( 'EditSubmitButtonLabelPublish' );
4289 
4290  // Can't use $this->isNew as that's also true if we're adding a new section to an extant page
4291  $newPage = !$this->mTitle->exists();
4292 
4293  if ( $labelAsPublish ) {
4294  $buttonLabelKey = $newPage ? 'publishpage' : 'publishchanges';
4295  } else {
4296  $buttonLabelKey = $newPage ? 'savearticle' : 'savechanges';
4297  }
4298 
4299  return $buttonLabelKey;
4300  }
4301 
4310  public function getEditButtons( &$tabindex ) {
4311  $buttons = [];
4312 
4313  $labelAsPublish =
4314  $this->context->getConfig()->get( 'EditSubmitButtonLabelPublish' );
4315 
4316  $buttonLabel = $this->context->msg( $this->getSubmitButtonLabel() )->text();
4317  $buttonTooltip = $labelAsPublish ? 'publish' : 'save';
4318 
4319  $buttons['save'] = new OOUI\ButtonInputWidget( [
4320  'name' => 'wpSave',
4321  'tabIndex' => ++$tabindex,
4322  'id' => 'wpSaveWidget',
4323  'inputId' => 'wpSave',
4324  // Support: IE 6 – Use <input>, otherwise it can't distinguish which button was clicked
4325  'useInputTag' => true,
4326  'flags' => [ 'progressive', 'primary' ],
4327  'label' => $buttonLabel,
4328  'infusable' => true,
4329  'type' => 'submit',
4330  // Messages used: tooltip-save, tooltip-publish
4331  'title' => Linker::titleAttrib( $buttonTooltip ),
4332  // Messages used: accesskey-save, accesskey-publish
4333  'accessKey' => Linker::accesskey( $buttonTooltip ),
4334  ] );
4335 
4336  $buttons['preview'] = new OOUI\ButtonInputWidget( [
4337  'name' => 'wpPreview',
4338  'tabIndex' => ++$tabindex,
4339  'id' => 'wpPreviewWidget',
4340  'inputId' => 'wpPreview',
4341  // Support: IE 6 – Use <input>, otherwise it can't distinguish which button was clicked
4342  'useInputTag' => true,
4343  'label' => $this->context->msg( 'showpreview' )->text(),
4344  'infusable' => true,
4345  'type' => 'submit',
4346  // Message used: tooltip-preview
4347  'title' => Linker::titleAttrib( 'preview' ),
4348  // Message used: accesskey-preview
4349  'accessKey' => Linker::accesskey( 'preview' ),
4350  ] );
4351 
4352  $buttons['diff'] = new OOUI\ButtonInputWidget( [
4353  'name' => 'wpDiff',
4354  'tabIndex' => ++$tabindex,
4355  'id' => 'wpDiffWidget',
4356  'inputId' => 'wpDiff',
4357  // Support: IE 6 – Use <input>, otherwise it can't distinguish which button was clicked
4358  'useInputTag' => true,
4359  'label' => $this->context->msg( 'showdiff' )->text(),
4360  'infusable' => true,
4361  'type' => 'submit',
4362  // Message used: tooltip-diff
4363  'title' => Linker::titleAttrib( 'diff' ),
4364  // Message used: accesskey-diff
4365  'accessKey' => Linker::accesskey( 'diff' ),
4366  ] );
4367 
4368  // Avoid PHP 7.1 warning of passing $this by reference
4369  $editPage = $this;
4370  Hooks::run( 'EditPageBeforeEditButtons', [ &$editPage, &$buttons, &$tabindex ] );
4371 
4372  return $buttons;
4373  }
4374 
4379  public function noSuchSectionPage() {
4380  $out = $this->context->getOutput();
4381  $out->prepareErrorPage( $this->context->msg( 'nosuchsectiontitle' ) );
4382 
4383  $res = $this->context->msg( 'nosuchsectiontext', $this->section )->parseAsBlock();
4384 
4385  // Avoid PHP 7.1 warning of passing $this by reference
4386  $editPage = $this;
4387  Hooks::run( 'EditPageNoSuchSection', [ &$editPage, &$res ] );
4388  $out->addHTML( $res );
4389 
4390  $out->returnToMain( false, $this->mTitle );
4391  }
4392 
4398  public function spamPageWithContent( $match = false ) {
4399  $this->textbox2 = $this->textbox1;
4400 
4401  if ( is_array( $match ) ) {
4402  $match = $this->context->getLanguage()->listToText( $match );
4403  }
4404  $out = $this->context->getOutput();
4405  $out->prepareErrorPage( $this->context->msg( 'spamprotectiontitle' ) );
4406 
4407  $out->addHTML( '<div id="spamprotected">' );
4408  $out->addWikiMsg( 'spamprotectiontext' );
4409  if ( $match ) {
4410  $out->addWikiMsg( 'spamprotectionmatch', wfEscapeWikiText( $match ) );
4411  }
4412  $out->addHTML( '</div>' );
4413 
4414  $out->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
4415  $this->showDiff();
4416 
4417  $out->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
4418  $this->showTextbox2();
4419 
4420  $out->addReturnTo( $this->getContextTitle(), [ 'action' => 'edit' ] );
4421  }
4422 
4426  protected function addEditNotices() {
4427  $out = $this->context->getOutput();
4428  $editNotices = $this->mTitle->getEditNotices( $this->oldid );
4429  if ( count( $editNotices ) ) {
4430  $out->addHTML( implode( "\n", $editNotices ) );
4431  } else {
4432  $msg = $this->context->msg( 'editnotice-notext' );
4433  if ( !$msg->isDisabled() ) {
4434  $out->addHTML(
4435  '<div class="mw-editnotice-notext">'
4436  . $msg->parseAsBlock()
4437  . '</div>'
4438  );
4439  }
4440  }
4441  }
4442 
4446  protected function addTalkPageText() {
4447  if ( $this->mTitle->isTalkPage() ) {
4448  $this->context->getOutput()->addWikiMsg( 'talkpagetext' );
4449  }
4450  }
4451 
4455  protected function addLongPageWarningHeader() {
4456  if ( $this->contentLength === false ) {
4457  $this->contentLength = strlen( $this->textbox1 );
4458  }
4459 
4460  $out = $this->context->getOutput();
4461  $lang = $this->context->getLanguage();
4462  $maxArticleSize = $this->context->getConfig()->get( 'MaxArticleSize' );
4463  if ( $this->tooBig || $this->contentLength > $maxArticleSize * 1024 ) {
4464  $out->wrapWikiMsg( "<div class='error' id='mw-edit-longpageerror'>\n$1\n</div>",
4465  [
4466  'longpageerror',
4467  $lang->formatNum( round( $this->contentLength / 1024, 3 ) ),
4468  $lang->formatNum( $maxArticleSize )
4469  ]
4470  );
4471  } elseif ( !$this->context->msg( 'longpage-hint' )->isDisabled() ) {
4472  $out->wrapWikiMsg( "<div id='mw-edit-longpage-hint'>\n$1\n</div>",
4473  [
4474  'longpage-hint',
4475  $lang->formatSize( strlen( $this->textbox1 ) ),
4476  strlen( $this->textbox1 )
4477  ]
4478  );
4479  }
4480  }
4481 
4485  protected function addPageProtectionWarningHeaders() {
4486  $out = $this->context->getOutput();
4487  if ( $this->mTitle->isProtected( 'edit' ) &&
4488  MediaWikiServices::getInstance()->getPermissionManager()->getNamespaceRestrictionLevels(
4489  $this->getTitle()->getNamespace()
4490  ) !== [ '' ]
4491  ) {
4492  # Is the title semi-protected?
4493  if ( $this->mTitle->isSemiProtected() ) {
4494  $noticeMsg = 'semiprotectedpagewarning';
4495  } else {
4496  # Then it must be protected based on static groups (regular)
4497  $noticeMsg = 'protectedpagewarning';
4498  }
4499  LogEventsList::showLogExtract( $out, 'protect', $this->mTitle, '',
4500  [ 'lim' => 1, 'msgKey' => [ $noticeMsg ] ] );
4501  }
4502  if ( $this->mTitle->isCascadeProtected() ) {
4503  # Is this page under cascading protection from some source pages?
4504 
4505  list( $cascadeSources, /* $restrictions */ ) = $this->mTitle->getCascadeProtectionSources();
4506  $notice = "<div class='mw-cascadeprotectedwarning'>\n$1\n";
4507  $cascadeSourcesCount = count( $cascadeSources );
4508  if ( $cascadeSourcesCount > 0 ) {
4509  # Explain, and list the titles responsible
4510  foreach ( $cascadeSources as $page ) {
4511  $notice .= '* [[:' . $page->getPrefixedText() . "]]\n";
4512  }
4513  }
4514  $notice .= '</div>';
4515  $out->wrapWikiMsg( $notice, [ 'cascadeprotectedwarning', $cascadeSourcesCount ] );
4516  }
4517  if ( !$this->mTitle->exists() && $this->mTitle->getRestrictions( 'create' ) ) {
4518  LogEventsList::showLogExtract( $out, 'protect', $this->mTitle, '',
4519  [ 'lim' => 1,
4520  'showIfEmpty' => false,
4521  'msgKey' => [ 'titleprotectedwarning' ],
4522  'wrap' => "<div class=\"mw-titleprotectedwarning\">\n$1</div>" ] );
4523  }
4524  }
4525 
4530  protected function addExplainConflictHeader( OutputPage $out ) {
4531  $out->addHTML(
4532  $this->getEditConflictHelper()->getExplainHeader()
4533  );
4534  }
4535 
4543  protected function buildTextboxAttribs( $name, array $customAttribs, User $user ) {
4544  return ( new TextboxBuilder() )->buildTextboxAttribs(
4545  $name, $customAttribs, $user, $this->mTitle
4546  );
4547  }
4548 
4554  protected function addNewLineAtEnd( $wikitext ) {
4555  return ( new TextboxBuilder() )->addNewLineAtEnd( $wikitext );
4556  }
4557 
4568  private function guessSectionName( $text ) {
4569  // Detect Microsoft browsers
4570  $userAgent = $this->context->getRequest()->getHeader( 'User-Agent' );
4571  $parser = MediaWikiServices::getInstance()->getParser();
4572  if ( $userAgent && preg_match( '/MSIE|Edge/', $userAgent ) ) {
4573  // ...and redirect them to legacy encoding, if available
4574  return $parser->guessLegacySectionNameFromWikiText( $text );
4575  }
4576  // Meanwhile, real browsers get real anchors
4577  $name = $parser->guessSectionNameFromWikiText( $text );
4578  // With one little caveat: per T216029, fragments in HTTP redirects need to be urlencoded,
4579  // otherwise Chrome double-escapes the rest of the URL.
4580  return '#' . urlencode( mb_substr( $name, 1 ) );
4581  }
4582 
4589  public function setEditConflictHelperFactory( callable $factory ) {
4590  $this->editConflictHelperFactory = $factory;
4591  $this->editConflictHelper = null;
4592  }
4593 
4597  private function getEditConflictHelper() {
4598  if ( !$this->editConflictHelper ) {
4599  $this->editConflictHelper = call_user_func(
4600  $this->editConflictHelperFactory,
4601  $this->getSubmitButtonLabel()
4602  );
4603  }
4604 
4606  }
4607 
4612  private function newTextConflictHelper( $submitButtonLabel ) {
4613  return new TextConflictHelper(
4614  $this->getTitle(),
4615  $this->getContext()->getOutput(),
4616  MediaWikiServices::getInstance()->getStatsdDataFactory(),
4617  $submitButtonLabel
4618  );
4619  }
4620 }
string $autoSumm
Definition: EditPage.php:302
Helps EditPage build textboxes.
static factory(Title $title)
Create a WikiPage object of the appropriate class for the given title.
Definition: WikiPage.php:142
displayPermissionsError(array $permErrors)
Display a permissions error page, like OutputPage::showPermissionsErrorPage(), but with the following...
Definition: EditPage.php:753
incrementConflictStats()
Definition: EditPage.php:3814
bool $nosummary
If true, hide the summary field.
Definition: EditPage.php:353
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition: router.php:42
getPreloadedContent( $preload, $params=[])
Get the contents to be preloaded into the box, either set by an earlier setPreloadText() or by loadin...
Definition: EditPage.php:1493
$editFormTextBottom
Definition: EditPage.php:426
$response
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking, formatting, etc.
spamPageWithContent( $match=false)
Show "your edit contains spam" page with your diff and text.
Definition: EditPage.php:4398
const AS_READ_ONLY_PAGE_ANON
Status: this anonymous user is not allowed to edit this page.
Definition: EditPage.php:86
wfWarn( $msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
bool $missingSummary
Definition: EditPage.php:284
bool $bot
Definition: EditPage.php:406
string $textbox2
Definition: EditPage.php:345
bool $mTokenOk
Definition: EditPage.php:266
$editFormTextAfterContent
Definition: EditPage.php:427
showContentForm()
Subpage overridable method for printing the form for page content editing By default this simply outp...
Definition: EditPage.php:3435
bool $allowBlankSummary
Definition: EditPage.php:287
fatal( $message,... $parameters)
Add an error and set OK to false, indicating that the operation as a whole was fatal.
getPreviewText()
Get the rendered text for previewing.
Definition: EditPage.php:3932
bool $isConflict
Whether an edit conflict needs to be resolved.
Definition: EditPage.php:245
int $oldid
Revision ID the edit is based on, or 0 if it&#39;s the current revision.
Definition: EditPage.php:390
getContentObject( $def_content=null)
Definition: EditPage.php:1201
handleStatus(Status $status, $resultDetails)
Handle status, such as after attempt save.
Definition: EditPage.php:1650
string $summary
Definition: EditPage.php:348
setHeaders()
Definition: EditPage.php:2541
WikiPage $page
Definition: EditPage.php:227
static matchSpamRegexInternal( $text, $regexes)
Definition: EditPage.php:2531
Show an error when the wiki is locked/read-only and the user tries to do something that requires writ...
static isIP( $name)
Does the string match an anonymous IP address?
Definition: User.php:947
const AS_ARTICLE_WAS_DELETED
Status: article was deleted while editing and param wpRecreate == false or form was not posted...
Definition: EditPage.php:107
Handles formatting for the "templates used on this page" lists.
const AS_HOOK_ERROR
Status: Article update aborted by a hook function.
Definition: EditPage.php:66
showTextbox2()
Definition: EditPage.php:3475
bool $tooBig
Definition: EditPage.php:278
showHeaderCopyrightWarning()
Show the header copyright warning.
Definition: EditPage.php:3624
wfExpandUrl( $url, $defaultProto=PROTO_CURRENT)
Expand a potentially local URL to a fully-qualified URL.
getPage()
Get the WikiPage object of this instance.
Definition: Article.php:234
if(!isset( $args[0])) $lang
static getCopyrightWarning( $title, $format='plain', $langcode=null)
Get the copyright warning, by default returns wikitext.
Definition: EditPage.php:3679
static openElement( $element, $attribs=[])
Identical to rawElement(), but has no third parameter and omits the end tag (and the self-closing &#39;/&#39;...
Definition: Html.php:251
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:209
showTosSummary()
Give a chance for site and per-namespace customizations of terms of service summary link that might e...
Definition: EditPage.php:3640
An IContextSource implementation which will inherit context from another source but allow individual ...
The edit page/HTML interface (split from Article) The actual database and text munging is still in Ar...
Definition: EditPage.php:47
Title $mTitle
Definition: EditPage.php:233
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:129
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
const AS_HOOK_ERROR_EXPECTED
Status: A hook function returned an error.
Definition: EditPage.php:71
getLocalURL( $query='', $query2=false)
Get a URL with no fragment or server name (relative URL) from a Title object.
Definition: Title.php:2172
static accesskey( $name)
Given the id of an interface element, constructs the appropriate accesskey attribute from the system ...
Definition: Linker.php:2070
static loadFromTimestamp( $db, $title, $timestamp)
Load the revision for the given title with the given timestamp.
Definition: Revision.php:297
getSummaryInputAttributes(array $inputAttrs=null)
Helper function for summary input functions, which returns the necessary attributes for the input...
Definition: EditPage.php:3295
getEditButtons(&$tabindex)
Returns an array of html code of the following buttons: save, diff and preview.
Definition: EditPage.php:4310
static newFromUserAndLang(User $user, Language $lang)
Get a ParserOptions object from a given user and language.
string $editintro
Definition: EditPage.php:400
Class for viewing MediaWiki article and history.
Definition: Article.php:41
static getSkinNames()
Fetch the set of available skins.
Definition: Skin.php:57
bool $allowBlankArticle
Definition: EditPage.php:293
toEditText( $content)
Gets an editable textual representation of $content.
Definition: EditPage.php:2764
IContextSource $context
Definition: EditPage.php:451
Article $mArticle
Definition: EditPage.php:225
null string $contentFormat
Definition: EditPage.php:412
const AS_BLOCKED_PAGE_FOR_USER
Status: User is blocked from editing this page.
Definition: EditPage.php:76
getWikiText( $shortContext=false, $longContext=false, $lang=null)
Get the error list as a wikitext formatted list.
Definition: Status.php:185
bool $blankArticle
Definition: EditPage.php:290
buildTextboxAttribs( $name, array $customAttribs, User $user)
Definition: EditPage.php:4543
Helper for displaying edit conflicts in text content models to users.
isGood()
Returns whether the operation completed and didn&#39;t have any error or warnings.
const AS_NO_CREATE_PERMISSION
Status: user tried to create this page, but is not allowed to do that ( Title->userCan(&#39;create&#39;) == f...
Definition: EditPage.php:113
getEditConflictMainTextBox(array $customAttribs=[])
HTML to build the textbox1 on edit conflicts.
getEditPermissionErrors( $rigor=PermissionManager::RIGOR_SECURE)
Definition: EditPage.php:709
bool $missingComment
Definition: EditPage.php:281
const EDIT_MINOR
Definition: Defines.php:134
const COMMENT_CHARACTER_LIMIT
Maximum length of a comment in UTF-8 characters.
const POST_EDIT_COOKIE_DURATION
Duration of PostEdit cookie, in seconds.
Definition: EditPage.php:219
const EDIT_UPDATE
Definition: Defines.php:133
static userWasLastToEdit( $db, $pageId, $userId, $since)
Check if no edits were made by other users since the time a user started editing the page...
Definition: Revision.php:1103
showFormBeforeText()
Definition: EditPage.php:3398
internalAttemptSave(&$result, $bot=false)
Attempt submission (no UI)
Definition: EditPage.php:1907
bool stdClass $lastDelete
Definition: EditPage.php:263
const AS_UNICODE_NOT_SUPPORTED
Status: edit rejected because browser doesn&#39;t support Unicode.
Definition: EditPage.php:193
addPageProtectionWarningHeaders()
Definition: EditPage.php:4485
static getLocalizedName( $name, Language $lang=null)
Returns the localized name for a given content model.
getCheckboxesWidget(&$tabindex, $checked)
Returns an array of checkboxes for the edit form, including &#39;minor&#39; and &#39;watch&#39; checkboxes and any ot...
Definition: EditPage.php:4241
const CONTENT_MODEL_JSON
Definition: Defines.php:219
edit()
This is the function that gets called for "action=edit".
Definition: EditPage.php:582
getContextTitle()
Get the context title object.
Definition: EditPage.php:539
mergeChangesIntoContent(&$editContent)
Attempts to do 3-way merge of edit content with a base revision and current content, in case of edit conflict, in whichever way appropriate for the content type.
Definition: EditPage.php:2443
const DB_MASTER
Definition: defines.php:26
displayPreviewArea( $previewOutput, $isOnTop=false)
Definition: EditPage.php:3493
addEditNotices()
Definition: EditPage.php:4426
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
static formatHiddenCategories( $hiddencats)
Returns HTML for the "hidden categories on this page" list.
Definition: Linker.php:1987
isRedirect()
Tests if the article content represents a redirect.
Definition: WikiPage.php:634
null Title $mContextTitle
Definition: EditPage.php:236
static textarea( $name, $value='', array $attribs=[])
Convenience function to produce a <textarea> element.
Definition: Html.php:818
getEditFormHtmlBeforeContent()
Content to go in the edit form before textbox1.
int $editRevId
Revision ID of the latest revision of the page when editing was initiated on the client.
Definition: EditPage.php:372
const AS_CONTENT_TOO_BIG
Status: Content too big (> $wgMaxArticleSize)
Definition: EditPage.php:81
The User object encapsulates all of the user-specific settings (user_id, name, rights, email address, options, last login time).
Definition: User.php:51
getContext()
Gets the context this Article is executed in.
Definition: Article.php:2273
addExplainConflictHeader(OutputPage $out)
Definition: EditPage.php:4530
wfArrayToCgi( $array1, $array2=null, $prefix='')
This function takes one or two arrays as input, and returns a CGI-style string, e.g.
attemptSave(&$resultDetails=false)
Attempt submission.
Definition: EditPage.php:1615
showIntro()
Show all applicable editing introductions.
Definition: EditPage.php:2606
setWikiPage(WikiPage $wikiPage)
getArticle()
Definition: EditPage.php:502
$wgSpamRegex
Edits matching these regular expressions in body text will be recognised as spam and rejected automat...
bool $watchthis
Definition: EditPage.php:334
$previewTextAfterContent
Definition: EditPage.php:428
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
const DELETED_COMMENT
Definition: LogPage.php:35
static addCallableUpdate( $callable, $stage=self::POSTSEND, $dbw=null)
Add a callable update.
getParentRevId()
Get the edit&#39;s parent revision ID.
Definition: EditPage.php:1416
addLongPageWarningHeader()
Definition: EditPage.php:4455
getTemplates()
Definition: EditPage.php:4144
bool $save
Definition: EditPage.php:322
wfReadOnly()
Check whether the wiki is in read-only mode.
$wgLang
Definition: Setup.php:858
static newMigration()
Static constructor.
setContextTitle( $title)
Set the context Title object.
Definition: EditPage.php:527
static openElement( $element, $attribs=null)
This opens an XML element.
Definition: Xml.php:108
TextConflictHelper null $editConflictHelper
Definition: EditPage.php:473
toEditContent( $text)
Turns the given text into a Content object by unserializing it.
Definition: EditPage.php:2792
static getForModelID( $modelId)
Returns the ContentHandler singleton for the given model ID.
const AS_SPAM_ERROR
Status: summary contained spam according to one of the regexes in $wgSummarySpamRegex.
Definition: EditPage.php:149
const EDIT_FORCE_BOT
Definition: Defines.php:136
An error page which can definitely be safely rendered using the OutputPage.
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:2022
static getPreviewLimitReport(ParserOutput $output=null)
Get the Limit report for page previews.
Definition: EditPage.php:3707
callable $editConflictHelperFactory
Factory function to create an edit conflict helper.
Definition: EditPage.php:468
getLastDelete()
Get the last log record of this page being deleted, if ever.
Definition: EditPage.php:3884
getActionURL(Title $title)
Returns the URL to use in the form&#39;s action attribute.
Definition: EditPage.php:3848
static doWatchOrUnwatch( $watch, Title $title, User $user)
Watch or unwatch a page.
Definition: WatchAction.php:91
$editFormTextAfterTools
Definition: EditPage.php:425
const AS_CANNOT_USE_CUSTOM_MODEL
Status: when changing the content model is disallowed due to $wgContentHandlerUseDB being false...
Definition: EditPage.php:188
$editFormTextAfterWarn
Definition: EditPage.php:424
bool $recreate
Definition: EditPage.php:337
setPreloadedContent(Content $content)
Use this method before edit() to preload some content into the edit box.
Definition: EditPage.php:1478
static getContentText(Content $content=null)
Convenience function for getting flat text from a Content object.
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:81
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
const AS_SUCCESS_UPDATE
Status: Article successfully updated.
Definition: EditPage.php:56
const AS_READ_ONLY_PAGE
Status: wiki is in readonly mode (wfReadOnly() == true)
Definition: EditPage.php:96
static extractSectionTitle( $text)
Extract the section title from current section text, if any.
Definition: EditPage.php:3110
bool $isOldRev
Whether an old revision is edited.
Definition: EditPage.php:456
showHeader()
Definition: EditPage.php:3120
const EDIT_AUTOSUMMARY
Definition: Defines.php:138
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
const UNICODE_CHECK
Used for Unicode support checks.
Definition: EditPage.php:51
const AS_NO_CHANGE_CONTENT_MODEL
Status: user tried to modify the content model, but is not allowed to do that ( User::isAllowed(&#39;edit...
Definition: EditPage.php:165
addContentModelChangeLogEntry(User $user, $oldModel, $newModel, $reason)
Definition: EditPage.php:2398
getTitle()
Get the title object of the article.
Definition: Article.php:224
const IGNORE_USER_RIGHTS
Definition: User.php:83
static tags( $element, $attribs, $contents)
Same as Xml::element(), but does not escape contents.
Definition: Xml.php:130
static closeElement( $element)
Returns "</$element>".
Definition: Html.php:315
string $edittime
Timestamp of the latest revision of the page when editing was initiated on the client.
Definition: EditPage.php:359
showSummaryInput( $isSubjectPreview, $summary="")
Definition: EditPage.php:3351
showEditForm( $formCallback=null)
Send the edit form and related headers to OutputPage.
Definition: EditPage.php:2815
static loadFromTitle( $db, $title, $id=0)
Load either the current, or a specified, revision that&#39;s attached to a given page.
Definition: Revision.php:279
initialiseForm()
Initialise form fields in the object Called on the first invocation, e.g.
Definition: EditPage.php:1137
wasDeletedSinceLastEdit()
Check if a page was deleted while the user was editing it, before submit.
Definition: EditPage.php:3859
isSectionEditSupported()
Returns whether section editing is supported for the current page.
Definition: EditPage.php:898
getEditFormHtmlAfterContent()
Content to go in the edit form after textbox1.
bool $firsttime
True the first time the edit form is rendered, false after re-rendering with diff, save prompts, etc.
Definition: EditPage.php:260
isPageExistingAndViewable( $title, User $user)
Verify if a given title exists and the given user is allowed to view it.
Definition: EditPage.php:1559
showFormAfterText()
Definition: EditPage.php:3407
showDiff()
Get a diff between the current contents of the edit box and the version of the page we&#39;re editing fro...
Definition: EditPage.php:3562
bool $isNew
New page or new section.
Definition: EditPage.php:248
$wgRightsText
If either $wgRightsUrl or $wgRightsPage is specified then this variable gives the text for the link...
previewOnOpen()
Should we show a preview when the edit form is first shown?
Definition: EditPage.php:836
const EDITFORM_ID
HTML id and name for the beginning of the edit form.
Definition: EditPage.php:198
const NS_FILE
Definition: Defines.php:66
getCopywarn()
Get the copyright warning.
Definition: EditPage.php:3667
bool $allowSelfRedirect
Definition: EditPage.php:299
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don&#39;t need a full Title object...
Definition: SpecialPage.php:83
const AS_SUCCESS_NEW_ARTICLE
Status: Article successfully created.
Definition: EditPage.php:61
Show an error when the user tries to do something whilst blocked.
isOK()
Returns whether the operation completed.
const AS_IMAGE_REDIRECT_LOGGED
Status: logged in user is not allowed to upload (User::isAllowed(&#39;upload&#39;) == false) ...
Definition: EditPage.php:159
$wgDisableAnonTalk
Disable links to talk pages of anonymous users (IPs) in listings on special pages like page history...
static getForTitle(Title $title)
Returns the appropriate ContentHandler singleton for the given title.
getPreviewParserOptions()
Get parser options for a preview.
Definition: EditPage.php:4097
getCancelLink()
Definition: EditPage.php:3821
bool int $contentLength
Definition: EditPage.php:441
const NS_MEDIAWIKI
Definition: Defines.php:68
showTextbox1( $customAttribs=null, $textoverride=null)
Method to output wpTextbox1 The $textoverride method can be used by subclasses overriding showContent...
Definition: EditPage.php:3447
incrementEditFailureStats( $failureType)
Definition: EditPage.php:4088
const AS_END
Status: WikiPage::doEdit() was unsuccessful.
Definition: EditPage.php:144
getSummaryPreview( $isSubjectPreview, $summary="")
Definition: EditPage.php:3377
makeTemplatesOnThisPageList(array $templates)
Wrapper around TemplatesOnThisPageFormatter to make a "templates on this page" list.
Definition: EditPage.php:3086
showPreview( $text)
Append preview output to OutputPage.
Definition: EditPage.php:3541
string $textbox1
Page content input field.
Definition: EditPage.php:342
static warningBox( $html, $className='')
Return a warning box.
Definition: Html.php:726
CONTENT_MODEL_JAVASCRIPT
Allow users to upload files.
const DELETED_USER
Definition: LogPage.php:36
importFormData(&$request)
This function collects the form data and uses it to populate various member variables.
Definition: EditPage.php:908
noSuchSectionPage()
Creates a basic error page which informs the user that they have attempted to edit a nonexistent sect...
Definition: EditPage.php:4379
$wgSummarySpamRegex
Same as the above except for edit summaries.
displayViewSourcePage(Content $content, $errorMessage='')
Display a read-only View Source page.
Definition: EditPage.php:783
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:584
ParserOutput $mParserOutput
Definition: EditPage.php:308
bool $mShowSummaryField
Definition: EditPage.php:317
string $sectiontitle
Definition: EditPage.php:378
string null $unicodeCheck
What the user submitted in the &#39;wpUnicodeCheck&#39; field.
Definition: EditPage.php:461
bool $minoredit
Definition: EditPage.php:331
bool $enableApiEditOverride
Set in ApiEditPage, based on ContentHandler::allowsDirectApiEditing.
Definition: EditPage.php:446
static closeElement( $element)
Shortcut to close an XML element.
Definition: Xml.php:117
int $parentRevId
Revision ID the edit is based on, adjusted when an edit conflict is resolved.
Definition: EditPage.php:397
doPreviewParse(Content $content)
Parse the page for a preview.
Definition: EditPage.php:4120
static matchSpamRegex( $text)
Check given input text against $wgSpamRegex, and return the text of the first match.
Definition: EditPage.php:2506
string $action
Definition: EditPage.php:239
newTextConflictHelper( $submitButtonLabel)
Definition: EditPage.php:4612
bool $deletedSinceEdit
Definition: EditPage.php:251
wfReadOnlyReason()
Check if the site is in read-only mode and return the message if so.
static getStore()
showCustomIntro()
Attempt to show a custom editing introduction, if supplied.
Definition: EditPage.php:2730
static matchSummarySpamRegex( $text)
Check given input text against $wgSummarySpamRegex, and return the text of the first match...
Definition: EditPage.php:2520
importContentFormData(&$request)
Subpage overridable method for extracting the page content data from the posted form to be placed in ...
Definition: EditPage.php:1128
const EDIT_NEW
Definition: Defines.php:132
Revision bool null $mBaseRevision
A revision object corresponding to $this->editRevId.
Definition: EditPage.php:314
newSectionSummary(&$sectionanchor=null)
Return the summary to be used for a new section.
Definition: EditPage.php:1859
const AS_RATE_LIMITED
Status: rate limiter for action &#39;edit&#39; was tripped.
Definition: EditPage.php:101
getBaseRevision()
Returns the revision that was current at the time editing was initiated on the client, even if the edit was based on an old revision.
Definition: EditPage.php:2489
static hasDifferencesOutsideMainSlot(Revision $a, Revision $b)
Helper method for checking whether two revisions have differences that go beyond the main slot...
Definition: WikiPage.php:1537
addNewLineAtEnd( $wikitext)
Definition: EditPage.php:4554
Variant of the Message class.
Definition: RawMessage.php:34
runPostMergeFilters(Content $content, Status $status, User $user)
Run hooks that can filter edits just before they get saved.
Definition: EditPage.php:1789
const AS_MAX_ARTICLE_SIZE_EXCEEDED
Status: article is too big (> $wgMaxArticleSize), after merging in the new section.
Definition: EditPage.php:139
static newFromUser( $user)
Get a ParserOptions object from a given user.
wfDebugLog( $logGroup, $text, $dest='all', array $context=[])
Send a line to a supplementary debug log file, if configured, or main debug log if not...
incrementResolvedConflicts()
Log when a page was successfully saved after the edit conflict view.
Definition: EditPage.php:1633
getCurrentContent()
Get the current content of the page.
Definition: EditPage.php:1432
updateWatchlist()
Register the change of watch status.
Definition: EditPage.php:2415
static hidden( $name, $value, array $attribs=[])
Convenience function to produce an input element with type=hidden.
Definition: Html.php:802
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
string $hookError
Definition: EditPage.php:305
showEditTools()
Inserts optional text shown below edit and upload forms.
Definition: EditPage.php:3655
bool $preview
Definition: EditPage.php:325
const AS_IMAGE_REDIRECT_ANON
Status: anonymous user is not allowed to upload (User::isAllowed(&#39;upload&#39;) == false) ...
Definition: EditPage.php:154
getCheckboxesDefinition( $checked)
Return an array of checkbox definitions.
Definition: EditPage.php:4195
showStandardInputs(&$tabindex=2)
Definition: EditPage.php:3753
preSaveTransform(Title $title, User $user, ParserOptions $parserOptions)
Returns a Content object with pre-save transformations applied (or this object if no transformations ...
const AS_TEXTBOX_EMPTY
Status: user tried to create a new section without content.
Definition: EditPage.php:134
getContent( $audience=RevisionRecord::FOR_PUBLIC, User $user=null)
Get the content of the current revision.
Definition: WikiPage.php:820
Show an error when a user tries to do something they do not have the necessary permissions for...
static element( $element, $attribs=null, $contents='', $allowShortTag=true)
Format an XML element with given attributes and, optionally, text content.
Definition: Xml.php:41
getErrors()
Get the list of errors.
setPostEditCookie( $statusValue)
Sets post-edit cookie indicating the user just saved a particular revision.
Definition: EditPage.php:1594
getRedirectTarget()
If this page is a redirect, get its target.
Definition: WikiPage.php:999
bool $mTriedSave
Definition: EditPage.php:272
const CONTENT_MODEL_CSS
Definition: Defines.php:217
$mPreloadContent
Definition: EditPage.php:429
getContext()
Definition: EditPage.php:510
showConflict()
Show an edit conflict.
Definition: EditPage.php:3803
addTalkPageText()
Definition: EditPage.php:4446
bool $hasPresetSummary
Has a summary been preset using GET parameter &summary= ?
Definition: EditPage.php:311
getEditConflictHelper()
Definition: EditPage.php:4597
const POST_EDIT_COOKIE_KEY_PREFIX
Prefix of key for cookie used to pass post-edit state.
Definition: EditPage.php:204
bool $diff
Definition: EditPage.php:328
const AS_BLANK_ARTICLE
Status: user tried to create a blank page and wpIgnoreBlankArticle == false.
Definition: EditPage.php:118
addHTML( $text)
Append $text to the body HTML.
setEditConflictHelperFactory(callable $factory)
Set a factory function to create an EditConflictHelper.
Definition: EditPage.php:4589
string $starttime
Timestamp from the first time the edit form was rendered.
Definition: EditPage.php:383
string $formtype
Definition: EditPage.php:254
string $section
Definition: EditPage.php:375
getSummaryInputWidget( $summary="", $labelText=null, $inputAttrs=null)
Builds a standard summary input with a label.
Definition: EditPage.php:3318
const DB_REPLICA
Definition: defines.php:25
static makeInternalOrExternalUrl( $name)
If url string starts with http, consider as external URL, else internal.
Definition: Skin.php:1224
$content
Definition: router.php:78
formatStatusErrors(Status $status)
Wrap status errors in an errorbox for increased visibility.
Definition: EditPage.php:1839
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:525
getTitle()
Definition: EditPage.php:518
bool $mTokenOkExceptSuffix
Definition: EditPage.php:269
if(isset( $_SERVER['PATH_INFO']) && $_SERVER['PATH_INFO'] !='') $wgTitle
Definition: api.php:53
static checkLabel( $label, $name, $id, $checked=false, $attribs=[])
Convenience function to build an HTML checkbox with a label.
Definition: Xml.php:419
wfArrayDiff2( $a, $b)
Like array_diff( $a, $b ) except that it works with two-dimensional arrays.
const AS_CONFLICT_DETECTED
Status: (non-resolvable) edit conflict.
Definition: EditPage.php:123
isSupportedContentModel( $modelId)
Returns if the given content model is editable.
Definition: EditPage.php:556
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
setApiEditOverride( $enableOverride)
Allow editing of content that supports API direct editing, but not general direct editing...
Definition: EditPage.php:567
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition: User.php:536
$suppressIntro
Definition: EditPage.php:435
getOriginalContent(User $user)
Get the content of the wanted revision, without section extraction.
Definition: EditPage.php:1391
static newFromId( $id, $flags=0)
Load a page revision from a given revision ID number.
Definition: Revision.php:120
bool $selfRedirect
Definition: EditPage.php:296
bool $incompleteForm
Definition: EditPage.php:275
bool $edit
Definition: EditPage.php:438
const AS_READ_ONLY_PAGE_LOGGED
Status: this logged in user is not allowed to edit this page.
Definition: EditPage.php:91
static makeContent( $text, Title $title=null, $modelId=null, $format=null)
Convenience function for creating a Content object from a given textual representation.
const AS_PARSE_ERROR
Status: can&#39;t parse content.
Definition: EditPage.php:182
const NS_USER_TALK
Definition: Defines.php:63
getSubmitButtonLabel()
Get the message key of the label for the button to save the page.
Definition: EditPage.php:4286
showTextbox( $text, $name, $customAttribs=[])
Definition: EditPage.php:3479
const AS_SELF_REDIRECT
Status: user tried to create self-redirect (redirect to the same article) and wpIgnoreSelfRedirect ==...
Definition: EditPage.php:171
setContentModel( $model)
Set a proposed content model for the page for permissions checking.
Definition: Title.php:1097
$editFormTextBeforeContent
Definition: EditPage.php:423
null array $changeTags
Definition: EditPage.php:415
setTextboxes( $yourtext, $storedversion)
isWrongCaseUserConfigPage()
Checks whether the user entered a skin name in uppercase, e.g.
Definition: EditPage.php:877
Show an error when the user hits a rate limit.
static plaintextParam( $plaintext)
Definition: Message.php:1108
return true
Definition: router.php:92
static getEditToolbar()
Allow extensions to provide a toolbar.
Definition: EditPage.php:4166
static commentBlock( $comment, $title=null, $local=false, $wikiId=null, $useParentheses=true)
Wrap a comment in standard punctuation and formatting if it&#39;s non-empty, otherwise return empty strin...
Definition: Linker.php:1543
string $contentModel
Definition: EditPage.php:409
Exception representing a failure to serialize or unserialize a content object.
tokenOk(&$request)
Make sure the form isn&#39;t faking a user&#39;s credentials.
Definition: EditPage.php:1572
string $editFormPageTop
Before even the preview.
Definition: EditPage.php:421
const AS_CHANGE_TAG_ERROR
Status: an error relating to change tagging.
Definition: EditPage.php:177
guessSectionName( $text)
Turns section name wikitext into anchors for use in HTTP redirects.
Definition: EditPage.php:4568
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
serialize( $format=null)
Convenience method for serializing this Content object.
__construct(Article $article)
Definition: EditPage.php:478
$editFormTextTop
Definition: EditPage.php:422
static showLogExtract(&$out, $types=[], $page='', $user='', $param=[])
Show log extract.
$matches
null $scrolltop
Definition: EditPage.php:403
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition: Title.php:317