MediaWiki  master
EditPage.php
Go to the documentation of this file.
1 <?php
28 
44 class EditPage {
48  const UNICODE_CHECK = 'ℳ𝒲♥𝓊𝓃𝒾𝒸ℴ𝒹ℯ';
49 
53  const AS_SUCCESS_UPDATE = 200;
54 
59 
63  const AS_HOOK_ERROR = 210;
64 
69 
74 
78  const AS_CONTENT_TOO_BIG = 216;
79 
84 
89 
93  const AS_READ_ONLY_PAGE = 220;
94 
98  const AS_RATE_LIMITED = 221;
99 
105 
111 
115  const AS_BLANK_ARTICLE = 224;
116 
120  const AS_CONFLICT_DETECTED = 225;
121 
126  const AS_SUMMARY_NEEDED = 226;
127 
131  const AS_TEXTBOX_EMPTY = 228;
132 
137 
141  const AS_END = 231;
142 
146  const AS_SPAM_ERROR = 232;
147 
152 
157 
163 
168  const AS_SELF_REDIRECT = 236;
169 
174  const AS_CHANGE_TAG_ERROR = 237;
175 
179  const AS_PARSE_ERROR = 240;
180 
186 
191 
195  const EDITFORM_ID = 'editform';
196 
201  const POST_EDIT_COOKIE_KEY_PREFIX = 'PostEditRevision';
202 
217 
222  public $mArticle;
224  private $page;
225 
230  public $mTitle;
231 
233  private $mContextTitle = null;
234 
236  public $action = 'submit';
237 
242  public $isConflict = false;
243 
245  public $isNew = false;
246 
249 
251  public $formtype;
252 
257  public $firsttime;
258 
260  public $lastDelete;
261 
263  public $mTokenOk = false;
264 
266  public $mTokenOkExceptSuffix = false;
267 
269  public $mTriedSave = false;
270 
272  public $incompleteForm = false;
273 
275  public $tooBig = false;
276 
278  public $missingComment = false;
279 
281  public $missingSummary = false;
282 
284  public $allowBlankSummary = false;
285 
287  protected $blankArticle = false;
288 
290  protected $allowBlankArticle = false;
291 
293  protected $selfRedirect = false;
294 
296  protected $allowSelfRedirect = false;
297 
299  public $autoSumm = '';
300 
302  public $hookError = '';
303 
306 
308  public $hasPresetSummary = false;
309 
311  public $mBaseRevision = false;
312 
314  public $mShowSummaryField = true;
315 
316  # Form values
317 
319  public $save = false;
320 
322  public $preview = false;
323 
325  public $diff = false;
326 
328  public $minoredit = false;
329 
331  public $watchthis = false;
332 
334  public $recreate = false;
335 
339  public $textbox1 = '';
340 
342  public $textbox2 = '';
343 
345  public $summary = '';
346 
350  public $nosummary = false;
351 
356  public $edittime = '';
357 
369  private $editRevId = null;
370 
372  public $section = '';
373 
375  public $sectiontitle = '';
376 
380  public $starttime = '';
381 
387  public $oldid = 0;
388 
394  public $parentRevId = 0;
395 
397  public $editintro = '';
398 
400  public $scrolltop = null;
401 
403  public $bot = true;
404 
407 
410 
412  private $changeTags = null;
413 
414  # Placeholders for text injection by hooks (must be HTML)
415  # extensions should take care to _append_ to the present value
416 
418  public $editFormPageTop = '';
419  public $editFormTextTop = '';
423  public $editFormTextBottom = '';
427 
428  /* $didSave should be set to true whenever an article was successfully altered. */
429  public $didSave = false;
430  public $undidRev = 0;
431 
432  public $suppressIntro = false;
433 
435  protected $edit;
436 
438  protected $contentLength = false;
439 
443  private $enableApiEditOverride = false;
444 
448  protected $context;
449 
453  private $isOldRev = false;
454 
458  private $unicodeCheck;
459 
466 
471 
475  public function __construct( Article $article ) {
476  $this->mArticle = $article;
477  $this->page = $article->getPage(); // model object
478  $this->mTitle = $article->getTitle();
479 
480  // Make sure the local context is in sync with other member variables.
481  // Particularly make sure everything is using the same WikiPage instance.
482  // This should probably be the case in Article as well, but it's
483  // particularly important for EditPage, to make use of the in-place caching
484  // facility in WikiPage::prepareContentForEdit.
485  $this->context = new DerivativeContext( $article->getContext() );
486  $this->context->setWikiPage( $this->page );
487  $this->context->setTitle( $this->mTitle );
488 
489  $this->contentModel = $this->mTitle->getContentModel();
490 
491  $handler = ContentHandler::getForModelID( $this->contentModel );
492  $this->contentFormat = $handler->getDefaultFormat();
493  $this->editConflictHelperFactory = [ $this, 'newTextConflictHelper' ];
494  }
495 
499  public function getArticle() {
500  return $this->mArticle;
501  }
502 
507  public function getContext() {
508  return $this->context;
509  }
510 
515  public function getTitle() {
516  return $this->mTitle;
517  }
518 
524  public function setContextTitle( $title ) {
525  $this->mContextTitle = $title;
526  }
527 
536  public function getContextTitle() {
537  if ( is_null( $this->mContextTitle ) ) {
538  wfDeprecated( __METHOD__ . ' called with no title set', '1.32' );
539  global $wgTitle;
540  return $wgTitle;
541  } else {
542  return $this->mContextTitle;
543  }
544  }
545 
553  public function isSupportedContentModel( $modelId ) {
554  return $this->enableApiEditOverride === true ||
555  ContentHandler::getForModelID( $modelId )->supportsDirectEditing();
556  }
557 
564  public function setApiEditOverride( $enableOverride ) {
565  $this->enableApiEditOverride = $enableOverride;
566  }
567 
571  public function submit() {
572  wfDeprecated( __METHOD__, '1.29' );
573  $this->edit();
574  }
575 
587  public function edit() {
588  // Allow extensions to modify/prevent this form or submission
589  if ( !Hooks::run( 'AlternateEdit', [ $this ] ) ) {
590  return;
591  }
592 
593  wfDebug( __METHOD__ . ": enter\n" );
594 
595  $request = $this->context->getRequest();
596  // If they used redlink=1 and the page exists, redirect to the main article
597  if ( $request->getBool( 'redlink' ) && $this->mTitle->exists() ) {
598  $this->context->getOutput()->redirect( $this->mTitle->getFullURL() );
599  return;
600  }
601 
602  $this->importFormData( $request );
603  $this->firsttime = false;
604 
605  if ( wfReadOnly() && $this->save ) {
606  // Force preview
607  $this->save = false;
608  $this->preview = true;
609  }
610 
611  if ( $this->save ) {
612  $this->formtype = 'save';
613  } elseif ( $this->preview ) {
614  $this->formtype = 'preview';
615  } elseif ( $this->diff ) {
616  $this->formtype = 'diff';
617  } else { # First time through
618  $this->firsttime = true;
619  if ( $this->previewOnOpen() ) {
620  $this->formtype = 'preview';
621  } else {
622  $this->formtype = 'initial';
623  }
624  }
625 
626  $permErrors = $this->getEditPermissionErrors( $this->save ? 'secure' : 'full' );
627  if ( $permErrors ) {
628  wfDebug( __METHOD__ . ": User can't edit\n" );
629 
630  if ( $this->context->getUser()->getBlock() ) {
631  // track block with a cookie if it doesn't exists already
632  $this->context->getUser()->trackBlockWithCookie();
633 
634  // Auto-block user's IP if the account was "hard" blocked
635  if ( !wfReadOnly() ) {
637  $this->context->getUser()->spreadAnyEditBlock();
638  } );
639  }
640 
641  $config = $this->context->getConfig();
642  if ( $config->get( 'EnableBlockNoticeStats' ) ) {
643  $wiki = $config->get( 'DBname' );
644  $statsd = MediaWikiServices::getInstance()->getStatsdDataFactory();
645  $statsd->increment( 'BlockNotices.' . $wiki . '.WikitextEditor.shown' );
646  }
647  }
648  $this->displayPermissionsError( $permErrors );
649 
650  return;
651  }
652 
653  $revision = $this->mArticle->getRevisionFetched();
654  // Disallow editing revisions with content models different from the current one
655  // Undo edits being an exception in order to allow reverting content model changes.
656  if ( $revision
657  && $revision->getContentModel() !== $this->contentModel
658  ) {
659  $prevRev = null;
660  if ( $this->undidRev ) {
661  $undidRevObj = Revision::newFromId( $this->undidRev );
662  $prevRev = $undidRevObj ? $undidRevObj->getPrevious() : null;
663  }
664  if ( !$this->undidRev
665  || !$prevRev
666  || $prevRev->getContentModel() !== $this->contentModel
667  ) {
668  $this->displayViewSourcePage(
669  $this->getContentObject(),
670  $this->context->msg(
671  'contentmodelediterror',
672  $revision->getContentModel(),
674  )->plain()
675  );
676  return;
677  }
678  }
679 
680  $this->isConflict = false;
681 
682  # Show applicable editing introductions
683  if ( $this->formtype == 'initial' || $this->firsttime ) {
684  $this->showIntro();
685  }
686 
687  # Attempt submission here. This will check for edit conflicts,
688  # and redundantly check for locked database, blocked IPs, etc.
689  # that edit() already checked just in case someone tries to sneak
690  # in the back door with a hand-edited submission URL.
691 
692  if ( $this->formtype == 'save' ) {
693  $resultDetails = null;
694  $status = $this->attemptSave( $resultDetails );
695  if ( !$this->handleStatus( $status, $resultDetails ) ) {
696  return;
697  }
698  }
699 
700  # First time through: get contents, set time for conflict
701  # checking, etc.
702  if ( $this->formtype == 'initial' || $this->firsttime ) {
703  if ( $this->initialiseForm() === false ) {
704  $out = $this->context->getOutput();
705  if ( $out->getRedirect() === '' ) { // mcrundo hack redirects, don't override it
706  $this->noSuchSectionPage();
707  }
708  return;
709  }
710 
711  if ( !$this->mTitle->getArticleID() ) {
712  Hooks::run( 'EditFormPreloadText', [ &$this->textbox1, &$this->mTitle ] );
713  } else {
714  Hooks::run( 'EditFormInitialText', [ $this ] );
715  }
716 
717  }
718 
719  $this->showEditForm();
720  }
721 
726  protected function getEditPermissionErrors( $rigor = 'secure' ) {
727  $user = $this->context->getUser();
728  $permErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $user, $rigor );
729  # Can this title be created?
730  if ( !$this->mTitle->exists() ) {
731  $permErrors = array_merge(
732  $permErrors,
733  wfArrayDiff2(
734  $this->mTitle->getUserPermissionsErrors( 'create', $user, $rigor ),
735  $permErrors
736  )
737  );
738  }
739  # Ignore some permissions errors when a user is just previewing/viewing diffs
740  $remove = [];
741  foreach ( $permErrors as $error ) {
742  if ( ( $this->preview || $this->diff )
743  && (
744  $error[0] == 'blockedtext' ||
745  $error[0] == 'autoblockedtext' ||
746  $error[0] == 'systemblockedtext'
747  )
748  ) {
749  $remove[] = $error;
750  }
751  }
752  $permErrors = wfArrayDiff2( $permErrors, $remove );
753 
754  return $permErrors;
755  }
756 
770  protected function displayPermissionsError( array $permErrors ) {
771  $out = $this->context->getOutput();
772  if ( $this->context->getRequest()->getBool( 'redlink' ) ) {
773  // The edit page was reached via a red link.
774  // Redirect to the article page and let them click the edit tab if
775  // they really want a permission error.
776  $out->redirect( $this->mTitle->getFullURL() );
777  return;
778  }
779 
780  $content = $this->getContentObject();
781 
782  # Use the normal message if there's nothing to display
783  if ( $this->firsttime && ( !$content || $content->isEmpty() ) ) {
784  $action = $this->mTitle->exists() ? 'edit' :
785  ( $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage' );
786  throw new PermissionsError( $action, $permErrors );
787  }
788 
789  $this->displayViewSourcePage(
790  $content,
791  $out->formatPermissionsErrorMessage( $permErrors, 'edit' )
792  );
793  }
794 
800  protected function displayViewSourcePage( Content $content, $errorMessage = '' ) {
801  $out = $this->context->getOutput();
802  Hooks::run( 'EditPage::showReadOnlyForm:initial', [ $this, &$out ] );
803 
804  $out->setRobotPolicy( 'noindex,nofollow' );
805  $out->setPageTitle( $this->context->msg(
806  'viewsource-title',
807  $this->getContextTitle()->getPrefixedText()
808  ) );
809  $out->addBacklinkSubtitle( $this->getContextTitle() );
810  $out->addHTML( $this->editFormPageTop );
811  $out->addHTML( $this->editFormTextTop );
812 
813  if ( $errorMessage !== '' ) {
814  $out->addWikiTextAsInterface( $errorMessage );
815  $out->addHTML( "<hr />\n" );
816  }
817 
818  # If the user made changes, preserve them when showing the markup
819  # (This happens when a user is blocked during edit, for instance)
820  if ( !$this->firsttime ) {
821  $text = $this->textbox1;
822  $out->addWikiMsg( 'viewyourtext' );
823  } else {
824  try {
825  $text = $this->toEditText( $content );
826  } catch ( MWException $e ) {
827  # Serialize using the default format if the content model is not supported
828  # (e.g. for an old revision with a different model)
829  $text = $content->serialize();
830  }
831  $out->addWikiMsg( 'viewsourcetext' );
832  }
833 
834  $out->addHTML( $this->editFormTextBeforeContent );
835  $this->showTextbox( $text, 'wpTextbox1', [ 'readonly' ] );
836  $out->addHTML( $this->editFormTextAfterContent );
837 
838  $out->addHTML( $this->makeTemplatesOnThisPageList( $this->getTemplates() ) );
839 
840  $out->addModules( 'mediawiki.action.edit.collapsibleFooter' );
841 
842  $out->addHTML( $this->editFormTextBottom );
843  if ( $this->mTitle->exists() ) {
844  $out->returnToMain( null, $this->mTitle );
845  }
846  }
847 
853  protected function previewOnOpen() {
854  $config = $this->context->getConfig();
855  $previewOnOpenNamespaces = $config->get( 'PreviewOnOpenNamespaces' );
856  $request = $this->context->getRequest();
857  if ( $config->get( 'RawHtml' ) ) {
858  // If raw HTML is enabled, disable preview on open
859  // since it has to be posted with a token for
860  // security reasons
861  return false;
862  }
863  if ( $request->getVal( 'preview' ) == 'yes' ) {
864  // Explicit override from request
865  return true;
866  } elseif ( $request->getVal( 'preview' ) == 'no' ) {
867  // Explicit override from request
868  return false;
869  } elseif ( $this->section == 'new' ) {
870  // Nothing *to* preview for new sections
871  return false;
872  } elseif ( ( $request->getVal( 'preload' ) !== null || $this->mTitle->exists() )
873  && $this->context->getUser()->getOption( 'previewonfirst' )
874  ) {
875  // Standard preference behavior
876  return true;
877  } elseif ( !$this->mTitle->exists()
878  && isset( $previewOnOpenNamespaces[$this->mTitle->getNamespace()] )
879  && $previewOnOpenNamespaces[$this->mTitle->getNamespace()]
880  ) {
881  // Categories are special
882  return true;
883  } else {
884  return false;
885  }
886  }
887 
894  protected function isWrongCaseUserConfigPage() {
895  if ( $this->mTitle->isUserConfigPage() ) {
896  $name = $this->mTitle->getSkinFromConfigSubpage();
897  $skins = array_merge(
898  array_keys( Skin::getSkinNames() ),
899  [ 'common' ]
900  );
901  return !in_array( $name, $skins )
902  && in_array( strtolower( $name ), $skins );
903  } else {
904  return false;
905  }
906  }
907 
915  protected function isSectionEditSupported() {
916  $contentHandler = ContentHandler::getForTitle( $this->mTitle );
917  return $contentHandler->supportsSections();
918  }
919 
925  public function importFormData( &$request ) {
926  # Section edit can come from either the form or a link
927  $this->section = $request->getVal( 'wpSection', $request->getVal( 'section' ) );
928 
929  if ( $this->section !== null && $this->section !== '' && !$this->isSectionEditSupported() ) {
930  throw new ErrorPageError( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
931  }
932 
933  $this->isNew = !$this->mTitle->exists() || $this->section == 'new';
934 
935  if ( $request->wasPosted() ) {
936  # These fields need to be checked for encoding.
937  # Also remove trailing whitespace, but don't remove _initial_
938  # whitespace from the text boxes. This may be significant formatting.
939  $this->textbox1 = rtrim( $request->getText( 'wpTextbox1' ) );
940  if ( !$request->getCheck( 'wpTextbox2' ) ) {
941  // Skip this if wpTextbox2 has input, it indicates that we came
942  // from a conflict page with raw page text, not a custom form
943  // modified by subclasses
945  if ( $textbox1 !== null ) {
946  $this->textbox1 = $textbox1;
947  }
948  }
949 
950  $this->unicodeCheck = $request->getText( 'wpUnicodeCheck' );
951 
952  $this->summary = $request->getText( 'wpSummary' );
953 
954  # If the summary consists of a heading, e.g. '==Foobar==', extract the title from the
955  # header syntax, e.g. 'Foobar'. This is mainly an issue when we are using wpSummary for
956  # section titles.
957  $this->summary = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->summary );
958 
959  # Treat sectiontitle the same way as summary.
960  # Note that wpSectionTitle is not yet a part of the actual edit form, as wpSummary is
961  # currently doing double duty as both edit summary and section title. Right now this
962  # is just to allow API edits to work around this limitation, but this should be
963  # incorporated into the actual edit form when EditPage is rewritten (T20654, T28312).
964  $this->sectiontitle = $request->getText( 'wpSectionTitle' );
965  $this->sectiontitle = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->sectiontitle );
966 
967  $this->edittime = $request->getVal( 'wpEdittime' );
968  $this->editRevId = $request->getIntOrNull( 'editRevId' );
969  $this->starttime = $request->getVal( 'wpStarttime' );
970 
971  $undidRev = $request->getInt( 'wpUndidRevision' );
972  if ( $undidRev ) {
973  $this->undidRev = $undidRev;
974  }
975 
976  $this->scrolltop = $request->getIntOrNull( 'wpScrolltop' );
977 
978  if ( $this->textbox1 === '' && $request->getVal( 'wpTextbox1' ) === null ) {
979  // wpTextbox1 field is missing, possibly due to being "too big"
980  // according to some filter rules such as Suhosin's setting for
981  // suhosin.request.max_value_length (d'oh)
982  $this->incompleteForm = true;
983  } else {
984  // If we receive the last parameter of the request, we can fairly
985  // claim the POST request has not been truncated.
986  $this->incompleteForm = !$request->getVal( 'wpUltimateParam' );
987  }
988  if ( $this->incompleteForm ) {
989  # If the form is incomplete, force to preview.
990  wfDebug( __METHOD__ . ": Form data appears to be incomplete\n" );
991  wfDebug( "POST DATA: " . var_export( $request->getPostValues(), true ) . "\n" );
992  $this->preview = true;
993  } else {
994  $this->preview = $request->getCheck( 'wpPreview' );
995  $this->diff = $request->getCheck( 'wpDiff' );
996 
997  // Remember whether a save was requested, so we can indicate
998  // if we forced preview due to session failure.
999  $this->mTriedSave = !$this->preview;
1000 
1001  if ( $this->tokenOk( $request ) ) {
1002  # Some browsers will not report any submit button
1003  # if the user hits enter in the comment box.
1004  # The unmarked state will be assumed to be a save,
1005  # if the form seems otherwise complete.
1006  wfDebug( __METHOD__ . ": Passed token check.\n" );
1007  } elseif ( $this->diff ) {
1008  # Failed token check, but only requested "Show Changes".
1009  wfDebug( __METHOD__ . ": Failed token check; Show Changes requested.\n" );
1010  } else {
1011  # Page might be a hack attempt posted from
1012  # an external site. Preview instead of saving.
1013  wfDebug( __METHOD__ . ": Failed token check; forcing preview\n" );
1014  $this->preview = true;
1015  }
1016  }
1017  $this->save = !$this->preview && !$this->diff;
1018  if ( !preg_match( '/^\d{14}$/', $this->edittime ) ) {
1019  $this->edittime = null;
1020  }
1021 
1022  if ( !preg_match( '/^\d{14}$/', $this->starttime ) ) {
1023  $this->starttime = null;
1024  }
1025 
1026  $this->recreate = $request->getCheck( 'wpRecreate' );
1027 
1028  $this->minoredit = $request->getCheck( 'wpMinoredit' );
1029  $this->watchthis = $request->getCheck( 'wpWatchthis' );
1030 
1031  $user = $this->context->getUser();
1032  # Don't force edit summaries when a user is editing their own user or talk page
1033  if ( ( $this->mTitle->mNamespace == NS_USER || $this->mTitle->mNamespace == NS_USER_TALK )
1034  && $this->mTitle->getText() == $user->getName()
1035  ) {
1036  $this->allowBlankSummary = true;
1037  } else {
1038  $this->allowBlankSummary = $request->getBool( 'wpIgnoreBlankSummary' )
1039  || !$user->getOption( 'forceeditsummary' );
1040  }
1041 
1042  $this->autoSumm = $request->getText( 'wpAutoSummary' );
1043 
1044  $this->allowBlankArticle = $request->getBool( 'wpIgnoreBlankArticle' );
1045  $this->allowSelfRedirect = $request->getBool( 'wpIgnoreSelfRedirect' );
1046 
1047  $changeTags = $request->getVal( 'wpChangeTags' );
1048  if ( is_null( $changeTags ) || $changeTags === '' ) {
1049  $this->changeTags = [];
1050  } else {
1051  $this->changeTags = array_filter( array_map( 'trim', explode( ',',
1052  $changeTags ) ) );
1053  }
1054  } else {
1055  # Not a posted form? Start with nothing.
1056  wfDebug( __METHOD__ . ": Not a posted form.\n" );
1057  $this->textbox1 = '';
1058  $this->summary = '';
1059  $this->sectiontitle = '';
1060  $this->edittime = '';
1061  $this->editRevId = null;
1062  $this->starttime = wfTimestampNow();
1063  $this->edit = false;
1064  $this->preview = false;
1065  $this->save = false;
1066  $this->diff = false;
1067  $this->minoredit = false;
1068  // Watch may be overridden by request parameters
1069  $this->watchthis = $request->getBool( 'watchthis', false );
1070  $this->recreate = false;
1071 
1072  // When creating a new section, we can preload a section title by passing it as the
1073  // preloadtitle parameter in the URL (T15100)
1074  if ( $this->section == 'new' && $request->getVal( 'preloadtitle' ) ) {
1075  $this->sectiontitle = $request->getVal( 'preloadtitle' );
1076  // Once wpSummary isn't being use for setting section titles, we should delete this.
1077  $this->summary = $request->getVal( 'preloadtitle' );
1078  } elseif ( $this->section != 'new' && $request->getVal( 'summary' ) !== '' ) {
1079  $this->summary = $request->getText( 'summary' );
1080  if ( $this->summary !== '' ) {
1081  $this->hasPresetSummary = true;
1082  }
1083  }
1084 
1085  if ( $request->getVal( 'minor' ) ) {
1086  $this->minoredit = true;
1087  }
1088  }
1089 
1090  $this->oldid = $request->getInt( 'oldid' );
1091  $this->parentRevId = $request->getInt( 'parentRevId' );
1092 
1093  $this->bot = $request->getBool( 'bot', true );
1094  $this->nosummary = $request->getBool( 'nosummary' );
1095 
1096  // May be overridden by revision.
1097  $this->contentModel = $request->getText( 'model', $this->contentModel );
1098  // May be overridden by revision.
1099  $this->contentFormat = $request->getText( 'format', $this->contentFormat );
1100 
1101  try {
1102  $handler = ContentHandler::getForModelID( $this->contentModel );
1103  } catch ( MWUnknownContentModelException $e ) {
1104  throw new ErrorPageError(
1105  'editpage-invalidcontentmodel-title',
1106  'editpage-invalidcontentmodel-text',
1107  [ wfEscapeWikiText( $this->contentModel ) ]
1108  );
1109  }
1110 
1111  if ( !$handler->isSupportedFormat( $this->contentFormat ) ) {
1112  throw new ErrorPageError(
1113  'editpage-notsupportedcontentformat-title',
1114  'editpage-notsupportedcontentformat-text',
1115  [
1116  wfEscapeWikiText( $this->contentFormat ),
1117  wfEscapeWikiText( ContentHandler::getLocalizedName( $this->contentModel ) )
1118  ]
1119  );
1120  }
1121 
1128  $this->editintro = $request->getText( 'editintro',
1129  // Custom edit intro for new sections
1130  $this->section === 'new' ? 'MediaWiki:addsection-editintro' : '' );
1131 
1132  // Allow extensions to modify form data
1133  Hooks::run( 'EditPage::importFormData', [ $this, $request ] );
1134  }
1135 
1145  protected function importContentFormData( &$request ) {
1146  return; // Don't do anything, EditPage already extracted wpTextbox1
1147  }
1148 
1154  public function initialiseForm() {
1155  $this->edittime = $this->page->getTimestamp();
1156  $this->editRevId = $this->page->getLatest();
1157 
1158  $content = $this->getContentObject( false ); # TODO: track content object?!
1159  if ( $content === false ) {
1160  return false;
1161  }
1162  $this->textbox1 = $this->toEditText( $content );
1163 
1164  $user = $this->context->getUser();
1165  // activate checkboxes if user wants them to be always active
1166  # Sort out the "watch" checkbox
1167  if ( $user->getOption( 'watchdefault' ) ) {
1168  # Watch all edits
1169  $this->watchthis = true;
1170  } elseif ( $user->getOption( 'watchcreations' ) && !$this->mTitle->exists() ) {
1171  # Watch creations
1172  $this->watchthis = true;
1173  } elseif ( $user->isWatched( $this->mTitle ) ) {
1174  # Already watched
1175  $this->watchthis = true;
1176  }
1177  if ( $user->getOption( 'minordefault' ) && !$this->isNew ) {
1178  $this->minoredit = true;
1179  }
1180  if ( $this->textbox1 === false ) {
1181  return false;
1182  }
1183  return true;
1184  }
1185 
1193  protected function getContentObject( $def_content = null ) {
1194  $content = false;
1195 
1196  $user = $this->context->getUser();
1197  $request = $this->context->getRequest();
1198  // For message page not locally set, use the i18n message.
1199  // For other non-existent articles, use preload text if any.
1200  if ( !$this->mTitle->exists() || $this->section == 'new' ) {
1201  if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && $this->section != 'new' ) {
1202  # If this is a system message, get the default text.
1203  $msg = $this->mTitle->getDefaultMessageText();
1204 
1205  $content = $this->toEditContent( $msg );
1206  }
1207  if ( $content === false ) {
1208  # If requested, preload some text.
1209  $preload = $request->getVal( 'preload',
1210  // Custom preload text for new sections
1211  $this->section === 'new' ? 'MediaWiki:addsection-preload' : '' );
1212  $params = $request->getArray( 'preloadparams', [] );
1213 
1214  $content = $this->getPreloadedContent( $preload, $params );
1215  }
1216  // For existing pages, get text based on "undo" or section parameters.
1217  } else {
1218  if ( $this->section != '' ) {
1219  // Get section edit text (returns $def_text for invalid sections)
1220  $orig = $this->getOriginalContent( $user );
1221  $content = $orig ? $orig->getSection( $this->section ) : null;
1222 
1223  if ( !$content ) {
1224  $content = $def_content;
1225  }
1226  } else {
1227  $undoafter = $request->getInt( 'undoafter' );
1228  $undo = $request->getInt( 'undo' );
1229 
1230  if ( $undo > 0 && $undoafter > 0 ) {
1231  $undorev = Revision::newFromId( $undo );
1232  $oldrev = Revision::newFromId( $undoafter );
1233  $undoMsg = null;
1234 
1235  # Sanity check, make sure it's the right page,
1236  # the revisions exist and they were not deleted.
1237  # Otherwise, $content will be left as-is.
1238  if ( !is_null( $undorev ) && !is_null( $oldrev ) &&
1239  !$undorev->isDeleted( Revision::DELETED_TEXT ) &&
1240  !$oldrev->isDeleted( Revision::DELETED_TEXT )
1241  ) {
1242  if ( WikiPage::hasDifferencesOutsideMainSlot( $undorev, $oldrev )
1243  || !$this->isSupportedContentModel( $oldrev->getContentModel() )
1244  ) {
1245  // Hack for undo while EditPage can't handle multi-slot editing
1246  $this->context->getOutput()->redirect( $this->mTitle->getFullURL( [
1247  'action' => 'mcrundo',
1248  'undo' => $undo,
1249  'undoafter' => $undoafter,
1250  ] ) );
1251  return false;
1252  } else {
1253  $content = $this->page->getUndoContent( $undorev, $oldrev );
1254 
1255  if ( $content === false ) {
1256  # Warn the user that something went wrong
1257  $undoMsg = 'failure';
1258  }
1259  }
1260 
1261  if ( $undoMsg === null ) {
1262  $oldContent = $this->page->getContent( Revision::RAW );
1264  $user, MediaWikiServices::getInstance()->getContentLanguage() );
1265  $newContent = $content->preSaveTransform( $this->mTitle, $user, $popts );
1266  if ( $newContent->getModel() !== $oldContent->getModel() ) {
1267  // The undo may change content
1268  // model if its reverting the top
1269  // edit. This can result in
1270  // mismatched content model/format.
1271  $this->contentModel = $newContent->getModel();
1272  $this->contentFormat = $oldrev->getContentFormat();
1273  }
1274 
1275  if ( $newContent->equals( $oldContent ) ) {
1276  # Tell the user that the undo results in no change,
1277  # i.e. the revisions were already undone.
1278  $undoMsg = 'nochange';
1279  $content = false;
1280  } else {
1281  # Inform the user of our success and set an automatic edit summary
1282  $undoMsg = 'success';
1283 
1284  # If we just undid one rev, use an autosummary
1285  $firstrev = $oldrev->getNext();
1286  if ( $firstrev && $firstrev->getId() == $undo ) {
1287  $userText = $undorev->getUserText();
1288  if ( $userText === '' ) {
1289  $undoSummary = $this->context->msg(
1290  'undo-summary-username-hidden',
1291  $undo
1292  )->inContentLanguage()->text();
1293  } else {
1294  $undoSummary = $this->context->msg(
1295  'undo-summary',
1296  $undo,
1297  $userText
1298  )->inContentLanguage()->text();
1299  }
1300  if ( $this->summary === '' ) {
1301  $this->summary = $undoSummary;
1302  } else {
1303  $this->summary = $undoSummary . $this->context->msg( 'colon-separator' )
1304  ->inContentLanguage()->text() . $this->summary;
1305  }
1306  $this->undidRev = $undo;
1307  }
1308  $this->formtype = 'diff';
1309  }
1310  }
1311  } else {
1312  // Failed basic sanity checks.
1313  // Older revisions may have been removed since the link
1314  // was created, or we may simply have got bogus input.
1315  $undoMsg = 'norev';
1316  }
1317 
1318  $out = $this->context->getOutput();
1319  // Messages: undo-success, undo-failure, undo-main-slot-only, undo-norev,
1320  // undo-nochange.
1321  $class = ( $undoMsg == 'success' ? '' : 'error ' ) . "mw-undo-{$undoMsg}";
1322  $this->editFormPageTop .= Html::rawElement(
1323  'div', [ 'class' => $class ],
1324  $out->parseAsInterface(
1325  $this->context->msg( 'undo-' . $undoMsg )->plain()
1326  )
1327  );
1328  }
1329 
1330  if ( $content === false ) {
1331  // Hack for restoring old revisions while EditPage
1332  // can't handle multi-slot editing.
1333 
1334  $curRevision = $this->page->getRevision();
1335  $oldRevision = $this->mArticle->getRevisionFetched();
1336 
1337  if ( $curRevision
1338  && $oldRevision
1339  && $curRevision->getId() !== $oldRevision->getId()
1340  && ( WikiPage::hasDifferencesOutsideMainSlot( $oldRevision, $curRevision )
1341  || !$this->isSupportedContentModel( $oldRevision->getContentModel() ) )
1342  ) {
1343  $this->context->getOutput()->redirect(
1344  $this->mTitle->getFullURL(
1345  [
1346  'action' => 'mcrrestore',
1347  'restore' => $oldRevision->getId(),
1348  ]
1349  )
1350  );
1351 
1352  return false;
1353  }
1354  }
1355 
1356  if ( $content === false ) {
1357  $content = $this->getOriginalContent( $user );
1358  }
1359  }
1360  }
1361 
1362  return $content;
1363  }
1364 
1380  private function getOriginalContent( User $user ) {
1381  if ( $this->section == 'new' ) {
1382  return $this->getCurrentContent();
1383  }
1384  $revision = $this->mArticle->getRevisionFetched();
1385  if ( $revision === null ) {
1386  $handler = ContentHandler::getForModelID( $this->contentModel );
1387  return $handler->makeEmptyContent();
1388  }
1389  $content = $revision->getContent( Revision::FOR_THIS_USER, $user );
1390  return $content;
1391  }
1392 
1405  public function getParentRevId() {
1406  if ( $this->parentRevId ) {
1407  return $this->parentRevId;
1408  } else {
1409  return $this->mArticle->getRevIdFetched();
1410  }
1411  }
1412 
1421  protected function getCurrentContent() {
1422  $rev = $this->page->getRevision();
1423  $content = $rev ? $rev->getContent( Revision::RAW ) : null;
1424 
1425  if ( $content === false || $content === null ) {
1426  $handler = ContentHandler::getForModelID( $this->contentModel );
1427  return $handler->makeEmptyContent();
1428  } elseif ( !$this->undidRev ) {
1429  // Content models should always be the same since we error
1430  // out if they are different before this point (in ->edit()).
1431  // The exception being, during an undo, the current revision might
1432  // differ from the prior revision.
1433  $logger = LoggerFactory::getInstance( 'editpage' );
1434  if ( $this->contentModel !== $rev->getContentModel() ) {
1435  $logger->warning( "Overriding content model from current edit {prev} to {new}", [
1436  'prev' => $this->contentModel,
1437  'new' => $rev->getContentModel(),
1438  'title' => $this->getTitle()->getPrefixedDBkey(),
1439  'method' => __METHOD__
1440  ] );
1441  $this->contentModel = $rev->getContentModel();
1442  }
1443 
1444  // Given that the content models should match, the current selected
1445  // format should be supported.
1446  if ( !$content->isSupportedFormat( $this->contentFormat ) ) {
1447  $logger->warning( "Current revision content format unsupported. Overriding {prev} to {new}", [
1448 
1449  'prev' => $this->contentFormat,
1450  'new' => $rev->getContentFormat(),
1451  'title' => $this->getTitle()->getPrefixedDBkey(),
1452  'method' => __METHOD__
1453  ] );
1454  $this->contentFormat = $rev->getContentFormat();
1455  }
1456  }
1457  return $content;
1458  }
1459 
1467  public function setPreloadedContent( Content $content ) {
1468  $this->mPreloadContent = $content;
1469  }
1470 
1482  protected function getPreloadedContent( $preload, $params = [] ) {
1483  if ( !empty( $this->mPreloadContent ) ) {
1484  return $this->mPreloadContent;
1485  }
1486 
1487  $handler = ContentHandler::getForModelID( $this->contentModel );
1488 
1489  if ( $preload === '' ) {
1490  return $handler->makeEmptyContent();
1491  }
1492 
1493  $user = $this->context->getUser();
1494  $title = Title::newFromText( $preload );
1495  # Check for existence to avoid getting MediaWiki:Noarticletext
1496  if ( $title === null || !$title->exists() || !$title->userCan( 'read', $user ) ) {
1497  // TODO: somehow show a warning to the user!
1498  return $handler->makeEmptyContent();
1499  }
1500 
1502  if ( $page->isRedirect() ) {
1504  # Same as before
1505  if ( $title === null || !$title->exists() || !$title->userCan( 'read', $user ) ) {
1506  // TODO: somehow show a warning to the user!
1507  return $handler->makeEmptyContent();
1508  }
1510  }
1511 
1512  $parserOptions = ParserOptions::newFromUser( $user );
1514 
1515  if ( !$content ) {
1516  // TODO: somehow show a warning to the user!
1517  return $handler->makeEmptyContent();
1518  }
1519 
1520  if ( $content->getModel() !== $handler->getModelID() ) {
1521  $converted = $content->convert( $handler->getModelID() );
1522 
1523  if ( !$converted ) {
1524  // TODO: somehow show a warning to the user!
1525  wfDebug( "Attempt to preload incompatible content: " .
1526  "can't convert " . $content->getModel() .
1527  " to " . $handler->getModelID() );
1528 
1529  return $handler->makeEmptyContent();
1530  }
1531 
1532  $content = $converted;
1533  }
1534 
1535  return $content->preloadTransform( $title, $parserOptions, $params );
1536  }
1537 
1545  public function tokenOk( &$request ) {
1546  $token = $request->getVal( 'wpEditToken' );
1547  $user = $this->context->getUser();
1548  $this->mTokenOk = $user->matchEditToken( $token );
1549  $this->mTokenOkExceptSuffix = $user->matchEditTokenNoSuffix( $token );
1550  return $this->mTokenOk;
1551  }
1552 
1567  protected function setPostEditCookie( $statusValue ) {
1568  $revisionId = $this->page->getLatest();
1569  $postEditKey = self::POST_EDIT_COOKIE_KEY_PREFIX . $revisionId;
1570 
1571  $val = 'saved';
1572  if ( $statusValue == self::AS_SUCCESS_NEW_ARTICLE ) {
1573  $val = 'created';
1574  } elseif ( $this->oldid ) {
1575  $val = 'restored';
1576  }
1577 
1578  $response = $this->context->getRequest()->response();
1579  $response->setCookie( $postEditKey, $val, time() + self::POST_EDIT_COOKIE_DURATION );
1580  }
1581 
1588  public function attemptSave( &$resultDetails = false ) {
1589  // TODO: MCR: treat $this->minoredit like $this->bot and check isAllowed( 'minoredit' )!
1590  // Also, add $this->autopatrol like $this->bot and check isAllowed( 'autopatrol' )!
1591  // This is needed since PageUpdater no longer checks these rights!
1592 
1593  // Allow bots to exempt some edits from bot flagging
1594  $bot = $this->context->getUser()->isAllowed( 'bot' ) && $this->bot;
1595  $status = $this->internalAttemptSave( $resultDetails, $bot );
1596 
1597  Hooks::run( 'EditPage::attemptSave:after', [ $this, $status, $resultDetails ] );
1598 
1599  return $status;
1600  }
1601 
1605  private function incrementResolvedConflicts() {
1606  if ( $this->context->getRequest()->getText( 'mode' ) !== 'conflict' ) {
1607  return;
1608  }
1609 
1610  $this->getEditConflictHelper()->incrementResolvedStats();
1611  }
1612 
1622  private function handleStatus( Status $status, $resultDetails ) {
1627  if ( $status->value == self::AS_SUCCESS_UPDATE
1628  || $status->value == self::AS_SUCCESS_NEW_ARTICLE
1629  ) {
1630  $this->incrementResolvedConflicts();
1631 
1632  $this->didSave = true;
1633  if ( !$resultDetails['nullEdit'] ) {
1634  $this->setPostEditCookie( $status->value );
1635  }
1636  }
1637 
1638  $out = $this->context->getOutput();
1639 
1640  // "wpExtraQueryRedirect" is a hidden input to modify
1641  // after save URL and is not used by actual edit form
1642  $request = $this->context->getRequest();
1643  $extraQueryRedirect = $request->getVal( 'wpExtraQueryRedirect' );
1644 
1645  switch ( $status->value ) {
1646  case self::AS_HOOK_ERROR_EXPECTED:
1647  case self::AS_CONTENT_TOO_BIG:
1648  case self::AS_ARTICLE_WAS_DELETED:
1649  case self::AS_CONFLICT_DETECTED:
1650  case self::AS_SUMMARY_NEEDED:
1651  case self::AS_TEXTBOX_EMPTY:
1652  case self::AS_MAX_ARTICLE_SIZE_EXCEEDED:
1653  case self::AS_END:
1654  case self::AS_BLANK_ARTICLE:
1655  case self::AS_SELF_REDIRECT:
1656  return true;
1657 
1658  case self::AS_HOOK_ERROR:
1659  return false;
1660 
1661  case self::AS_CANNOT_USE_CUSTOM_MODEL:
1662  case self::AS_PARSE_ERROR:
1663  case self::AS_UNICODE_NOT_SUPPORTED:
1664  $out->wrapWikiTextAsInterface( 'error', $status->getWikiText() );
1665  return true;
1666 
1667  case self::AS_SUCCESS_NEW_ARTICLE:
1668  $query = $resultDetails['redirect'] ? 'redirect=no' : '';
1669  if ( $extraQueryRedirect ) {
1670  if ( $query === '' ) {
1671  $query = $extraQueryRedirect;
1672  } else {
1673  $query = $query . '&' . $extraQueryRedirect;
1674  }
1675  }
1676  $anchor = $resultDetails['sectionanchor'] ?? '';
1677  $out->redirect( $this->mTitle->getFullURL( $query ) . $anchor );
1678  return false;
1679 
1680  case self::AS_SUCCESS_UPDATE:
1681  $extraQuery = '';
1682  $sectionanchor = $resultDetails['sectionanchor'];
1683 
1684  // Give extensions a chance to modify URL query on update
1685  Hooks::run(
1686  'ArticleUpdateBeforeRedirect',
1687  [ $this->mArticle, &$sectionanchor, &$extraQuery ]
1688  );
1689 
1690  if ( $resultDetails['redirect'] ) {
1691  if ( $extraQuery == '' ) {
1692  $extraQuery = 'redirect=no';
1693  } else {
1694  $extraQuery = 'redirect=no&' . $extraQuery;
1695  }
1696  }
1697  if ( $extraQueryRedirect ) {
1698  if ( $extraQuery === '' ) {
1699  $extraQuery = $extraQueryRedirect;
1700  } else {
1701  $extraQuery = $extraQuery . '&' . $extraQueryRedirect;
1702  }
1703  }
1704 
1705  $out->redirect( $this->mTitle->getFullURL( $extraQuery ) . $sectionanchor );
1706  return false;
1707 
1708  case self::AS_SPAM_ERROR:
1709  $this->spamPageWithContent( $resultDetails['spam'] );
1710  return false;
1711 
1712  case self::AS_BLOCKED_PAGE_FOR_USER:
1713  throw new UserBlockedError( $this->context->getUser()->getBlock() );
1714 
1715  case self::AS_IMAGE_REDIRECT_ANON:
1716  case self::AS_IMAGE_REDIRECT_LOGGED:
1717  throw new PermissionsError( 'upload' );
1718 
1719  case self::AS_READ_ONLY_PAGE_ANON:
1720  case self::AS_READ_ONLY_PAGE_LOGGED:
1721  throw new PermissionsError( 'edit' );
1722 
1723  case self::AS_READ_ONLY_PAGE:
1724  throw new ReadOnlyError;
1725 
1726  case self::AS_RATE_LIMITED:
1727  throw new ThrottledError();
1728 
1729  case self::AS_NO_CREATE_PERMISSION:
1730  $permission = $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage';
1731  throw new PermissionsError( $permission );
1732 
1733  case self::AS_NO_CHANGE_CONTENT_MODEL:
1734  throw new PermissionsError( 'editcontentmodel' );
1735 
1736  default:
1737  // We don't recognize $status->value. The only way that can happen
1738  // is if an extension hook aborted from inside ArticleSave.
1739  // Render the status object into $this->hookError
1740  // FIXME this sucks, we should just use the Status object throughout
1741  $this->hookError = '<div class="error">' . "\n" . $status->getWikiText() .
1742  '</div>';
1743  return true;
1744  }
1745  }
1746 
1757  // Run old style post-section-merge edit filter
1758  if ( $this->hookError != '' ) {
1759  # ...or the hook could be expecting us to produce an error
1760  $status->fatal( 'hookaborted' );
1761  $status->value = self::AS_HOOK_ERROR_EXPECTED;
1762  return false;
1763  }
1764 
1765  // Run new style post-section-merge edit filter
1766  if ( !Hooks::run( 'EditFilterMergedContent',
1767  [ $this->context, $content, $status, $this->summary,
1768  $user, $this->minoredit ] )
1769  ) {
1770  # Error messages etc. could be handled within the hook...
1771  if ( $status->isGood() ) {
1772  $status->fatal( 'hookaborted' );
1773  // Not setting $this->hookError here is a hack to allow the hook
1774  // to cause a return to the edit page without $this->hookError
1775  // being set. This is used by ConfirmEdit to display a captcha
1776  // without any error message cruft.
1777  } else {
1778  $this->hookError = $this->formatStatusErrors( $status );
1779  }
1780  // Use the existing $status->value if the hook set it
1781  if ( !$status->value ) {
1782  $status->value = self::AS_HOOK_ERROR;
1783  }
1784  return false;
1785  } elseif ( !$status->isOK() ) {
1786  # ...or the hook could be expecting us to produce an error
1787  // FIXME this sucks, we should just use the Status object throughout
1788  $this->hookError = $this->formatStatusErrors( $status );
1789  $status->fatal( 'hookaborted' );
1790  $status->value = self::AS_HOOK_ERROR_EXPECTED;
1791  return false;
1792  }
1793 
1794  return true;
1795  }
1796 
1803  private function formatStatusErrors( Status $status ) {
1804  $errmsg = $status->getWikiText(
1805  'edit-error-short',
1806  'edit-error-long',
1807  $this->context->getLanguage()
1808  );
1809  return <<<ERROR
1810 <div class="errorbox">
1811 {$errmsg}
1812 </div>
1813 <br clear="all" />
1814 ERROR;
1815  }
1816 
1823  private function newSectionSummary( &$sectionanchor = null ) {
1824  global $wgParser;
1825 
1826  if ( $this->sectiontitle !== '' ) {
1827  $sectionanchor = $this->guessSectionName( $this->sectiontitle );
1828  // If no edit summary was specified, create one automatically from the section
1829  // title and have it link to the new section. Otherwise, respect the summary as
1830  // passed.
1831  if ( $this->summary === '' ) {
1832  $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
1833  return $this->context->msg( 'newsectionsummary' )
1834  ->plaintextParams( $cleanSectionTitle )->inContentLanguage()->text();
1835  }
1836  } elseif ( $this->summary !== '' ) {
1837  $sectionanchor = $this->guessSectionName( $this->summary );
1838  # This is a new section, so create a link to the new section
1839  # in the revision summary.
1840  $cleanSummary = $wgParser->stripSectionName( $this->summary );
1841  return $this->context->msg( 'newsectionsummary' )
1842  ->plaintextParams( $cleanSummary )->inContentLanguage()->text();
1843  }
1844  return $this->summary;
1845  }
1846 
1871  public function internalAttemptSave( &$result, $bot = false ) {
1873  $user = $this->context->getUser();
1874 
1875  if ( !Hooks::run( 'EditPage::attemptSave', [ $this ] ) ) {
1876  wfDebug( "Hook 'EditPage::attemptSave' aborted article saving\n" );
1877  $status->fatal( 'hookaborted' );
1878  $status->value = self::AS_HOOK_ERROR;
1879  return $status;
1880  }
1881 
1882  if ( $this->unicodeCheck !== self::UNICODE_CHECK ) {
1883  $status->fatal( 'unicode-support-fail' );
1884  $status->value = self::AS_UNICODE_NOT_SUPPORTED;
1885  return $status;
1886  }
1887 
1888  $request = $this->context->getRequest();
1889  $spam = $request->getText( 'wpAntispam' );
1890  if ( $spam !== '' ) {
1891  wfDebugLog(
1892  'SimpleAntiSpam',
1893  $user->getName() .
1894  ' editing "' .
1895  $this->mTitle->getPrefixedText() .
1896  '" submitted bogus field "' .
1897  $spam .
1898  '"'
1899  );
1900  $status->fatal( 'spamprotectionmatch', false );
1901  $status->value = self::AS_SPAM_ERROR;
1902  return $status;
1903  }
1904 
1905  try {
1906  # Construct Content object
1907  $textbox_content = $this->toEditContent( $this->textbox1 );
1908  } catch ( MWContentSerializationException $ex ) {
1909  $status->fatal(
1910  'content-failed-to-parse',
1911  $this->contentModel,
1912  $this->contentFormat,
1913  $ex->getMessage()
1914  );
1915  $status->value = self::AS_PARSE_ERROR;
1916  return $status;
1917  }
1918 
1919  # Check image redirect
1920  if ( $this->mTitle->getNamespace() == NS_FILE &&
1921  $textbox_content->isRedirect() &&
1922  !$user->isAllowed( 'upload' )
1923  ) {
1924  $code = $user->isAnon() ? self::AS_IMAGE_REDIRECT_ANON : self::AS_IMAGE_REDIRECT_LOGGED;
1925  $status->setResult( false, $code );
1926 
1927  return $status;
1928  }
1929 
1930  # Check for spam
1931  $match = self::matchSummarySpamRegex( $this->summary );
1932  if ( $match === false && $this->section == 'new' ) {
1933  # $wgSpamRegex is enforced on this new heading/summary because, unlike
1934  # regular summaries, it is added to the actual wikitext.
1935  if ( $this->sectiontitle !== '' ) {
1936  # This branch is taken when the API is used with the 'sectiontitle' parameter.
1937  $match = self::matchSpamRegex( $this->sectiontitle );
1938  } else {
1939  # This branch is taken when the "Add Topic" user interface is used, or the API
1940  # is used with the 'summary' parameter.
1941  $match = self::matchSpamRegex( $this->summary );
1942  }
1943  }
1944  if ( $match === false ) {
1945  $match = self::matchSpamRegex( $this->textbox1 );
1946  }
1947  if ( $match !== false ) {
1948  $result['spam'] = $match;
1949  $ip = $request->getIP();
1950  $pdbk = $this->mTitle->getPrefixedDBkey();
1951  $match = str_replace( "\n", '', $match );
1952  wfDebugLog( 'SpamRegex', "$ip spam regex hit [[$pdbk]]: \"$match\"" );
1953  $status->fatal( 'spamprotectionmatch', $match );
1954  $status->value = self::AS_SPAM_ERROR;
1955  return $status;
1956  }
1957  if ( !Hooks::run(
1958  'EditFilter',
1959  [ $this, $this->textbox1, $this->section, &$this->hookError, $this->summary ] )
1960  ) {
1961  # Error messages etc. could be handled within the hook...
1962  $status->fatal( 'hookaborted' );
1963  $status->value = self::AS_HOOK_ERROR;
1964  return $status;
1965  } elseif ( $this->hookError != '' ) {
1966  # ...or the hook could be expecting us to produce an error
1967  $status->fatal( 'hookaborted' );
1968  $status->value = self::AS_HOOK_ERROR_EXPECTED;
1969  return $status;
1970  }
1971 
1972  if ( $user->isBlockedFrom( $this->mTitle ) ) {
1973  // Auto-block user's IP if the account was "hard" blocked
1974  if ( !wfReadOnly() ) {
1975  $user->spreadAnyEditBlock();
1976  }
1977  # Check block state against master, thus 'false'.
1978  $status->setResult( false, self::AS_BLOCKED_PAGE_FOR_USER );
1979  return $status;
1980  }
1981 
1982  $this->contentLength = strlen( $this->textbox1 );
1983  $config = $this->context->getConfig();
1984  $maxArticleSize = $config->get( 'MaxArticleSize' );
1985  if ( $this->contentLength > $maxArticleSize * 1024 ) {
1986  // Error will be displayed by showEditForm()
1987  $this->tooBig = true;
1988  $status->setResult( false, self::AS_CONTENT_TOO_BIG );
1989  return $status;
1990  }
1991 
1992  if ( !$user->isAllowed( 'edit' ) ) {
1993  if ( $user->isAnon() ) {
1994  $status->setResult( false, self::AS_READ_ONLY_PAGE_ANON );
1995  return $status;
1996  } else {
1997  $status->fatal( 'readonlytext' );
1998  $status->value = self::AS_READ_ONLY_PAGE_LOGGED;
1999  return $status;
2000  }
2001  }
2002 
2003  $changingContentModel = false;
2004  if ( $this->contentModel !== $this->mTitle->getContentModel() ) {
2005  if ( !$config->get( 'ContentHandlerUseDB' ) ) {
2006  $status->fatal( 'editpage-cannot-use-custom-model' );
2007  $status->value = self::AS_CANNOT_USE_CUSTOM_MODEL;
2008  return $status;
2009  } elseif ( !$user->isAllowed( 'editcontentmodel' ) ) {
2010  $status->setResult( false, self::AS_NO_CHANGE_CONTENT_MODEL );
2011  return $status;
2012  }
2013  // Make sure the user can edit the page under the new content model too
2014  $titleWithNewContentModel = clone $this->mTitle;
2015  $titleWithNewContentModel->setContentModel( $this->contentModel );
2016  if ( !$titleWithNewContentModel->userCan( 'editcontentmodel', $user )
2017  || !$titleWithNewContentModel->userCan( 'edit', $user )
2018  ) {
2019  $status->setResult( false, self::AS_NO_CHANGE_CONTENT_MODEL );
2020  return $status;
2021  }
2022 
2023  $changingContentModel = true;
2024  $oldContentModel = $this->mTitle->getContentModel();
2025  }
2026 
2027  if ( $this->changeTags ) {
2028  $changeTagsStatus = ChangeTags::canAddTagsAccompanyingChange(
2029  $this->changeTags, $user );
2030  if ( !$changeTagsStatus->isOK() ) {
2031  $changeTagsStatus->value = self::AS_CHANGE_TAG_ERROR;
2032  return $changeTagsStatus;
2033  }
2034  }
2035 
2036  if ( wfReadOnly() ) {
2037  $status->fatal( 'readonlytext' );
2038  $status->value = self::AS_READ_ONLY_PAGE;
2039  return $status;
2040  }
2041  if ( $user->pingLimiter() || $user->pingLimiter( 'linkpurge', 0 )
2042  || ( $changingContentModel && $user->pingLimiter( 'editcontentmodel' ) )
2043  ) {
2044  $status->fatal( 'actionthrottledtext' );
2045  $status->value = self::AS_RATE_LIMITED;
2046  return $status;
2047  }
2048 
2049  # If the article has been deleted while editing, don't save it without
2050  # confirmation
2051  if ( $this->wasDeletedSinceLastEdit() && !$this->recreate ) {
2052  $status->setResult( false, self::AS_ARTICLE_WAS_DELETED );
2053  return $status;
2054  }
2055 
2056  # Load the page data from the master. If anything changes in the meantime,
2057  # we detect it by using page_latest like a token in a 1 try compare-and-swap.
2058  $this->page->loadPageData( 'fromdbmaster' );
2059  $new = !$this->page->exists();
2060 
2061  if ( $new ) {
2062  // Late check for create permission, just in case *PARANOIA*
2063  if ( !$this->mTitle->userCan( 'create', $user ) ) {
2064  $status->fatal( 'nocreatetext' );
2065  $status->value = self::AS_NO_CREATE_PERMISSION;
2066  wfDebug( __METHOD__ . ": no create permission\n" );
2067  return $status;
2068  }
2069 
2070  // Don't save a new page if it's blank or if it's a MediaWiki:
2071  // message with content equivalent to default (allow empty pages
2072  // in this case to disable messages, see T52124)
2073  $defaultMessageText = $this->mTitle->getDefaultMessageText();
2074  if ( $this->mTitle->getNamespace() === NS_MEDIAWIKI && $defaultMessageText !== false ) {
2075  $defaultText = $defaultMessageText;
2076  } else {
2077  $defaultText = '';
2078  }
2079 
2080  if ( !$this->allowBlankArticle && $this->textbox1 === $defaultText ) {
2081  $this->blankArticle = true;
2082  $status->fatal( 'blankarticle' );
2083  $status->setResult( false, self::AS_BLANK_ARTICLE );
2084  return $status;
2085  }
2086 
2087  if ( !$this->runPostMergeFilters( $textbox_content, $status, $user ) ) {
2088  return $status;
2089  }
2090 
2091  $content = $textbox_content;
2092 
2093  $result['sectionanchor'] = '';
2094  if ( $this->section == 'new' ) {
2095  if ( $this->sectiontitle !== '' ) {
2096  // Insert the section title above the content.
2097  $content = $content->addSectionHeader( $this->sectiontitle );
2098  } elseif ( $this->summary !== '' ) {
2099  // Insert the section title above the content.
2100  $content = $content->addSectionHeader( $this->summary );
2101  }
2102  $this->summary = $this->newSectionSummary( $result['sectionanchor'] );
2103  }
2104 
2105  $status->value = self::AS_SUCCESS_NEW_ARTICLE;
2106 
2107  } else { # not $new
2108 
2109  # Article exists. Check for edit conflict.
2110 
2111  $this->page->clear(); # Force reload of dates, etc.
2112  $timestamp = $this->page->getTimestamp();
2113  $latest = $this->page->getLatest();
2114 
2115  wfDebug( "timestamp: {$timestamp}, edittime: {$this->edittime}\n" );
2116 
2117  // An edit conflict is detected if the current revision is different from the
2118  // revision that was current when editing was initiated on the client.
2119  // This is checked based on the timestamp and revision ID.
2120  // TODO: the timestamp based check can probably go away now.
2121  if ( $timestamp != $this->edittime
2122  || ( $this->editRevId !== null && $this->editRevId != $latest )
2123  ) {
2124  $this->isConflict = true;
2125  if ( $this->section == 'new' ) {
2126  if ( $this->page->getUserText() == $user->getName() &&
2127  $this->page->getComment() == $this->newSectionSummary()
2128  ) {
2129  // Probably a duplicate submission of a new comment.
2130  // This can happen when CDN resends a request after
2131  // a timeout but the first one actually went through.
2132  wfDebug( __METHOD__
2133  . ": duplicate new section submission; trigger edit conflict!\n" );
2134  } else {
2135  // New comment; suppress conflict.
2136  $this->isConflict = false;
2137  wfDebug( __METHOD__ . ": conflict suppressed; new section\n" );
2138  }
2139  } elseif ( $this->section == ''
2141  DB_MASTER, $this->mTitle->getArticleID(),
2142  $user->getId(), $this->edittime
2143  )
2144  ) {
2145  # Suppress edit conflict with self, except for section edits where merging is required.
2146  wfDebug( __METHOD__ . ": Suppressing edit conflict, same user.\n" );
2147  $this->isConflict = false;
2148  }
2149  }
2150 
2151  // If sectiontitle is set, use it, otherwise use the summary as the section title.
2152  if ( $this->sectiontitle !== '' ) {
2153  $sectionTitle = $this->sectiontitle;
2154  } else {
2155  $sectionTitle = $this->summary;
2156  }
2157 
2158  $content = null;
2159 
2160  if ( $this->isConflict ) {
2161  wfDebug( __METHOD__
2162  . ": conflict! getting section '{$this->section}' for time '{$this->edittime}'"
2163  . " (id '{$this->editRevId}') (article time '{$timestamp}')\n" );
2164  // @TODO: replaceSectionAtRev() with base ID (not prior current) for ?oldid=X case
2165  // ...or disable section editing for non-current revisions (not exposed anyway).
2166  if ( $this->editRevId !== null ) {
2167  $content = $this->page->replaceSectionAtRev(
2168  $this->section,
2169  $textbox_content,
2170  $sectionTitle,
2171  $this->editRevId
2172  );
2173  } else {
2174  $content = $this->page->replaceSectionContent(
2175  $this->section,
2176  $textbox_content,
2177  $sectionTitle,
2178  $this->edittime
2179  );
2180  }
2181  } else {
2182  wfDebug( __METHOD__ . ": getting section '{$this->section}'\n" );
2183  $content = $this->page->replaceSectionContent(
2184  $this->section,
2185  $textbox_content,
2186  $sectionTitle
2187  );
2188  }
2189 
2190  if ( is_null( $content ) ) {
2191  wfDebug( __METHOD__ . ": activating conflict; section replace failed.\n" );
2192  $this->isConflict = true;
2193  $content = $textbox_content; // do not try to merge here!
2194  } elseif ( $this->isConflict ) {
2195  # Attempt merge
2196  if ( $this->mergeChangesIntoContent( $content ) ) {
2197  // Successful merge! Maybe we should tell the user the good news?
2198  $this->isConflict = false;
2199  wfDebug( __METHOD__ . ": Suppressing edit conflict, successful merge.\n" );
2200  } else {
2201  $this->section = '';
2202  $this->textbox1 = ContentHandler::getContentText( $content );
2203  wfDebug( __METHOD__ . ": Keeping edit conflict, failed merge.\n" );
2204  }
2205  }
2206 
2207  if ( $this->isConflict ) {
2208  $status->setResult( false, self::AS_CONFLICT_DETECTED );
2209  return $status;
2210  }
2211 
2212  if ( !$this->runPostMergeFilters( $content, $status, $user ) ) {
2213  return $status;
2214  }
2215 
2216  if ( $this->section == 'new' ) {
2217  // Handle the user preference to force summaries here
2218  if ( !$this->allowBlankSummary && trim( $this->summary ) == '' ) {
2219  $this->missingSummary = true;
2220  $status->fatal( 'missingsummary' ); // or 'missingcommentheader' if $section == 'new'. Blegh
2221  $status->value = self::AS_SUMMARY_NEEDED;
2222  return $status;
2223  }
2224 
2225  // Do not allow the user to post an empty comment
2226  if ( $this->textbox1 == '' ) {
2227  $this->missingComment = true;
2228  $status->fatal( 'missingcommenttext' );
2229  $status->value = self::AS_TEXTBOX_EMPTY;
2230  return $status;
2231  }
2232  } elseif ( !$this->allowBlankSummary
2233  && !$content->equals( $this->getOriginalContent( $user ) )
2234  && !$content->isRedirect()
2235  && md5( $this->summary ) == $this->autoSumm
2236  ) {
2237  $this->missingSummary = true;
2238  $status->fatal( 'missingsummary' );
2239  $status->value = self::AS_SUMMARY_NEEDED;
2240  return $status;
2241  }
2242 
2243  # All's well
2244  $sectionanchor = '';
2245  if ( $this->section == 'new' ) {
2246  $this->summary = $this->newSectionSummary( $sectionanchor );
2247  } elseif ( $this->section != '' ) {
2248  # Try to get a section anchor from the section source, redirect
2249  # to edited section if header found.
2250  # XXX: Might be better to integrate this into Article::replaceSectionAtRev
2251  # for duplicate heading checking and maybe parsing.
2252  $hasmatch = preg_match( "/^ *([=]{1,6})(.*?)(\\1) *\\n/i", $this->textbox1, $matches );
2253  # We can't deal with anchors, includes, html etc in the header for now,
2254  # headline would need to be parsed to improve this.
2255  if ( $hasmatch && strlen( $matches[2] ) > 0 ) {
2256  $sectionanchor = $this->guessSectionName( $matches[2] );
2257  }
2258  }
2259  $result['sectionanchor'] = $sectionanchor;
2260 
2261  // Save errors may fall down to the edit form, but we've now
2262  // merged the section into full text. Clear the section field
2263  // so that later submission of conflict forms won't try to
2264  // replace that into a duplicated mess.
2265  $this->textbox1 = $this->toEditText( $content );
2266  $this->section = '';
2267 
2268  $status->value = self::AS_SUCCESS_UPDATE;
2269  }
2270 
2271  if ( !$this->allowSelfRedirect
2272  && $content->isRedirect()
2273  && $content->getRedirectTarget()->equals( $this->getTitle() )
2274  ) {
2275  // If the page already redirects to itself, don't warn.
2276  $currentTarget = $this->getCurrentContent()->getRedirectTarget();
2277  if ( !$currentTarget || !$currentTarget->equals( $this->getTitle() ) ) {
2278  $this->selfRedirect = true;
2279  $status->fatal( 'selfredirect' );
2280  $status->value = self::AS_SELF_REDIRECT;
2281  return $status;
2282  }
2283  }
2284 
2285  // Check for length errors again now that the section is merged in
2286  $this->contentLength = strlen( $this->toEditText( $content ) );
2287  if ( $this->contentLength > $maxArticleSize * 1024 ) {
2288  $this->tooBig = true;
2289  $status->setResult( false, self::AS_MAX_ARTICLE_SIZE_EXCEEDED );
2290  return $status;
2291  }
2292 
2293  $flags = EDIT_AUTOSUMMARY |
2294  ( $new ? EDIT_NEW : EDIT_UPDATE ) |
2295  ( ( $this->minoredit && !$this->isNew ) ? EDIT_MINOR : 0 ) |
2296  ( $bot ? EDIT_FORCE_BOT : 0 );
2297 
2298  $doEditStatus = $this->page->doEditContent(
2299  $content,
2300  $this->summary,
2301  $flags,
2302  false,
2303  $user,
2304  $content->getDefaultFormat(),
2307  );
2308 
2309  if ( !$doEditStatus->isOK() ) {
2310  // Failure from doEdit()
2311  // Show the edit conflict page for certain recognized errors from doEdit(),
2312  // but don't show it for errors from extension hooks
2313  $errors = $doEditStatus->getErrorsArray();
2314  if ( in_array( $errors[0][0],
2315  [ 'edit-gone-missing', 'edit-conflict', 'edit-already-exists' ] )
2316  ) {
2317  $this->isConflict = true;
2318  // Destroys data doEdit() put in $status->value but who cares
2319  $doEditStatus->value = self::AS_END;
2320  }
2321  return $doEditStatus;
2322  }
2323 
2324  $result['nullEdit'] = $doEditStatus->hasMessage( 'edit-no-change' );
2325  if ( $result['nullEdit'] ) {
2326  // We don't know if it was a null edit until now, so increment here
2327  $user->pingLimiter( 'linkpurge' );
2328  }
2329  $result['redirect'] = $content->isRedirect();
2330 
2331  $this->updateWatchlist();
2332 
2333  // If the content model changed, add a log entry
2334  if ( $changingContentModel ) {
2336  $user,
2337  $new ? false : $oldContentModel,
2338  $this->contentModel,
2339  $this->summary
2340  );
2341  }
2342 
2343  return $status;
2344  }
2345 
2352  protected function addContentModelChangeLogEntry( User $user, $oldModel, $newModel, $reason ) {
2353  $new = $oldModel === false;
2354  $log = new ManualLogEntry( 'contentmodel', $new ? 'new' : 'change' );
2355  $log->setPerformer( $user );
2356  $log->setTarget( $this->mTitle );
2357  $log->setComment( $reason );
2358  $log->setParameters( [
2359  '4::oldmodel' => $oldModel,
2360  '5::newmodel' => $newModel
2361  ] );
2362  $logid = $log->insert();
2363  $log->publish( $logid );
2364  }
2365 
2369  protected function updateWatchlist() {
2370  $user = $this->context->getUser();
2371  if ( !$user->isLoggedIn() ) {
2372  return;
2373  }
2374 
2376  $watch = $this->watchthis;
2377  // Do this in its own transaction to reduce contention...
2378  DeferredUpdates::addCallableUpdate( function () use ( $user, $title, $watch ) {
2379  if ( $watch == $user->isWatched( $title, User::IGNORE_USER_RIGHTS ) ) {
2380  return; // nothing to change
2381  }
2383  } );
2384  }
2385 
2397  private function mergeChangesIntoContent( &$editContent ) {
2398  $db = wfGetDB( DB_MASTER );
2399 
2400  // This is the revision that was current at the time editing was initiated on the client,
2401  // even if the edit was based on an old revision.
2402  $baseRevision = $this->getBaseRevision();
2403  $baseContent = $baseRevision ? $baseRevision->getContent() : null;
2404 
2405  if ( is_null( $baseContent ) ) {
2406  return false;
2407  }
2408 
2409  // The current state, we want to merge updates into it
2410  $currentRevision = Revision::loadFromTitle( $db, $this->mTitle );
2411  $currentContent = $currentRevision ? $currentRevision->getContent() : null;
2412 
2413  if ( is_null( $currentContent ) ) {
2414  return false;
2415  }
2416 
2417  $handler = ContentHandler::getForModelID( $baseContent->getModel() );
2418 
2419  $result = $handler->merge3( $baseContent, $editContent, $currentContent );
2420 
2421  if ( $result ) {
2422  $editContent = $result;
2423  // Update parentRevId to what we just merged.
2424  $this->parentRevId = $currentRevision->getId();
2425  return true;
2426  }
2427 
2428  return false;
2429  }
2430 
2443  public function getBaseRevision() {
2444  if ( !$this->mBaseRevision ) {
2445  $db = wfGetDB( DB_MASTER );
2446  $this->mBaseRevision = $this->editRevId
2447  ? Revision::newFromId( $this->editRevId, Revision::READ_LATEST )
2448  : Revision::loadFromTimestamp( $db, $this->mTitle, $this->edittime );
2449  }
2450  return $this->mBaseRevision;
2451  }
2452 
2460  public static function matchSpamRegex( $text ) {
2461  global $wgSpamRegex;
2462  // For back compatibility, $wgSpamRegex may be a single string or an array of regexes.
2463  $regexes = (array)$wgSpamRegex;
2464  return self::matchSpamRegexInternal( $text, $regexes );
2465  }
2466 
2474  public static function matchSummarySpamRegex( $text ) {
2475  global $wgSummarySpamRegex;
2476  $regexes = (array)$wgSummarySpamRegex;
2477  return self::matchSpamRegexInternal( $text, $regexes );
2478  }
2479 
2485  protected static function matchSpamRegexInternal( $text, $regexes ) {
2486  foreach ( $regexes as $regex ) {
2487  $matches = [];
2488  if ( preg_match( $regex, $text, $matches ) ) {
2489  return $matches[0];
2490  }
2491  }
2492  return false;
2493  }
2494 
2495  public function setHeaders() {
2496  $out = $this->context->getOutput();
2497 
2498  $out->addModules( 'mediawiki.action.edit' );
2499  $out->addModuleStyles( 'mediawiki.action.edit.styles' );
2500  $out->addModuleStyles( 'mediawiki.editfont.styles' );
2501 
2502  $user = $this->context->getUser();
2503 
2504  if ( $user->getOption( 'uselivepreview' ) ) {
2505  $out->addModules( 'mediawiki.action.edit.preview' );
2506  }
2507 
2508  if ( $user->getOption( 'useeditwarning' ) ) {
2509  $out->addModules( 'mediawiki.action.edit.editWarning' );
2510  }
2511 
2512  # Enabled article-related sidebar, toplinks, etc.
2513  $out->setArticleRelated( true );
2514 
2515  $contextTitle = $this->getContextTitle();
2516  if ( $this->isConflict ) {
2517  $msg = 'editconflict';
2518  } elseif ( $contextTitle->exists() && $this->section != '' ) {
2519  $msg = $this->section == 'new' ? 'editingcomment' : 'editingsection';
2520  } else {
2521  $msg = $contextTitle->exists()
2522  || ( $contextTitle->getNamespace() == NS_MEDIAWIKI
2523  && $contextTitle->getDefaultMessageText() !== false
2524  )
2525  ? 'editing'
2526  : 'creating';
2527  }
2528 
2529  # Use the title defined by DISPLAYTITLE magic word when present
2530  # NOTE: getDisplayTitle() returns HTML while getPrefixedText() returns plain text.
2531  # setPageTitle() treats the input as wikitext, which should be safe in either case.
2532  $displayTitle = isset( $this->mParserOutput ) ? $this->mParserOutput->getDisplayTitle() : false;
2533  if ( $displayTitle === false ) {
2534  $displayTitle = $contextTitle->getPrefixedText();
2535  } else {
2536  $out->setDisplayTitle( $displayTitle );
2537  }
2538  $out->setPageTitle( $this->context->msg( $msg, $displayTitle ) );
2539 
2540  $config = $this->context->getConfig();
2541 
2542  # Transmit the name of the message to JavaScript for live preview
2543  # Keep Resources.php/mediawiki.action.edit.preview in sync with the possible keys
2544  $out->addJsConfigVars( [
2545  'wgEditMessage' => $msg,
2546  'wgAjaxEditStash' => $config->get( 'AjaxEditStash' ),
2547  ] );
2548 
2549  // Add whether to use 'save' or 'publish' messages to JavaScript for post-edit, other
2550  // editors, etc.
2551  $out->addJsConfigVars(
2552  'wgEditSubmitButtonLabelPublish',
2553  $config->get( 'EditSubmitButtonLabelPublish' )
2554  );
2555  }
2556 
2560  protected function showIntro() {
2561  if ( $this->suppressIntro ) {
2562  return;
2563  }
2564 
2565  $out = $this->context->getOutput();
2566  $namespace = $this->mTitle->getNamespace();
2567 
2568  if ( $namespace == NS_MEDIAWIKI ) {
2569  # Show a warning if editing an interface message
2570  $out->wrapWikiMsg( "<div class='mw-editinginterface'>\n$1\n</div>", 'editinginterface' );
2571  # If this is a default message (but not css, json, or js),
2572  # show a hint that it is translatable on translatewiki.net
2573  if (
2574  !$this->mTitle->hasContentModel( CONTENT_MODEL_CSS )
2575  && !$this->mTitle->hasContentModel( CONTENT_MODEL_JSON )
2576  && !$this->mTitle->hasContentModel( CONTENT_MODEL_JAVASCRIPT )
2577  ) {
2578  $defaultMessageText = $this->mTitle->getDefaultMessageText();
2579  if ( $defaultMessageText !== false ) {
2580  $out->wrapWikiMsg( "<div class='mw-translateinterface'>\n$1\n</div>",
2581  'translateinterface' );
2582  }
2583  }
2584  } elseif ( $namespace == NS_FILE ) {
2585  # Show a hint to shared repo
2586  $file = wfFindFile( $this->mTitle );
2587  if ( $file && !$file->isLocal() ) {
2588  $descUrl = $file->getDescriptionUrl();
2589  # there must be a description url to show a hint to shared repo
2590  if ( $descUrl ) {
2591  if ( !$this->mTitle->exists() ) {
2592  $out->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-create\">\n$1\n</div>", [
2593  'sharedupload-desc-create', $file->getRepo()->getDisplayName(), $descUrl
2594  ] );
2595  } else {
2596  $out->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-edit\">\n$1\n</div>", [
2597  'sharedupload-desc-edit', $file->getRepo()->getDisplayName(), $descUrl
2598  ] );
2599  }
2600  }
2601  }
2602  }
2603 
2604  # Show a warning message when someone creates/edits a user (talk) page but the user does not exist
2605  # Show log extract when the user is currently blocked
2606  if ( $namespace == NS_USER || $namespace == NS_USER_TALK ) {
2607  $username = explode( '/', $this->mTitle->getText(), 2 )[0];
2608  $user = User::newFromName( $username, false /* allow IP users */ );
2609  $ip = User::isIP( $username );
2610  $block = Block::newFromTarget( $user, $user );
2611  if ( !( $user && $user->isLoggedIn() ) && !$ip ) { # User does not exist
2612  $out->wrapWikiMsg( "<div class=\"mw-userpage-userdoesnotexist error\">\n$1\n</div>",
2613  [ 'userpage-userdoesnotexist', wfEscapeWikiText( $username ) ] );
2614  } elseif (
2615  !is_null( $block ) &&
2616  $block->getType() != Block::TYPE_AUTO &&
2617  ( $block->isSitewide() || $user->isBlockedFrom( $this->mTitle ) )
2618  ) {
2619  // Show log extract if the user is sitewide blocked or is partially
2620  // blocked and not allowed to edit their user page or user talk page
2622  $out,
2623  'block',
2624  MWNamespace::getCanonicalName( NS_USER ) . ':' . $block->getTarget(),
2625  '',
2626  [
2627  'lim' => 1,
2628  'showIfEmpty' => false,
2629  'msgKey' => [
2630  'blocked-notice-logextract',
2631  $user->getName() # Support GENDER in notice
2632  ]
2633  ]
2634  );
2635  }
2636  }
2637  # Try to add a custom edit intro, or use the standard one if this is not possible.
2638  if ( !$this->showCustomIntro() && !$this->mTitle->exists() ) {
2640  $this->context->msg( 'helppage' )->inContentLanguage()->text()
2641  ) );
2642  if ( $this->context->getUser()->isLoggedIn() ) {
2643  $out->wrapWikiMsg(
2644  // Suppress the external link icon, consider the help url an internal one
2645  "<div class=\"mw-newarticletext plainlinks\">\n$1\n</div>",
2646  [
2647  'newarticletext',
2648  $helpLink
2649  ]
2650  );
2651  } else {
2652  $out->wrapWikiMsg(
2653  // Suppress the external link icon, consider the help url an internal one
2654  "<div class=\"mw-newarticletextanon plainlinks\">\n$1\n</div>",
2655  [
2656  'newarticletextanon',
2657  $helpLink
2658  ]
2659  );
2660  }
2661  }
2662  # Give a notice if the user is editing a deleted/moved page...
2663  if ( !$this->mTitle->exists() ) {
2664  $dbr = wfGetDB( DB_REPLICA );
2665 
2666  LogEventsList::showLogExtract( $out, [ 'delete', 'move' ], $this->mTitle,
2667  '',
2668  [
2669  'lim' => 10,
2670  'conds' => [ 'log_action != ' . $dbr->addQuotes( 'revision' ) ],
2671  'showIfEmpty' => false,
2672  'msgKey' => [ 'recreate-moveddeleted-warn' ]
2673  ]
2674  );
2675  }
2676  }
2677 
2683  protected function showCustomIntro() {
2684  if ( $this->editintro ) {
2685  $title = Title::newFromText( $this->editintro );
2686  if ( $title instanceof Title && $title->exists() && $title->userCan( 'read' ) ) {
2687  // Added using template syntax, to take <noinclude>'s into account.
2688  $this->context->getOutput()->addWikiTextAsContent(
2689  '<div class="mw-editintro">{{:' . $title->getFullText() . '}}</div>',
2690  /*linestart*/true,
2692  );
2693  return true;
2694  }
2695  }
2696  return false;
2697  }
2698 
2717  protected function toEditText( $content ) {
2718  if ( $content === null || $content === false || is_string( $content ) ) {
2719  return $content;
2720  }
2721 
2722  if ( !$this->isSupportedContentModel( $content->getModel() ) ) {
2723  throw new MWException( 'This content model is not supported: ' . $content->getModel() );
2724  }
2725 
2726  return $content->serialize( $this->contentFormat );
2727  }
2728 
2745  protected function toEditContent( $text ) {
2746  if ( $text === false || $text === null ) {
2747  return $text;
2748  }
2749 
2750  $content = ContentHandler::makeContent( $text, $this->getTitle(),
2751  $this->contentModel, $this->contentFormat );
2752 
2753  if ( !$this->isSupportedContentModel( $content->getModel() ) ) {
2754  throw new MWException( 'This content model is not supported: ' . $content->getModel() );
2755  }
2756 
2757  return $content;
2758  }
2759 
2768  public function showEditForm( $formCallback = null ) {
2769  # need to parse the preview early so that we know which templates are used,
2770  # otherwise users with "show preview after edit box" will get a blank list
2771  # we parse this near the beginning so that setHeaders can do the title
2772  # setting work instead of leaving it in getPreviewText
2773  $previewOutput = '';
2774  if ( $this->formtype == 'preview' ) {
2775  $previewOutput = $this->getPreviewText();
2776  }
2777 
2778  $out = $this->context->getOutput();
2779 
2780  // Avoid PHP 7.1 warning of passing $this by reference
2781  $editPage = $this;
2782  Hooks::run( 'EditPage::showEditForm:initial', [ &$editPage, &$out ] );
2783 
2784  $this->setHeaders();
2785 
2786  $this->addTalkPageText();
2787  $this->addEditNotices();
2788 
2789  if ( !$this->isConflict &&
2790  $this->section != '' &&
2791  !$this->isSectionEditSupported() ) {
2792  // We use $this->section to much before this and getVal('wgSection') directly in other places
2793  // at this point we can't reset $this->section to '' to fallback to non-section editing.
2794  // Someone is welcome to try refactoring though
2795  $out->showErrorPage( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
2796  return;
2797  }
2798 
2799  $this->showHeader();
2800 
2801  $out->addHTML( $this->editFormPageTop );
2802 
2803  $user = $this->context->getUser();
2804  if ( $user->getOption( 'previewontop' ) ) {
2805  $this->displayPreviewArea( $previewOutput, true );
2806  }
2807 
2808  $out->addHTML( $this->editFormTextTop );
2809 
2810  if ( $this->wasDeletedSinceLastEdit() ) {
2811  if ( $this->formtype !== 'save' ) {
2812  $out->wrapWikiMsg( "<div class='error mw-deleted-while-editing'>\n$1\n</div>",
2813  'deletedwhileediting' );
2814  }
2815  }
2816 
2817  // @todo add EditForm plugin interface and use it here!
2818  // search for textarea1 and textarea2, and allow EditForm to override all uses.
2819  $out->addHTML( Html::openElement(
2820  'form',
2821  [
2822  'class' => 'mw-editform',
2823  'id' => self::EDITFORM_ID,
2824  'name' => self::EDITFORM_ID,
2825  'method' => 'post',
2826  'action' => $this->getActionURL( $this->getContextTitle() ),
2827  'enctype' => 'multipart/form-data'
2828  ]
2829  ) );
2830 
2831  if ( is_callable( $formCallback ) ) {
2832  wfWarn( 'The $formCallback parameter to ' . __METHOD__ . 'is deprecated' );
2833  call_user_func_array( $formCallback, [ &$out ] );
2834  }
2835 
2836  // Add a check for Unicode support
2837  $out->addHTML( Html::hidden( 'wpUnicodeCheck', self::UNICODE_CHECK ) );
2838 
2839  // Add an empty field to trip up spambots
2840  $out->addHTML(
2841  Xml::openElement( 'div', [ 'id' => 'antispam-container', 'style' => 'display: none;' ] )
2842  . Html::rawElement(
2843  'label',
2844  [ 'for' => 'wpAntispam' ],
2845  $this->context->msg( 'simpleantispam-label' )->parse()
2846  )
2847  . Xml::element(
2848  'input',
2849  [
2850  'type' => 'text',
2851  'name' => 'wpAntispam',
2852  'id' => 'wpAntispam',
2853  'value' => ''
2854  ]
2855  )
2856  . Xml::closeElement( 'div' )
2857  );
2858 
2859  // Avoid PHP 7.1 warning of passing $this by reference
2860  $editPage = $this;
2861  Hooks::run( 'EditPage::showEditForm:fields', [ &$editPage, &$out ] );
2862 
2863  // Put these up at the top to ensure they aren't lost on early form submission
2864  $this->showFormBeforeText();
2865 
2866  if ( $this->wasDeletedSinceLastEdit() && $this->formtype == 'save' ) {
2867  $username = $this->lastDelete->user_name;
2868  $comment = CommentStore::getStore()
2869  ->getComment( 'log_comment', $this->lastDelete )->text;
2870 
2871  // It is better to not parse the comment at all than to have templates expanded in the middle
2872  // TODO: can the checkLabel be moved outside of the div so that wrapWikiMsg could be used?
2873  $key = $comment === ''
2874  ? 'confirmrecreate-noreason'
2875  : 'confirmrecreate';
2876  $out->addHTML(
2877  '<div class="mw-confirm-recreate">' .
2878  $this->context->msg( $key, $username, "<nowiki>$comment</nowiki>" )->parse() .
2879  Xml::checkLabel( $this->context->msg( 'recreate' )->text(), 'wpRecreate', 'wpRecreate', false,
2880  [ 'title' => Linker::titleAttrib( 'recreate' ), 'tabindex' => 1, 'id' => 'wpRecreate' ]
2881  ) .
2882  '</div>'
2883  );
2884  }
2885 
2886  # When the summary is hidden, also hide them on preview/show changes
2887  if ( $this->nosummary ) {
2888  $out->addHTML( Html::hidden( 'nosummary', true ) );
2889  }
2890 
2891  # If a blank edit summary was previously provided, and the appropriate
2892  # user preference is active, pass a hidden tag as wpIgnoreBlankSummary. This will stop the
2893  # user being bounced back more than once in the event that a summary
2894  # is not required.
2895  # ####
2896  # For a bit more sophisticated detection of blank summaries, hash the
2897  # automatic one and pass that in the hidden field wpAutoSummary.
2898  if ( $this->missingSummary || ( $this->section == 'new' && $this->nosummary ) ) {
2899  $out->addHTML( Html::hidden( 'wpIgnoreBlankSummary', true ) );
2900  }
2901 
2902  if ( $this->undidRev ) {
2903  $out->addHTML( Html::hidden( 'wpUndidRevision', $this->undidRev ) );
2904  }
2905 
2906  if ( $this->selfRedirect ) {
2907  $out->addHTML( Html::hidden( 'wpIgnoreSelfRedirect', true ) );
2908  }
2909 
2910  if ( $this->hasPresetSummary ) {
2911  // If a summary has been preset using &summary= we don't want to prompt for
2912  // a different summary. Only prompt for a summary if the summary is blanked.
2913  // (T19416)
2914  $this->autoSumm = md5( '' );
2915  }
2916 
2917  $autosumm = $this->autoSumm !== '' ? $this->autoSumm : md5( $this->summary );
2918  $out->addHTML( Html::hidden( 'wpAutoSummary', $autosumm ) );
2919 
2920  $out->addHTML( Html::hidden( 'oldid', $this->oldid ) );
2921  $out->addHTML( Html::hidden( 'parentRevId', $this->getParentRevId() ) );
2922 
2923  $out->addHTML( Html::hidden( 'format', $this->contentFormat ) );
2924  $out->addHTML( Html::hidden( 'model', $this->contentModel ) );
2925 
2926  $out->enableOOUI();
2927 
2928  if ( $this->section == 'new' ) {
2929  $this->showSummaryInput( true, $this->summary );
2930  $out->addHTML( $this->getSummaryPreview( true, $this->summary ) );
2931  }
2932 
2933  $out->addHTML( $this->editFormTextBeforeContent );
2934  if ( $this->isConflict ) {
2935  // In an edit conflict, we turn textbox2 into the user's text,
2936  // and textbox1 into the stored version
2937  $this->textbox2 = $this->textbox1;
2938 
2939  $content = $this->getCurrentContent();
2940  $this->textbox1 = $this->toEditText( $content );
2941 
2943  $editConflictHelper->setTextboxes( $this->textbox2, $this->textbox1 );
2944  $editConflictHelper->setContentModel( $this->contentModel );
2945  $editConflictHelper->setContentFormat( $this->contentFormat );
2947  }
2948 
2949  if ( !$this->mTitle->isUserConfigPage() ) {
2950  $out->addHTML( self::getEditToolbar( $this->mTitle ) );
2951  }
2952 
2953  if ( $this->blankArticle ) {
2954  $out->addHTML( Html::hidden( 'wpIgnoreBlankArticle', true ) );
2955  }
2956 
2957  if ( $this->isConflict ) {
2958  // In an edit conflict bypass the overridable content form method
2959  // and fallback to the raw wpTextbox1 since editconflicts can't be
2960  // resolved between page source edits and custom ui edits using the
2961  // custom edit ui.
2962  $conflictTextBoxAttribs = [];
2963  if ( $this->wasDeletedSinceLastEdit() ) {
2964  $conflictTextBoxAttribs['style'] = 'display:none;';
2965  } elseif ( $this->isOldRev ) {
2966  $conflictTextBoxAttribs['class'] = 'mw-textarea-oldrev';
2967  }
2968 
2969  $out->addHTML( $editConflictHelper->getEditConflictMainTextBox( $conflictTextBoxAttribs ) );
2971  } else {
2972  $this->showContentForm();
2973  }
2974 
2975  $out->addHTML( $this->editFormTextAfterContent );
2976 
2977  $this->showStandardInputs();
2978 
2979  $this->showFormAfterText();
2980 
2981  $this->showTosSummary();
2982 
2983  $this->showEditTools();
2984 
2985  $out->addHTML( $this->editFormTextAfterTools . "\n" );
2986 
2987  $out->addHTML( $this->makeTemplatesOnThisPageList( $this->getTemplates() ) );
2988 
2989  $out->addHTML( Html::rawElement( 'div', [ 'class' => 'hiddencats' ],
2990  Linker::formatHiddenCategories( $this->page->getHiddenCategories() ) ) );
2991 
2992  $out->addHTML( Html::rawElement( 'div', [ 'class' => 'limitreport' ],
2993  self::getPreviewLimitReport( $this->mParserOutput ) ) );
2994 
2995  $out->addModules( 'mediawiki.action.edit.collapsibleFooter' );
2996 
2997  if ( $this->isConflict ) {
2998  try {
2999  $this->showConflict();
3000  } catch ( MWContentSerializationException $ex ) {
3001  // this can't really happen, but be nice if it does.
3002  $msg = $this->context->msg(
3003  'content-failed-to-parse',
3004  $this->contentModel,
3005  $this->contentFormat,
3006  $ex->getMessage()
3007  );
3008  $out->wrapWikiTextAsInterface( 'error', $msg->plain() );
3009  }
3010  }
3011 
3012  // Set a hidden field so JS knows what edit form mode we are in
3013  if ( $this->isConflict ) {
3014  $mode = 'conflict';
3015  } elseif ( $this->preview ) {
3016  $mode = 'preview';
3017  } elseif ( $this->diff ) {
3018  $mode = 'diff';
3019  } else {
3020  $mode = 'text';
3021  }
3022  $out->addHTML( Html::hidden( 'mode', $mode, [ 'id' => 'mw-edit-mode' ] ) );
3023 
3024  // Marker for detecting truncated form data. This must be the last
3025  // parameter sent in order to be of use, so do not move me.
3026  $out->addHTML( Html::hidden( 'wpUltimateParam', true ) );
3027  $out->addHTML( $this->editFormTextBottom . "\n</form>\n" );
3028 
3029  if ( !$user->getOption( 'previewontop' ) ) {
3030  $this->displayPreviewArea( $previewOutput, false );
3031  }
3032  }
3033 
3041  public function makeTemplatesOnThisPageList( array $templates ) {
3042  $templateListFormatter = new TemplatesOnThisPageFormatter(
3043  $this->context, MediaWikiServices::getInstance()->getLinkRenderer()
3044  );
3045 
3046  // preview if preview, else section if section, else false
3047  $type = false;
3048  if ( $this->preview ) {
3049  $type = 'preview';
3050  } elseif ( $this->section != '' ) {
3051  $type = 'section';
3052  }
3053 
3054  return Html::rawElement( 'div', [ 'class' => 'templatesUsed' ],
3055  $templateListFormatter->format( $templates, $type )
3056  );
3057  }
3058 
3065  public static function extractSectionTitle( $text ) {
3066  preg_match( "/^(=+)(.+)\\1\\s*(\n|$)/i", $text, $matches );
3067  if ( !empty( $matches[2] ) ) {
3068  global $wgParser;
3069  return $wgParser->stripSectionName( trim( $matches[2] ) );
3070  } else {
3071  return false;
3072  }
3073  }
3074 
3075  protected function showHeader() {
3076  $out = $this->context->getOutput();
3077  $user = $this->context->getUser();
3078  if ( $this->isConflict ) {
3079  $this->addExplainConflictHeader( $out );
3080  $this->editRevId = $this->page->getLatest();
3081  } else {
3082  if ( $this->section != '' && $this->section != 'new' ) {
3083  if ( !$this->summary && !$this->preview && !$this->diff ) {
3084  $sectionTitle = self::extractSectionTitle( $this->textbox1 ); // FIXME: use Content object
3085  if ( $sectionTitle !== false ) {
3086  $this->summary = "/* $sectionTitle */ ";
3087  }
3088  }
3089  }
3090 
3091  $buttonLabel = $this->context->msg( $this->getSubmitButtonLabel() )->text();
3092 
3093  if ( $this->missingComment ) {
3094  $out->wrapWikiMsg( "<div id='mw-missingcommenttext'>\n$1\n</div>", 'missingcommenttext' );
3095  }
3096 
3097  if ( $this->missingSummary && $this->section != 'new' ) {
3098  $out->wrapWikiMsg(
3099  "<div id='mw-missingsummary'>\n$1\n</div>",
3100  [ 'missingsummary', $buttonLabel ]
3101  );
3102  }
3103 
3104  if ( $this->missingSummary && $this->section == 'new' ) {
3105  $out->wrapWikiMsg(
3106  "<div id='mw-missingcommentheader'>\n$1\n</div>",
3107  [ 'missingcommentheader', $buttonLabel ]
3108  );
3109  }
3110 
3111  if ( $this->blankArticle ) {
3112  $out->wrapWikiMsg(
3113  "<div id='mw-blankarticle'>\n$1\n</div>",
3114  [ 'blankarticle', $buttonLabel ]
3115  );
3116  }
3117 
3118  if ( $this->selfRedirect ) {
3119  $out->wrapWikiMsg(
3120  "<div id='mw-selfredirect'>\n$1\n</div>",
3121  [ 'selfredirect', $buttonLabel ]
3122  );
3123  }
3124 
3125  if ( $this->hookError !== '' ) {
3126  $out->addWikiTextAsInterface( $this->hookError );
3127  }
3128 
3129  if ( $this->section != 'new' ) {
3130  $revision = $this->mArticle->getRevisionFetched();
3131  if ( $revision ) {
3132  // Let sysop know that this will make private content public if saved
3133 
3134  if ( !$revision->userCan( Revision::DELETED_TEXT, $user ) ) {
3135  $out->wrapWikiMsg(
3136  "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
3137  'rev-deleted-text-permission'
3138  );
3139  } elseif ( $revision->isDeleted( Revision::DELETED_TEXT ) ) {
3140  $out->wrapWikiMsg(
3141  "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
3142  'rev-deleted-text-view'
3143  );
3144  }
3145 
3146  if ( !$revision->isCurrent() ) {
3147  $this->mArticle->setOldSubtitle( $revision->getId() );
3148  $out->wrapWikiMsg(
3149  Html::warningBox( "\n$1\n" ),
3150  'editingold'
3151  );
3152  $this->isOldRev = true;
3153  }
3154  } elseif ( $this->mTitle->exists() ) {
3155  // Something went wrong
3156 
3157  $out->wrapWikiMsg( "<div class='errorbox'>\n$1\n</div>\n",
3158  [ 'missing-revision', $this->oldid ] );
3159  }
3160  }
3161  }
3162 
3163  if ( wfReadOnly() ) {
3164  $out->wrapWikiMsg(
3165  "<div id=\"mw-read-only-warning\">\n$1\n</div>",
3166  [ 'readonlywarning', wfReadOnlyReason() ]
3167  );
3168  } elseif ( $user->isAnon() ) {
3169  if ( $this->formtype != 'preview' ) {
3170  $returntoquery = array_diff_key(
3171  $this->context->getRequest()->getValues(),
3172  [ 'title' => true, 'returnto' => true, 'returntoquery' => true ]
3173  );
3174  $out->wrapWikiMsg(
3175  "<div id='mw-anon-edit-warning' class='warningbox'>\n$1\n</div>",
3176  [ 'anoneditwarning',
3177  // Log-in link
3178  SpecialPage::getTitleFor( 'Userlogin' )->getFullURL( [
3179  'returnto' => $this->getTitle()->getPrefixedDBkey(),
3180  'returntoquery' => wfArrayToCgi( $returntoquery ),
3181  ] ),
3182  // Sign-up link
3183  SpecialPage::getTitleFor( 'CreateAccount' )->getFullURL( [
3184  'returnto' => $this->getTitle()->getPrefixedDBkey(),
3185  'returntoquery' => wfArrayToCgi( $returntoquery ),
3186  ] )
3187  ]
3188  );
3189  } else {
3190  $out->wrapWikiMsg( "<div id=\"mw-anon-preview-warning\" class=\"warningbox\">\n$1</div>",
3191  'anonpreviewwarning'
3192  );
3193  }
3194  } else {
3195  if ( $this->mTitle->isUserConfigPage() ) {
3196  # Check the skin exists
3197  if ( $this->isWrongCaseUserConfigPage() ) {
3198  $out->wrapWikiMsg(
3199  "<div class='error' id='mw-userinvalidconfigtitle'>\n$1\n</div>",
3200  [ 'userinvalidconfigtitle', $this->mTitle->getSkinFromConfigSubpage() ]
3201  );
3202  }
3203  if ( $this->getTitle()->isSubpageOf( $user->getUserPage() ) ) {
3204  $isUserCssConfig = $this->mTitle->isUserCssConfigPage();
3205  $isUserJsonConfig = $this->mTitle->isUserJsonConfigPage();
3206  $isUserJsConfig = $this->mTitle->isUserJsConfigPage();
3207 
3208  $warning = $isUserCssConfig
3209  ? 'usercssispublic'
3210  : ( $isUserJsonConfig ? 'userjsonispublic' : 'userjsispublic' );
3211 
3212  $out->wrapWikiMsg( '<div class="mw-userconfigpublic">$1</div>', $warning );
3213 
3214  if ( $this->formtype !== 'preview' ) {
3215  $config = $this->context->getConfig();
3216  if ( $isUserCssConfig && $config->get( 'AllowUserCss' ) ) {
3217  $out->wrapWikiMsg(
3218  "<div id='mw-usercssyoucanpreview'>\n$1\n</div>",
3219  [ 'usercssyoucanpreview' ]
3220  );
3221  } elseif ( $isUserJsonConfig /* No comparable 'AllowUserJson' */ ) {
3222  $out->wrapWikiMsg(
3223  "<div id='mw-userjsonyoucanpreview'>\n$1\n</div>",
3224  [ 'userjsonyoucanpreview' ]
3225  );
3226  } elseif ( $isUserJsConfig && $config->get( 'AllowUserJs' ) ) {
3227  $out->wrapWikiMsg(
3228  "<div id='mw-userjsyoucanpreview'>\n$1\n</div>",
3229  [ 'userjsyoucanpreview' ]
3230  );
3231  }
3232  }
3233  }
3234  }
3235  }
3236 
3238 
3239  $this->addLongPageWarningHeader();
3240 
3241  # Add header copyright warning
3242  $this->showHeaderCopyrightWarning();
3243  }
3244 
3252  private function getSummaryInputAttributes( array $inputAttrs = null ) {
3253  $conf = $this->context->getConfig();
3254  $oldCommentSchema = $conf->get( 'CommentTableSchemaMigrationStage' ) === MIGRATION_OLD;
3255  // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
3256  // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
3257  // Unicode codepoints (or 255 UTF-8 bytes for old schema).
3258  return ( is_array( $inputAttrs ) ? $inputAttrs : [] ) + [
3259  'id' => 'wpSummary',
3260  'name' => 'wpSummary',
3261  'maxlength' => $oldCommentSchema ? 200 : CommentStore::COMMENT_CHARACTER_LIMIT,
3262  'tabindex' => 1,
3263  'size' => 60,
3264  'spellcheck' => 'true',
3265  ];
3266  }
3267 
3277  function getSummaryInputWidget( $summary = "", $labelText = null, $inputAttrs = null ) {
3278  $inputAttrs = OOUI\Element::configFromHtmlAttributes(
3279  $this->getSummaryInputAttributes( $inputAttrs )
3280  );
3281  $inputAttrs += [
3282  'title' => Linker::titleAttrib( 'summary' ),
3283  'accessKey' => Linker::accesskey( 'summary' ),
3284  ];
3285 
3286  // For compatibility with old scripts and extensions, we want the legacy 'id' on the `<input>`
3287  $inputAttrs['inputId'] = $inputAttrs['id'];
3288  $inputAttrs['id'] = 'wpSummaryWidget';
3289 
3290  return new OOUI\FieldLayout(
3291  new OOUI\TextInputWidget( [
3292  'value' => $summary,
3293  'infusable' => true,
3294  ] + $inputAttrs ),
3295  [
3296  'label' => new OOUI\HtmlSnippet( $labelText ),
3297  'align' => 'top',
3298  'id' => 'wpSummaryLabel',
3299  'classes' => [ $this->missingSummary ? 'mw-summarymissed' : 'mw-summary' ],
3300  ]
3301  );
3302  }
3303 
3310  protected function showSummaryInput( $isSubjectPreview, $summary = "" ) {
3311  # Add a class if 'missingsummary' is triggered to allow styling of the summary line
3312  $summaryClass = $this->missingSummary ? 'mw-summarymissed' : 'mw-summary';
3313  if ( $isSubjectPreview ) {
3314  if ( $this->nosummary ) {
3315  return;
3316  }
3317  } else {
3318  if ( !$this->mShowSummaryField ) {
3319  return;
3320  }
3321  }
3322 
3323  $labelText = $this->context->msg( $isSubjectPreview ? 'subject' : 'summary' )->parse();
3324  $this->context->getOutput()->addHTML( $this->getSummaryInputWidget(
3325  $summary,
3326  $labelText,
3327  [ 'class' => $summaryClass ]
3328  ) );
3329  }
3330 
3338  protected function getSummaryPreview( $isSubjectPreview, $summary = "" ) {
3339  // avoid spaces in preview, gets always trimmed on save
3340  $summary = trim( $summary );
3341  if ( !$summary || ( !$this->preview && !$this->diff ) ) {
3342  return "";
3343  }
3344 
3345  global $wgParser;
3346 
3347  if ( $isSubjectPreview ) {
3348  $summary = $this->context->msg( 'newsectionsummary' )
3349  ->rawParams( $wgParser->stripSectionName( $summary ) )
3350  ->inContentLanguage()->text();
3351  }
3352 
3353  $message = $isSubjectPreview ? 'subject-preview' : 'summary-preview';
3354 
3355  $summary = $this->context->msg( $message )->parse()
3356  . Linker::commentBlock( $summary, $this->mTitle, $isSubjectPreview );
3357  return Xml::tags( 'div', [ 'class' => 'mw-summary-preview' ], $summary );
3358  }
3359 
3360  protected function showFormBeforeText() {
3361  $out = $this->context->getOutput();
3362  $out->addHTML( Html::hidden( 'wpSection', $this->section ) );
3363  $out->addHTML( Html::hidden( 'wpStarttime', $this->starttime ) );
3364  $out->addHTML( Html::hidden( 'wpEdittime', $this->edittime ) );
3365  $out->addHTML( Html::hidden( 'editRevId', $this->editRevId ) );
3366  $out->addHTML( Html::hidden( 'wpScrolltop', $this->scrolltop, [ 'id' => 'wpScrolltop' ] ) );
3367  }
3368 
3369  protected function showFormAfterText() {
3382  $this->context->getOutput()->addHTML(
3383  "\n" .
3384  Html::hidden( "wpEditToken", $this->context->getUser()->getEditToken() ) .
3385  "\n"
3386  );
3387  }
3388 
3397  protected function showContentForm() {
3398  $this->showTextbox1();
3399  }
3400 
3409  protected function showTextbox1( $customAttribs = null, $textoverride = null ) {
3410  if ( $this->wasDeletedSinceLastEdit() && $this->formtype == 'save' ) {
3411  $attribs = [ 'style' => 'display:none;' ];
3412  } else {
3413  $builder = new TextboxBuilder();
3414  $classes = $builder->getTextboxProtectionCSSClasses( $this->getTitle() );
3415 
3416  # Is an old revision being edited?
3417  if ( $this->isOldRev ) {
3418  $classes[] = 'mw-textarea-oldrev';
3419  }
3420 
3421  $attribs = [ 'tabindex' => 1 ];
3422 
3423  if ( is_array( $customAttribs ) ) {
3425  }
3426 
3427  $attribs = $builder->mergeClassesIntoAttributes( $classes, $attribs );
3428  }
3429 
3430  $this->showTextbox(
3431  $textoverride ?? $this->textbox1,
3432  'wpTextbox1',
3433  $attribs
3434  );
3435  }
3436 
3437  protected function showTextbox2() {
3438  $this->showTextbox( $this->textbox2, 'wpTextbox2', [ 'tabindex' => 6, 'readonly' ] );
3439  }
3440 
3441  protected function showTextbox( $text, $name, $customAttribs = [] ) {
3442  $builder = new TextboxBuilder();
3443  $attribs = $builder->buildTextboxAttribs(
3444  $name,
3446  $this->context->getUser(),
3448  );
3449 
3450  $this->context->getOutput()->addHTML(
3451  Html::textarea( $name, $builder->addNewLineAtEnd( $text ), $attribs )
3452  );
3453  }
3454 
3455  protected function displayPreviewArea( $previewOutput, $isOnTop = false ) {
3456  $classes = [];
3457  if ( $isOnTop ) {
3458  $classes[] = 'ontop';
3459  }
3460 
3461  $attribs = [ 'id' => 'wikiPreview', 'class' => implode( ' ', $classes ) ];
3462 
3463  if ( $this->formtype != 'preview' ) {
3464  $attribs['style'] = 'display: none;';
3465  }
3466 
3467  $out = $this->context->getOutput();
3468  $out->addHTML( Xml::openElement( 'div', $attribs ) );
3469 
3470  if ( $this->formtype == 'preview' ) {
3471  $this->showPreview( $previewOutput );
3472  } else {
3473  // Empty content container for LivePreview
3474  $pageViewLang = $this->mTitle->getPageViewLanguage();
3475  $attribs = [ 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(),
3476  'class' => 'mw-content-' . $pageViewLang->getDir() ];
3477  $out->addHTML( Html::rawElement( 'div', $attribs ) );
3478  }
3479 
3480  $out->addHTML( '</div>' );
3481 
3482  if ( $this->formtype == 'diff' ) {
3483  try {
3484  $this->showDiff();
3485  } catch ( MWContentSerializationException $ex ) {
3486  $msg = $this->context->msg(
3487  'content-failed-to-parse',
3488  $this->contentModel,
3489  $this->contentFormat,
3490  $ex->getMessage()
3491  );
3492  $out->wrapWikiTextAsInterface( 'error', $msg->plain() );
3493  }
3494  }
3495  }
3496 
3503  protected function showPreview( $text ) {
3504  if ( $this->mArticle instanceof CategoryPage ) {
3505  $this->mArticle->openShowCategory();
3506  }
3507  # This hook seems slightly odd here, but makes things more
3508  # consistent for extensions.
3509  $out = $this->context->getOutput();
3510  Hooks::run( 'OutputPageBeforeHTML', [ &$out, &$text ] );
3511  $out->addHTML( $text );
3512  if ( $this->mArticle instanceof CategoryPage ) {
3513  $this->mArticle->closeShowCategory();
3514  }
3515  }
3516 
3524  public function showDiff() {
3525  $oldtitlemsg = 'currentrev';
3526  # if message does not exist, show diff against the preloaded default
3527  if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && !$this->mTitle->exists() ) {
3528  $oldtext = $this->mTitle->getDefaultMessageText();
3529  if ( $oldtext !== false ) {
3530  $oldtitlemsg = 'defaultmessagetext';
3531  $oldContent = $this->toEditContent( $oldtext );
3532  } else {
3533  $oldContent = null;
3534  }
3535  } else {
3536  $oldContent = $this->getCurrentContent();
3537  }
3538 
3539  $textboxContent = $this->toEditContent( $this->textbox1 );
3540  if ( $this->editRevId !== null ) {
3541  $newContent = $this->page->replaceSectionAtRev(
3542  $this->section, $textboxContent, $this->summary, $this->editRevId
3543  );
3544  } else {
3545  $newContent = $this->page->replaceSectionContent(
3546  $this->section, $textboxContent, $this->summary, $this->edittime
3547  );
3548  }
3549 
3550  if ( $newContent ) {
3551  Hooks::run( 'EditPageGetDiffContent', [ $this, &$newContent ] );
3552 
3553  $user = $this->context->getUser();
3555  MediaWikiServices::getInstance()->getContentLanguage() );
3556  $newContent = $newContent->preSaveTransform( $this->mTitle, $user, $popts );
3557  }
3558 
3559  if ( ( $oldContent && !$oldContent->isEmpty() ) || ( $newContent && !$newContent->isEmpty() ) ) {
3560  $oldtitle = $this->context->msg( $oldtitlemsg )->parse();
3561  $newtitle = $this->context->msg( 'yourtext' )->parse();
3562 
3563  if ( !$oldContent ) {
3564  $oldContent = $newContent->getContentHandler()->makeEmptyContent();
3565  }
3566 
3567  if ( !$newContent ) {
3568  $newContent = $oldContent->getContentHandler()->makeEmptyContent();
3569  }
3570 
3571  $de = $oldContent->getContentHandler()->createDifferenceEngine( $this->context );
3572  $de->setContent( $oldContent, $newContent );
3573 
3574  $difftext = $de->getDiff( $oldtitle, $newtitle );
3575  $de->showDiffStyle();
3576  } else {
3577  $difftext = '';
3578  }
3579 
3580  $this->context->getOutput()->addHTML( '<div id="wikiDiff">' . $difftext . '</div>' );
3581  }
3582 
3586  protected function showHeaderCopyrightWarning() {
3587  $msg = 'editpage-head-copy-warn';
3588  if ( !$this->context->msg( $msg )->isDisabled() ) {
3589  $this->context->getOutput()->wrapWikiMsg( "<div class='editpage-head-copywarn'>\n$1\n</div>",
3590  'editpage-head-copy-warn' );
3591  }
3592  }
3593 
3602  protected function showTosSummary() {
3603  $msg = 'editpage-tos-summary';
3604  Hooks::run( 'EditPageTosSummary', [ $this->mTitle, &$msg ] );
3605  if ( !$this->context->msg( $msg )->isDisabled() ) {
3606  $out = $this->context->getOutput();
3607  $out->addHTML( '<div class="mw-tos-summary">' );
3608  $out->addWikiMsg( $msg );
3609  $out->addHTML( '</div>' );
3610  }
3611  }
3612 
3617  protected function showEditTools() {
3618  $this->context->getOutput()->addHTML( '<div class="mw-editTools">' .
3619  $this->context->msg( 'edittools' )->inContentLanguage()->parse() .
3620  '</div>' );
3621  }
3622 
3629  protected function getCopywarn() {
3630  return self::getCopyrightWarning( $this->mTitle );
3631  }
3632 
3641  public static function getCopyrightWarning( $title, $format = 'plain', $langcode = null ) {
3642  global $wgRightsText;
3643  if ( $wgRightsText ) {
3644  $copywarnMsg = [ 'copyrightwarning',
3645  '[[' . wfMessage( 'copyrightpage' )->inContentLanguage()->text() . ']]',
3646  $wgRightsText ];
3647  } else {
3648  $copywarnMsg = [ 'copyrightwarning2',
3649  '[[' . wfMessage( 'copyrightpage' )->inContentLanguage()->text() . ']]' ];
3650  }
3651  // Allow for site and per-namespace customization of contribution/copyright notice.
3652  Hooks::run( 'EditPageCopyrightWarning', [ $title, &$copywarnMsg ] );
3653 
3654  $msg = wfMessage( ...$copywarnMsg )->title( $title );
3655  if ( $langcode ) {
3656  $msg->inLanguage( $langcode );
3657  }
3658  return "<div id=\"editpage-copywarn\">\n" .
3659  $msg->$format() . "\n</div>";
3660  }
3661 
3669  public static function getPreviewLimitReport( ParserOutput $output = null ) {
3670  global $wgLang;
3671 
3672  if ( !$output || !$output->getLimitReportData() ) {
3673  return '';
3674  }
3675 
3676  $limitReport = Html::rawElement( 'div', [ 'class' => 'mw-limitReportExplanation' ],
3677  wfMessage( 'limitreport-title' )->parseAsBlock()
3678  );
3679 
3680  // Show/hide animation doesn't work correctly on a table, so wrap it in a div.
3681  $limitReport .= Html::openElement( 'div', [ 'class' => 'preview-limit-report-wrapper' ] );
3682 
3683  $limitReport .= Html::openElement( 'table', [
3684  'class' => 'preview-limit-report wikitable'
3685  ] ) .
3686  Html::openElement( 'tbody' );
3687 
3688  foreach ( $output->getLimitReportData() as $key => $value ) {
3689  if ( Hooks::run( 'ParserLimitReportFormat',
3690  [ $key, &$value, &$limitReport, true, true ]
3691  ) ) {
3692  $keyMsg = wfMessage( $key );
3693  $valueMsg = wfMessage( [ "$key-value-html", "$key-value" ] );
3694  if ( !$valueMsg->exists() ) {
3695  $valueMsg = new RawMessage( '$1' );
3696  }
3697  if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) {
3698  $limitReport .= Html::openElement( 'tr' ) .
3699  Html::rawElement( 'th', null, $keyMsg->parse() ) .
3700  Html::rawElement( 'td', null,
3701  $wgLang->formatNum( $valueMsg->params( $value )->parse() )
3702  ) .
3703  Html::closeElement( 'tr' );
3704  }
3705  }
3706  }
3707 
3708  $limitReport .= Html::closeElement( 'tbody' ) .
3709  Html::closeElement( 'table' ) .
3710  Html::closeElement( 'div' );
3711 
3712  return $limitReport;
3713  }
3714 
3715  protected function showStandardInputs( &$tabindex = 2 ) {
3716  $out = $this->context->getOutput();
3717  $out->addHTML( "<div class='editOptions'>\n" );
3718 
3719  if ( $this->section != 'new' ) {
3720  $this->showSummaryInput( false, $this->summary );
3721  $out->addHTML( $this->getSummaryPreview( false, $this->summary ) );
3722  }
3723 
3724  $checkboxes = $this->getCheckboxesWidget(
3725  $tabindex,
3726  [ 'minor' => $this->minoredit, 'watch' => $this->watchthis ]
3727  );
3728  $checkboxesHTML = new OOUI\HorizontalLayout( [ 'items' => $checkboxes ] );
3729 
3730  $out->addHTML( "<div class='editCheckboxes'>" . $checkboxesHTML . "</div>\n" );
3731 
3732  // Show copyright warning.
3733  $out->addWikiTextAsInterface( $this->getCopywarn() );
3734  $out->addHTML( $this->editFormTextAfterWarn );
3735 
3736  $out->addHTML( "<div class='editButtons'>\n" );
3737  $out->addHTML( implode( "\n", $this->getEditButtons( $tabindex ) ) . "\n" );
3738 
3739  $cancel = $this->getCancelLink();
3740 
3741  $message = $this->context->msg( 'edithelppage' )->inContentLanguage()->text();
3742  $edithelpurl = Skin::makeInternalOrExternalUrl( $message );
3743  $edithelp =
3745  $this->context->msg( 'edithelp' )->text(),
3746  [ 'target' => 'helpwindow', 'href' => $edithelpurl ],
3747  [ 'mw-ui-quiet' ]
3748  ) .
3749  $this->context->msg( 'word-separator' )->escaped() .
3750  $this->context->msg( 'newwindow' )->parse();
3751 
3752  $out->addHTML( " <span class='cancelLink'>{$cancel}</span>\n" );
3753  $out->addHTML( " <span class='editHelp'>{$edithelp}</span>\n" );
3754  $out->addHTML( "</div><!-- editButtons -->\n" );
3755 
3756  Hooks::run( 'EditPage::showStandardInputs:options', [ $this, $out, &$tabindex ] );
3757 
3758  $out->addHTML( "</div><!-- editOptions -->\n" );
3759  }
3760 
3765  protected function showConflict() {
3766  $out = $this->context->getOutput();
3767  // Avoid PHP 7.1 warning of passing $this by reference
3768  $editPage = $this;
3769  if ( Hooks::run( 'EditPageBeforeConflictDiff', [ &$editPage, &$out ] ) ) {
3770  $this->incrementConflictStats();
3771 
3772  $this->getEditConflictHelper()->showEditFormTextAfterFooters();
3773  }
3774  }
3775 
3776  protected function incrementConflictStats() {
3777  $this->getEditConflictHelper()->incrementConflictStats();
3778  }
3779 
3783  public function getCancelLink() {
3784  $cancelParams = [];
3785  if ( !$this->isConflict && $this->oldid > 0 ) {
3786  $cancelParams['oldid'] = $this->oldid;
3787  } elseif ( $this->getContextTitle()->isRedirect() ) {
3788  $cancelParams['redirect'] = 'no';
3789  }
3790 
3791  return new OOUI\ButtonWidget( [
3792  'id' => 'mw-editform-cancel',
3793  'href' => $this->getContextTitle()->getLinkURL( $cancelParams ),
3794  'label' => new OOUI\HtmlSnippet( $this->context->msg( 'cancel' )->parse() ),
3795  'framed' => false,
3796  'infusable' => true,
3797  'flags' => 'destructive',
3798  ] );
3799  }
3800 
3810  protected function getActionURL( Title $title ) {
3811  return $title->getLocalURL( [ 'action' => $this->action ] );
3812  }
3813 
3821  protected function wasDeletedSinceLastEdit() {
3822  if ( $this->deletedSinceEdit !== null ) {
3823  return $this->deletedSinceEdit;
3824  }
3825 
3826  $this->deletedSinceEdit = false;
3827 
3828  if ( !$this->mTitle->exists() && $this->mTitle->isDeletedQuick() ) {
3829  $this->lastDelete = $this->getLastDelete();
3830  if ( $this->lastDelete ) {
3831  $deleteTime = wfTimestamp( TS_MW, $this->lastDelete->log_timestamp );
3832  if ( $deleteTime > $this->starttime ) {
3833  $this->deletedSinceEdit = true;
3834  }
3835  }
3836  }
3837 
3838  return $this->deletedSinceEdit;
3839  }
3840 
3846  protected function getLastDelete() {
3847  $dbr = wfGetDB( DB_REPLICA );
3848  $commentQuery = CommentStore::getStore()->getJoin( 'log_comment' );
3849  $actorQuery = ActorMigration::newMigration()->getJoin( 'log_user' );
3850  $data = $dbr->selectRow(
3851  array_merge( [ 'logging' ], $commentQuery['tables'], $actorQuery['tables'], [ 'user' ] ),
3852  [
3853  'log_type',
3854  'log_action',
3855  'log_timestamp',
3856  'log_namespace',
3857  'log_title',
3858  'log_params',
3859  'log_deleted',
3860  'user_name'
3861  ] + $commentQuery['fields'] + $actorQuery['fields'],
3862  [
3863  'log_namespace' => $this->mTitle->getNamespace(),
3864  'log_title' => $this->mTitle->getDBkey(),
3865  'log_type' => 'delete',
3866  'log_action' => 'delete',
3867  ],
3868  __METHOD__,
3869  [ 'LIMIT' => 1, 'ORDER BY' => 'log_timestamp DESC' ],
3870  [
3871  'user' => [ 'JOIN', 'user_id=' . $actorQuery['fields']['log_user'] ],
3872  ] + $commentQuery['joins'] + $actorQuery['joins']
3873  );
3874  // Quick paranoid permission checks...
3875  if ( is_object( $data ) ) {
3876  if ( $data->log_deleted & LogPage::DELETED_USER ) {
3877  $data->user_name = $this->context->msg( 'rev-deleted-user' )->escaped();
3878  }
3879 
3880  if ( $data->log_deleted & LogPage::DELETED_COMMENT ) {
3881  $data->log_comment_text = $this->context->msg( 'rev-deleted-comment' )->escaped();
3882  $data->log_comment_data = null;
3883  }
3884  }
3885 
3886  return $data;
3887  }
3888 
3894  public function getPreviewText() {
3895  $out = $this->context->getOutput();
3896  $config = $this->context->getConfig();
3897 
3898  if ( $config->get( 'RawHtml' ) && !$this->mTokenOk ) {
3899  // Could be an offsite preview attempt. This is very unsafe if
3900  // HTML is enabled, as it could be an attack.
3901  $parsedNote = '';
3902  if ( $this->textbox1 !== '' ) {
3903  // Do not put big scary notice, if previewing the empty
3904  // string, which happens when you initially edit
3905  // a category page, due to automatic preview-on-open.
3906  $parsedNote = Html::rawElement( 'div', [ 'class' => 'previewnote' ],
3907  $out->parseAsInterface(
3908  $this->context->msg( 'session_fail_preview_html' )->plain()
3909  ) );
3910  }
3911  $this->incrementEditFailureStats( 'session_loss' );
3912  return $parsedNote;
3913  }
3914 
3915  $note = '';
3916 
3917  try {
3918  $content = $this->toEditContent( $this->textbox1 );
3919 
3920  $previewHTML = '';
3921  if ( !Hooks::run(
3922  'AlternateEditPreview',
3923  [ $this, &$content, &$previewHTML, &$this->mParserOutput ] )
3924  ) {
3925  return $previewHTML;
3926  }
3927 
3928  # provide a anchor link to the editform
3929  $continueEditing = '<span class="mw-continue-editing">' .
3930  '[[#' . self::EDITFORM_ID . '|' .
3931  $this->context->getLanguage()->getArrow() . ' ' .
3932  $this->context->msg( 'continue-editing' )->text() . ']]</span>';
3933  if ( $this->mTriedSave && !$this->mTokenOk ) {
3934  if ( $this->mTokenOkExceptSuffix ) {
3935  $note = $this->context->msg( 'token_suffix_mismatch' )->plain();
3936  $this->incrementEditFailureStats( 'bad_token' );
3937  } else {
3938  $note = $this->context->msg( 'session_fail_preview' )->plain();
3939  $this->incrementEditFailureStats( 'session_loss' );
3940  }
3941  } elseif ( $this->incompleteForm ) {
3942  $note = $this->context->msg( 'edit_form_incomplete' )->plain();
3943  if ( $this->mTriedSave ) {
3944  $this->incrementEditFailureStats( 'incomplete_form' );
3945  }
3946  } else {
3947  $note = $this->context->msg( 'previewnote' )->plain() . ' ' . $continueEditing;
3948  }
3949 
3950  # don't parse non-wikitext pages, show message about preview
3951  if ( $this->mTitle->isUserConfigPage() || $this->mTitle->isSiteConfigPage() ) {
3952  if ( $this->mTitle->isUserConfigPage() ) {
3953  $level = 'user';
3954  } elseif ( $this->mTitle->isSiteConfigPage() ) {
3955  $level = 'site';
3956  } else {
3957  $level = false;
3958  }
3959 
3960  if ( $content->getModel() == CONTENT_MODEL_CSS ) {
3961  $format = 'css';
3962  if ( $level === 'user' && !$config->get( 'AllowUserCss' ) ) {
3963  $format = false;
3964  }
3965  } elseif ( $content->getModel() == CONTENT_MODEL_JSON ) {
3966  $format = 'json';
3967  if ( $level === 'user' /* No comparable 'AllowUserJson' */ ) {
3968  $format = false;
3969  }
3970  } elseif ( $content->getModel() == CONTENT_MODEL_JAVASCRIPT ) {
3971  $format = 'js';
3972  if ( $level === 'user' && !$config->get( 'AllowUserJs' ) ) {
3973  $format = false;
3974  }
3975  } else {
3976  $format = false;
3977  }
3978 
3979  # Used messages to make sure grep find them:
3980  # Messages: usercsspreview, userjsonpreview, userjspreview,
3981  # sitecsspreview, sitejsonpreview, sitejspreview
3982  if ( $level && $format ) {
3983  $note = "<div id='mw-{$level}{$format}preview'>" .
3984  $this->context->msg( "{$level}{$format}preview" )->plain() .
3985  ' ' . $continueEditing . "</div>";
3986  }
3987  }
3988 
3989  # If we're adding a comment, we need to show the
3990  # summary as the headline
3991  if ( $this->section === "new" && $this->summary !== "" ) {
3992  $content = $content->addSectionHeader( $this->summary );
3993  }
3994 
3995  $hook_args = [ $this, &$content ];
3996  Hooks::run( 'EditPageGetPreviewContent', $hook_args );
3997 
3998  $parserResult = $this->doPreviewParse( $content );
3999  $parserOutput = $parserResult['parserOutput'];
4000  $previewHTML = $parserResult['html'];
4001  $this->mParserOutput = $parserOutput;
4002  $out->addParserOutputMetadata( $parserOutput );
4003  if ( $out->userCanPreview() ) {
4004  $out->addContentOverride( $this->getTitle(), $content );
4005  }
4006 
4007  if ( count( $parserOutput->getWarnings() ) ) {
4008  $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
4009  }
4010 
4011  } catch ( MWContentSerializationException $ex ) {
4012  $m = $this->context->msg(
4013  'content-failed-to-parse',
4014  $this->contentModel,
4015  $this->contentFormat,
4016  $ex->getMessage()
4017  );
4018  $note .= "\n\n" . $m->plain(); # gets parsed down below
4019  $previewHTML = '';
4020  }
4021 
4022  if ( $this->isConflict ) {
4023  $conflict = Html::rawElement(
4024  'h2', [ 'id' => 'mw-previewconflict' ],
4025  $this->context->msg( 'previewconflict' )->escaped()
4026  );
4027  } else {
4028  $conflict = '<hr />';
4029  }
4030 
4031  $previewhead = Html::rawElement(
4032  'div', [ 'class' => 'previewnote' ],
4034  'h2', [ 'id' => 'mw-previewheader' ],
4035  $this->context->msg( 'preview' )->escaped()
4036  ) .
4037  $out->parseAsInterface( $note ) . $conflict
4038  );
4039 
4040  $pageViewLang = $this->mTitle->getPageViewLanguage();
4041  $attribs = [ 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(),
4042  'class' => 'mw-content-' . $pageViewLang->getDir() ];
4043  $previewHTML = Html::rawElement( 'div', $attribs, $previewHTML );
4044 
4045  return $previewhead . $previewHTML . $this->previewTextAfterContent;
4046  }
4047 
4048  private function incrementEditFailureStats( $failureType ) {
4049  $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
4050  $stats->increment( 'edit.failures.' . $failureType );
4051  }
4052 
4057  protected function getPreviewParserOptions() {
4058  $parserOptions = $this->page->makeParserOptions( $this->context );
4059  $parserOptions->setIsPreview( true );
4060  $parserOptions->setIsSectionPreview( !is_null( $this->section ) && $this->section !== '' );
4061  $parserOptions->enableLimitReport();
4062 
4063  // XXX: we could call $parserOptions->setCurrentRevisionCallback here to force the
4064  // current revision to be null during PST, until setupFakeRevision is called on
4065  // the ParserOptions. Currently, we rely on Parser::getRevisionObject() to ignore
4066  // existing revisions in preview mode.
4067 
4068  return $parserOptions;
4069  }
4070 
4080  protected function doPreviewParse( Content $content ) {
4081  $user = $this->context->getUser();
4082  $parserOptions = $this->getPreviewParserOptions();
4083 
4084  // NOTE: preSaveTransform doesn't have a fake revision to operate on.
4085  // Parser::getRevisionObject() will return null in preview mode,
4086  // causing the context user to be used for {{subst:REVISIONUSER}}.
4087  // XXX: Alternatively, we could also call setupFakeRevision() a second time:
4088  // once before PST with $content, and then after PST with $pstContent.
4089  $pstContent = $content->preSaveTransform( $this->mTitle, $user, $parserOptions );
4090  $scopedCallback = $parserOptions->setupFakeRevision( $this->mTitle, $pstContent, $user );
4091  $parserOutput = $pstContent->getParserOutput( $this->mTitle, null, $parserOptions );
4092  ScopedCallback::consume( $scopedCallback );
4093  return [
4094  'parserOutput' => $parserOutput,
4095  'html' => $parserOutput->getText( [
4096  'enableSectionEditLinks' => false
4097  ] )
4098  ];
4099  }
4100 
4104  public function getTemplates() {
4105  if ( $this->preview || $this->section != '' ) {
4106  $templates = [];
4107  if ( !isset( $this->mParserOutput ) ) {
4108  return $templates;
4109  }
4110  foreach ( $this->mParserOutput->getTemplates() as $ns => $template ) {
4111  foreach ( array_keys( $template ) as $dbk ) {
4112  $templates[] = Title::makeTitle( $ns, $dbk );
4113  }
4114  }
4115  return $templates;
4116  } else {
4117  return $this->mTitle->getTemplateLinksFrom();
4118  }
4119  }
4120 
4127  public static function getEditToolbar( $title = null ) {
4128  $startingToolbar = '<div id="toolbar"></div>';
4129  $toolbar = $startingToolbar;
4130 
4131  if ( !Hooks::run( 'EditPageBeforeEditToolbar', [ &$toolbar ] ) ) {
4132  return null;
4133  };
4134  // Don't add a pointless `<div>` to the page unless a hook caller populated it
4135  return ( $toolbar === $startingToolbar ) ? null : $toolbar;
4136  }
4137 
4156  public function getCheckboxesDefinition( $checked ) {
4157  $checkboxes = [];
4158 
4159  $user = $this->context->getUser();
4160  // don't show the minor edit checkbox if it's a new page or section
4161  if ( !$this->isNew && $user->isAllowed( 'minoredit' ) ) {
4162  $checkboxes['wpMinoredit'] = [
4163  'id' => 'wpMinoredit',
4164  'label-message' => 'minoredit',
4165  // Uses messages: tooltip-minoredit, accesskey-minoredit
4166  'tooltip' => 'minoredit',
4167  'label-id' => 'mw-editpage-minoredit',
4168  'legacy-name' => 'minor',
4169  'default' => $checked['minor'],
4170  ];
4171  }
4172 
4173  if ( $user->isLoggedIn() ) {
4174  $checkboxes['wpWatchthis'] = [
4175  'id' => 'wpWatchthis',
4176  'label-message' => 'watchthis',
4177  // Uses messages: tooltip-watch, accesskey-watch
4178  'tooltip' => 'watch',
4179  'label-id' => 'mw-editpage-watch',
4180  'legacy-name' => 'watch',
4181  'default' => $checked['watch'],
4182  ];
4183  }
4184 
4185  $editPage = $this;
4186  Hooks::run( 'EditPageGetCheckboxesDefinition', [ $editPage, &$checkboxes ] );
4187 
4188  return $checkboxes;
4189  }
4190 
4201  public function getCheckboxesWidget( &$tabindex, $checked ) {
4202  $checkboxes = [];
4203  $checkboxesDef = $this->getCheckboxesDefinition( $checked );
4204 
4205  foreach ( $checkboxesDef as $name => $options ) {
4206  $legacyName = $options['legacy-name'] ?? $name;
4207 
4208  $title = null;
4209  $accesskey = null;
4210  if ( isset( $options['tooltip'] ) ) {
4211  $accesskey = $this->context->msg( "accesskey-{$options['tooltip']}" )->text();
4212  $title = Linker::titleAttrib( $options['tooltip'] );
4213  }
4214  if ( isset( $options['title-message'] ) ) {
4215  $title = $this->context->msg( $options['title-message'] )->text();
4216  }
4217 
4218  $checkboxes[ $legacyName ] = new OOUI\FieldLayout(
4219  new OOUI\CheckboxInputWidget( [
4220  'tabIndex' => ++$tabindex,
4221  'accessKey' => $accesskey,
4222  'id' => $options['id'] . 'Widget',
4223  'inputId' => $options['id'],
4224  'name' => $name,
4225  'selected' => $options['default'],
4226  'infusable' => true,
4227  ] ),
4228  [
4229  'align' => 'inline',
4230  'label' => new OOUI\HtmlSnippet( $this->context->msg( $options['label-message'] )->parse() ),
4231  'title' => $title,
4232  'id' => $options['label-id'] ?? null,
4233  ]
4234  );
4235  }
4236 
4237  return $checkboxes;
4238  }
4239 
4246  protected function getSubmitButtonLabel() {
4247  $labelAsPublish =
4248  $this->context->getConfig()->get( 'EditSubmitButtonLabelPublish' );
4249 
4250  // Can't use $this->isNew as that's also true if we're adding a new section to an extant page
4251  $newPage = !$this->mTitle->exists();
4252 
4253  if ( $labelAsPublish ) {
4254  $buttonLabelKey = $newPage ? 'publishpage' : 'publishchanges';
4255  } else {
4256  $buttonLabelKey = $newPage ? 'savearticle' : 'savechanges';
4257  }
4258 
4259  return $buttonLabelKey;
4260  }
4261 
4270  public function getEditButtons( &$tabindex ) {
4271  $buttons = [];
4272 
4273  $labelAsPublish =
4274  $this->context->getConfig()->get( 'EditSubmitButtonLabelPublish' );
4275 
4276  $buttonLabel = $this->context->msg( $this->getSubmitButtonLabel() )->text();
4277  $buttonTooltip = $labelAsPublish ? 'publish' : 'save';
4278 
4279  $buttons['save'] = new OOUI\ButtonInputWidget( [
4280  'name' => 'wpSave',
4281  'tabIndex' => ++$tabindex,
4282  'id' => 'wpSaveWidget',
4283  'inputId' => 'wpSave',
4284  // Support: IE 6 – Use <input>, otherwise it can't distinguish which button was clicked
4285  'useInputTag' => true,
4286  'flags' => [ 'progressive', 'primary' ],
4287  'label' => $buttonLabel,
4288  'infusable' => true,
4289  'type' => 'submit',
4290  // Messages used: tooltip-save, tooltip-publish
4291  'title' => Linker::titleAttrib( $buttonTooltip ),
4292  // Messages used: accesskey-save, accesskey-publish
4293  'accessKey' => Linker::accesskey( $buttonTooltip ),
4294  ] );
4295 
4296  $buttons['preview'] = new OOUI\ButtonInputWidget( [
4297  'name' => 'wpPreview',
4298  'tabIndex' => ++$tabindex,
4299  'id' => 'wpPreviewWidget',
4300  'inputId' => 'wpPreview',
4301  // Support: IE 6 – Use <input>, otherwise it can't distinguish which button was clicked
4302  'useInputTag' => true,
4303  'label' => $this->context->msg( 'showpreview' )->text(),
4304  'infusable' => true,
4305  'type' => 'submit',
4306  // Message used: tooltip-preview
4307  'title' => Linker::titleAttrib( 'preview' ),
4308  // Message used: accesskey-preview
4309  'accessKey' => Linker::accesskey( 'preview' ),
4310  ] );
4311 
4312  $buttons['diff'] = new OOUI\ButtonInputWidget( [
4313  'name' => 'wpDiff',
4314  'tabIndex' => ++$tabindex,
4315  'id' => 'wpDiffWidget',
4316  'inputId' => 'wpDiff',
4317  // Support: IE 6 – Use <input>, otherwise it can't distinguish which button was clicked
4318  'useInputTag' => true,
4319  'label' => $this->context->msg( 'showdiff' )->text(),
4320  'infusable' => true,
4321  'type' => 'submit',
4322  // Message used: tooltip-diff
4323  'title' => Linker::titleAttrib( 'diff' ),
4324  // Message used: accesskey-diff
4325  'accessKey' => Linker::accesskey( 'diff' ),
4326  ] );
4327 
4328  // Avoid PHP 7.1 warning of passing $this by reference
4329  $editPage = $this;
4330  Hooks::run( 'EditPageBeforeEditButtons', [ &$editPage, &$buttons, &$tabindex ] );
4331 
4332  return $buttons;
4333  }
4334 
4339  public function noSuchSectionPage() {
4340  $out = $this->context->getOutput();
4341  $out->prepareErrorPage( $this->context->msg( 'nosuchsectiontitle' ) );
4342 
4343  $res = $this->context->msg( 'nosuchsectiontext', $this->section )->parseAsBlock();
4344 
4345  // Avoid PHP 7.1 warning of passing $this by reference
4346  $editPage = $this;
4347  Hooks::run( 'EditPageNoSuchSection', [ &$editPage, &$res ] );
4348  $out->addHTML( $res );
4349 
4350  $out->returnToMain( false, $this->mTitle );
4351  }
4352 
4358  public function spamPageWithContent( $match = false ) {
4359  $this->textbox2 = $this->textbox1;
4360 
4361  if ( is_array( $match ) ) {
4362  $match = $this->context->getLanguage()->listToText( $match );
4363  }
4364  $out = $this->context->getOutput();
4365  $out->prepareErrorPage( $this->context->msg( 'spamprotectiontitle' ) );
4366 
4367  $out->addHTML( '<div id="spamprotected">' );
4368  $out->addWikiMsg( 'spamprotectiontext' );
4369  if ( $match ) {
4370  $out->addWikiMsg( 'spamprotectionmatch', wfEscapeWikiText( $match ) );
4371  }
4372  $out->addHTML( '</div>' );
4373 
4374  $out->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
4375  $this->showDiff();
4376 
4377  $out->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
4378  $this->showTextbox2();
4379 
4380  $out->addReturnTo( $this->getContextTitle(), [ 'action' => 'edit' ] );
4381  }
4382 
4393  protected function safeUnicodeInput( $request, $field ) {
4394  return rtrim( $request->getText( $field ) );
4395  }
4396 
4406  protected function safeUnicodeOutput( $text ) {
4407  return $text;
4408  }
4409 
4413  protected function addEditNotices() {
4414  $out = $this->context->getOutput();
4415  $editNotices = $this->mTitle->getEditNotices( $this->oldid );
4416  if ( count( $editNotices ) ) {
4417  $out->addHTML( implode( "\n", $editNotices ) );
4418  } else {
4419  $msg = $this->context->msg( 'editnotice-notext' );
4420  if ( !$msg->isDisabled() ) {
4421  $out->addHTML(
4422  '<div class="mw-editnotice-notext">'
4423  . $msg->parseAsBlock()
4424  . '</div>'
4425  );
4426  }
4427  }
4428  }
4429 
4433  protected function addTalkPageText() {
4434  if ( $this->mTitle->isTalkPage() ) {
4435  $this->context->getOutput()->addWikiMsg( 'talkpagetext' );
4436  }
4437  }
4438 
4442  protected function addLongPageWarningHeader() {
4443  if ( $this->contentLength === false ) {
4444  $this->contentLength = strlen( $this->textbox1 );
4445  }
4446 
4447  $out = $this->context->getOutput();
4448  $lang = $this->context->getLanguage();
4449  $maxArticleSize = $this->context->getConfig()->get( 'MaxArticleSize' );
4450  if ( $this->tooBig || $this->contentLength > $maxArticleSize * 1024 ) {
4451  $out->wrapWikiMsg( "<div class='error' id='mw-edit-longpageerror'>\n$1\n</div>",
4452  [
4453  'longpageerror',
4454  $lang->formatNum( round( $this->contentLength / 1024, 3 ) ),
4455  $lang->formatNum( $maxArticleSize )
4456  ]
4457  );
4458  } else {
4459  if ( !$this->context->msg( 'longpage-hint' )->isDisabled() ) {
4460  $out->wrapWikiMsg( "<div id='mw-edit-longpage-hint'>\n$1\n</div>",
4461  [
4462  'longpage-hint',
4463  $lang->formatSize( strlen( $this->textbox1 ) ),
4464  strlen( $this->textbox1 )
4465  ]
4466  );
4467  }
4468  }
4469  }
4470 
4474  protected function addPageProtectionWarningHeaders() {
4475  $out = $this->context->getOutput();
4476  if ( $this->mTitle->isProtected( 'edit' ) &&
4477  MWNamespace::getRestrictionLevels( $this->mTitle->getNamespace() ) !== [ '' ]
4478  ) {
4479  # Is the title semi-protected?
4480  if ( $this->mTitle->isSemiProtected() ) {
4481  $noticeMsg = 'semiprotectedpagewarning';
4482  } else {
4483  # Then it must be protected based on static groups (regular)
4484  $noticeMsg = 'protectedpagewarning';
4485  }
4486  LogEventsList::showLogExtract( $out, 'protect', $this->mTitle, '',
4487  [ 'lim' => 1, 'msgKey' => [ $noticeMsg ] ] );
4488  }
4489  if ( $this->mTitle->isCascadeProtected() ) {
4490  # Is this page under cascading protection from some source pages?
4491 
4492  list( $cascadeSources, /* $restrictions */ ) = $this->mTitle->getCascadeProtectionSources();
4493  $notice = "<div class='mw-cascadeprotectedwarning'>\n$1\n";
4494  $cascadeSourcesCount = count( $cascadeSources );
4495  if ( $cascadeSourcesCount > 0 ) {
4496  # Explain, and list the titles responsible
4497  foreach ( $cascadeSources as $page ) {
4498  $notice .= '* [[:' . $page->getPrefixedText() . "]]\n";
4499  }
4500  }
4501  $notice .= '</div>';
4502  $out->wrapWikiMsg( $notice, [ 'cascadeprotectedwarning', $cascadeSourcesCount ] );
4503  }
4504  if ( !$this->mTitle->exists() && $this->mTitle->getRestrictions( 'create' ) ) {
4505  LogEventsList::showLogExtract( $out, 'protect', $this->mTitle, '',
4506  [ 'lim' => 1,
4507  'showIfEmpty' => false,
4508  'msgKey' => [ 'titleprotectedwarning' ],
4509  'wrap' => "<div class=\"mw-titleprotectedwarning\">\n$1</div>" ] );
4510  }
4511  }
4512 
4517  protected function addExplainConflictHeader( OutputPage $out ) {
4518  $out->addHTML(
4519  $this->getEditConflictHelper()->getExplainHeader()
4520  );
4521  }
4522 
4531  return ( new TextboxBuilder() )->buildTextboxAttribs(
4532  $name, $customAttribs, $user, $this->mTitle
4533  );
4534  }
4535 
4541  protected function addNewLineAtEnd( $wikitext ) {
4542  return ( new TextboxBuilder() )->addNewLineAtEnd( $wikitext );
4543  }
4544 
4555  private function guessSectionName( $text ) {
4556  global $wgParser;
4557 
4558  // Detect Microsoft browsers
4559  $userAgent = $this->context->getRequest()->getHeader( 'User-Agent' );
4560  if ( $userAgent && preg_match( '/MSIE|Edge/', $userAgent ) ) {
4561  // ...and redirect them to legacy encoding, if available
4562  return $wgParser->guessLegacySectionNameFromWikiText( $text );
4563  }
4564  // Meanwhile, real browsers get real anchors
4565  return $wgParser->guessSectionNameFromWikiText( $text );
4566  }
4567 
4574  public function setEditConflictHelperFactory( callable $factory ) {
4575  $this->editConflictHelperFactory = $factory;
4576  $this->editConflictHelper = null;
4577  }
4578 
4582  private function getEditConflictHelper() {
4583  if ( !$this->editConflictHelper ) {
4584  $this->editConflictHelper = call_user_func(
4585  $this->editConflictHelperFactory,
4586  $this->getSubmitButtonLabel()
4587  );
4588  }
4589 
4591  }
4592 
4597  private function newTextConflictHelper( $submitButtonLabel ) {
4598  return new TextConflictHelper(
4599  $this->getTitle(),
4600  $this->getContext()->getOutput(),
4601  MediaWikiServices::getInstance()->getStatsdDataFactory(),
4602  $submitButtonLabel
4603  );
4604  }
4605 }
string $autoSumm
Definition: EditPage.php:299
getContent( $audience=Revision::FOR_PUBLIC, User $user=null)
Get the content of the current revision.
Definition: WikiPage.php:806
Helps EditPage build textboxes.
The wiki should then use memcached to cache various data To use multiple just add more items to the array To increase the weight of a make its entry a array("192.168.0.1:11211", 2))
static factory(Title $title)
Create a WikiPage object of the appropriate class for the given title.
Definition: WikiPage.php:128
displayPermissionsError(array $permErrors)
Display a permissions error page, like OutputPage::showPermissionsErrorPage(), but with the following...
Definition: EditPage.php:770
safeUnicodeInput( $request, $field)
Filter an input field through a Unicode de-armoring process if it came from an old browser with known...
Definition: EditPage.php:4393
incrementConflictStats()
Definition: EditPage.php:3776
const FOR_THIS_USER
Definition: Revision.php:57
bool $nosummary
If true, hide the summary field.
Definition: EditPage.php:350
getPreloadedContent( $preload, $params=[])
Get the contents to be preloaded into the box, either set by an earlier setPreloadText() or by loadin...
Definition: EditPage.php:1482
$editFormTextBottom
Definition: EditPage.php:423
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition: deferred.txt:11
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:4358
const AS_READ_ONLY_PAGE_ANON
Status: this anonymous user is not allowed to edit this page.
Definition: EditPage.php:83
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:281
bool $bot
Definition: EditPage.php:403
null for the local wiki Added should default to null in handler for backwards compatibility add a value to it if you want to add a cookie that have to vary cache options can modify $query
Definition: hooks.txt:1598
string $textbox2
Definition: EditPage.php:342
either a plain
Definition: hooks.txt:2056
bool $mTokenOk
Definition: EditPage.php:263
$editFormTextAfterContent
Definition: EditPage.php:424
showContentForm()
Subpage overridable method for printing the form for page content editing By default this simply outp...
Definition: EditPage.php:3397
bool $allowBlankSummary
Definition: EditPage.php:284
getPreviewText()
Get the rendered text for previewing.
Definition: EditPage.php:3894
bool $isConflict
Whether an edit conflict needs to be resolved.
Definition: EditPage.php:242
int $oldid
Revision ID the edit is based on, or 0 if it&#39;s the current revision.
Definition: EditPage.php:387
getContentObject( $def_content=null)
Definition: EditPage.php:1193
handleStatus(Status $status, $resultDetails)
Handle status, such as after attempt save.
Definition: EditPage.php:1622
string $summary
Definition: EditPage.php:345
processing should stop and the error should be shown to the user * false
Definition: hooks.txt:187
setHeaders()
Definition: EditPage.php:2495
WikiPage $page
Definition: EditPage.php:224
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not it can be in the form of< username >< more info > e g for bot passwords intended to be added to log contexts Fields it might only if the login was with a bot password probably a stub it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping $template
Definition: hooks.txt:780
per default it will return the text for text based content
static matchSpamRegexInternal( $text, $regexes)
Definition: EditPage.php:2485
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:966
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
const AS_ARTICLE_WAS_DELETED
Status: article was deleted while editing and param wpRecreate == false or form was not posted...
Definition: EditPage.php:104
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:63
div flags Integer display flags(NO_ACTION_LINK, NO_EXTRA_USER_LINKS) 'LogException' returning false will NOT prevent logging $e
Definition: hooks.txt:2172
showTextbox2()
Definition: EditPage.php:3437
bool $tooBig
Definition: EditPage.php:275
$wgParser
Definition: Setup.php:920
showHeaderCopyrightWarning()
Show the header copyright warning.
Definition: EditPage.php:3586
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:231
globals txt Globals are evil The original MediaWiki code relied on globals for processing context far too often MediaWiki development since then has been a story of slowly moving context out of global variables and into objects Storing processing context in object member variables allows those objects to be reused in a much more flexible way Consider the elegance of
database rows
Definition: globals.txt:10
if(!isset( $args[0])) $lang
static getCopyrightWarning( $title, $format='plain', $langcode=null)
Get the copyright warning, by default returns wikitext.
Definition: EditPage.php:3641
static newFromTarget( $specificTarget, $vagueTarget=null, $fromMaster=false)
Given a target and the target&#39;s type, get an existing Block object if possible.
Definition: Block.php:1272
Status::newGood()` to allow deletion, and then `return false` from the hook function. Ensure you consume the 'ChangeTagAfterDelete' hook to carry out custom deletion actions. $tag:name of the tag $user:user initiating the action & $status:Status object. See above. 'ChangeTagsListActive':Allows you to nominate which of the tags your extension uses are in active use. & $tags:list of all active tags. Append to this array. 'ChangeTagsAfterUpdateTags':Called after tags have been updated with the ChangeTags::updateTags function. Params:$addedTags:tags effectively added in the update $removedTags:tags effectively removed in the update $prevTags:tags that were present prior to the update $rc_id:recentchanges table id $rev_id:revision table id $log_id:logging table id $params:tag params $rc:RecentChange being tagged when the tagging accompanies the action, or null $user:User who performed the tagging when the tagging is subsequent to the action, or null 'ChangeTagsAllowedAdd':Called when checking if a user can add tags to a change. & $allowedTags:List of all the tags the user is allowed to add. Any tags the user wants to add( $addTags) that are not in this array will cause it to fail. You may add or remove tags to this array as required. $addTags:List of tags user intends to add. $user:User who is adding the tags. 'ChangeUserGroups':Called before user groups are changed. $performer:The User who will perform the change $user:The User whose groups will be changed & $add:The groups that will be added & $remove:The groups that will be removed 'Collation::factory':Called if $wgCategoryCollation is an unknown collation. $collationName:Name of the collation in question & $collationObject:Null. Replace with a subclass of the Collation class that implements the collation given in $collationName. 'ConfirmEmailComplete':Called after a user 's email has been confirmed successfully. $user:user(object) whose email is being confirmed 'ContentAlterParserOutput':Modify parser output for a given content object. Called by Content::getParserOutput after parsing has finished. Can be used for changes that depend on the result of the parsing but have to be done before LinksUpdate is called(such as adding tracking categories based on the rendered HTML). $content:The Content to render $title:Title of the page, as context $parserOutput:ParserOutput to manipulate 'ContentGetParserOutput':Customize parser output for a given content object, called by AbstractContent::getParserOutput. May be used to override the normal model-specific rendering of page content. $content:The Content to render $title:Title of the page, as context $revId:The revision ID, as context $options:ParserOptions for rendering. To avoid confusing the parser cache, the output can only depend on parameters provided to this hook function, not on global state. $generateHtml:boolean, indicating whether full HTML should be generated. If false, generation of HTML may be skipped, but other information should still be present in the ParserOutput object. & $output:ParserOutput, to manipulate or replace 'ContentHandlerDefaultModelFor':Called when the default content model is determined for a given title. May be used to assign a different model for that title. $title:the Title in question & $model:the model name. Use with CONTENT_MODEL_XXX constants. 'ContentHandlerForModelID':Called when a ContentHandler is requested for a given content model name, but no entry for that model exists in $wgContentHandlers. Note:if your extension implements additional models via this hook, please use GetContentModels hook to make them known to core. $modeName:the requested content model name & $handler:set this to a ContentHandler object, if desired. 'ContentModelCanBeUsedOn':Called to determine whether that content model can be used on a given page. This is especially useful to prevent some content models to be used in some special location. $contentModel:ID of the content model in question $title:the Title in question. & $ok:Output parameter, whether it is OK to use $contentModel on $title. Handler functions that modify $ok should generally return false to prevent further hooks from further modifying $ok. 'ContribsPager::getQueryInfo':Before the contributions query is about to run & $pager:Pager object for contributions & $queryInfo:The query for the contribs Pager 'ContribsPager::reallyDoQuery':Called before really executing the query for My Contributions & $data:an array of results of all contribs queries $pager:The ContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'ContributionsLineEnding':Called before a contributions HTML line is finished $page:SpecialPage object for contributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'ContributionsToolLinks':Change tool links above Special:Contributions $id:User identifier $title:User page title & $tools:Array of tool links $specialPage:SpecialPage instance for context and services. Can be either SpecialContributions or DeletedContributionsPage. Extensions should type hint against a generic SpecialPage though. 'ConvertContent':Called by AbstractContent::convert when a conversion to another content model is requested. Handler functions that modify $result should generally return false to disable further attempts at conversion. $content:The Content object to be converted. $toModel:The ID of the content model to convert to. $lossy:boolean indicating whether lossy conversion is allowed. & $result:Output parameter, in case the handler function wants to provide a converted Content object. Note that $result->getContentModel() must return $toModel. 'ContentSecurityPolicyDefaultSource':Modify the allowed CSP load sources. This affects all directives except for the script directive. If you want to add a script source, see ContentSecurityPolicyScriptSource hook. & $defaultSrc:Array of Content-Security-Policy allowed sources $policyConfig:Current configuration for the Content-Security-Policy header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'ContentSecurityPolicyDirectives':Modify the content security policy directives. Use this only if ContentSecurityPolicyDefaultSource and ContentSecurityPolicyScriptSource do not meet your needs. & $directives:Array of CSP directives $policyConfig:Current configuration for the CSP header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'ContentSecurityPolicyScriptSource':Modify the allowed CSP script sources. Note that you also have to use ContentSecurityPolicyDefaultSource if you want non-script sources to be loaded from whatever you add. & $scriptSrc:Array of CSP directives $policyConfig:Current configuration for the CSP header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'CustomEditor':When invoking the page editor Return true to allow the normal editor to be used, or false if implementing a custom editor, e.g. for a special namespace, etc. $article:Article being edited $user:User performing the edit 'DatabaseOraclePostInit':Called after initialising an Oracle database $db:the DatabaseOracle object 'DeletedContribsPager::reallyDoQuery':Called before really executing the query for Special:DeletedContributions Similar to ContribsPager::reallyDoQuery & $data:an array of results of all contribs queries $pager:The DeletedContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'DeletedContributionsLineEnding':Called before a DeletedContributions HTML line is finished. Similar to ContributionsLineEnding $page:SpecialPage object for DeletedContributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'DeleteUnknownPreferences':Called by the cleanupPreferences.php maintenance script to build a WHERE clause with which to delete preferences that are not known about. This hook is used by extensions that have dynamically-named preferences that should not be deleted in the usual cleanup process. For example, the Gadgets extension creates preferences prefixed with 'gadget-', and so anything with that prefix is excluded from the deletion. &where:An array that will be passed as the $cond parameter to IDatabase::select() to determine what will be deleted from the user_properties table. $db:The IDatabase object, useful for accessing $db->buildLike() etc. 'DifferenceEngineAfterLoadNewText':called in DifferenceEngine::loadNewText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before returning true from this function. $differenceEngine:DifferenceEngine object 'DifferenceEngineLoadTextAfterNewContentIsLoaded':called in DifferenceEngine::loadText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before checking if the variable 's value is null. This hook can be used to inject content into said class member variable. $differenceEngine:DifferenceEngine object 'DifferenceEngineMarkPatrolledLink':Allows extensions to change the "mark as patrolled" link which is shown both on the diff header as well as on the bottom of a page, usually wrapped in a span element which has class="patrollink". $differenceEngine:DifferenceEngine object & $markAsPatrolledLink:The "mark as patrolled" link HTML(string) $rcid:Recent change ID(rc_id) for this change(int) 'DifferenceEngineMarkPatrolledRCID':Allows extensions to possibly change the rcid parameter. For example the rcid might be set to zero due to the user being the same as the performer of the change but an extension might still want to show it under certain conditions. & $rcid:rc_id(int) of the change or 0 $differenceEngine:DifferenceEngine object $change:RecentChange object $user:User object representing the current user 'DifferenceEngineNewHeader':Allows extensions to change the $newHeader variable, which contains information about the new revision, such as the revision 's author, whether the revision was marked as a minor edit or not, etc. $differenceEngine:DifferenceEngine object & $newHeader:The string containing the various #mw-diff-otitle[1-5] divs, which include things like revision author info, revision comment, RevisionDelete link and more $formattedRevisionTools:Array containing revision tools, some of which may have been injected with the DiffRevisionTools hook $nextlink:String containing the link to the next revision(if any) $status
Definition: hooks.txt:1276
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:252
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:210
showTosSummary()
Give a chance for site and per-namespace customizations of terms of service summary link that might e...
Definition: EditPage.php:3602
Special handling for category description pages, showing pages, subcategories and file that belong to...
An IContextSource implementation which will inherit context from another source but allow individual ...
static warningBox( $html)
Return a warning box.
Definition: Html.php:729
The edit page/HTML interface (split from Article) The actual database and text munging is still in Ar...
Definition: EditPage.php:44
Title $mTitle
Definition: EditPage.php:230
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:126
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
const AS_HOOK_ERROR_EXPECTED
Status: A hook function returned an error.
Definition: EditPage.php:68
getLocalURL( $query='', $query2=false)
Get a URL with no fragment or server name (relative URL) from a Title object.
Definition: Title.php:1983
static accesskey( $name)
Given the id of an interface element, constructs the appropriate accesskey attribute from the system ...
Definition: Linker.php:2047
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:3252
getEditButtons(&$tabindex)
Returns an array of html code of the following buttons: save, diff and preview.
Definition: EditPage.php:4270
static newFromUserAndLang(User $user, Language $lang)
Get a ParserOptions object from a given user and language.
also included in $newHeader if any indicating whether we should show just the diff
Definition: hooks.txt:1279
string $editintro
Definition: EditPage.php:397
Class for viewing MediaWiki article and history.
Definition: Article.php:38
null for the local wiki Added in
Definition: hooks.txt:1598
static getSkinNames()
Fetch the set of available skins.
Definition: Skin.php:57
bool $allowBlankArticle
Definition: EditPage.php:290
toEditText( $content)
Gets an editable textual representation of $content.
Definition: EditPage.php:2717
IContextSource $context
Definition: EditPage.php:448
$value
Article $mArticle
Definition: EditPage.php:222
null string $contentFormat
Definition: EditPage.php:409
const AS_BLOCKED_PAGE_FOR_USER
Status: User is blocked from editing this page.
Definition: EditPage.php:73
getWikiText( $shortContext=false, $longContext=false, $lang=null)
Get the error list as a wikitext formatted list.
Definition: Status.php:177
static linkButton( $contents, array $attrs, array $modifiers=[])
Returns an HTML link element in a string styled as a button (when $wgUseMediaWikiUIEverywhere is enab...
Definition: Html.php:166
bool $blankArticle
Definition: EditPage.php:287
buildTextboxAttribs( $name, array $customAttribs, User $user)
Definition: EditPage.php:4530
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:110
The First
Definition: primes.txt:1
getEditConflictMainTextBox(array $customAttribs=[])
HTML to build the textbox1 on edit conflicts.
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency MediaWikiServices
Definition: injection.txt:23
bool $missingComment
Definition: EditPage.php:278
const EDIT_MINOR
Definition: Defines.php:154
getEditPermissionErrors( $rigor='secure')
Definition: EditPage.php:726
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:216
const EDIT_UPDATE
Definition: Defines.php:153
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:1324
this hook is for auditing only $response
Definition: hooks.txt:780
showFormBeforeText()
Definition: EditPage.php:3360
null means default & $customAttribs
Definition: hooks.txt:1995
internalAttemptSave(&$result, $bot=false)
Attempt submission (no UI)
Definition: EditPage.php:1871
bool stdClass $lastDelete
Definition: EditPage.php:260
const AS_UNICODE_NOT_SUPPORTED
Status: edit rejected because browser doesn&#39;t support Unicode.
Definition: EditPage.php:190
target page
addPageProtectionWarningHeaders()
Definition: EditPage.php:4474
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:4201
const CONTENT_MODEL_JSON
Definition: Defines.php:239
edit()
This is the function that gets called for "action=edit".
Definition: EditPage.php:587
getContextTitle()
Get the context title object.
Definition: EditPage.php:536
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:2397
const DB_MASTER
Definition: defines.php:26
displayPreviewArea( $previewOutput, $isOnTop=false)
Definition: EditPage.php:3455
addEditNotices()
Definition: EditPage.php:4413
static formatHiddenCategories( $hiddencats)
Returns HTML for the "hidden categories on this page" list.
Definition: Linker.php:1947
isRedirect()
Tests if the article content represents a redirect.
Definition: WikiPage.php:620
null Title $mContextTitle
Definition: EditPage.php:233
static textarea( $name, $value='', array $attribs=[])
Convenience function to produce a <textarea> element.
Definition: Html.php:817
getEditFormHtmlBeforeContent()
Content to go in the edit form before textbox1.
static configuration should be added through ResourceLoaderGetConfigVars instead can be used to get the real title e g db for database replication lag or jobqueue for job queue size converted to pseudo seconds It is possible to add more fields and they will be returned to the user in the API response after the basic globals have been set but before ordinary actions take place $output
Definition: hooks.txt:2220
int $editRevId
Revision ID of the latest revision of the page when editing was initiated on the client.
Definition: EditPage.php:369
The index of the header message $result[1]=The index of the body text message $result[2 through n]=Parameters passed to body text message. Please note the header message cannot receive/use parameters. 'ImportHandleLogItemXMLTag':When parsing a XML tag in a log item. Return false to stop further processing of the tag $reader:XMLReader object $logInfo:Array of information 'ImportHandlePageXMLTag':When parsing a XML tag in a page. Return false to stop further processing of the tag $reader:XMLReader object & $pageInfo:Array of information 'ImportHandleRevisionXMLTag':When parsing a XML tag in a page revision. Return false to stop further processing of the tag $reader:XMLReader object $pageInfo:Array of page information $revisionInfo:Array of revision information 'ImportHandleToplevelXMLTag':When parsing a top level XML tag. Return false to stop further processing of the tag $reader:XMLReader object 'ImportHandleUnknownUser':When a user doesn 't exist locally, this hook is called to give extensions an opportunity to auto-create it. If the auto-creation is successful, return false. $name:User name 'ImportHandleUploadXMLTag':When parsing a XML tag in a file upload. Return false to stop further processing of the tag $reader:XMLReader object $revisionInfo:Array of information 'ImportLogInterwikiLink':Hook to change the interwiki link used in log entries and edit summaries for transwiki imports. & $fullInterwikiPrefix:Interwiki prefix, may contain colons. & $pageTitle:String that contains page title. 'ImportSources':Called when reading from the $wgImportSources configuration variable. Can be used to lazy-load the import sources list. & $importSources:The value of $wgImportSources. Modify as necessary. See the comment in DefaultSettings.php for the detail of how to structure this array. 'InfoAction':When building information to display on the action=info page. $context:IContextSource object & $pageInfo:Array of information 'InitializeArticleMaybeRedirect':MediaWiki check to see if title is a redirect. & $title:Title object for the current page & $request:WebRequest & $ignoreRedirect:boolean to skip redirect check & $target:Title/string of redirect target & $article:Article object 'InternalParseBeforeLinks':during Parser 's internalParse method before links but after nowiki/noinclude/includeonly/onlyinclude and other processings. & $parser:Parser object & $text:string containing partially parsed text & $stripState:Parser 's internal StripState object 'InternalParseBeforeSanitize':during Parser 's internalParse method just before the parser removes unwanted/dangerous HTML tags and after nowiki/noinclude/includeonly/onlyinclude and other processings. Ideal for syntax-extensions after template/parser function execution which respect nowiki and HTML-comments. & $parser:Parser object & $text:string containing partially parsed text & $stripState:Parser 's internal StripState object 'InterwikiLoadPrefix':When resolving if a given prefix is an interwiki or not. Return true without providing an interwiki to continue interwiki search. $prefix:interwiki prefix we are looking for. & $iwData:output array describing the interwiki with keys iw_url, iw_local, iw_trans and optionally iw_api and iw_wikiid. 'InvalidateEmailComplete':Called after a user 's email has been invalidated successfully. $user:user(object) whose email is being invalidated 'IRCLineURL':When constructing the URL to use in an IRC notification. Callee may modify $url and $query, URL will be constructed as $url . $query & $url:URL to index.php & $query:Query string $rc:RecentChange object that triggered url generation 'IsFileCacheable':Override the result of Article::isFileCacheable()(if true) & $article:article(object) being checked 'IsTrustedProxy':Override the result of IP::isTrustedProxy() & $ip:IP being check & $result:Change this value to override the result of IP::isTrustedProxy() 'IsUploadAllowedFromUrl':Override the result of UploadFromUrl::isAllowedUrl() $url:URL used to upload from & $allowed:Boolean indicating if uploading is allowed for given URL 'isValidEmailAddr':Override the result of Sanitizer::validateEmail(), for instance to return false if the domain name doesn 't match your organization. $addr:The e-mail address entered by the user & $result:Set this and return false to override the internal checks 'isValidPassword':Override the result of User::isValidPassword() $password:The password entered by the user & $result:Set this and return false to override the internal checks $user:User the password is being validated for 'Language::getMessagesFileName':$code:The language code or the language we 're looking for a messages file for & $file:The messages file path, you can override this to change the location. 'LanguageGetNamespaces':Provide custom ordering for namespaces or remove namespaces. Do not use this hook to add namespaces. Use CanonicalNamespaces for that. & $namespaces:Array of namespaces indexed by their numbers 'LanguageGetTranslatedLanguageNames':Provide translated language names. & $names:array of language code=> language name $code:language of the preferred translations 'LanguageLinks':Manipulate a page 's language links. This is called in various places to allow extensions to define the effective language links for a page. $title:The page 's Title. & $links:Array with elements of the form "language:title" in the order that they will be output. & $linkFlags:Associative array mapping prefixed links to arrays of flags. Currently unused, but planned to provide support for marking individual language links in the UI, e.g. for featured articles. 'LanguageSelector':Hook to change the language selector available on a page. $out:The output page. $cssClassName:CSS class name of the language selector. 'LinkBegin':DEPRECATED since 1.28! Use HtmlPageLinkRendererBegin instead. Used when generating internal and interwiki links in Linker::link(), before processing starts. Return false to skip default processing and return $ret. See documentation for Linker::link() for details on the expected meanings of parameters. $skin:the Skin object $target:the Title that the link is pointing to & $html:the contents that the< a > tag should have(raw HTML) $result
Definition: hooks.txt:1993
const AS_CONTENT_TOO_BIG
Status: Content too big (> $wgMaxArticleSize)
Definition: EditPage.php:78
The User object encapsulates all of the user-specific settings (user_id, name, rights, email address, options, last login time).
Definition: User.php:47
getContext()
Gets the context this Article is executed in.
Definition: Article.php:2238
addExplainConflictHeader(OutputPage $out)
Definition: EditPage.php:4517
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:1588
showIntro()
Show all applicable editing introductions.
Definition: EditPage.php:2560
setWikiPage(WikiPage $wikiPage)
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return true
Definition: hooks.txt:1995
getArticle()
Definition: EditPage.php:499
$wgSpamRegex
Edits matching these regular expressions in body text will be recognised as spam and rejected automat...
bool $watchthis
Definition: EditPage.php:331
$previewTextAfterContent
Definition: EditPage.php:425
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
const DELETED_COMMENT
Definition: LogPage.php:35
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not it can be in the form of< username >< more info > e g for bot passwords intended to be added to log contexts Fields it might only if the login was with a bot password probably a stub it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output modifiable modifiable after all normalizations have been except for the $wgMaxImageArea check set to true or false to override the $wgMaxImageArea check result gives extension the possibility to transform it themselves $handler
Definition: hooks.txt:780
static addCallableUpdate( $callable, $stage=self::POSTSEND, $dbw=null)
Add a callable update.
getParentRevId()
Get the edit&#39;s parent revision ID.
Definition: EditPage.php:1405
addLongPageWarningHeader()
Definition: EditPage.php:4442
getTemplates()
Definition: EditPage.php:4104
bool $save
Definition: EditPage.php:319
wfReadOnly()
Check whether the wiki is in read-only mode.
$wgLang
Definition: Setup.php:909
static newMigration()
Static constructor.
either a unescaped string or a HtmlArmor object after in associative array form externallinks including delete and has completed for all link tables whether this was an auto creation use $formDescriptor instead default is conds Array Extra conditions for the No matching items in log is displayed if loglist is empty msgKey Array If you want a nice box with a set this to the key of the message First element is the message additional optional elements are parameters for the key that are processed with wfMessage() -> params() ->parseAsBlock() - offset Set to overwrite offset parameter in $wgRequest set to '' to unset offset - wrap String Wrap the message in html(usually something like "&lt
setContextTitle( $title)
Set the context Title object.
Definition: EditPage.php:524
static openElement( $element, $attribs=null)
This opens an XML element.
Definition: Xml.php:110
TextConflictHelper null $editConflictHelper
Definition: EditPage.php:470
toEditContent( $text)
Turns the given text into a Content object by unserializing it.
Definition: EditPage.php:2745
static getForModelID( $modelId)
Returns the ContentHandler singleton for the given model ID.
See &</td >< td > &Fill in a specific reason below(for example, citing particular pages that were vandalized).</td >< td >
const AS_SPAM_ERROR
Status: summary contained spam according to one of the regexes in $wgSummarySpamRegex.
Definition: EditPage.php:146
const EDIT_FORCE_BOT
Definition: Defines.php:156
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:1999
static getPreviewLimitReport(ParserOutput $output=null)
Get the Limit report for page previews.
Definition: EditPage.php:3669
callable $editConflictHelperFactory
Factory function to create an edit conflict helper.
Definition: EditPage.php:465
getLastDelete()
Get the last log record of this page being deleted, if ever.
Definition: EditPage.php:3846
in this case you re responsible for computing and outputting the entire conflict i the difference between revisions and your text headers and sections and Diff & $tabindex
Definition: hooks.txt:1433
getActionURL(Title $title)
Returns the URL to use in the form&#39;s action attribute.
Definition: EditPage.php:3810
static doWatchOrUnwatch( $watch, Title $title, User $user)
Watch or unwatch a page.
Definition: WatchAction.php:89
$editFormTextAfterTools
Definition: EditPage.php:422
const AS_CANNOT_USE_CUSTOM_MODEL
Status: when changing the content model is disallowed due to $wgContentHandlerUseDB being false...
Definition: EditPage.php:185
wfFindFile( $title, $options=[])
Find a file.
const MIGRATION_OLD
Definition: Defines.php:315
$editFormTextAfterWarn
Definition: EditPage.php:421
$res
Definition: database.txt:21
bool $recreate
Definition: EditPage.php:334
setPreloadedContent(Content $content)
Use this method before edit() to preload some content into the edit box.
Definition: EditPage.php:1467
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:53
Using a hook running we can avoid having all this option specific stuff in our mainline code Using the function We ve cleaned up the code here by removing clumps of infrequently used code and moving them off somewhere else It s much easier for someone working with this code to see what s _really_ going and make changes or fix bugs In we can take all the code that deals with the little used title reversing etc
Definition: hooks.txt:91
const AS_READ_ONLY_PAGE
Status: wiki is in readonly mode (wfReadOnly() == true)
Definition: EditPage.php:93
static extractSectionTitle( $text)
Extract the section title from current section text, if any.
Definition: EditPage.php:3065
bool $isOldRev
Whether an old revision is edited.
Definition: EditPage.php:453
showHeader()
Definition: EditPage.php:3075
const EDIT_AUTOSUMMARY
Definition: Defines.php:158
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
const UNICODE_CHECK
Used for Unicode support checks.
Definition: EditPage.php:48
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:162
addContentModelChangeLogEntry(User $user, $oldModel, $newModel, $reason)
Definition: EditPage.php:2352
getTitle()
Get the title object of the article.
Definition: Article.php:221
const IGNORE_USER_RIGHTS
Definition: User.php:77
$params
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped & $options
Definition: hooks.txt:1995
static tags( $element, $attribs, $contents)
Same as Xml::element(), but does not escape contents.
Definition: Xml.php:132
static closeElement( $element)
Returns "</$element>".
Definition: Html.php:316
string $edittime
Timestamp of the latest revision of the page when editing was initiated on the client.
Definition: EditPage.php:356
showSummaryInput( $isSubjectPreview, $summary="")
Definition: EditPage.php:3310
showEditForm( $formCallback=null)
Send the edit form and related headers to OutputPage.
Definition: EditPage.php:2768
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:1154
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return an< a > element with HTML attributes $attribs and contents $html will be returned If you return $ret will be returned and may include noclasses after processing & $attribs
Definition: hooks.txt:1995
wasDeletedSinceLastEdit()
Check if a page was deleted while the user was editing it, before submit.
Definition: EditPage.php:3821
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a save
Definition: deferred.txt:4
isSectionEditSupported()
Returns whether section editing is supported for the current page.
Definition: EditPage.php:915
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not null
Definition: hooks.txt:780
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:257
namespace and then decline to actually register it file or subcat img or subcat $title
Definition: hooks.txt:935
showFormAfterText()
Definition: EditPage.php:3369
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:3524
bool $isNew
New page or new section.
Definition: EditPage.php:245
$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:853
const EDITFORM_ID
HTML id and name for the beginning of the edit form.
Definition: EditPage.php:195
const NS_FILE
Definition: Defines.php:70
getCopywarn()
Get the copyright warning.
Definition: EditPage.php:3629
bool $allowSelfRedirect
Definition: EditPage.php:296
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:82
const TYPE_AUTO
Definition: Block.php:96
const AS_SUCCESS_NEW_ARTICLE
Status: Article successfully created.
Definition: EditPage.php:58
Show an error when the user tries to do something whilst blocked.
safeUnicodeOutput( $text)
Filter an output field through a Unicode armoring process if it is going to an old browser with known...
Definition: EditPage.php:4406
presenting them properly to the user as errors is done by the caller return true use this to change the list i e etc $rev
Definition: hooks.txt:1779
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:156
const RAW
Definition: Revision.php:58
static getForTitle(Title $title)
Returns the appropriate ContentHandler singleton for the given title.
getPreviewParserOptions()
Get parser options for a preview.
Definition: EditPage.php:4057
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not it can be in the form of< username >< more info > e g for bot passwords intended to be added to log contexts Fields it might only if the login was with a bot password probably a stub it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output modifiable & $code
Definition: hooks.txt:780
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
Definition: distributors.txt:9
getCancelLink()
Definition: EditPage.php:3783
bool int $contentLength
Definition: EditPage.php:438
const NS_MEDIAWIKI
Definition: Defines.php:72
showTextbox1( $customAttribs=null, $textoverride=null)
Method to output wpTextbox1 The $textoverride method can be used by subclasses overriding showContent...
Definition: EditPage.php:3409
incrementEditFailureStats( $failureType)
Definition: EditPage.php:4048
const AS_END
Status: WikiPage::doEdit() was unsuccessful.
Definition: EditPage.php:141
getSummaryPreview( $isSubjectPreview, $summary="")
Definition: EditPage.php:3338
makeTemplatesOnThisPageList(array $templates)
Wrapper around TemplatesOnThisPageFormatter to make a "templates on this page" list.
Definition: EditPage.php:3041
showPreview( $text)
Append preview output to OutputPage.
Definition: EditPage.php:3503
string $textbox1
Page content input field.
Definition: EditPage.php:339
CONTENT_MODEL_JAVASCRIPT
Allow users to upload files.
const DELETED_USER
Definition: LogPage.php:36
const DELETED_TEXT
Definition: Revision.php:48
importFormData(&$request)
This function collects the form data and uses it to populate various member variables.
Definition: EditPage.php:925
noSuchSectionPage()
Creates a basic error page which informs the user that they have attempted to edit a nonexistent sect...
Definition: EditPage.php:4339
$wgSummarySpamRegex
Same as the above except for edit summaries.
displayViewSourcePage(Content $content, $errorMessage='')
Display a read-only View Source page.
Definition: EditPage.php:800
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:545
static getEditToolbar( $title=null)
Allow extensions to provide a toolbar.
Definition: EditPage.php:4127
ParserOutput $mParserOutput
Definition: EditPage.php:305
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition: injection.txt:35
bool $mShowSummaryField
Definition: EditPage.php:314
string $sectiontitle
Definition: EditPage.php:375
string null $unicodeCheck
What the user submitted in the &#39;wpUnicodeCheck&#39; field.
Definition: EditPage.php:458
bool $minoredit
Definition: EditPage.php:328
this hook is for auditing only or null if authentication failed before getting that far $username
Definition: hooks.txt:780
bool $enableApiEditOverride
Set in ApiEditPage, based on ContentHandler::allowsDirectApiEditing.
Definition: EditPage.php:443
static getCanonicalName( $index)
Returns the canonical (English) name for a given index.
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not it can be in the form of< username >< more info > e g for bot passwords intended to be added to log contexts Fields it might only if the login was with a bot password probably a stub it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output $out
Definition: hooks.txt:780
static closeElement( $element)
Shortcut to close an XML element.
Definition: Xml.php:119
int $parentRevId
Revision ID the edit is based on, adjusted when an edit conflict is resolved.
Definition: EditPage.php:394
doPreviewParse(Content $content)
Parse the page for a preview.
Definition: EditPage.php:4080
static matchSpamRegex( $text)
Check given input text against $wgSpamRegex, and return the text of the first match.
Definition: EditPage.php:2460
string $action
Definition: EditPage.php:236
newTextConflictHelper( $submitButtonLabel)
Definition: EditPage.php:4597
bool $deletedSinceEdit
Definition: EditPage.php:248
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:2683
static matchSummarySpamRegex( $text)
Check given input text against $wgSummarySpamRegex, and return the text of the first match...
Definition: EditPage.php:2474
importContentFormData(&$request)
Subpage overridable method for extracting the page content data from the posted form to be placed in ...
Definition: EditPage.php:1145
const EDIT_NEW
Definition: Defines.php:152
Revision bool null $mBaseRevision
A revision object corresponding to $this->editRevId.
Definition: EditPage.php:311
newSectionSummary(&$sectionanchor=null)
Return the summary to be used for a new section.
Definition: EditPage.php:1823
const AS_RATE_LIMITED
Status: rate limiter for action &#39;edit&#39; was tripped.
Definition: EditPage.php:98
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:2443
static hasDifferencesOutsideMainSlot(Revision $a, Revision $b)
Helper method for checking whether two revisions have differences that go beyond the main slot...
Definition: WikiPage.php:1515
addNewLineAtEnd( $wikitext)
Definition: EditPage.php:4541
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:1756
const AS_MAX_ARTICLE_SIZE_EXCEEDED
Status: article is too big (> $wgMaxArticleSize), after merging in the new section.
Definition: EditPage.php:136
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:1605
getCurrentContent()
Get the current content of the page.
Definition: EditPage.php:1421
updateWatchlist()
Register the change of watch status.
Definition: EditPage.php:2369
static hidden( $name, $value, array $attribs=[])
Convenience function to produce an input element with type=hidden.
Definition: Html.php:801
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
string $hookError
Definition: EditPage.php:302
showEditTools()
Inserts optional text shown below edit and upload forms.
Definition: EditPage.php:3617
bool $preview
Definition: EditPage.php:322
const AS_IMAGE_REDIRECT_ANON
Status: anonymous user is not allowed to upload (User::isAllowed(&#39;upload&#39;) == false) ...
Definition: EditPage.php:151
getCheckboxesDefinition( $checked)
Return an array of checkbox definitions.
Definition: EditPage.php:4156
showStandardInputs(&$tabindex=2)
Definition: EditPage.php:3715
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:131
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
setPostEditCookie( $statusValue)
Sets post-edit cookie indicating the user just saved a particular revision.
Definition: EditPage.php:1567
static getRestrictionLevels( $index, User $user=null)
Determine which restriction levels it makes sense to use in a namespace, optionally filtered by a use...
getRedirectTarget()
If this page is a redirect, get its target.
Definition: WikiPage.php:984
bool $mTriedSave
Definition: EditPage.php:269
const CONTENT_MODEL_CSS
Definition: Defines.php:237
$mPreloadContent
Definition: EditPage.php:426
getContext()
Definition: EditPage.php:507
showConflict()
Show an edit conflict.
Definition: EditPage.php:3765
addTalkPageText()
Definition: EditPage.php:4433
bool $hasPresetSummary
Has a summary been preset using GET parameter &summary= ?
Definition: EditPage.php:308
getEditConflictHelper()
Definition: EditPage.php:4582
MediaWiki Logger LoggerFactory implements a PSR [0] compatible message logging system Named Psr Log LoggerInterface instances can be obtained from the MediaWiki Logger LoggerFactory::getInstance() static method. MediaWiki\Logger\LoggerFactory expects a class implementing the MediaWiki\Logger\Spi interface to act as a factory for new Psr\Log\LoggerInterface instances. The "Spi" in MediaWiki\Logger\Spi stands for "service provider interface". An SPI is an API intended to be implemented or extended by a third party. This software design pattern is intended to enable framework extension and replaceable components. It is specifically used in the MediaWiki\Logger\LoggerFactory service to allow alternate PSR-3 logging implementations to be easily integrated with MediaWiki. The service provider interface allows the backend logging library to be implemented in multiple ways. The $wgMWLoggerDefaultSpi global provides the classname of the default MediaWiki\Logger\Spi implementation to be loaded at runtime. This can either be the name of a class implementing the MediaWiki\Logger\Spi with a zero argument const ructor or a callable that will return an MediaWiki\Logger\Spi instance. Alternately the MediaWiki\Logger\LoggerFactory MediaWiki Logger LoggerFactory
Definition: logger.txt:5
const POST_EDIT_COOKIE_KEY_PREFIX
Prefix of key for cookie used to pass post-edit state.
Definition: EditPage.php:201
bool $diff
Definition: EditPage.php:325
Allows to change the fields on the form that will be generated $name
Definition: hooks.txt:271
const AS_BLANK_ARTICLE
Status: user tried to create a blank page and wpIgnoreBlankArticle == false.
Definition: EditPage.php:115
addHTML( $text)
Append $text to the body HTML.
setEditConflictHelperFactory(callable $factory)
Set a factory function to create an EditConflictHelper.
Definition: EditPage.php:4574
string $starttime
Timestamp from the first time the edit form was rendered.
Definition: EditPage.php:380
string $formtype
Definition: EditPage.php:251
string $section
Definition: EditPage.php:372
getSummaryInputWidget( $summary="", $labelText=null, $inputAttrs=null)
Builds a standard summary input with a label.
Definition: EditPage.php:3277
const DB_REPLICA
Definition: defines.php:25
return true to allow those checks to and false if checking is done remove or add to the links of a group of changes in EnhancedChangesList Hook subscribers can return false to omit this line from recentchanges use this to change the tables headers change it to an object instance and return false override the list derivative used the name of the old file & $article
Definition: hooks.txt:1486
static makeInternalOrExternalUrl( $name)
If url string starts with http, consider as external URL, else internal.
Definition: Skin.php:1193
formatStatusErrors(Status $status)
Wrap status errors in an errorbox for increased visibility.
Definition: EditPage.php:1803
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:481
getTitle()
Definition: EditPage.php:515
bool $mTokenOkExceptSuffix
Definition: EditPage.php:266
if(! $wgRequest->checkUrlExtension()) if(isset( $_SERVER['PATH_INFO']) && $_SERVER['PATH_INFO'] !='') $wgTitle
Definition: api.php:57
static checkLabel( $label, $name, $id, $checked=false, $attribs=[])
Convenience function to build an HTML checkbox with a label.
Definition: Xml.php:421
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:120
isSupportedContentModel( $modelId)
Returns if the given content model is editable.
Definition: EditPage.php:553
setApiEditOverride( $enableOverride)
Allow editing of content that supports API direct editing, but not general direct editing...
Definition: EditPage.php:564
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition: User.php:585
$suppressIntro
Definition: EditPage.php:432
getOriginalContent(User $user)
Get the content of the wanted revision, without section extraction.
Definition: EditPage.php:1380
static newFromId( $id, $flags=0)
Load a page revision from a given revision ID number.
Definition: Revision.php:120
bool $selfRedirect
Definition: EditPage.php:293
bool $incompleteForm
Definition: EditPage.php:272
bool $edit
Definition: EditPage.php:435
const AS_READ_ONLY_PAGE_LOGGED
Status: this logged in user is not allowed to edit this page.
Definition: EditPage.php:88
static makeContent( $text, Title $title=null, $modelId=null, $format=null)
Convenience function for creating a Content object from a given textual representation.
fatal( $message)
Add an error and set OK to false, indicating that the operation as a whole was fatal.
$content
Definition: pageupdater.txt:72
const AS_PARSE_ERROR
Status: can&#39;t parse content.
Definition: EditPage.php:179
const NS_USER_TALK
Definition: Defines.php:67
getSubmitButtonLabel()
Get the message key of the label for the button to save the page.
Definition: EditPage.php:4246
showTextbox( $text, $name, $customAttribs=[])
Definition: EditPage.php:3441
const AS_SELF_REDIRECT
Status: user tried to create self-redirect (redirect to the same article) and wpIgnoreSelfRedirect ==...
Definition: EditPage.php:168
setContentModel( $model)
Set a proposed content model for the page for permissions checking.
Definition: Title.php:1025
$editFormTextBeforeContent
Definition: EditPage.php:420
do that in ParserLimitReportFormat instead use this to modify the parameters of the image all existing parser cache entries will be invalid To avoid you ll need to handle that somehow(e.g. with the RejectParserCacheValue hook) because MediaWiki won 't do it for you. & $defaults also a ContextSource after deleting those rows but within the same transaction you ll probably need to make sure the header is varied on $request
Definition: hooks.txt:2625
null array $changeTags
Definition: EditPage.php:412
setTextboxes( $yourtext, $storedversion)
isWrongCaseUserConfigPage()
Checks whether the user entered a skin name in uppercase, e.g.
Definition: EditPage.php:894
Show an error when the user hits a rate limit.
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:1458
string $contentModel
Definition: EditPage.php:406
return true to allow those checks to and false if checking is done & $user
Definition: hooks.txt:1486
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:1545
string $editFormPageTop
Before even the preview.
Definition: EditPage.php:418
const AS_CHANGE_TAG_ERROR
Status: an error relating to change tagging.
Definition: EditPage.php:174
guessSectionName( $text)
Turns section name wikitext into anchors for use in HTTP redirects.
Definition: EditPage.php:4555
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:475
$editFormTextTop
Definition: EditPage.php:419
static showLogExtract(&$out, $types=[], $page='', $user='', $param=[])
Show log extract.
$matches
null $scrolltop
Definition: EditPage.php:400
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition: Title.php:280