MediaWiki  master
EditPage.php
Go to the documentation of this file.
1 <?php
29 
45 class EditPage {
49  const UNICODE_CHECK = 'ℳ𝒲♥𝓊𝓃𝒾𝒸ℴ𝒹ℯ';
50 
54  const AS_SUCCESS_UPDATE = 200;
55 
60 
64  const AS_HOOK_ERROR = 210;
65 
70 
75 
79  const AS_CONTENT_TOO_BIG = 216;
80 
85 
90 
94  const AS_READ_ONLY_PAGE = 220;
95 
99  const AS_RATE_LIMITED = 221;
100 
106 
112 
116  const AS_BLANK_ARTICLE = 224;
117 
121  const AS_CONFLICT_DETECTED = 225;
122 
127  const AS_SUMMARY_NEEDED = 226;
128 
132  const AS_TEXTBOX_EMPTY = 228;
133 
138 
142  const AS_END = 231;
143 
147  const AS_SPAM_ERROR = 232;
148 
153 
158 
164 
169  const AS_SELF_REDIRECT = 236;
170 
175  const AS_CHANGE_TAG_ERROR = 237;
176 
180  const AS_PARSE_ERROR = 240;
181 
187 
192 
196  const EDITFORM_ID = 'editform';
197 
202  const POST_EDIT_COOKIE_KEY_PREFIX = 'PostEditRevision';
203 
218 
223  public $mArticle;
225  private $page;
226 
231  public $mTitle;
232 
234  private $mContextTitle = null;
235 
237  public $action = 'submit';
238 
243  public $isConflict = false;
244 
246  public $isNew = false;
247 
250 
252  public $formtype;
253 
258  public $firsttime;
259 
261  public $lastDelete;
262 
264  public $mTokenOk = false;
265 
267  public $mTokenOkExceptSuffix = false;
268 
270  public $mTriedSave = false;
271 
273  public $incompleteForm = false;
274 
276  public $tooBig = false;
277 
279  public $missingComment = false;
280 
282  public $missingSummary = false;
283 
285  public $allowBlankSummary = false;
286 
288  protected $blankArticle = false;
289 
291  protected $allowBlankArticle = false;
292 
294  protected $selfRedirect = false;
295 
297  protected $allowSelfRedirect = false;
298 
300  public $autoSumm = '';
301 
303  public $hookError = '';
304 
307 
309  public $hasPresetSummary = false;
310 
312  public $mBaseRevision = false;
313 
315  public $mShowSummaryField = true;
316 
317  # Form values
318 
320  public $save = false;
321 
323  public $preview = false;
324 
326  public $diff = false;
327 
329  public $minoredit = false;
330 
332  public $watchthis = false;
333 
335  public $recreate = false;
336 
340  public $textbox1 = '';
341 
343  public $textbox2 = '';
344 
346  public $summary = '';
347 
351  public $nosummary = false;
352 
357  public $edittime = '';
358 
370  private $editRevId = null;
371 
373  public $section = '';
374 
376  public $sectiontitle = '';
377 
381  public $starttime = '';
382 
388  public $oldid = 0;
389 
395  public $parentRevId = 0;
396 
398  public $editintro = '';
399 
401  public $scrolltop = null;
402 
404  public $bot = true;
405 
408 
411 
413  private $changeTags = null;
414 
415  # Placeholders for text injection by hooks (must be HTML)
416  # extensions should take care to _append_ to the present value
417 
419  public $editFormPageTop = '';
420  public $editFormTextTop = '';
424  public $editFormTextBottom = '';
428 
429  /* $didSave should be set to true whenever an article was successfully altered. */
430  public $didSave = false;
431  public $undidRev = 0;
432 
433  public $suppressIntro = false;
434 
436  protected $edit;
437 
439  protected $contentLength = false;
440 
444  private $enableApiEditOverride = false;
445 
449  protected $context;
450 
454  private $isOldRev = false;
455 
459  private $unicodeCheck;
460 
467 
472 
476  public function __construct( Article $article ) {
477  $this->mArticle = $article;
478  $this->page = $article->getPage(); // model object
479  $this->mTitle = $article->getTitle();
480 
481  // Make sure the local context is in sync with other member variables.
482  // Particularly make sure everything is using the same WikiPage instance.
483  // This should probably be the case in Article as well, but it's
484  // particularly important for EditPage, to make use of the in-place caching
485  // facility in WikiPage::prepareContentForEdit.
486  $this->context = new DerivativeContext( $article->getContext() );
487  $this->context->setWikiPage( $this->page );
488  $this->context->setTitle( $this->mTitle );
489 
490  $this->contentModel = $this->mTitle->getContentModel();
491 
492  $handler = ContentHandler::getForModelID( $this->contentModel );
493  $this->contentFormat = $handler->getDefaultFormat();
494  $this->editConflictHelperFactory = [ $this, 'newTextConflictHelper' ];
495  }
496 
500  public function getArticle() {
501  return $this->mArticle;
502  }
503 
508  public function getContext() {
509  return $this->context;
510  }
511 
516  public function getTitle() {
517  return $this->mTitle;
518  }
519 
525  public function setContextTitle( $title ) {
526  $this->mContextTitle = $title;
527  }
528 
537  public function getContextTitle() {
538  if ( is_null( $this->mContextTitle ) ) {
539  wfDeprecated( __METHOD__ . ' called with no title set', '1.32' );
540  global $wgTitle;
541  return $wgTitle;
542  } else {
543  return $this->mContextTitle;
544  }
545  }
546 
554  public function isSupportedContentModel( $modelId ) {
555  return $this->enableApiEditOverride === true ||
556  ContentHandler::getForModelID( $modelId )->supportsDirectEditing();
557  }
558 
565  public function setApiEditOverride( $enableOverride ) {
566  $this->enableApiEditOverride = $enableOverride;
567  }
568 
580  public function edit() {
581  // Allow extensions to modify/prevent this form or submission
582  if ( !Hooks::run( 'AlternateEdit', [ $this ] ) ) {
583  return;
584  }
585 
586  wfDebug( __METHOD__ . ": enter\n" );
587 
588  $request = $this->context->getRequest();
589  // If they used redlink=1 and the page exists, redirect to the main article
590  if ( $request->getBool( 'redlink' ) && $this->mTitle->exists() ) {
591  $this->context->getOutput()->redirect( $this->mTitle->getFullURL() );
592  return;
593  }
594 
595  $this->importFormData( $request );
596  $this->firsttime = false;
597 
598  if ( wfReadOnly() && $this->save ) {
599  // Force preview
600  $this->save = false;
601  $this->preview = true;
602  }
603 
604  if ( $this->save ) {
605  $this->formtype = 'save';
606  } elseif ( $this->preview ) {
607  $this->formtype = 'preview';
608  } elseif ( $this->diff ) {
609  $this->formtype = 'diff';
610  } else { # First time through
611  $this->firsttime = true;
612  if ( $this->previewOnOpen() ) {
613  $this->formtype = 'preview';
614  } else {
615  $this->formtype = 'initial';
616  }
617  }
618 
619  $permErrors = $this->getEditPermissionErrors( $this->save ? 'secure' : 'full' );
620  if ( $permErrors ) {
621  wfDebug( __METHOD__ . ": User can't edit\n" );
622 
623  if ( $this->context->getUser()->getBlock() ) {
624  // track block with a cookie if it doesn't exists already
625  MediaWikiServices::getInstance()->getBlockManager()
626  ->trackBlockWithCookie( $this->context->getUser() );
627 
628  // Auto-block user's IP if the account was "hard" blocked
629  if ( !wfReadOnly() ) {
631  $this->context->getUser()->spreadAnyEditBlock();
632  } );
633  }
634  }
635  $this->displayPermissionsError( $permErrors );
636 
637  return;
638  }
639 
640  $revision = $this->mArticle->getRevisionFetched();
641  // Disallow editing revisions with content models different from the current one
642  // Undo edits being an exception in order to allow reverting content model changes.
643  if ( $revision
644  && $revision->getContentModel() !== $this->contentModel
645  ) {
646  $prevRev = null;
647  if ( $this->undidRev ) {
648  $undidRevObj = Revision::newFromId( $this->undidRev );
649  $prevRev = $undidRevObj ? $undidRevObj->getPrevious() : null;
650  }
651  if ( !$this->undidRev
652  || !$prevRev
653  || $prevRev->getContentModel() !== $this->contentModel
654  ) {
655  $this->displayViewSourcePage(
656  $this->getContentObject(),
657  $this->context->msg(
658  'contentmodelediterror',
659  $revision->getContentModel(),
661  )->plain()
662  );
663  return;
664  }
665  }
666 
667  $this->isConflict = false;
668 
669  # Show applicable editing introductions
670  if ( $this->formtype == 'initial' || $this->firsttime ) {
671  $this->showIntro();
672  }
673 
674  # Attempt submission here. This will check for edit conflicts,
675  # and redundantly check for locked database, blocked IPs, etc.
676  # that edit() already checked just in case someone tries to sneak
677  # in the back door with a hand-edited submission URL.
678 
679  if ( $this->formtype == 'save' ) {
680  $resultDetails = null;
681  $status = $this->attemptSave( $resultDetails );
682  if ( !$this->handleStatus( $status, $resultDetails ) ) {
683  return;
684  }
685  }
686 
687  # First time through: get contents, set time for conflict
688  # checking, etc.
689  if ( $this->formtype == 'initial' || $this->firsttime ) {
690  if ( $this->initialiseForm() === false ) {
691  $out = $this->context->getOutput();
692  if ( $out->getRedirect() === '' ) { // mcrundo hack redirects, don't override it
693  $this->noSuchSectionPage();
694  }
695  return;
696  }
697 
698  if ( !$this->mTitle->getArticleID() ) {
699  Hooks::run( 'EditFormPreloadText', [ &$this->textbox1, &$this->mTitle ] );
700  } else {
701  Hooks::run( 'EditFormInitialText', [ $this ] );
702  }
703 
704  }
705 
706  $this->showEditForm();
707  }
708 
713  protected function getEditPermissionErrors( $rigor = 'secure' ) {
714  $user = $this->context->getUser();
715  $permErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $user, $rigor );
716  # Can this title be created?
717  if ( !$this->mTitle->exists() ) {
718  $permErrors = array_merge(
719  $permErrors,
720  wfArrayDiff2(
721  $this->mTitle->getUserPermissionsErrors( 'create', $user, $rigor ),
722  $permErrors
723  )
724  );
725  }
726  # Ignore some permissions errors when a user is just previewing/viewing diffs
727  $remove = [];
728  foreach ( $permErrors as $error ) {
729  if ( ( $this->preview || $this->diff )
730  && (
731  $error[0] == 'blockedtext' ||
732  $error[0] == 'autoblockedtext' ||
733  $error[0] == 'systemblockedtext'
734  )
735  ) {
736  $remove[] = $error;
737  }
738  }
739  $permErrors = wfArrayDiff2( $permErrors, $remove );
740 
741  return $permErrors;
742  }
743 
757  protected function displayPermissionsError( array $permErrors ) {
758  $out = $this->context->getOutput();
759  if ( $this->context->getRequest()->getBool( 'redlink' ) ) {
760  // The edit page was reached via a red link.
761  // Redirect to the article page and let them click the edit tab if
762  // they really want a permission error.
763  $out->redirect( $this->mTitle->getFullURL() );
764  return;
765  }
766 
767  $content = $this->getContentObject();
768 
769  # Use the normal message if there's nothing to display
770  if ( $this->firsttime && ( !$content || $content->isEmpty() ) ) {
771  $action = $this->mTitle->exists() ? 'edit' :
772  ( $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage' );
773  throw new PermissionsError( $action, $permErrors );
774  }
775 
776  $this->displayViewSourcePage(
777  $content,
778  $out->formatPermissionsErrorMessage( $permErrors, 'edit' )
779  );
780  }
781 
787  protected function displayViewSourcePage( Content $content, $errorMessage = '' ) {
788  $out = $this->context->getOutput();
789  Hooks::run( 'EditPage::showReadOnlyForm:initial', [ $this, &$out ] );
790 
791  $out->setRobotPolicy( 'noindex,nofollow' );
792  $out->setPageTitle( $this->context->msg(
793  'viewsource-title',
794  $this->getContextTitle()->getPrefixedText()
795  ) );
796  $out->addBacklinkSubtitle( $this->getContextTitle() );
797  $out->addHTML( $this->editFormPageTop );
798  $out->addHTML( $this->editFormTextTop );
799 
800  if ( $errorMessage !== '' ) {
801  $out->addWikiTextAsInterface( $errorMessage );
802  $out->addHTML( "<hr />\n" );
803  }
804 
805  # If the user made changes, preserve them when showing the markup
806  # (This happens when a user is blocked during edit, for instance)
807  if ( !$this->firsttime ) {
808  $text = $this->textbox1;
809  $out->addWikiMsg( 'viewyourtext' );
810  } else {
811  try {
812  $text = $this->toEditText( $content );
813  } catch ( MWException $e ) {
814  # Serialize using the default format if the content model is not supported
815  # (e.g. for an old revision with a different model)
816  $text = $content->serialize();
817  }
818  $out->addWikiMsg( 'viewsourcetext' );
819  }
820 
821  $out->addHTML( $this->editFormTextBeforeContent );
822  $this->showTextbox( $text, 'wpTextbox1', [ 'readonly' ] );
823  $out->addHTML( $this->editFormTextAfterContent );
824 
825  $out->addHTML( $this->makeTemplatesOnThisPageList( $this->getTemplates() ) );
826 
827  $out->addModules( 'mediawiki.action.edit.collapsibleFooter' );
828 
829  $out->addHTML( $this->editFormTextBottom );
830  if ( $this->mTitle->exists() ) {
831  $out->returnToMain( null, $this->mTitle );
832  }
833  }
834 
840  protected function previewOnOpen() {
841  $config = $this->context->getConfig();
842  $previewOnOpenNamespaces = $config->get( 'PreviewOnOpenNamespaces' );
843  $request = $this->context->getRequest();
844  if ( $config->get( 'RawHtml' ) ) {
845  // If raw HTML is enabled, disable preview on open
846  // since it has to be posted with a token for
847  // security reasons
848  return false;
849  }
850  if ( $request->getVal( 'preview' ) == 'yes' ) {
851  // Explicit override from request
852  return true;
853  } elseif ( $request->getVal( 'preview' ) == 'no' ) {
854  // Explicit override from request
855  return false;
856  } elseif ( $this->section == 'new' ) {
857  // Nothing *to* preview for new sections
858  return false;
859  } elseif ( ( $request->getCheck( 'preload' ) || $this->mTitle->exists() )
860  && $this->context->getUser()->getOption( 'previewonfirst' )
861  ) {
862  // Standard preference behavior
863  return true;
864  } elseif ( !$this->mTitle->exists()
865  && isset( $previewOnOpenNamespaces[$this->mTitle->getNamespace()] )
866  && $previewOnOpenNamespaces[$this->mTitle->getNamespace()]
867  ) {
868  // Categories are special
869  return true;
870  } else {
871  return false;
872  }
873  }
874 
881  protected function isWrongCaseUserConfigPage() {
882  if ( $this->mTitle->isUserConfigPage() ) {
883  $name = $this->mTitle->getSkinFromConfigSubpage();
884  $skins = array_merge(
885  array_keys( Skin::getSkinNames() ),
886  [ 'common' ]
887  );
888  return !in_array( $name, $skins )
889  && in_array( strtolower( $name ), $skins );
890  } else {
891  return false;
892  }
893  }
894 
902  protected function isSectionEditSupported() {
903  $contentHandler = ContentHandler::getForTitle( $this->mTitle );
904  return $contentHandler->supportsSections();
905  }
906 
912  public function importFormData( &$request ) {
913  # Section edit can come from either the form or a link
914  $this->section = $request->getVal( 'wpSection', $request->getVal( 'section' ) );
915 
916  if ( $this->section !== null && $this->section !== '' && !$this->isSectionEditSupported() ) {
917  throw new ErrorPageError( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
918  }
919 
920  $this->isNew = !$this->mTitle->exists() || $this->section == 'new';
921 
922  if ( $request->wasPosted() ) {
923  # These fields need to be checked for encoding.
924  # Also remove trailing whitespace, but don't remove _initial_
925  # whitespace from the text boxes. This may be significant formatting.
926  $this->textbox1 = rtrim( $request->getText( 'wpTextbox1' ) );
927  if ( !$request->getCheck( 'wpTextbox2' ) ) {
928  // Skip this if wpTextbox2 has input, it indicates that we came
929  // from a conflict page with raw page text, not a custom form
930  // modified by subclasses
932  if ( $textbox1 !== null ) {
933  $this->textbox1 = $textbox1;
934  }
935  }
936 
937  $this->unicodeCheck = $request->getText( 'wpUnicodeCheck' );
938 
939  $this->summary = $request->getText( 'wpSummary' );
940 
941  # If the summary consists of a heading, e.g. '==Foobar==', extract the title from the
942  # header syntax, e.g. 'Foobar'. This is mainly an issue when we are using wpSummary for
943  # section titles.
944  $this->summary = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->summary );
945 
946  # Treat sectiontitle the same way as summary.
947  # Note that wpSectionTitle is not yet a part of the actual edit form, as wpSummary is
948  # currently doing double duty as both edit summary and section title. Right now this
949  # is just to allow API edits to work around this limitation, but this should be
950  # incorporated into the actual edit form when EditPage is rewritten (T20654, T28312).
951  $this->sectiontitle = $request->getText( 'wpSectionTitle' );
952  $this->sectiontitle = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->sectiontitle );
953 
954  $this->edittime = $request->getVal( 'wpEdittime' );
955  $this->editRevId = $request->getIntOrNull( 'editRevId' );
956  $this->starttime = $request->getVal( 'wpStarttime' );
957 
958  $undidRev = $request->getInt( 'wpUndidRevision' );
959  if ( $undidRev ) {
960  $this->undidRev = $undidRev;
961  }
962 
963  $this->scrolltop = $request->getIntOrNull( 'wpScrolltop' );
964 
965  if ( $this->textbox1 === '' && !$request->getCheck( 'wpTextbox1' ) ) {
966  // wpTextbox1 field is missing, possibly due to being "too big"
967  // according to some filter rules such as Suhosin's setting for
968  // suhosin.request.max_value_length (d'oh)
969  $this->incompleteForm = true;
970  } else {
971  // If we receive the last parameter of the request, we can fairly
972  // claim the POST request has not been truncated.
973  $this->incompleteForm = !$request->getVal( 'wpUltimateParam' );
974  }
975  if ( $this->incompleteForm ) {
976  # If the form is incomplete, force to preview.
977  wfDebug( __METHOD__ . ": Form data appears to be incomplete\n" );
978  wfDebug( "POST DATA: " . var_export( $request->getPostValues(), true ) . "\n" );
979  $this->preview = true;
980  } else {
981  $this->preview = $request->getCheck( 'wpPreview' );
982  $this->diff = $request->getCheck( 'wpDiff' );
983 
984  // Remember whether a save was requested, so we can indicate
985  // if we forced preview due to session failure.
986  $this->mTriedSave = !$this->preview;
987 
988  if ( $this->tokenOk( $request ) ) {
989  # Some browsers will not report any submit button
990  # if the user hits enter in the comment box.
991  # The unmarked state will be assumed to be a save,
992  # if the form seems otherwise complete.
993  wfDebug( __METHOD__ . ": Passed token check.\n" );
994  } elseif ( $this->diff ) {
995  # Failed token check, but only requested "Show Changes".
996  wfDebug( __METHOD__ . ": Failed token check; Show Changes requested.\n" );
997  } else {
998  # Page might be a hack attempt posted from
999  # an external site. Preview instead of saving.
1000  wfDebug( __METHOD__ . ": Failed token check; forcing preview\n" );
1001  $this->preview = true;
1002  }
1003  }
1004  $this->save = !$this->preview && !$this->diff;
1005  if ( !preg_match( '/^\d{14}$/', $this->edittime ) ) {
1006  $this->edittime = null;
1007  }
1008 
1009  if ( !preg_match( '/^\d{14}$/', $this->starttime ) ) {
1010  $this->starttime = null;
1011  }
1012 
1013  $this->recreate = $request->getCheck( 'wpRecreate' );
1014 
1015  $this->minoredit = $request->getCheck( 'wpMinoredit' );
1016  $this->watchthis = $request->getCheck( 'wpWatchthis' );
1017 
1018  $user = $this->context->getUser();
1019  # Don't force edit summaries when a user is editing their own user or talk page
1020  if ( ( $this->mTitle->mNamespace == NS_USER || $this->mTitle->mNamespace == NS_USER_TALK )
1021  && $this->mTitle->getText() == $user->getName()
1022  ) {
1023  $this->allowBlankSummary = true;
1024  } else {
1025  $this->allowBlankSummary = $request->getBool( 'wpIgnoreBlankSummary' )
1026  || !$user->getOption( 'forceeditsummary' );
1027  }
1028 
1029  $this->autoSumm = $request->getText( 'wpAutoSummary' );
1030 
1031  $this->allowBlankArticle = $request->getBool( 'wpIgnoreBlankArticle' );
1032  $this->allowSelfRedirect = $request->getBool( 'wpIgnoreSelfRedirect' );
1033 
1034  $changeTags = $request->getVal( 'wpChangeTags' );
1035  if ( is_null( $changeTags ) || $changeTags === '' ) {
1036  $this->changeTags = [];
1037  } else {
1038  $this->changeTags = array_filter( array_map( 'trim', explode( ',',
1039  $changeTags ) ) );
1040  }
1041  } else {
1042  # Not a posted form? Start with nothing.
1043  wfDebug( __METHOD__ . ": Not a posted form.\n" );
1044  $this->textbox1 = '';
1045  $this->summary = '';
1046  $this->sectiontitle = '';
1047  $this->edittime = '';
1048  $this->editRevId = null;
1049  $this->starttime = wfTimestampNow();
1050  $this->edit = false;
1051  $this->preview = false;
1052  $this->save = false;
1053  $this->diff = false;
1054  $this->minoredit = false;
1055  // Watch may be overridden by request parameters
1056  $this->watchthis = $request->getBool( 'watchthis', false );
1057  $this->recreate = false;
1058 
1059  // When creating a new section, we can preload a section title by passing it as the
1060  // preloadtitle parameter in the URL (T15100)
1061  if ( $this->section == 'new' && $request->getVal( 'preloadtitle' ) ) {
1062  $this->sectiontitle = $request->getVal( 'preloadtitle' );
1063  // Once wpSummary isn't being use for setting section titles, we should delete this.
1064  $this->summary = $request->getVal( 'preloadtitle' );
1065  } elseif ( $this->section != 'new' && $request->getVal( 'summary' ) !== '' ) {
1066  $this->summary = $request->getText( 'summary' );
1067  if ( $this->summary !== '' ) {
1068  $this->hasPresetSummary = true;
1069  }
1070  }
1071 
1072  if ( $request->getVal( 'minor' ) ) {
1073  $this->minoredit = true;
1074  }
1075  }
1076 
1077  $this->oldid = $request->getInt( 'oldid' );
1078  $this->parentRevId = $request->getInt( 'parentRevId' );
1079 
1080  $this->bot = $request->getBool( 'bot', true );
1081  $this->nosummary = $request->getBool( 'nosummary' );
1082 
1083  // May be overridden by revision.
1084  $this->contentModel = $request->getText( 'model', $this->contentModel );
1085  // May be overridden by revision.
1086  $this->contentFormat = $request->getText( 'format', $this->contentFormat );
1087 
1088  try {
1089  $handler = ContentHandler::getForModelID( $this->contentModel );
1090  } catch ( MWUnknownContentModelException $e ) {
1091  throw new ErrorPageError(
1092  'editpage-invalidcontentmodel-title',
1093  'editpage-invalidcontentmodel-text',
1094  [ wfEscapeWikiText( $this->contentModel ) ]
1095  );
1096  }
1097 
1098  if ( !$handler->isSupportedFormat( $this->contentFormat ) ) {
1099  throw new ErrorPageError(
1100  'editpage-notsupportedcontentformat-title',
1101  'editpage-notsupportedcontentformat-text',
1102  [
1103  wfEscapeWikiText( $this->contentFormat ),
1104  wfEscapeWikiText( ContentHandler::getLocalizedName( $this->contentModel ) )
1105  ]
1106  );
1107  }
1108 
1115  $this->editintro = $request->getText( 'editintro',
1116  // Custom edit intro for new sections
1117  $this->section === 'new' ? 'MediaWiki:addsection-editintro' : '' );
1118 
1119  // Allow extensions to modify form data
1120  Hooks::run( 'EditPage::importFormData', [ $this, $request ] );
1121  }
1122 
1132  protected function importContentFormData( &$request ) {
1133  return; // Don't do anything, EditPage already extracted wpTextbox1
1134  }
1135 
1141  public function initialiseForm() {
1142  $this->edittime = $this->page->getTimestamp();
1143  $this->editRevId = $this->page->getLatest();
1144 
1145  $content = $this->getContentObject( false ); # TODO: track content object?!
1146  if ( $content === false ) {
1147  return false;
1148  }
1149  $this->textbox1 = $this->toEditText( $content );
1150 
1151  $user = $this->context->getUser();
1152  // activate checkboxes if user wants them to be always active
1153  # Sort out the "watch" checkbox
1154  if ( $user->getOption( 'watchdefault' ) ) {
1155  # Watch all edits
1156  $this->watchthis = true;
1157  } elseif ( $user->getOption( 'watchcreations' ) && !$this->mTitle->exists() ) {
1158  # Watch creations
1159  $this->watchthis = true;
1160  } elseif ( $user->isWatched( $this->mTitle ) ) {
1161  # Already watched
1162  $this->watchthis = true;
1163  }
1164  if ( $user->getOption( 'minordefault' ) && !$this->isNew ) {
1165  $this->minoredit = true;
1166  }
1167  if ( $this->textbox1 === false ) {
1168  return false;
1169  }
1170  return true;
1171  }
1172 
1180  protected function getContentObject( $def_content = null ) {
1181  $content = false;
1182 
1183  $user = $this->context->getUser();
1184  $request = $this->context->getRequest();
1185  // For message page not locally set, use the i18n message.
1186  // For other non-existent articles, use preload text if any.
1187  if ( !$this->mTitle->exists() || $this->section == 'new' ) {
1188  if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && $this->section != 'new' ) {
1189  # If this is a system message, get the default text.
1190  $msg = $this->mTitle->getDefaultMessageText();
1191 
1192  $content = $this->toEditContent( $msg );
1193  }
1194  if ( $content === false ) {
1195  # If requested, preload some text.
1196  $preload = $request->getVal( 'preload',
1197  // Custom preload text for new sections
1198  $this->section === 'new' ? 'MediaWiki:addsection-preload' : '' );
1199  $params = $request->getArray( 'preloadparams', [] );
1200 
1201  $content = $this->getPreloadedContent( $preload, $params );
1202  }
1203  // For existing pages, get text based on "undo" or section parameters.
1204  } elseif ( $this->section != '' ) {
1205  // Get section edit text (returns $def_text for invalid sections)
1206  $orig = $this->getOriginalContent( $user );
1207  $content = $orig ? $orig->getSection( $this->section ) : null;
1208 
1209  if ( !$content ) {
1210  $content = $def_content;
1211  }
1212  } else {
1213  $undoafter = $request->getInt( 'undoafter' );
1214  $undo = $request->getInt( 'undo' );
1215 
1216  if ( $undo > 0 && $undoafter > 0 ) {
1217  $undorev = Revision::newFromId( $undo );
1218  $oldrev = Revision::newFromId( $undoafter );
1219  $undoMsg = null;
1220 
1221  # Sanity check, make sure it's the right page,
1222  # the revisions exist and they were not deleted.
1223  # Otherwise, $content will be left as-is.
1224  if ( !is_null( $undorev ) && !is_null( $oldrev ) &&
1225  !$undorev->isDeleted( Revision::DELETED_TEXT ) &&
1226  !$oldrev->isDeleted( Revision::DELETED_TEXT )
1227  ) {
1228  if ( WikiPage::hasDifferencesOutsideMainSlot( $undorev, $oldrev )
1229  || !$this->isSupportedContentModel( $oldrev->getContentModel() )
1230  ) {
1231  // Hack for undo while EditPage can't handle multi-slot editing
1232  $this->context->getOutput()->redirect( $this->mTitle->getFullURL( [
1233  'action' => 'mcrundo',
1234  'undo' => $undo,
1235  'undoafter' => $undoafter,
1236  ] ) );
1237  return false;
1238  } else {
1239  $content = $this->page->getUndoContent( $undorev, $oldrev );
1240 
1241  if ( $content === false ) {
1242  # Warn the user that something went wrong
1243  $undoMsg = 'failure';
1244  }
1245  }
1246 
1247  if ( $undoMsg === null ) {
1248  $oldContent = $this->page->getContent( Revision::RAW );
1250  $user, MediaWikiServices::getInstance()->getContentLanguage() );
1251  $newContent = $content->preSaveTransform( $this->mTitle, $user, $popts );
1252  if ( $newContent->getModel() !== $oldContent->getModel() ) {
1253  // The undo may change content
1254  // model if its reverting the top
1255  // edit. This can result in
1256  // mismatched content model/format.
1257  $this->contentModel = $newContent->getModel();
1258  $this->contentFormat = $oldrev->getContentFormat();
1259  }
1260 
1261  if ( $newContent->equals( $oldContent ) ) {
1262  # Tell the user that the undo results in no change,
1263  # i.e. the revisions were already undone.
1264  $undoMsg = 'nochange';
1265  $content = false;
1266  } else {
1267  # Inform the user of our success and set an automatic edit summary
1268  $undoMsg = 'success';
1269 
1270  # If we just undid one rev, use an autosummary
1271  $firstrev = $oldrev->getNext();
1272  if ( $firstrev && $firstrev->getId() == $undo ) {
1273  $userText = $undorev->getUserText();
1274  if ( $userText === '' ) {
1275  $undoSummary = $this->context->msg(
1276  'undo-summary-username-hidden',
1277  $undo
1278  )->inContentLanguage()->text();
1279  } else {
1280  $undoSummary = $this->context->msg(
1281  'undo-summary',
1282  $undo,
1283  $userText
1284  )->inContentLanguage()->text();
1285  }
1286  if ( $this->summary === '' ) {
1287  $this->summary = $undoSummary;
1288  } else {
1289  $this->summary = $undoSummary . $this->context->msg( 'colon-separator' )
1290  ->inContentLanguage()->text() . $this->summary;
1291  }
1292  $this->undidRev = $undo;
1293  }
1294  $this->formtype = 'diff';
1295  }
1296  }
1297  } else {
1298  // Failed basic sanity checks.
1299  // Older revisions may have been removed since the link
1300  // was created, or we may simply have got bogus input.
1301  $undoMsg = 'norev';
1302  }
1303 
1304  $out = $this->context->getOutput();
1305  // Messages: undo-success, undo-failure, undo-main-slot-only, undo-norev,
1306  // undo-nochange.
1307  $class = ( $undoMsg == 'success' ? '' : 'error ' ) . "mw-undo-{$undoMsg}";
1308  $this->editFormPageTop .= Html::rawElement(
1309  'div', [ 'class' => $class ],
1310  $out->parseAsInterface(
1311  $this->context->msg( 'undo-' . $undoMsg )->plain()
1312  )
1313  );
1314  }
1315 
1316  if ( $content === false ) {
1317  // Hack for restoring old revisions while EditPage
1318  // can't handle multi-slot editing.
1319 
1320  $curRevision = $this->page->getRevision();
1321  $oldRevision = $this->mArticle->getRevisionFetched();
1322 
1323  if ( $curRevision
1324  && $oldRevision
1325  && $curRevision->getId() !== $oldRevision->getId()
1326  && ( WikiPage::hasDifferencesOutsideMainSlot( $oldRevision, $curRevision )
1327  || !$this->isSupportedContentModel( $oldRevision->getContentModel() ) )
1328  ) {
1329  $this->context->getOutput()->redirect(
1330  $this->mTitle->getFullURL(
1331  [
1332  'action' => 'mcrrestore',
1333  'restore' => $oldRevision->getId(),
1334  ]
1335  )
1336  );
1337 
1338  return false;
1339  }
1340  }
1341 
1342  if ( $content === false ) {
1343  $content = $this->getOriginalContent( $user );
1344  }
1345  }
1346 
1347  return $content;
1348  }
1349 
1365  private function getOriginalContent( User $user ) {
1366  if ( $this->section == 'new' ) {
1367  return $this->getCurrentContent();
1368  }
1369  $revision = $this->mArticle->getRevisionFetched();
1370  if ( $revision === null ) {
1371  $handler = ContentHandler::getForModelID( $this->contentModel );
1372  return $handler->makeEmptyContent();
1373  }
1374  $content = $revision->getContent( Revision::FOR_THIS_USER, $user );
1375  return $content;
1376  }
1377 
1390  public function getParentRevId() {
1391  if ( $this->parentRevId ) {
1392  return $this->parentRevId;
1393  } else {
1394  return $this->mArticle->getRevIdFetched();
1395  }
1396  }
1397 
1406  protected function getCurrentContent() {
1407  $rev = $this->page->getRevision();
1408  $content = $rev ? $rev->getContent( Revision::RAW ) : null;
1409 
1410  if ( $content === false || $content === null ) {
1411  $handler = ContentHandler::getForModelID( $this->contentModel );
1412  return $handler->makeEmptyContent();
1413  } elseif ( !$this->undidRev ) {
1414  // Content models should always be the same since we error
1415  // out if they are different before this point (in ->edit()).
1416  // The exception being, during an undo, the current revision might
1417  // differ from the prior revision.
1418  $logger = LoggerFactory::getInstance( 'editpage' );
1419  if ( $this->contentModel !== $rev->getContentModel() ) {
1420  $logger->warning( "Overriding content model from current edit {prev} to {new}", [
1421  'prev' => $this->contentModel,
1422  'new' => $rev->getContentModel(),
1423  'title' => $this->getTitle()->getPrefixedDBkey(),
1424  'method' => __METHOD__
1425  ] );
1426  $this->contentModel = $rev->getContentModel();
1427  }
1428 
1429  // Given that the content models should match, the current selected
1430  // format should be supported.
1431  if ( !$content->isSupportedFormat( $this->contentFormat ) ) {
1432  $logger->warning( "Current revision content format unsupported. Overriding {prev} to {new}", [
1433 
1434  'prev' => $this->contentFormat,
1435  'new' => $rev->getContentFormat(),
1436  'title' => $this->getTitle()->getPrefixedDBkey(),
1437  'method' => __METHOD__
1438  ] );
1439  $this->contentFormat = $rev->getContentFormat();
1440  }
1441  }
1442  return $content;
1443  }
1444 
1452  public function setPreloadedContent( Content $content ) {
1453  $this->mPreloadContent = $content;
1454  }
1455 
1467  protected function getPreloadedContent( $preload, $params = [] ) {
1468  if ( !empty( $this->mPreloadContent ) ) {
1469  return $this->mPreloadContent;
1470  }
1471 
1472  $handler = ContentHandler::getForModelID( $this->contentModel );
1473 
1474  if ( $preload === '' ) {
1475  return $handler->makeEmptyContent();
1476  }
1477 
1478  $user = $this->context->getUser();
1479  $title = Title::newFromText( $preload );
1480 
1481  # Check for existence to avoid getting MediaWiki:Noarticletext
1482  if ( !$this->isPageExistingAndViewable( $title, $user ) ) {
1483  // TODO: somehow show a warning to the user!
1484  return $handler->makeEmptyContent();
1485  }
1486 
1488  if ( $page->isRedirect() ) {
1490  # Same as before
1491  if ( !$this->isPageExistingAndViewable( $title, $user ) ) {
1492  // TODO: somehow show a warning to the user!
1493  return $handler->makeEmptyContent();
1494  }
1496  }
1497 
1498  $parserOptions = ParserOptions::newFromUser( $user );
1500 
1501  if ( !$content ) {
1502  // TODO: somehow show a warning to the user!
1503  return $handler->makeEmptyContent();
1504  }
1505 
1506  if ( $content->getModel() !== $handler->getModelID() ) {
1507  $converted = $content->convert( $handler->getModelID() );
1508 
1509  if ( !$converted ) {
1510  // TODO: somehow show a warning to the user!
1511  wfDebug( "Attempt to preload incompatible content: " .
1512  "can't convert " . $content->getModel() .
1513  " to " . $handler->getModelID() );
1514 
1515  return $handler->makeEmptyContent();
1516  }
1517 
1518  $content = $converted;
1519  }
1520 
1521  return $content->preloadTransform( $title, $parserOptions, $params );
1522  }
1523 
1534  $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
1535 
1536  return $title && $title->exists() && $permissionManager->userCan( 'read', $user, $title );
1537  }
1538 
1546  public function tokenOk( &$request ) {
1547  $token = $request->getVal( 'wpEditToken' );
1548  $user = $this->context->getUser();
1549  $this->mTokenOk = $user->matchEditToken( $token );
1550  $this->mTokenOkExceptSuffix = $user->matchEditTokenNoSuffix( $token );
1551  return $this->mTokenOk;
1552  }
1553 
1568  protected function setPostEditCookie( $statusValue ) {
1569  $revisionId = $this->page->getLatest();
1570  $postEditKey = self::POST_EDIT_COOKIE_KEY_PREFIX . $revisionId;
1571 
1572  $val = 'saved';
1573  if ( $statusValue == self::AS_SUCCESS_NEW_ARTICLE ) {
1574  $val = 'created';
1575  } elseif ( $this->oldid ) {
1576  $val = 'restored';
1577  }
1578 
1579  $response = $this->context->getRequest()->response();
1580  $response->setCookie( $postEditKey, $val, time() + self::POST_EDIT_COOKIE_DURATION );
1581  }
1582 
1589  public function attemptSave( &$resultDetails = false ) {
1590  // TODO: MCR: treat $this->minoredit like $this->bot and check isAllowed( 'minoredit' )!
1591  // Also, add $this->autopatrol like $this->bot and check isAllowed( 'autopatrol' )!
1592  // This is needed since PageUpdater no longer checks these rights!
1593 
1594  // Allow bots to exempt some edits from bot flagging
1595  $bot = $this->context->getUser()->isAllowed( 'bot' ) && $this->bot;
1596  $status = $this->internalAttemptSave( $resultDetails, $bot );
1597 
1598  Hooks::run( 'EditPage::attemptSave:after', [ $this, $status, $resultDetails ] );
1599 
1600  return $status;
1601  }
1602 
1606  private function incrementResolvedConflicts() {
1607  if ( $this->context->getRequest()->getText( 'mode' ) !== 'conflict' ) {
1608  return;
1609  }
1610 
1611  $this->getEditConflictHelper()->incrementResolvedStats();
1612  }
1613 
1623  private function handleStatus( Status $status, $resultDetails ) {
1628  if ( $status->value == self::AS_SUCCESS_UPDATE
1629  || $status->value == self::AS_SUCCESS_NEW_ARTICLE
1630  ) {
1631  $this->incrementResolvedConflicts();
1632 
1633  $this->didSave = true;
1634  if ( !$resultDetails['nullEdit'] ) {
1635  $this->setPostEditCookie( $status->value );
1636  }
1637  }
1638 
1639  $out = $this->context->getOutput();
1640 
1641  // "wpExtraQueryRedirect" is a hidden input to modify
1642  // after save URL and is not used by actual edit form
1643  $request = $this->context->getRequest();
1644  $extraQueryRedirect = $request->getVal( 'wpExtraQueryRedirect' );
1645 
1646  switch ( $status->value ) {
1647  case self::AS_HOOK_ERROR_EXPECTED:
1648  case self::AS_CONTENT_TOO_BIG:
1649  case self::AS_ARTICLE_WAS_DELETED:
1650  case self::AS_CONFLICT_DETECTED:
1651  case self::AS_SUMMARY_NEEDED:
1652  case self::AS_TEXTBOX_EMPTY:
1653  case self::AS_MAX_ARTICLE_SIZE_EXCEEDED:
1654  case self::AS_END:
1655  case self::AS_BLANK_ARTICLE:
1656  case self::AS_SELF_REDIRECT:
1657  return true;
1658 
1659  case self::AS_HOOK_ERROR:
1660  return false;
1661 
1662  case self::AS_CANNOT_USE_CUSTOM_MODEL:
1663  case self::AS_PARSE_ERROR:
1664  case self::AS_UNICODE_NOT_SUPPORTED:
1665  $out->wrapWikiTextAsInterface( 'error', $status->getWikiText() );
1666  return true;
1667 
1668  case self::AS_SUCCESS_NEW_ARTICLE:
1669  $query = $resultDetails['redirect'] ? 'redirect=no' : '';
1670  if ( $extraQueryRedirect ) {
1671  if ( $query !== '' ) {
1672  $query .= '&';
1673  }
1674  $query .= $extraQueryRedirect;
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 = '&' . $extraQuery;
1693  }
1694  $extraQuery = 'redirect=no' . $extraQuery;
1695  }
1696  if ( $extraQueryRedirect ) {
1697  if ( $extraQuery !== '' ) {
1698  $extraQuery .= '&';
1699  }
1700  $extraQuery .= $extraQueryRedirect;
1701  }
1702 
1703  $out->redirect( $this->mTitle->getFullURL( $extraQuery ) . $sectionanchor );
1704  return false;
1705 
1706  case self::AS_SPAM_ERROR:
1707  $this->spamPageWithContent( $resultDetails['spam'] );
1708  return false;
1709 
1710  case self::AS_BLOCKED_PAGE_FOR_USER:
1711  throw new UserBlockedError( $this->context->getUser()->getBlock() );
1712 
1713  case self::AS_IMAGE_REDIRECT_ANON:
1714  case self::AS_IMAGE_REDIRECT_LOGGED:
1715  throw new PermissionsError( 'upload' );
1716 
1717  case self::AS_READ_ONLY_PAGE_ANON:
1718  case self::AS_READ_ONLY_PAGE_LOGGED:
1719  throw new PermissionsError( 'edit' );
1720 
1721  case self::AS_READ_ONLY_PAGE:
1722  throw new ReadOnlyError;
1723 
1724  case self::AS_RATE_LIMITED:
1725  throw new ThrottledError();
1726 
1727  case self::AS_NO_CREATE_PERMISSION:
1728  $permission = $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage';
1729  throw new PermissionsError( $permission );
1730 
1731  case self::AS_NO_CHANGE_CONTENT_MODEL:
1732  throw new PermissionsError( 'editcontentmodel' );
1733 
1734  default:
1735  // We don't recognize $status->value. The only way that can happen
1736  // is if an extension hook aborted from inside ArticleSave.
1737  // Render the status object into $this->hookError
1738  // FIXME this sucks, we should just use the Status object throughout
1739  $this->hookError = '<div class="error">' . "\n" . $status->getWikiText() .
1740  '</div>';
1741  return true;
1742  }
1743  }
1744 
1755  // Run old style post-section-merge edit filter
1756  if ( $this->hookError != '' ) {
1757  # ...or the hook could be expecting us to produce an error
1758  $status->fatal( 'hookaborted' );
1759  $status->value = self::AS_HOOK_ERROR_EXPECTED;
1760  return false;
1761  }
1762 
1763  // Run new style post-section-merge edit filter
1764  if ( !Hooks::run( 'EditFilterMergedContent',
1765  [ $this->context, $content, $status, $this->summary,
1766  $user, $this->minoredit ] )
1767  ) {
1768  # Error messages etc. could be handled within the hook...
1769  if ( $status->isGood() ) {
1770  $status->fatal( 'hookaborted' );
1771  // Not setting $this->hookError here is a hack to allow the hook
1772  // to cause a return to the edit page without $this->hookError
1773  // being set. This is used by ConfirmEdit to display a captcha
1774  // without any error message cruft.
1775  } else {
1776  $this->hookError = $this->formatStatusErrors( $status );
1777  }
1778  // Use the existing $status->value if the hook set it
1779  if ( !$status->value ) {
1780  $status->value = self::AS_HOOK_ERROR;
1781  }
1782  return false;
1783  } elseif ( !$status->isOK() ) {
1784  # ...or the hook could be expecting us to produce an error
1785  // FIXME this sucks, we should just use the Status object throughout
1786  $this->hookError = $this->formatStatusErrors( $status );
1787  $status->fatal( 'hookaborted' );
1788  $status->value = self::AS_HOOK_ERROR_EXPECTED;
1789  return false;
1790  }
1791 
1792  return true;
1793  }
1794 
1801  private function formatStatusErrors( Status $status ) {
1802  $errmsg = $status->getWikiText(
1803  'edit-error-short',
1804  'edit-error-long',
1805  $this->context->getLanguage()
1806  );
1807  return <<<ERROR
1808 <div class="errorbox">
1809 {$errmsg}
1810 </div>
1811 <br clear="all" />
1812 ERROR;
1813  }
1814 
1821  private function newSectionSummary( &$sectionanchor = null ) {
1822  if ( $this->sectiontitle !== '' ) {
1823  $sectionanchor = $this->guessSectionName( $this->sectiontitle );
1824  // If no edit summary was specified, create one automatically from the section
1825  // title and have it link to the new section. Otherwise, respect the summary as
1826  // passed.
1827  if ( $this->summary === '' ) {
1828  $cleanSectionTitle = MediaWikiServices::getInstance()->getParser()
1829  ->stripSectionName( $this->sectiontitle );
1830  return $this->context->msg( 'newsectionsummary' )
1831  ->plaintextParams( $cleanSectionTitle )->inContentLanguage()->text();
1832  }
1833  } elseif ( $this->summary !== '' ) {
1834  $sectionanchor = $this->guessSectionName( $this->summary );
1835  # This is a new section, so create a link to the new section
1836  # in the revision summary.
1837  $cleanSummary = MediaWikiServices::getInstance()->getParser()
1838  ->stripSectionName( $this->summary );
1839  return $this->context->msg( 'newsectionsummary' )
1840  ->plaintextParams( $cleanSummary )->inContentLanguage()->text();
1841  }
1842  return $this->summary;
1843  }
1844 
1869  public function internalAttemptSave( &$result, $bot = false ) {
1871  $user = $this->context->getUser();
1872 
1873  if ( !Hooks::run( 'EditPage::attemptSave', [ $this ] ) ) {
1874  wfDebug( "Hook 'EditPage::attemptSave' aborted article saving\n" );
1875  $status->fatal( 'hookaborted' );
1876  $status->value = self::AS_HOOK_ERROR;
1877  return $status;
1878  }
1879 
1880  if ( $this->unicodeCheck !== self::UNICODE_CHECK ) {
1881  $status->fatal( 'unicode-support-fail' );
1882  $status->value = self::AS_UNICODE_NOT_SUPPORTED;
1883  return $status;
1884  }
1885 
1886  $request = $this->context->getRequest();
1887  $spam = $request->getText( 'wpAntispam' );
1888  if ( $spam !== '' ) {
1889  wfDebugLog(
1890  'SimpleAntiSpam',
1891  $user->getName() .
1892  ' editing "' .
1893  $this->mTitle->getPrefixedText() .
1894  '" submitted bogus field "' .
1895  $spam .
1896  '"'
1897  );
1898  $status->fatal( 'spamprotectionmatch', false );
1899  $status->value = self::AS_SPAM_ERROR;
1900  return $status;
1901  }
1902 
1903  try {
1904  # Construct Content object
1905  $textbox_content = $this->toEditContent( $this->textbox1 );
1906  } catch ( MWContentSerializationException $ex ) {
1907  $status->fatal(
1908  'content-failed-to-parse',
1909  $this->contentModel,
1910  $this->contentFormat,
1911  $ex->getMessage()
1912  );
1913  $status->value = self::AS_PARSE_ERROR;
1914  return $status;
1915  }
1916 
1917  # Check image redirect
1918  if ( $this->mTitle->getNamespace() == NS_FILE &&
1919  $textbox_content->isRedirect() &&
1920  !$user->isAllowed( 'upload' )
1921  ) {
1922  $code = $user->isAnon() ? self::AS_IMAGE_REDIRECT_ANON : self::AS_IMAGE_REDIRECT_LOGGED;
1923  $status->setResult( false, $code );
1924 
1925  return $status;
1926  }
1927 
1928  # Check for spam
1929  $match = self::matchSummarySpamRegex( $this->summary );
1930  if ( $match === false && $this->section == 'new' ) {
1931  # $wgSpamRegex is enforced on this new heading/summary because, unlike
1932  # regular summaries, it is added to the actual wikitext.
1933  if ( $this->sectiontitle !== '' ) {
1934  # This branch is taken when the API is used with the 'sectiontitle' parameter.
1935  $match = self::matchSpamRegex( $this->sectiontitle );
1936  } else {
1937  # This branch is taken when the "Add Topic" user interface is used, or the API
1938  # is used with the 'summary' parameter.
1939  $match = self::matchSpamRegex( $this->summary );
1940  }
1941  }
1942  if ( $match === false ) {
1943  $match = self::matchSpamRegex( $this->textbox1 );
1944  }
1945  if ( $match !== false ) {
1946  $result['spam'] = $match;
1947  $ip = $request->getIP();
1948  $pdbk = $this->mTitle->getPrefixedDBkey();
1949  $match = str_replace( "\n", '', $match );
1950  wfDebugLog( 'SpamRegex', "$ip spam regex hit [[$pdbk]]: \"$match\"" );
1951  $status->fatal( 'spamprotectionmatch', $match );
1952  $status->value = self::AS_SPAM_ERROR;
1953  return $status;
1954  }
1955  if ( !Hooks::run(
1956  'EditFilter',
1957  [ $this, $this->textbox1, $this->section, &$this->hookError, $this->summary ] )
1958  ) {
1959  # Error messages etc. could be handled within the hook...
1960  $status->fatal( 'hookaborted' );
1961  $status->value = self::AS_HOOK_ERROR;
1962  return $status;
1963  } elseif ( $this->hookError != '' ) {
1964  # ...or the hook could be expecting us to produce an error
1965  $status->fatal( 'hookaborted' );
1966  $status->value = self::AS_HOOK_ERROR_EXPECTED;
1967  return $status;
1968  }
1969 
1970  if ( $user->isBlockedFrom( $this->mTitle ) ) {
1971  // Auto-block user's IP if the account was "hard" blocked
1972  if ( !wfReadOnly() ) {
1973  $user->spreadAnyEditBlock();
1974  }
1975  # Check block state against master, thus 'false'.
1976  $status->setResult( false, self::AS_BLOCKED_PAGE_FOR_USER );
1977  return $status;
1978  }
1979 
1980  $this->contentLength = strlen( $this->textbox1 );
1981  $config = $this->context->getConfig();
1982  $maxArticleSize = $config->get( 'MaxArticleSize' );
1983  if ( $this->contentLength > $maxArticleSize * 1024 ) {
1984  // Error will be displayed by showEditForm()
1985  $this->tooBig = true;
1986  $status->setResult( false, self::AS_CONTENT_TOO_BIG );
1987  return $status;
1988  }
1989 
1990  if ( !$user->isAllowed( 'edit' ) ) {
1991  if ( $user->isAnon() ) {
1992  $status->setResult( false, self::AS_READ_ONLY_PAGE_ANON );
1993  return $status;
1994  } else {
1995  $status->fatal( 'readonlytext' );
1996  $status->value = self::AS_READ_ONLY_PAGE_LOGGED;
1997  return $status;
1998  }
1999  }
2000 
2001  $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
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 
2017  $canEditModel = $permissionManager->userCan(
2018  'editcontentmodel',
2019  $user,
2020  $titleWithNewContentModel
2021  );
2022 
2023  if (
2024  !$canEditModel
2025  || !$permissionManager->userCan( 'edit', $user, $titleWithNewContentModel )
2026  ) {
2027  $status->setResult( false, self::AS_NO_CHANGE_CONTENT_MODEL );
2028 
2029  return $status;
2030  }
2031 
2032  $changingContentModel = true;
2033  $oldContentModel = $this->mTitle->getContentModel();
2034  }
2035 
2036  if ( $this->changeTags ) {
2037  $changeTagsStatus = ChangeTags::canAddTagsAccompanyingChange(
2038  $this->changeTags, $user );
2039  if ( !$changeTagsStatus->isOK() ) {
2040  $changeTagsStatus->value = self::AS_CHANGE_TAG_ERROR;
2041  return $changeTagsStatus;
2042  }
2043  }
2044 
2045  if ( wfReadOnly() ) {
2046  $status->fatal( 'readonlytext' );
2047  $status->value = self::AS_READ_ONLY_PAGE;
2048  return $status;
2049  }
2050  if ( $user->pingLimiter() || $user->pingLimiter( 'linkpurge', 0 )
2051  || ( $changingContentModel && $user->pingLimiter( 'editcontentmodel' ) )
2052  ) {
2053  $status->fatal( 'actionthrottledtext' );
2054  $status->value = self::AS_RATE_LIMITED;
2055  return $status;
2056  }
2057 
2058  # If the article has been deleted while editing, don't save it without
2059  # confirmation
2060  if ( $this->wasDeletedSinceLastEdit() && !$this->recreate ) {
2061  $status->setResult( false, self::AS_ARTICLE_WAS_DELETED );
2062  return $status;
2063  }
2064 
2065  # Load the page data from the master. If anything changes in the meantime,
2066  # we detect it by using page_latest like a token in a 1 try compare-and-swap.
2067  $this->page->loadPageData( 'fromdbmaster' );
2068  $new = !$this->page->exists();
2069 
2070  if ( $new ) {
2071  // Late check for create permission, just in case *PARANOIA*
2072  if ( !$permissionManager->userCan( 'create', $user, $this->mTitle ) ) {
2073  $status->fatal( 'nocreatetext' );
2074  $status->value = self::AS_NO_CREATE_PERMISSION;
2075  wfDebug( __METHOD__ . ": no create permission\n" );
2076  return $status;
2077  }
2078 
2079  // Don't save a new page if it's blank or if it's a MediaWiki:
2080  // message with content equivalent to default (allow empty pages
2081  // in this case to disable messages, see T52124)
2082  $defaultMessageText = $this->mTitle->getDefaultMessageText();
2083  if ( $this->mTitle->getNamespace() === NS_MEDIAWIKI && $defaultMessageText !== false ) {
2084  $defaultText = $defaultMessageText;
2085  } else {
2086  $defaultText = '';
2087  }
2088 
2089  if ( !$this->allowBlankArticle && $this->textbox1 === $defaultText ) {
2090  $this->blankArticle = true;
2091  $status->fatal( 'blankarticle' );
2092  $status->setResult( false, self::AS_BLANK_ARTICLE );
2093  return $status;
2094  }
2095 
2096  if ( !$this->runPostMergeFilters( $textbox_content, $status, $user ) ) {
2097  return $status;
2098  }
2099 
2100  $content = $textbox_content;
2101 
2102  $result['sectionanchor'] = '';
2103  if ( $this->section == 'new' ) {
2104  if ( $this->sectiontitle !== '' ) {
2105  // Insert the section title above the content.
2106  $content = $content->addSectionHeader( $this->sectiontitle );
2107  } elseif ( $this->summary !== '' ) {
2108  // Insert the section title above the content.
2109  $content = $content->addSectionHeader( $this->summary );
2110  }
2111  $this->summary = $this->newSectionSummary( $result['sectionanchor'] );
2112  }
2113 
2114  $status->value = self::AS_SUCCESS_NEW_ARTICLE;
2115 
2116  } else { # not $new
2117 
2118  # Article exists. Check for edit conflict.
2119 
2120  $this->page->clear(); # Force reload of dates, etc.
2121  $timestamp = $this->page->getTimestamp();
2122  $latest = $this->page->getLatest();
2123 
2124  wfDebug( "timestamp: {$timestamp}, edittime: {$this->edittime}\n" );
2125 
2126  // An edit conflict is detected if the current revision is different from the
2127  // revision that was current when editing was initiated on the client.
2128  // This is checked based on the timestamp and revision ID.
2129  // TODO: the timestamp based check can probably go away now.
2130  if ( $timestamp != $this->edittime
2131  || ( $this->editRevId !== null && $this->editRevId != $latest )
2132  ) {
2133  $this->isConflict = true;
2134  if ( $this->section == 'new' ) {
2135  if ( $this->page->getUserText() == $user->getName() &&
2136  $this->page->getComment() == $this->newSectionSummary()
2137  ) {
2138  // Probably a duplicate submission of a new comment.
2139  // This can happen when CDN resends a request after
2140  // a timeout but the first one actually went through.
2141  wfDebug( __METHOD__
2142  . ": duplicate new section submission; trigger edit conflict!\n" );
2143  } else {
2144  // New comment; suppress conflict.
2145  $this->isConflict = false;
2146  wfDebug( __METHOD__ . ": conflict suppressed; new section\n" );
2147  }
2148  } elseif ( $this->section == ''
2150  DB_MASTER, $this->mTitle->getArticleID(),
2151  $user->getId(), $this->edittime
2152  )
2153  ) {
2154  # Suppress edit conflict with self, except for section edits where merging is required.
2155  wfDebug( __METHOD__ . ": Suppressing edit conflict, same user.\n" );
2156  $this->isConflict = false;
2157  }
2158  }
2159 
2160  // If sectiontitle is set, use it, otherwise use the summary as the section title.
2161  if ( $this->sectiontitle !== '' ) {
2162  $sectionTitle = $this->sectiontitle;
2163  } else {
2164  $sectionTitle = $this->summary;
2165  }
2166 
2167  $content = null;
2168 
2169  if ( $this->isConflict ) {
2170  wfDebug( __METHOD__
2171  . ": conflict! getting section '{$this->section}' for time '{$this->edittime}'"
2172  . " (id '{$this->editRevId}') (article time '{$timestamp}')\n" );
2173  // @TODO: replaceSectionAtRev() with base ID (not prior current) for ?oldid=X case
2174  // ...or disable section editing for non-current revisions (not exposed anyway).
2175  if ( $this->editRevId !== null ) {
2176  $content = $this->page->replaceSectionAtRev(
2177  $this->section,
2178  $textbox_content,
2179  $sectionTitle,
2180  $this->editRevId
2181  );
2182  } else {
2183  $content = $this->page->replaceSectionContent(
2184  $this->section,
2185  $textbox_content,
2186  $sectionTitle,
2187  $this->edittime
2188  );
2189  }
2190  } else {
2191  wfDebug( __METHOD__ . ": getting section '{$this->section}'\n" );
2192  $content = $this->page->replaceSectionContent(
2193  $this->section,
2194  $textbox_content,
2195  $sectionTitle
2196  );
2197  }
2198 
2199  if ( is_null( $content ) ) {
2200  wfDebug( __METHOD__ . ": activating conflict; section replace failed.\n" );
2201  $this->isConflict = true;
2202  $content = $textbox_content; // do not try to merge here!
2203  } elseif ( $this->isConflict ) {
2204  # Attempt merge
2205  if ( $this->mergeChangesIntoContent( $content ) ) {
2206  // Successful merge! Maybe we should tell the user the good news?
2207  $this->isConflict = false;
2208  wfDebug( __METHOD__ . ": Suppressing edit conflict, successful merge.\n" );
2209  } else {
2210  $this->section = '';
2211  $this->textbox1 = ContentHandler::getContentText( $content );
2212  wfDebug( __METHOD__ . ": Keeping edit conflict, failed merge.\n" );
2213  }
2214  }
2215 
2216  if ( $this->isConflict ) {
2217  $status->setResult( false, self::AS_CONFLICT_DETECTED );
2218  return $status;
2219  }
2220 
2221  if ( !$this->runPostMergeFilters( $content, $status, $user ) ) {
2222  return $status;
2223  }
2224 
2225  if ( $this->section == 'new' ) {
2226  // Handle the user preference to force summaries here
2227  if ( !$this->allowBlankSummary && trim( $this->summary ) == '' ) {
2228  $this->missingSummary = true;
2229  $status->fatal( 'missingsummary' ); // or 'missingcommentheader' if $section == 'new'. Blegh
2230  $status->value = self::AS_SUMMARY_NEEDED;
2231  return $status;
2232  }
2233 
2234  // Do not allow the user to post an empty comment
2235  if ( $this->textbox1 == '' ) {
2236  $this->missingComment = true;
2237  $status->fatal( 'missingcommenttext' );
2238  $status->value = self::AS_TEXTBOX_EMPTY;
2239  return $status;
2240  }
2241  } elseif ( !$this->allowBlankSummary
2242  && !$content->equals( $this->getOriginalContent( $user ) )
2243  && !$content->isRedirect()
2244  && md5( $this->summary ) == $this->autoSumm
2245  ) {
2246  $this->missingSummary = true;
2247  $status->fatal( 'missingsummary' );
2248  $status->value = self::AS_SUMMARY_NEEDED;
2249  return $status;
2250  }
2251 
2252  # All's well
2253  $sectionanchor = '';
2254  if ( $this->section == 'new' ) {
2255  $this->summary = $this->newSectionSummary( $sectionanchor );
2256  } elseif ( $this->section != '' ) {
2257  # Try to get a section anchor from the section source, redirect
2258  # to edited section if header found.
2259  # XXX: Might be better to integrate this into Article::replaceSectionAtRev
2260  # for duplicate heading checking and maybe parsing.
2261  $hasmatch = preg_match( "/^ *([=]{1,6})(.*?)(\\1) *\\n/i", $this->textbox1, $matches );
2262  # We can't deal with anchors, includes, html etc in the header for now,
2263  # headline would need to be parsed to improve this.
2264  if ( $hasmatch && strlen( $matches[2] ) > 0 ) {
2265  $sectionanchor = $this->guessSectionName( $matches[2] );
2266  }
2267  }
2268  $result['sectionanchor'] = $sectionanchor;
2269 
2270  // Save errors may fall down to the edit form, but we've now
2271  // merged the section into full text. Clear the section field
2272  // so that later submission of conflict forms won't try to
2273  // replace that into a duplicated mess.
2274  $this->textbox1 = $this->toEditText( $content );
2275  $this->section = '';
2276 
2277  $status->value = self::AS_SUCCESS_UPDATE;
2278  }
2279 
2280  if ( !$this->allowSelfRedirect
2281  && $content->isRedirect()
2282  && $content->getRedirectTarget()->equals( $this->getTitle() )
2283  ) {
2284  // If the page already redirects to itself, don't warn.
2285  $currentTarget = $this->getCurrentContent()->getRedirectTarget();
2286  if ( !$currentTarget || !$currentTarget->equals( $this->getTitle() ) ) {
2287  $this->selfRedirect = true;
2288  $status->fatal( 'selfredirect' );
2289  $status->value = self::AS_SELF_REDIRECT;
2290  return $status;
2291  }
2292  }
2293 
2294  // Check for length errors again now that the section is merged in
2295  $this->contentLength = strlen( $this->toEditText( $content ) );
2296  if ( $this->contentLength > $maxArticleSize * 1024 ) {
2297  $this->tooBig = true;
2298  $status->setResult( false, self::AS_MAX_ARTICLE_SIZE_EXCEEDED );
2299  return $status;
2300  }
2301 
2302  $flags = EDIT_AUTOSUMMARY |
2303  ( $new ? EDIT_NEW : EDIT_UPDATE ) |
2304  ( ( $this->minoredit && !$this->isNew ) ? EDIT_MINOR : 0 ) |
2305  ( $bot ? EDIT_FORCE_BOT : 0 );
2306 
2307  $doEditStatus = $this->page->doEditContent(
2308  $content,
2309  $this->summary,
2310  $flags,
2311  false,
2312  $user,
2313  $content->getDefaultFormat(),
2316  );
2317 
2318  if ( !$doEditStatus->isOK() ) {
2319  // Failure from doEdit()
2320  // Show the edit conflict page for certain recognized errors from doEdit(),
2321  // but don't show it for errors from extension hooks
2322  $errors = $doEditStatus->getErrorsArray();
2323  if ( in_array( $errors[0][0],
2324  [ 'edit-gone-missing', 'edit-conflict', 'edit-already-exists' ] )
2325  ) {
2326  $this->isConflict = true;
2327  // Destroys data doEdit() put in $status->value but who cares
2328  $doEditStatus->value = self::AS_END;
2329  }
2330  return $doEditStatus;
2331  }
2332 
2333  $result['nullEdit'] = $doEditStatus->hasMessage( 'edit-no-change' );
2334  if ( $result['nullEdit'] ) {
2335  // We don't know if it was a null edit until now, so increment here
2336  $user->pingLimiter( 'linkpurge' );
2337  }
2338  $result['redirect'] = $content->isRedirect();
2339 
2340  $this->updateWatchlist();
2341 
2342  // If the content model changed, add a log entry
2343  if ( $changingContentModel ) {
2345  $user,
2346  $new ? false : $oldContentModel,
2347  $this->contentModel,
2348  $this->summary
2349  );
2350  }
2351 
2352  return $status;
2353  }
2354 
2361  protected function addContentModelChangeLogEntry( User $user, $oldModel, $newModel, $reason ) {
2362  $new = $oldModel === false;
2363  $log = new ManualLogEntry( 'contentmodel', $new ? 'new' : 'change' );
2364  $log->setPerformer( $user );
2365  $log->setTarget( $this->mTitle );
2366  $log->setComment( $reason );
2367  $log->setParameters( [
2368  '4::oldmodel' => $oldModel,
2369  '5::newmodel' => $newModel
2370  ] );
2371  $logid = $log->insert();
2372  $log->publish( $logid );
2373  }
2374 
2378  protected function updateWatchlist() {
2379  $user = $this->context->getUser();
2380  if ( !$user->isLoggedIn() ) {
2381  return;
2382  }
2383 
2385  $watch = $this->watchthis;
2386  // Do this in its own transaction to reduce contention...
2387  DeferredUpdates::addCallableUpdate( function () use ( $user, $title, $watch ) {
2388  if ( $watch == $user->isWatched( $title, User::IGNORE_USER_RIGHTS ) ) {
2389  return; // nothing to change
2390  }
2392  } );
2393  }
2394 
2406  private function mergeChangesIntoContent( &$editContent ) {
2407  $db = wfGetDB( DB_MASTER );
2408 
2409  // This is the revision that was current at the time editing was initiated on the client,
2410  // even if the edit was based on an old revision.
2411  $baseRevision = $this->getBaseRevision();
2412  $baseContent = $baseRevision ? $baseRevision->getContent() : null;
2413 
2414  if ( is_null( $baseContent ) ) {
2415  return false;
2416  }
2417 
2418  // The current state, we want to merge updates into it
2419  $currentRevision = Revision::loadFromTitle( $db, $this->mTitle );
2420  $currentContent = $currentRevision ? $currentRevision->getContent() : null;
2421 
2422  if ( is_null( $currentContent ) ) {
2423  return false;
2424  }
2425 
2426  $handler = ContentHandler::getForModelID( $baseContent->getModel() );
2427 
2428  $result = $handler->merge3( $baseContent, $editContent, $currentContent );
2429 
2430  if ( $result ) {
2431  $editContent = $result;
2432  // Update parentRevId to what we just merged.
2433  $this->parentRevId = $currentRevision->getId();
2434  return true;
2435  }
2436 
2437  return false;
2438  }
2439 
2452  public function getBaseRevision() {
2453  if ( !$this->mBaseRevision ) {
2454  $db = wfGetDB( DB_MASTER );
2455  $this->mBaseRevision = $this->editRevId
2456  ? Revision::newFromId( $this->editRevId, Revision::READ_LATEST )
2457  : Revision::loadFromTimestamp( $db, $this->mTitle, $this->edittime );
2458  }
2459  return $this->mBaseRevision;
2460  }
2461 
2469  public static function matchSpamRegex( $text ) {
2470  global $wgSpamRegex;
2471  // For back compatibility, $wgSpamRegex may be a single string or an array of regexes.
2472  $regexes = (array)$wgSpamRegex;
2473  return self::matchSpamRegexInternal( $text, $regexes );
2474  }
2475 
2483  public static function matchSummarySpamRegex( $text ) {
2484  global $wgSummarySpamRegex;
2485  $regexes = (array)$wgSummarySpamRegex;
2486  return self::matchSpamRegexInternal( $text, $regexes );
2487  }
2488 
2494  protected static function matchSpamRegexInternal( $text, $regexes ) {
2495  foreach ( $regexes as $regex ) {
2496  $matches = [];
2497  if ( preg_match( $regex, $text, $matches ) ) {
2498  return $matches[0];
2499  }
2500  }
2501  return false;
2502  }
2503 
2504  public function setHeaders() {
2505  $out = $this->context->getOutput();
2506 
2507  $out->addModules( 'mediawiki.action.edit' );
2508  $out->addModuleStyles( 'mediawiki.action.edit.styles' );
2509  $out->addModuleStyles( 'mediawiki.editfont.styles' );
2510 
2511  $user = $this->context->getUser();
2512 
2513  if ( $user->getOption( 'uselivepreview' ) ) {
2514  $out->addModules( 'mediawiki.action.edit.preview' );
2515  }
2516 
2517  if ( $user->getOption( 'useeditwarning' ) ) {
2518  $out->addModules( 'mediawiki.action.edit.editWarning' );
2519  }
2520 
2521  # Enabled article-related sidebar, toplinks, etc.
2522  $out->setArticleRelated( true );
2523 
2524  $contextTitle = $this->getContextTitle();
2525  if ( $this->isConflict ) {
2526  $msg = 'editconflict';
2527  } elseif ( $contextTitle->exists() && $this->section != '' ) {
2528  $msg = $this->section == 'new' ? 'editingcomment' : 'editingsection';
2529  } else {
2530  $msg = $contextTitle->exists()
2531  || ( $contextTitle->getNamespace() == NS_MEDIAWIKI
2532  && $contextTitle->getDefaultMessageText() !== false
2533  )
2534  ? 'editing'
2535  : 'creating';
2536  }
2537 
2538  # Use the title defined by DISPLAYTITLE magic word when present
2539  # NOTE: getDisplayTitle() returns HTML while getPrefixedText() returns plain text.
2540  # setPageTitle() treats the input as wikitext, which should be safe in either case.
2541  $displayTitle = isset( $this->mParserOutput ) ? $this->mParserOutput->getDisplayTitle() : false;
2542  if ( $displayTitle === false ) {
2543  $displayTitle = $contextTitle->getPrefixedText();
2544  } else {
2545  $out->setDisplayTitle( $displayTitle );
2546  }
2547  $out->setPageTitle( $this->context->msg( $msg, $displayTitle ) );
2548 
2549  $config = $this->context->getConfig();
2550 
2551  # Transmit the name of the message to JavaScript for live preview
2552  # Keep Resources.php/mediawiki.action.edit.preview in sync with the possible keys
2553  $out->addJsConfigVars( [
2554  'wgEditMessage' => $msg,
2555  'wgAjaxEditStash' => $config->get( 'AjaxEditStash' ),
2556  ] );
2557 
2558  // Add whether to use 'save' or 'publish' messages to JavaScript for post-edit, other
2559  // editors, etc.
2560  $out->addJsConfigVars(
2561  'wgEditSubmitButtonLabelPublish',
2562  $config->get( 'EditSubmitButtonLabelPublish' )
2563  );
2564  }
2565 
2569  protected function showIntro() {
2570  if ( $this->suppressIntro ) {
2571  return;
2572  }
2573 
2574  $out = $this->context->getOutput();
2575  $namespace = $this->mTitle->getNamespace();
2576 
2577  if ( $namespace == NS_MEDIAWIKI ) {
2578  # Show a warning if editing an interface message
2579  $out->wrapWikiMsg( "<div class='mw-editinginterface'>\n$1\n</div>", 'editinginterface' );
2580  # If this is a default message (but not css, json, or js),
2581  # show a hint that it is translatable on translatewiki.net
2582  if (
2583  !$this->mTitle->hasContentModel( CONTENT_MODEL_CSS )
2584  && !$this->mTitle->hasContentModel( CONTENT_MODEL_JSON )
2585  && !$this->mTitle->hasContentModel( CONTENT_MODEL_JAVASCRIPT )
2586  ) {
2587  $defaultMessageText = $this->mTitle->getDefaultMessageText();
2588  if ( $defaultMessageText !== false ) {
2589  $out->wrapWikiMsg( "<div class='mw-translateinterface'>\n$1\n</div>",
2590  'translateinterface' );
2591  }
2592  }
2593  } elseif ( $namespace == NS_FILE ) {
2594  # Show a hint to shared repo
2595  $file = MediaWikiServices::getInstance()->getRepoGroup()->findFile( $this->mTitle );
2596  if ( $file && !$file->isLocal() ) {
2597  $descUrl = $file->getDescriptionUrl();
2598  # there must be a description url to show a hint to shared repo
2599  if ( $descUrl ) {
2600  if ( !$this->mTitle->exists() ) {
2601  $out->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-create\">\n$1\n</div>", [
2602  'sharedupload-desc-create', $file->getRepo()->getDisplayName(), $descUrl
2603  ] );
2604  } else {
2605  $out->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-edit\">\n$1\n</div>", [
2606  'sharedupload-desc-edit', $file->getRepo()->getDisplayName(), $descUrl
2607  ] );
2608  }
2609  }
2610  }
2611  }
2612 
2613  # Show a warning message when someone creates/edits a user (talk) page but the user does not exist
2614  # Show log extract when the user is currently blocked
2615  if ( $namespace == NS_USER || $namespace == NS_USER_TALK ) {
2616  $username = explode( '/', $this->mTitle->getText(), 2 )[0];
2617  $user = User::newFromName( $username, false /* allow IP users */ );
2618  $ip = User::isIP( $username );
2619  $block = DatabaseBlock::newFromTarget( $user, $user );
2620  if ( !( $user && $user->isLoggedIn() ) && !$ip ) { # User does not exist
2621  $out->wrapWikiMsg( "<div class=\"mw-userpage-userdoesnotexist error\">\n$1\n</div>",
2622  [ 'userpage-userdoesnotexist', wfEscapeWikiText( $username ) ] );
2623  } elseif (
2624  !is_null( $block ) &&
2625  $block->getType() != DatabaseBlock::TYPE_AUTO &&
2626  ( $block->isSitewide() || $user->isBlockedFrom( $this->mTitle ) )
2627  ) {
2628  // Show log extract if the user is sitewide blocked or is partially
2629  // blocked and not allowed to edit their user page or user talk page
2631  $out,
2632  'block',
2633  MediaWikiServices::getInstance()->getNamespaceInfo()->
2634  getCanonicalName( NS_USER ) . ':' . $block->getTarget(),
2635  '',
2636  [
2637  'lim' => 1,
2638  'showIfEmpty' => false,
2639  'msgKey' => [
2640  'blocked-notice-logextract',
2641  $user->getName() # Support GENDER in notice
2642  ]
2643  ]
2644  );
2645  }
2646  }
2647  # Try to add a custom edit intro, or use the standard one if this is not possible.
2648  if ( !$this->showCustomIntro() && !$this->mTitle->exists() ) {
2650  $this->context->msg( 'helppage' )->inContentLanguage()->text()
2651  ) );
2652  if ( $this->context->getUser()->isLoggedIn() ) {
2653  $out->wrapWikiMsg(
2654  // Suppress the external link icon, consider the help url an internal one
2655  "<div class=\"mw-newarticletext plainlinks\">\n$1\n</div>",
2656  [
2657  'newarticletext',
2658  $helpLink
2659  ]
2660  );
2661  } else {
2662  $out->wrapWikiMsg(
2663  // Suppress the external link icon, consider the help url an internal one
2664  "<div class=\"mw-newarticletextanon plainlinks\">\n$1\n</div>",
2665  [
2666  'newarticletextanon',
2667  $helpLink
2668  ]
2669  );
2670  }
2671  }
2672  # Give a notice if the user is editing a deleted/moved page...
2673  if ( !$this->mTitle->exists() ) {
2674  $dbr = wfGetDB( DB_REPLICA );
2675 
2676  LogEventsList::showLogExtract( $out, [ 'delete', 'move' ], $this->mTitle,
2677  '',
2678  [
2679  'lim' => 10,
2680  'conds' => [ 'log_action != ' . $dbr->addQuotes( 'revision' ) ],
2681  'showIfEmpty' => false,
2682  'msgKey' => [ 'recreate-moveddeleted-warn' ]
2683  ]
2684  );
2685  }
2686  }
2687 
2693  protected function showCustomIntro() {
2694  if ( $this->editintro ) {
2695  $title = Title::newFromText( $this->editintro );
2696  if ( $this->isPageExistingAndViewable( $title, $this->context->getUser() ) ) {
2697  // Added using template syntax, to take <noinclude>'s into account.
2698  $this->context->getOutput()->addWikiTextAsContent(
2699  '<div class="mw-editintro">{{:' . $title->getFullText() . '}}</div>',
2700  /*linestart*/true,
2702  );
2703  return true;
2704  }
2705  }
2706  return false;
2707  }
2708 
2727  protected function toEditText( $content ) {
2728  if ( $content === null || $content === false || is_string( $content ) ) {
2729  return $content;
2730  }
2731 
2732  if ( !$this->isSupportedContentModel( $content->getModel() ) ) {
2733  throw new MWException( 'This content model is not supported: ' . $content->getModel() );
2734  }
2735 
2736  return $content->serialize( $this->contentFormat );
2737  }
2738 
2755  protected function toEditContent( $text ) {
2756  if ( $text === false || $text === null ) {
2757  return $text;
2758  }
2759 
2760  $content = ContentHandler::makeContent( $text, $this->getTitle(),
2761  $this->contentModel, $this->contentFormat );
2762 
2763  if ( !$this->isSupportedContentModel( $content->getModel() ) ) {
2764  throw new MWException( 'This content model is not supported: ' . $content->getModel() );
2765  }
2766 
2767  return $content;
2768  }
2769 
2778  public function showEditForm( $formCallback = null ) {
2779  # need to parse the preview early so that we know which templates are used,
2780  # otherwise users with "show preview after edit box" will get a blank list
2781  # we parse this near the beginning so that setHeaders can do the title
2782  # setting work instead of leaving it in getPreviewText
2783  $previewOutput = '';
2784  if ( $this->formtype == 'preview' ) {
2785  $previewOutput = $this->getPreviewText();
2786  }
2787 
2788  $out = $this->context->getOutput();
2789 
2790  // Avoid PHP 7.1 warning of passing $this by reference
2791  $editPage = $this;
2792  Hooks::run( 'EditPage::showEditForm:initial', [ &$editPage, &$out ] );
2793 
2794  $this->setHeaders();
2795 
2796  $this->addTalkPageText();
2797  $this->addEditNotices();
2798 
2799  if ( !$this->isConflict &&
2800  $this->section != '' &&
2801  !$this->isSectionEditSupported() ) {
2802  // We use $this->section to much before this and getVal('wgSection') directly in other places
2803  // at this point we can't reset $this->section to '' to fallback to non-section editing.
2804  // Someone is welcome to try refactoring though
2805  $out->showErrorPage( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
2806  return;
2807  }
2808 
2809  $this->showHeader();
2810 
2811  $out->addHTML( $this->editFormPageTop );
2812 
2813  $user = $this->context->getUser();
2814  if ( $user->getOption( 'previewontop' ) ) {
2815  $this->displayPreviewArea( $previewOutput, true );
2816  }
2817 
2818  $out->addHTML( $this->editFormTextTop );
2819 
2820  if ( $this->wasDeletedSinceLastEdit() && $this->formtype !== 'save' ) {
2821  $out->wrapWikiMsg( "<div class='error mw-deleted-while-editing'>\n$1\n</div>",
2822  'deletedwhileediting' );
2823  }
2824 
2825  // @todo add EditForm plugin interface and use it here!
2826  // search for textarea1 and textarea2, and allow EditForm to override all uses.
2827  $out->addHTML( Html::openElement(
2828  'form',
2829  [
2830  'class' => 'mw-editform',
2831  'id' => self::EDITFORM_ID,
2832  'name' => self::EDITFORM_ID,
2833  'method' => 'post',
2834  'action' => $this->getActionURL( $this->getContextTitle() ),
2835  'enctype' => 'multipart/form-data'
2836  ]
2837  ) );
2838 
2839  if ( is_callable( $formCallback ) ) {
2840  wfWarn( 'The $formCallback parameter to ' . __METHOD__ . 'is deprecated' );
2841  call_user_func_array( $formCallback, [ &$out ] );
2842  }
2843 
2844  // Add a check for Unicode support
2845  $out->addHTML( Html::hidden( 'wpUnicodeCheck', self::UNICODE_CHECK ) );
2846 
2847  // Add an empty field to trip up spambots
2848  $out->addHTML(
2849  Xml::openElement( 'div', [ 'id' => 'antispam-container', 'style' => 'display: none;' ] )
2850  . Html::rawElement(
2851  'label',
2852  [ 'for' => 'wpAntispam' ],
2853  $this->context->msg( 'simpleantispam-label' )->parse()
2854  )
2855  . Xml::element(
2856  'input',
2857  [
2858  'type' => 'text',
2859  'name' => 'wpAntispam',
2860  'id' => 'wpAntispam',
2861  'value' => ''
2862  ]
2863  )
2864  . Xml::closeElement( 'div' )
2865  );
2866 
2867  // Avoid PHP 7.1 warning of passing $this by reference
2868  $editPage = $this;
2869  Hooks::run( 'EditPage::showEditForm:fields', [ &$editPage, &$out ] );
2870 
2871  // Put these up at the top to ensure they aren't lost on early form submission
2872  $this->showFormBeforeText();
2873 
2874  if ( $this->wasDeletedSinceLastEdit() && $this->formtype == 'save' ) {
2875  $username = $this->lastDelete->user_name;
2876  $comment = CommentStore::getStore()
2877  ->getComment( 'log_comment', $this->lastDelete )->text;
2878 
2879  // It is better to not parse the comment at all than to have templates expanded in the middle
2880  // TODO: can the checkLabel be moved outside of the div so that wrapWikiMsg could be used?
2881  $key = $comment === ''
2882  ? 'confirmrecreate-noreason'
2883  : 'confirmrecreate';
2884  $out->addHTML(
2885  '<div class="mw-confirm-recreate">' .
2886  $this->context->msg( $key, $username, "<nowiki>$comment</nowiki>" )->parse() .
2887  Xml::checkLabel( $this->context->msg( 'recreate' )->text(), 'wpRecreate', 'wpRecreate', false,
2888  [ 'title' => Linker::titleAttrib( 'recreate' ), 'tabindex' => 1, 'id' => 'wpRecreate' ]
2889  ) .
2890  '</div>'
2891  );
2892  }
2893 
2894  # When the summary is hidden, also hide them on preview/show changes
2895  if ( $this->nosummary ) {
2896  $out->addHTML( Html::hidden( 'nosummary', true ) );
2897  }
2898 
2899  # If a blank edit summary was previously provided, and the appropriate
2900  # user preference is active, pass a hidden tag as wpIgnoreBlankSummary. This will stop the
2901  # user being bounced back more than once in the event that a summary
2902  # is not required.
2903  # ####
2904  # For a bit more sophisticated detection of blank summaries, hash the
2905  # automatic one and pass that in the hidden field wpAutoSummary.
2906  if ( $this->missingSummary || ( $this->section == 'new' && $this->nosummary ) ) {
2907  $out->addHTML( Html::hidden( 'wpIgnoreBlankSummary', true ) );
2908  }
2909 
2910  if ( $this->undidRev ) {
2911  $out->addHTML( Html::hidden( 'wpUndidRevision', $this->undidRev ) );
2912  }
2913 
2914  if ( $this->selfRedirect ) {
2915  $out->addHTML( Html::hidden( 'wpIgnoreSelfRedirect', true ) );
2916  }
2917 
2918  if ( $this->hasPresetSummary ) {
2919  // If a summary has been preset using &summary= we don't want to prompt for
2920  // a different summary. Only prompt for a summary if the summary is blanked.
2921  // (T19416)
2922  $this->autoSumm = md5( '' );
2923  }
2924 
2925  $autosumm = $this->autoSumm !== '' ? $this->autoSumm : md5( $this->summary );
2926  $out->addHTML( Html::hidden( 'wpAutoSummary', $autosumm ) );
2927 
2928  $out->addHTML( Html::hidden( 'oldid', $this->oldid ) );
2929  $out->addHTML( Html::hidden( 'parentRevId', $this->getParentRevId() ) );
2930 
2931  $out->addHTML( Html::hidden( 'format', $this->contentFormat ) );
2932  $out->addHTML( Html::hidden( 'model', $this->contentModel ) );
2933 
2934  $out->enableOOUI();
2935 
2936  if ( $this->section == 'new' ) {
2937  $this->showSummaryInput( true, $this->summary );
2938  $out->addHTML( $this->getSummaryPreview( true, $this->summary ) );
2939  }
2940 
2941  $out->addHTML( $this->editFormTextBeforeContent );
2942  if ( $this->isConflict ) {
2943  // In an edit conflict, we turn textbox2 into the user's text,
2944  // and textbox1 into the stored version
2945  $this->textbox2 = $this->textbox1;
2946 
2947  $content = $this->getCurrentContent();
2948  $this->textbox1 = $this->toEditText( $content );
2949 
2951  $editConflictHelper->setTextboxes( $this->textbox2, $this->textbox1 );
2952  $editConflictHelper->setContentModel( $this->contentModel );
2953  $editConflictHelper->setContentFormat( $this->contentFormat );
2955  }
2956 
2957  if ( !$this->mTitle->isUserConfigPage() ) {
2958  $out->addHTML( self::getEditToolbar() );
2959  }
2960 
2961  if ( $this->blankArticle ) {
2962  $out->addHTML( Html::hidden( 'wpIgnoreBlankArticle', true ) );
2963  }
2964 
2965  if ( $this->isConflict ) {
2966  // In an edit conflict bypass the overridable content form method
2967  // and fallback to the raw wpTextbox1 since editconflicts can't be
2968  // resolved between page source edits and custom ui edits using the
2969  // custom edit ui.
2970  $conflictTextBoxAttribs = [];
2971  if ( $this->wasDeletedSinceLastEdit() ) {
2972  $conflictTextBoxAttribs['style'] = 'display:none;';
2973  } elseif ( $this->isOldRev ) {
2974  $conflictTextBoxAttribs['class'] = 'mw-textarea-oldrev';
2975  }
2976 
2977  $out->addHTML( $editConflictHelper->getEditConflictMainTextBox( $conflictTextBoxAttribs ) );
2979  } else {
2980  $this->showContentForm();
2981  }
2982 
2983  $out->addHTML( $this->editFormTextAfterContent );
2984 
2985  $this->showStandardInputs();
2986 
2987  $this->showFormAfterText();
2988 
2989  $this->showTosSummary();
2990 
2991  $this->showEditTools();
2992 
2993  $out->addHTML( $this->editFormTextAfterTools . "\n" );
2994 
2995  $out->addHTML( $this->makeTemplatesOnThisPageList( $this->getTemplates() ) );
2996 
2997  $out->addHTML( Html::rawElement( 'div', [ 'class' => 'hiddencats' ],
2998  Linker::formatHiddenCategories( $this->page->getHiddenCategories() ) ) );
2999 
3000  $out->addHTML( Html::rawElement( 'div', [ 'class' => 'limitreport' ],
3001  self::getPreviewLimitReport( $this->mParserOutput ) ) );
3002 
3003  $out->addModules( 'mediawiki.action.edit.collapsibleFooter' );
3004 
3005  if ( $this->isConflict ) {
3006  try {
3007  $this->showConflict();
3008  } catch ( MWContentSerializationException $ex ) {
3009  // this can't really happen, but be nice if it does.
3010  $msg = $this->context->msg(
3011  'content-failed-to-parse',
3012  $this->contentModel,
3013  $this->contentFormat,
3014  $ex->getMessage()
3015  );
3016  $out->wrapWikiTextAsInterface( 'error', $msg->plain() );
3017  }
3018  }
3019 
3020  // Set a hidden field so JS knows what edit form mode we are in
3021  if ( $this->isConflict ) {
3022  $mode = 'conflict';
3023  } elseif ( $this->preview ) {
3024  $mode = 'preview';
3025  } elseif ( $this->diff ) {
3026  $mode = 'diff';
3027  } else {
3028  $mode = 'text';
3029  }
3030  $out->addHTML( Html::hidden( 'mode', $mode, [ 'id' => 'mw-edit-mode' ] ) );
3031 
3032  // Marker for detecting truncated form data. This must be the last
3033  // parameter sent in order to be of use, so do not move me.
3034  $out->addHTML( Html::hidden( 'wpUltimateParam', true ) );
3035  $out->addHTML( $this->editFormTextBottom . "\n</form>\n" );
3036 
3037  if ( !$user->getOption( 'previewontop' ) ) {
3038  $this->displayPreviewArea( $previewOutput, false );
3039  }
3040  }
3041 
3049  public function makeTemplatesOnThisPageList( array $templates ) {
3050  $templateListFormatter = new TemplatesOnThisPageFormatter(
3051  $this->context, MediaWikiServices::getInstance()->getLinkRenderer()
3052  );
3053 
3054  // preview if preview, else section if section, else false
3055  $type = false;
3056  if ( $this->preview ) {
3057  $type = 'preview';
3058  } elseif ( $this->section != '' ) {
3059  $type = 'section';
3060  }
3061 
3062  return Html::rawElement( 'div', [ 'class' => 'templatesUsed' ],
3063  $templateListFormatter->format( $templates, $type )
3064  );
3065  }
3066 
3073  public static function extractSectionTitle( $text ) {
3074  preg_match( "/^(=+)(.+)\\1\\s*(\n|$)/i", $text, $matches );
3075  if ( !empty( $matches[2] ) ) {
3076  return MediaWikiServices::getInstance()->getParser()
3077  ->stripSectionName( trim( $matches[2] ) );
3078  } else {
3079  return false;
3080  }
3081  }
3082 
3083  protected function showHeader() {
3084  $out = $this->context->getOutput();
3085  $user = $this->context->getUser();
3086  if ( $this->isConflict ) {
3087  $this->addExplainConflictHeader( $out );
3088  $this->editRevId = $this->page->getLatest();
3089  } else {
3090  if ( $this->section != '' && $this->section != 'new' && !$this->summary &&
3091  !$this->preview && !$this->diff
3092  ) {
3093  $sectionTitle = self::extractSectionTitle( $this->textbox1 ); // FIXME: use Content object
3094  if ( $sectionTitle !== false ) {
3095  $this->summary = "/* $sectionTitle */ ";
3096  }
3097  }
3098 
3099  $buttonLabel = $this->context->msg( $this->getSubmitButtonLabel() )->text();
3100 
3101  if ( $this->missingComment ) {
3102  $out->wrapWikiMsg( "<div id='mw-missingcommenttext'>\n$1\n</div>", 'missingcommenttext' );
3103  }
3104 
3105  if ( $this->missingSummary && $this->section != 'new' ) {
3106  $out->wrapWikiMsg(
3107  "<div id='mw-missingsummary'>\n$1\n</div>",
3108  [ 'missingsummary', $buttonLabel ]
3109  );
3110  }
3111 
3112  if ( $this->missingSummary && $this->section == 'new' ) {
3113  $out->wrapWikiMsg(
3114  "<div id='mw-missingcommentheader'>\n$1\n</div>",
3115  [ 'missingcommentheader', $buttonLabel ]
3116  );
3117  }
3118 
3119  if ( $this->blankArticle ) {
3120  $out->wrapWikiMsg(
3121  "<div id='mw-blankarticle'>\n$1\n</div>",
3122  [ 'blankarticle', $buttonLabel ]
3123  );
3124  }
3125 
3126  if ( $this->selfRedirect ) {
3127  $out->wrapWikiMsg(
3128  "<div id='mw-selfredirect'>\n$1\n</div>",
3129  [ 'selfredirect', $buttonLabel ]
3130  );
3131  }
3132 
3133  if ( $this->hookError !== '' ) {
3134  $out->addWikiTextAsInterface( $this->hookError );
3135  }
3136 
3137  if ( $this->section != 'new' ) {
3138  $revision = $this->mArticle->getRevisionFetched();
3139  if ( $revision ) {
3140  // Let sysop know that this will make private content public if saved
3141 
3142  if ( !$revision->userCan( Revision::DELETED_TEXT, $user ) ) {
3143  $out->wrapWikiMsg(
3144  "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
3145  'rev-deleted-text-permission'
3146  );
3147  } elseif ( $revision->isDeleted( Revision::DELETED_TEXT ) ) {
3148  $out->wrapWikiMsg(
3149  "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
3150  'rev-deleted-text-view'
3151  );
3152  }
3153 
3154  if ( !$revision->isCurrent() ) {
3155  $this->mArticle->setOldSubtitle( $revision->getId() );
3156  $out->wrapWikiMsg(
3157  Html::warningBox( "\n$1\n" ),
3158  'editingold'
3159  );
3160  $this->isOldRev = true;
3161  }
3162  } elseif ( $this->mTitle->exists() ) {
3163  // Something went wrong
3164 
3165  $out->wrapWikiMsg( "<div class='errorbox'>\n$1\n</div>\n",
3166  [ 'missing-revision', $this->oldid ] );
3167  }
3168  }
3169  }
3170 
3171  if ( wfReadOnly() ) {
3172  $out->wrapWikiMsg(
3173  "<div id=\"mw-read-only-warning\">\n$1\n</div>",
3174  [ 'readonlywarning', wfReadOnlyReason() ]
3175  );
3176  } elseif ( $user->isAnon() ) {
3177  if ( $this->formtype != 'preview' ) {
3178  $returntoquery = array_diff_key(
3179  $this->context->getRequest()->getValues(),
3180  [ 'title' => true, 'returnto' => true, 'returntoquery' => true ]
3181  );
3182  $out->wrapWikiMsg(
3183  "<div id='mw-anon-edit-warning' class='warningbox'>\n$1\n</div>",
3184  [ 'anoneditwarning',
3185  // Log-in link
3186  SpecialPage::getTitleFor( 'Userlogin' )->getFullURL( [
3187  'returnto' => $this->getTitle()->getPrefixedDBkey(),
3188  'returntoquery' => wfArrayToCgi( $returntoquery ),
3189  ] ),
3190  // Sign-up link
3191  SpecialPage::getTitleFor( 'CreateAccount' )->getFullURL( [
3192  'returnto' => $this->getTitle()->getPrefixedDBkey(),
3193  'returntoquery' => wfArrayToCgi( $returntoquery ),
3194  ] )
3195  ]
3196  );
3197  } else {
3198  $out->wrapWikiMsg( "<div id=\"mw-anon-preview-warning\" class=\"warningbox\">\n$1</div>",
3199  'anonpreviewwarning'
3200  );
3201  }
3202  } elseif ( $this->mTitle->isUserConfigPage() ) {
3203  # Check the skin exists
3204  if ( $this->isWrongCaseUserConfigPage() ) {
3205  $out->wrapWikiMsg(
3206  "<div class='error' id='mw-userinvalidconfigtitle'>\n$1\n</div>",
3207  [ 'userinvalidconfigtitle', $this->mTitle->getSkinFromConfigSubpage() ]
3208  );
3209  }
3210  if ( $this->getTitle()->isSubpageOf( $user->getUserPage() ) ) {
3211  $isUserCssConfig = $this->mTitle->isUserCssConfigPage();
3212  $isUserJsonConfig = $this->mTitle->isUserJsonConfigPage();
3213  $isUserJsConfig = $this->mTitle->isUserJsConfigPage();
3214 
3215  $warning = $isUserCssConfig
3216  ? 'usercssispublic'
3217  : ( $isUserJsonConfig ? 'userjsonispublic' : 'userjsispublic' );
3218 
3219  $out->wrapWikiMsg( '<div class="mw-userconfigpublic">$1</div>', $warning );
3220 
3221  if ( $this->formtype !== 'preview' ) {
3222  $config = $this->context->getConfig();
3223  if ( $isUserCssConfig && $config->get( 'AllowUserCss' ) ) {
3224  $out->wrapWikiMsg(
3225  "<div id='mw-usercssyoucanpreview'>\n$1\n</div>",
3226  [ 'usercssyoucanpreview' ]
3227  );
3228  } elseif ( $isUserJsonConfig /* No comparable 'AllowUserJson' */ ) {
3229  $out->wrapWikiMsg(
3230  "<div id='mw-userjsonyoucanpreview'>\n$1\n</div>",
3231  [ 'userjsonyoucanpreview' ]
3232  );
3233  } elseif ( $isUserJsConfig && $config->get( 'AllowUserJs' ) ) {
3234  $out->wrapWikiMsg(
3235  "<div id='mw-userjsyoucanpreview'>\n$1\n</div>",
3236  [ 'userjsyoucanpreview' ]
3237  );
3238  }
3239  }
3240  }
3241  }
3242 
3244 
3245  $this->addLongPageWarningHeader();
3246 
3247  # Add header copyright warning
3248  $this->showHeaderCopyrightWarning();
3249  }
3250 
3258  private function getSummaryInputAttributes( array $inputAttrs = null ) {
3259  // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
3260  // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
3261  // Unicode codepoints.
3262  return ( is_array( $inputAttrs ) ? $inputAttrs : [] ) + [
3263  'id' => 'wpSummary',
3264  'name' => 'wpSummary',
3266  'tabindex' => 1,
3267  'size' => 60,
3268  'spellcheck' => 'true',
3269  ];
3270  }
3271 
3281  function getSummaryInputWidget( $summary = "", $labelText = null, $inputAttrs = null ) {
3282  $inputAttrs = OOUI\Element::configFromHtmlAttributes(
3283  $this->getSummaryInputAttributes( $inputAttrs )
3284  );
3285  $inputAttrs += [
3286  'title' => Linker::titleAttrib( 'summary' ),
3287  'accessKey' => Linker::accesskey( 'summary' ),
3288  ];
3289 
3290  // For compatibility with old scripts and extensions, we want the legacy 'id' on the `<input>`
3291  $inputAttrs['inputId'] = $inputAttrs['id'];
3292  $inputAttrs['id'] = 'wpSummaryWidget';
3293 
3294  return new OOUI\FieldLayout(
3295  new OOUI\TextInputWidget( [
3296  'value' => $summary,
3297  'infusable' => true,
3298  ] + $inputAttrs ),
3299  [
3300  'label' => new OOUI\HtmlSnippet( $labelText ),
3301  'align' => 'top',
3302  'id' => 'wpSummaryLabel',
3303  'classes' => [ $this->missingSummary ? 'mw-summarymissed' : 'mw-summary' ],
3304  ]
3305  );
3306  }
3307 
3314  protected function showSummaryInput( $isSubjectPreview, $summary = "" ) {
3315  # Add a class if 'missingsummary' is triggered to allow styling of the summary line
3316  $summaryClass = $this->missingSummary ? 'mw-summarymissed' : 'mw-summary';
3317  if ( $isSubjectPreview ) {
3318  if ( $this->nosummary ) {
3319  return;
3320  }
3321  } elseif ( !$this->mShowSummaryField ) {
3322  return;
3323  }
3324 
3325  $labelText = $this->context->msg( $isSubjectPreview ? 'subject' : 'summary' )->parse();
3326  $this->context->getOutput()->addHTML( $this->getSummaryInputWidget(
3327  $summary,
3328  $labelText,
3329  [ 'class' => $summaryClass ]
3330  ) );
3331  }
3332 
3340  protected function getSummaryPreview( $isSubjectPreview, $summary = "" ) {
3341  // avoid spaces in preview, gets always trimmed on save
3342  $summary = trim( $summary );
3343  if ( !$summary || ( !$this->preview && !$this->diff ) ) {
3344  return "";
3345  }
3346 
3347  if ( $isSubjectPreview ) {
3348  $summary = $this->context->msg( 'newsectionsummary' )
3349  ->rawParams( MediaWikiServices::getInstance()->getParser()
3350  ->stripSectionName( $summary ) )
3351  ->inContentLanguage()->text();
3352  }
3353 
3354  $message = $isSubjectPreview ? 'subject-preview' : 'summary-preview';
3355 
3356  $summary = $this->context->msg( $message )->parse()
3357  . Linker::commentBlock( $summary, $this->mTitle, $isSubjectPreview );
3358  return Xml::tags( 'div', [ 'class' => 'mw-summary-preview' ], $summary );
3359  }
3360 
3361  protected function showFormBeforeText() {
3362  $out = $this->context->getOutput();
3363  $out->addHTML( Html::hidden( 'wpSection', $this->section ) );
3364  $out->addHTML( Html::hidden( 'wpStarttime', $this->starttime ) );
3365  $out->addHTML( Html::hidden( 'wpEdittime', $this->edittime ) );
3366  $out->addHTML( Html::hidden( 'editRevId', $this->editRevId ) );
3367  $out->addHTML( Html::hidden( 'wpScrolltop', $this->scrolltop, [ 'id' => 'wpScrolltop' ] ) );
3368  }
3369 
3370  protected function showFormAfterText() {
3383  $this->context->getOutput()->addHTML(
3384  "\n" .
3385  Html::hidden( "wpEditToken", $this->context->getUser()->getEditToken() ) .
3386  "\n"
3387  );
3388  }
3389 
3398  protected function showContentForm() {
3399  $this->showTextbox1();
3400  }
3401 
3410  protected function showTextbox1( $customAttribs = null, $textoverride = null ) {
3411  if ( $this->wasDeletedSinceLastEdit() && $this->formtype == 'save' ) {
3412  $attribs = [ 'style' => 'display:none;' ];
3413  } else {
3414  $builder = new TextboxBuilder();
3415  $classes = $builder->getTextboxProtectionCSSClasses( $this->getTitle() );
3416 
3417  # Is an old revision being edited?
3418  if ( $this->isOldRev ) {
3419  $classes[] = 'mw-textarea-oldrev';
3420  }
3421 
3422  $attribs = [ 'tabindex' => 1 ];
3423 
3424  if ( is_array( $customAttribs ) ) {
3426  }
3427 
3428  $attribs = $builder->mergeClassesIntoAttributes( $classes, $attribs );
3429  }
3430 
3431  $this->showTextbox(
3432  $textoverride ?? $this->textbox1,
3433  'wpTextbox1',
3434  $attribs
3435  );
3436  }
3437 
3438  protected function showTextbox2() {
3439  $this->showTextbox( $this->textbox2, 'wpTextbox2', [ 'tabindex' => 6, 'readonly' ] );
3440  }
3441 
3442  protected function showTextbox( $text, $name, $customAttribs = [] ) {
3443  $builder = new TextboxBuilder();
3444  $attribs = $builder->buildTextboxAttribs(
3445  $name,
3447  $this->context->getUser(),
3449  );
3450 
3451  $this->context->getOutput()->addHTML(
3452  Html::textarea( $name, $builder->addNewLineAtEnd( $text ), $attribs )
3453  );
3454  }
3455 
3456  protected function displayPreviewArea( $previewOutput, $isOnTop = false ) {
3457  $classes = [];
3458  if ( $isOnTop ) {
3459  $classes[] = 'ontop';
3460  }
3461 
3462  $attribs = [ 'id' => 'wikiPreview', 'class' => implode( ' ', $classes ) ];
3463 
3464  if ( $this->formtype != 'preview' ) {
3465  $attribs['style'] = 'display: none;';
3466  }
3467 
3468  $out = $this->context->getOutput();
3469  $out->addHTML( Xml::openElement( 'div', $attribs ) );
3470 
3471  if ( $this->formtype == 'preview' ) {
3472  $this->showPreview( $previewOutput );
3473  } else {
3474  // Empty content container for LivePreview
3475  $pageViewLang = $this->mTitle->getPageViewLanguage();
3476  $attribs = [ 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(),
3477  'class' => 'mw-content-' . $pageViewLang->getDir() ];
3478  $out->addHTML( Html::rawElement( 'div', $attribs ) );
3479  }
3480 
3481  $out->addHTML( '</div>' );
3482 
3483  if ( $this->formtype == 'diff' ) {
3484  try {
3485  $this->showDiff();
3486  } catch ( MWContentSerializationException $ex ) {
3487  $msg = $this->context->msg(
3488  'content-failed-to-parse',
3489  $this->contentModel,
3490  $this->contentFormat,
3491  $ex->getMessage()
3492  );
3493  $out->wrapWikiTextAsInterface( 'error', $msg->plain() );
3494  }
3495  }
3496  }
3497 
3504  protected function showPreview( $text ) {
3505  if ( $this->mArticle instanceof CategoryPage ) {
3506  $this->mArticle->openShowCategory();
3507  }
3508  # This hook seems slightly odd here, but makes things more
3509  # consistent for extensions.
3510  $out = $this->context->getOutput();
3511  Hooks::run( 'OutputPageBeforeHTML', [ &$out, &$text ] );
3512  $out->addHTML( $text );
3513  if ( $this->mArticle instanceof CategoryPage ) {
3514  $this->mArticle->closeShowCategory();
3515  }
3516  }
3517 
3525  public function showDiff() {
3526  $oldtitlemsg = 'currentrev';
3527  # if message does not exist, show diff against the preloaded default
3528  if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && !$this->mTitle->exists() ) {
3529  $oldtext = $this->mTitle->getDefaultMessageText();
3530  if ( $oldtext !== false ) {
3531  $oldtitlemsg = 'defaultmessagetext';
3532  $oldContent = $this->toEditContent( $oldtext );
3533  } else {
3534  $oldContent = null;
3535  }
3536  } else {
3537  $oldContent = $this->getCurrentContent();
3538  }
3539 
3540  $textboxContent = $this->toEditContent( $this->textbox1 );
3541  if ( $this->editRevId !== null ) {
3542  $newContent = $this->page->replaceSectionAtRev(
3543  $this->section, $textboxContent, $this->summary, $this->editRevId
3544  );
3545  } else {
3546  $newContent = $this->page->replaceSectionContent(
3547  $this->section, $textboxContent, $this->summary, $this->edittime
3548  );
3549  }
3550 
3551  if ( $newContent ) {
3552  Hooks::run( 'EditPageGetDiffContent', [ $this, &$newContent ] );
3553 
3554  $user = $this->context->getUser();
3556  MediaWikiServices::getInstance()->getContentLanguage() );
3557  $newContent = $newContent->preSaveTransform( $this->mTitle, $user, $popts );
3558  }
3559 
3560  if ( ( $oldContent && !$oldContent->isEmpty() ) || ( $newContent && !$newContent->isEmpty() ) ) {
3561  $oldtitle = $this->context->msg( $oldtitlemsg )->parse();
3562  $newtitle = $this->context->msg( 'yourtext' )->parse();
3563 
3564  if ( !$oldContent ) {
3565  $oldContent = $newContent->getContentHandler()->makeEmptyContent();
3566  }
3567 
3568  if ( !$newContent ) {
3569  $newContent = $oldContent->getContentHandler()->makeEmptyContent();
3570  }
3571 
3572  $de = $oldContent->getContentHandler()->createDifferenceEngine( $this->context );
3573  $de->setContent( $oldContent, $newContent );
3574 
3575  $difftext = $de->getDiff( $oldtitle, $newtitle );
3576  $de->showDiffStyle();
3577  } else {
3578  $difftext = '';
3579  }
3580 
3581  $this->context->getOutput()->addHTML( '<div id="wikiDiff">' . $difftext . '</div>' );
3582  }
3583 
3587  protected function showHeaderCopyrightWarning() {
3588  $msg = 'editpage-head-copy-warn';
3589  if ( !$this->context->msg( $msg )->isDisabled() ) {
3590  $this->context->getOutput()->wrapWikiMsg( "<div class='editpage-head-copywarn'>\n$1\n</div>",
3591  'editpage-head-copy-warn' );
3592  }
3593  }
3594 
3603  protected function showTosSummary() {
3604  $msg = 'editpage-tos-summary';
3605  Hooks::run( 'EditPageTosSummary', [ $this->mTitle, &$msg ] );
3606  if ( !$this->context->msg( $msg )->isDisabled() ) {
3607  $out = $this->context->getOutput();
3608  $out->addHTML( '<div class="mw-tos-summary">' );
3609  $out->addWikiMsg( $msg );
3610  $out->addHTML( '</div>' );
3611  }
3612  }
3613 
3618  protected function showEditTools() {
3619  $this->context->getOutput()->addHTML( '<div class="mw-editTools">' .
3620  $this->context->msg( 'edittools' )->inContentLanguage()->parse() .
3621  '</div>' );
3622  }
3623 
3630  protected function getCopywarn() {
3631  return self::getCopyrightWarning( $this->mTitle );
3632  }
3633 
3642  public static function getCopyrightWarning( $title, $format = 'plain', $langcode = null ) {
3643  global $wgRightsText;
3644  if ( $wgRightsText ) {
3645  $copywarnMsg = [ 'copyrightwarning',
3646  '[[' . wfMessage( 'copyrightpage' )->inContentLanguage()->text() . ']]',
3647  $wgRightsText ];
3648  } else {
3649  $copywarnMsg = [ 'copyrightwarning2',
3650  '[[' . wfMessage( 'copyrightpage' )->inContentLanguage()->text() . ']]' ];
3651  }
3652  // Allow for site and per-namespace customization of contribution/copyright notice.
3653  Hooks::run( 'EditPageCopyrightWarning', [ $title, &$copywarnMsg ] );
3654 
3655  $msg = wfMessage( ...$copywarnMsg )->title( $title );
3656  if ( $langcode ) {
3657  $msg->inLanguage( $langcode );
3658  }
3659  return "<div id=\"editpage-copywarn\">\n" .
3660  $msg->$format() . "\n</div>";
3661  }
3662 
3670  public static function getPreviewLimitReport( ParserOutput $output = null ) {
3671  global $wgLang;
3672 
3673  if ( !$output || !$output->getLimitReportData() ) {
3674  return '';
3675  }
3676 
3677  $limitReport = Html::rawElement( 'div', [ 'class' => 'mw-limitReportExplanation' ],
3678  wfMessage( 'limitreport-title' )->parseAsBlock()
3679  );
3680 
3681  // Show/hide animation doesn't work correctly on a table, so wrap it in a div.
3682  $limitReport .= Html::openElement( 'div', [ 'class' => 'preview-limit-report-wrapper' ] );
3683 
3684  $limitReport .= Html::openElement( 'table', [
3685  'class' => 'preview-limit-report wikitable'
3686  ] ) .
3687  Html::openElement( 'tbody' );
3688 
3689  foreach ( $output->getLimitReportData() as $key => $value ) {
3690  if ( Hooks::run( 'ParserLimitReportFormat',
3691  [ $key, &$value, &$limitReport, true, true ]
3692  ) ) {
3693  $keyMsg = wfMessage( $key );
3694  $valueMsg = wfMessage( [ "$key-value-html", "$key-value" ] );
3695  if ( !$valueMsg->exists() ) {
3696  $valueMsg = new RawMessage( '$1' );
3697  }
3698  if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) {
3699  $limitReport .= Html::openElement( 'tr' ) .
3700  Html::rawElement( 'th', null, $keyMsg->parse() ) .
3701  Html::rawElement( 'td', null,
3702  $wgLang->formatNum( $valueMsg->params( $value )->parse() )
3703  ) .
3704  Html::closeElement( 'tr' );
3705  }
3706  }
3707  }
3708 
3709  $limitReport .= Html::closeElement( 'tbody' ) .
3710  Html::closeElement( 'table' ) .
3711  Html::closeElement( 'div' );
3712 
3713  return $limitReport;
3714  }
3715 
3716  protected function showStandardInputs( &$tabindex = 2 ) {
3717  $out = $this->context->getOutput();
3718  $out->addHTML( "<div class='editOptions'>\n" );
3719 
3720  if ( $this->section != 'new' ) {
3721  $this->showSummaryInput( false, $this->summary );
3722  $out->addHTML( $this->getSummaryPreview( false, $this->summary ) );
3723  }
3724 
3725  $checkboxes = $this->getCheckboxesWidget(
3726  $tabindex,
3727  [ 'minor' => $this->minoredit, 'watch' => $this->watchthis ]
3728  );
3729  $checkboxesHTML = new OOUI\HorizontalLayout( [ 'items' => $checkboxes ] );
3730 
3731  $out->addHTML( "<div class='editCheckboxes'>" . $checkboxesHTML . "</div>\n" );
3732 
3733  // Show copyright warning.
3734  $out->addWikiTextAsInterface( $this->getCopywarn() );
3735  $out->addHTML( $this->editFormTextAfterWarn );
3736 
3737  $out->addHTML( "<div class='editButtons'>\n" );
3738  $out->addHTML( implode( "\n", $this->getEditButtons( $tabindex ) ) . "\n" );
3739 
3740  $cancel = $this->getCancelLink();
3741 
3742  $message = $this->context->msg( 'edithelppage' )->inContentLanguage()->text();
3743  $edithelpurl = Skin::makeInternalOrExternalUrl( $message );
3744  $edithelp =
3746  $this->context->msg( 'edithelp' )->text(),
3747  [ 'target' => 'helpwindow', 'href' => $edithelpurl ],
3748  [ 'mw-ui-quiet' ]
3749  ) .
3750  $this->context->msg( 'word-separator' )->escaped() .
3751  $this->context->msg( 'newwindow' )->parse();
3752 
3753  $out->addHTML( " <span class='cancelLink'>{$cancel}</span>\n" );
3754  $out->addHTML( " <span class='editHelp'>{$edithelp}</span>\n" );
3755  $out->addHTML( "</div><!-- editButtons -->\n" );
3756 
3757  Hooks::run( 'EditPage::showStandardInputs:options', [ $this, $out, &$tabindex ] );
3758 
3759  $out->addHTML( "</div><!-- editOptions -->\n" );
3760  }
3761 
3766  protected function showConflict() {
3767  $out = $this->context->getOutput();
3768  // Avoid PHP 7.1 warning of passing $this by reference
3769  $editPage = $this;
3770  if ( Hooks::run( 'EditPageBeforeConflictDiff', [ &$editPage, &$out ] ) ) {
3771  $this->incrementConflictStats();
3772 
3773  $this->getEditConflictHelper()->showEditFormTextAfterFooters();
3774  }
3775  }
3776 
3777  protected function incrementConflictStats() {
3778  $this->getEditConflictHelper()->incrementConflictStats();
3779  }
3780 
3784  public function getCancelLink() {
3785  $cancelParams = [];
3786  if ( !$this->isConflict && $this->oldid > 0 ) {
3787  $cancelParams['oldid'] = $this->oldid;
3788  } elseif ( $this->getContextTitle()->isRedirect() ) {
3789  $cancelParams['redirect'] = 'no';
3790  }
3791 
3792  return new OOUI\ButtonWidget( [
3793  'id' => 'mw-editform-cancel',
3794  'href' => $this->getContextTitle()->getLinkURL( $cancelParams ),
3795  'label' => new OOUI\HtmlSnippet( $this->context->msg( 'cancel' )->parse() ),
3796  'framed' => false,
3797  'infusable' => true,
3798  'flags' => 'destructive',
3799  ] );
3800  }
3801 
3811  protected function getActionURL( Title $title ) {
3812  return $title->getLocalURL( [ 'action' => $this->action ] );
3813  }
3814 
3822  protected function wasDeletedSinceLastEdit() {
3823  if ( $this->deletedSinceEdit !== null ) {
3824  return $this->deletedSinceEdit;
3825  }
3826 
3827  $this->deletedSinceEdit = false;
3828 
3829  if ( !$this->mTitle->exists() && $this->mTitle->isDeletedQuick() ) {
3830  $this->lastDelete = $this->getLastDelete();
3831  if ( $this->lastDelete ) {
3832  $deleteTime = wfTimestamp( TS_MW, $this->lastDelete->log_timestamp );
3833  if ( $deleteTime > $this->starttime ) {
3834  $this->deletedSinceEdit = true;
3835  }
3836  }
3837  }
3838 
3839  return $this->deletedSinceEdit;
3840  }
3841 
3847  protected function getLastDelete() {
3848  $dbr = wfGetDB( DB_REPLICA );
3849  $commentQuery = CommentStore::getStore()->getJoin( 'log_comment' );
3850  $actorQuery = ActorMigration::newMigration()->getJoin( 'log_user' );
3851  $data = $dbr->selectRow(
3852  array_merge( [ 'logging' ], $commentQuery['tables'], $actorQuery['tables'], [ 'user' ] ),
3853  [
3854  'log_type',
3855  'log_action',
3856  'log_timestamp',
3857  'log_namespace',
3858  'log_title',
3859  'log_params',
3860  'log_deleted',
3861  'user_name'
3862  ] + $commentQuery['fields'] + $actorQuery['fields'],
3863  [
3864  'log_namespace' => $this->mTitle->getNamespace(),
3865  'log_title' => $this->mTitle->getDBkey(),
3866  'log_type' => 'delete',
3867  'log_action' => 'delete',
3868  ],
3869  __METHOD__,
3870  [ 'LIMIT' => 1, 'ORDER BY' => 'log_timestamp DESC' ],
3871  [
3872  'user' => [ 'JOIN', 'user_id=' . $actorQuery['fields']['log_user'] ],
3873  ] + $commentQuery['joins'] + $actorQuery['joins']
3874  );
3875  // Quick paranoid permission checks...
3876  if ( is_object( $data ) ) {
3877  if ( $data->log_deleted & LogPage::DELETED_USER ) {
3878  $data->user_name = $this->context->msg( 'rev-deleted-user' )->escaped();
3879  }
3880 
3881  if ( $data->log_deleted & LogPage::DELETED_COMMENT ) {
3882  $data->log_comment_text = $this->context->msg( 'rev-deleted-comment' )->escaped();
3883  $data->log_comment_data = null;
3884  }
3885  }
3886 
3887  return $data;
3888  }
3889 
3895  public function getPreviewText() {
3896  $out = $this->context->getOutput();
3897  $config = $this->context->getConfig();
3898 
3899  if ( $config->get( 'RawHtml' ) && !$this->mTokenOk ) {
3900  // Could be an offsite preview attempt. This is very unsafe if
3901  // HTML is enabled, as it could be an attack.
3902  $parsedNote = '';
3903  if ( $this->textbox1 !== '' ) {
3904  // Do not put big scary notice, if previewing the empty
3905  // string, which happens when you initially edit
3906  // a category page, due to automatic preview-on-open.
3907  $parsedNote = Html::rawElement( 'div', [ 'class' => 'previewnote' ],
3908  $out->parseAsInterface(
3909  $this->context->msg( 'session_fail_preview_html' )->plain()
3910  ) );
3911  }
3912  $this->incrementEditFailureStats( 'session_loss' );
3913  return $parsedNote;
3914  }
3915 
3916  $note = '';
3917 
3918  try {
3919  $content = $this->toEditContent( $this->textbox1 );
3920 
3921  $previewHTML = '';
3922  if ( !Hooks::run(
3923  'AlternateEditPreview',
3924  [ $this, &$content, &$previewHTML, &$this->mParserOutput ] )
3925  ) {
3926  return $previewHTML;
3927  }
3928 
3929  # provide a anchor link to the editform
3930  $continueEditing = '<span class="mw-continue-editing">' .
3931  '[[#' . self::EDITFORM_ID . '|' .
3932  $this->context->getLanguage()->getArrow() . ' ' .
3933  $this->context->msg( 'continue-editing' )->text() . ']]</span>';
3934  if ( $this->mTriedSave && !$this->mTokenOk ) {
3935  if ( $this->mTokenOkExceptSuffix ) {
3936  $note = $this->context->msg( 'token_suffix_mismatch' )->plain();
3937  $this->incrementEditFailureStats( 'bad_token' );
3938  } else {
3939  $note = $this->context->msg( 'session_fail_preview' )->plain();
3940  $this->incrementEditFailureStats( 'session_loss' );
3941  }
3942  } elseif ( $this->incompleteForm ) {
3943  $note = $this->context->msg( 'edit_form_incomplete' )->plain();
3944  if ( $this->mTriedSave ) {
3945  $this->incrementEditFailureStats( 'incomplete_form' );
3946  }
3947  } else {
3948  $note = $this->context->msg( 'previewnote' )->plain() . ' ' . $continueEditing;
3949  }
3950 
3951  # don't parse non-wikitext pages, show message about preview
3952  if ( $this->mTitle->isUserConfigPage() || $this->mTitle->isSiteConfigPage() ) {
3953  if ( $this->mTitle->isUserConfigPage() ) {
3954  $level = 'user';
3955  } elseif ( $this->mTitle->isSiteConfigPage() ) {
3956  $level = 'site';
3957  } else {
3958  $level = false;
3959  }
3960 
3961  if ( $content->getModel() == CONTENT_MODEL_CSS ) {
3962  $format = 'css';
3963  if ( $level === 'user' && !$config->get( 'AllowUserCss' ) ) {
3964  $format = false;
3965  }
3966  } elseif ( $content->getModel() == CONTENT_MODEL_JSON ) {
3967  $format = 'json';
3968  if ( $level === 'user' /* No comparable 'AllowUserJson' */ ) {
3969  $format = false;
3970  }
3971  } elseif ( $content->getModel() == CONTENT_MODEL_JAVASCRIPT ) {
3972  $format = 'js';
3973  if ( $level === 'user' && !$config->get( 'AllowUserJs' ) ) {
3974  $format = false;
3975  }
3976  } else {
3977  $format = false;
3978  }
3979 
3980  # Used messages to make sure grep find them:
3981  # Messages: usercsspreview, userjsonpreview, userjspreview,
3982  # sitecsspreview, sitejsonpreview, sitejspreview
3983  if ( $level && $format ) {
3984  $note = "<div id='mw-{$level}{$format}preview'>" .
3985  $this->context->msg( "{$level}{$format}preview" )->plain() .
3986  ' ' . $continueEditing . "</div>";
3987  }
3988  }
3989 
3990  # If we're adding a comment, we need to show the
3991  # summary as the headline
3992  if ( $this->section === "new" && $this->summary !== "" ) {
3993  $content = $content->addSectionHeader( $this->summary );
3994  }
3995 
3996  $hook_args = [ $this, &$content ];
3997  Hooks::run( 'EditPageGetPreviewContent', $hook_args );
3998 
3999  $parserResult = $this->doPreviewParse( $content );
4000  $parserOutput = $parserResult['parserOutput'];
4001  $previewHTML = $parserResult['html'];
4002  $this->mParserOutput = $parserOutput;
4003  $out->addParserOutputMetadata( $parserOutput );
4004  if ( $out->userCanPreview() ) {
4005  $out->addContentOverride( $this->getTitle(), $content );
4006  }
4007 
4008  if ( count( $parserOutput->getWarnings() ) ) {
4009  $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
4010  }
4011 
4012  } catch ( MWContentSerializationException $ex ) {
4013  $m = $this->context->msg(
4014  'content-failed-to-parse',
4015  $this->contentModel,
4016  $this->contentFormat,
4017  $ex->getMessage()
4018  );
4019  $note .= "\n\n" . $m->plain(); # gets parsed down below
4020  $previewHTML = '';
4021  }
4022 
4023  if ( $this->isConflict ) {
4024  $conflict = Html::rawElement(
4025  'h2', [ 'id' => 'mw-previewconflict' ],
4026  $this->context->msg( 'previewconflict' )->escaped()
4027  );
4028  } else {
4029  $conflict = '<hr />';
4030  }
4031 
4032  $previewhead = Html::rawElement(
4033  'div', [ 'class' => 'previewnote' ],
4035  'h2', [ 'id' => 'mw-previewheader' ],
4036  $this->context->msg( 'preview' )->escaped()
4037  ) .
4038  $out->parseAsInterface( $note ) . $conflict
4039  );
4040 
4041  $pageViewLang = $this->mTitle->getPageViewLanguage();
4042  $attribs = [ 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(),
4043  'class' => 'mw-content-' . $pageViewLang->getDir() ];
4044  $previewHTML = Html::rawElement( 'div', $attribs, $previewHTML );
4045 
4046  return $previewhead . $previewHTML . $this->previewTextAfterContent;
4047  }
4048 
4049  private function incrementEditFailureStats( $failureType ) {
4050  $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
4051  $stats->increment( 'edit.failures.' . $failureType );
4052  }
4053 
4058  protected function getPreviewParserOptions() {
4059  $parserOptions = $this->page->makeParserOptions( $this->context );
4060  $parserOptions->setIsPreview( true );
4061  $parserOptions->setIsSectionPreview( !is_null( $this->section ) && $this->section !== '' );
4062  $parserOptions->enableLimitReport();
4063 
4064  // XXX: we could call $parserOptions->setCurrentRevisionCallback here to force the
4065  // current revision to be null during PST, until setupFakeRevision is called on
4066  // the ParserOptions. Currently, we rely on Parser::getRevisionObject() to ignore
4067  // existing revisions in preview mode.
4068 
4069  return $parserOptions;
4070  }
4071 
4081  protected function doPreviewParse( Content $content ) {
4082  $user = $this->context->getUser();
4083  $parserOptions = $this->getPreviewParserOptions();
4084 
4085  // NOTE: preSaveTransform doesn't have a fake revision to operate on.
4086  // Parser::getRevisionObject() will return null in preview mode,
4087  // causing the context user to be used for {{subst:REVISIONUSER}}.
4088  // XXX: Alternatively, we could also call setupFakeRevision() a second time:
4089  // once before PST with $content, and then after PST with $pstContent.
4090  $pstContent = $content->preSaveTransform( $this->mTitle, $user, $parserOptions );
4091  $scopedCallback = $parserOptions->setupFakeRevision( $this->mTitle, $pstContent, $user );
4092  $parserOutput = $pstContent->getParserOutput( $this->mTitle, null, $parserOptions );
4093  ScopedCallback::consume( $scopedCallback );
4094  return [
4095  'parserOutput' => $parserOutput,
4096  'html' => $parserOutput->getText( [
4097  'enableSectionEditLinks' => false
4098  ] )
4099  ];
4100  }
4101 
4105  public function getTemplates() {
4106  if ( $this->preview || $this->section != '' ) {
4107  $templates = [];
4108  if ( !isset( $this->mParserOutput ) ) {
4109  return $templates;
4110  }
4111  foreach ( $this->mParserOutput->getTemplates() as $ns => $template ) {
4112  foreach ( array_keys( $template ) as $dbk ) {
4113  $templates[] = Title::makeTitle( $ns, $dbk );
4114  }
4115  }
4116  return $templates;
4117  } else {
4118  return $this->mTitle->getTemplateLinksFrom();
4119  }
4120  }
4121 
4127  public static function getEditToolbar() {
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 
4386  protected function addEditNotices() {
4387  $out = $this->context->getOutput();
4388  $editNotices = $this->mTitle->getEditNotices( $this->oldid );
4389  if ( count( $editNotices ) ) {
4390  $out->addHTML( implode( "\n", $editNotices ) );
4391  } else {
4392  $msg = $this->context->msg( 'editnotice-notext' );
4393  if ( !$msg->isDisabled() ) {
4394  $out->addHTML(
4395  '<div class="mw-editnotice-notext">'
4396  . $msg->parseAsBlock()
4397  . '</div>'
4398  );
4399  }
4400  }
4401  }
4402 
4406  protected function addTalkPageText() {
4407  if ( $this->mTitle->isTalkPage() ) {
4408  $this->context->getOutput()->addWikiMsg( 'talkpagetext' );
4409  }
4410  }
4411 
4415  protected function addLongPageWarningHeader() {
4416  if ( $this->contentLength === false ) {
4417  $this->contentLength = strlen( $this->textbox1 );
4418  }
4419 
4420  $out = $this->context->getOutput();
4421  $lang = $this->context->getLanguage();
4422  $maxArticleSize = $this->context->getConfig()->get( 'MaxArticleSize' );
4423  if ( $this->tooBig || $this->contentLength > $maxArticleSize * 1024 ) {
4424  $out->wrapWikiMsg( "<div class='error' id='mw-edit-longpageerror'>\n$1\n</div>",
4425  [
4426  'longpageerror',
4427  $lang->formatNum( round( $this->contentLength / 1024, 3 ) ),
4428  $lang->formatNum( $maxArticleSize )
4429  ]
4430  );
4431  } elseif ( !$this->context->msg( 'longpage-hint' )->isDisabled() ) {
4432  $out->wrapWikiMsg( "<div id='mw-edit-longpage-hint'>\n$1\n</div>",
4433  [
4434  'longpage-hint',
4435  $lang->formatSize( strlen( $this->textbox1 ) ),
4436  strlen( $this->textbox1 )
4437  ]
4438  );
4439  }
4440  }
4441 
4445  protected function addPageProtectionWarningHeaders() {
4446  $out = $this->context->getOutput();
4447  if ( $this->mTitle->isProtected( 'edit' ) &&
4448  MediaWikiServices::getInstance()->getNamespaceInfo()->getRestrictionLevels(
4449  $this->mTitle->getNamespace()
4450  ) !== [ '' ]
4451  ) {
4452  # Is the title semi-protected?
4453  if ( $this->mTitle->isSemiProtected() ) {
4454  $noticeMsg = 'semiprotectedpagewarning';
4455  } else {
4456  # Then it must be protected based on static groups (regular)
4457  $noticeMsg = 'protectedpagewarning';
4458  }
4459  LogEventsList::showLogExtract( $out, 'protect', $this->mTitle, '',
4460  [ 'lim' => 1, 'msgKey' => [ $noticeMsg ] ] );
4461  }
4462  if ( $this->mTitle->isCascadeProtected() ) {
4463  # Is this page under cascading protection from some source pages?
4464 
4465  list( $cascadeSources, /* $restrictions */ ) = $this->mTitle->getCascadeProtectionSources();
4466  $notice = "<div class='mw-cascadeprotectedwarning'>\n$1\n";
4467  $cascadeSourcesCount = count( $cascadeSources );
4468  if ( $cascadeSourcesCount > 0 ) {
4469  # Explain, and list the titles responsible
4470  foreach ( $cascadeSources as $page ) {
4471  $notice .= '* [[:' . $page->getPrefixedText() . "]]\n";
4472  }
4473  }
4474  $notice .= '</div>';
4475  $out->wrapWikiMsg( $notice, [ 'cascadeprotectedwarning', $cascadeSourcesCount ] );
4476  }
4477  if ( !$this->mTitle->exists() && $this->mTitle->getRestrictions( 'create' ) ) {
4478  LogEventsList::showLogExtract( $out, 'protect', $this->mTitle, '',
4479  [ 'lim' => 1,
4480  'showIfEmpty' => false,
4481  'msgKey' => [ 'titleprotectedwarning' ],
4482  'wrap' => "<div class=\"mw-titleprotectedwarning\">\n$1</div>" ] );
4483  }
4484  }
4485 
4490  protected function addExplainConflictHeader( OutputPage $out ) {
4491  $out->addHTML(
4492  $this->getEditConflictHelper()->getExplainHeader()
4493  );
4494  }
4495 
4504  return ( new TextboxBuilder() )->buildTextboxAttribs(
4505  $name, $customAttribs, $user, $this->mTitle
4506  );
4507  }
4508 
4514  protected function addNewLineAtEnd( $wikitext ) {
4515  return ( new TextboxBuilder() )->addNewLineAtEnd( $wikitext );
4516  }
4517 
4528  private function guessSectionName( $text ) {
4529  // Detect Microsoft browsers
4530  $userAgent = $this->context->getRequest()->getHeader( 'User-Agent' );
4531  $parser = MediaWikiServices::getInstance()->getParser();
4532  if ( $userAgent && preg_match( '/MSIE|Edge/', $userAgent ) ) {
4533  // ...and redirect them to legacy encoding, if available
4534  return $parser->guessLegacySectionNameFromWikiText( $text );
4535  }
4536  // Meanwhile, real browsers get real anchors
4537  $name = $parser->guessSectionNameFromWikiText( $text );
4538  // With one little caveat: per T216029, fragments in HTTP redirects need to be urlencoded,
4539  // otherwise Chrome double-escapes the rest of the URL.
4540  return '#' . urlencode( mb_substr( $name, 1 ) );
4541  }
4542 
4549  public function setEditConflictHelperFactory( callable $factory ) {
4550  $this->editConflictHelperFactory = $factory;
4551  $this->editConflictHelper = null;
4552  }
4553 
4557  private function getEditConflictHelper() {
4558  if ( !$this->editConflictHelper ) {
4559  $this->editConflictHelper = call_user_func(
4560  $this->editConflictHelperFactory,
4561  $this->getSubmitButtonLabel()
4562  );
4563  }
4564 
4566  }
4567 
4572  private function newTextConflictHelper( $submitButtonLabel ) {
4573  return new TextConflictHelper(
4574  $this->getTitle(),
4575  $this->getContext()->getOutput(),
4576  MediaWikiServices::getInstance()->getStatsdDataFactory(),
4577  $submitButtonLabel
4578  );
4579  }
4580 }
string $autoSumm
Definition: EditPage.php:300
getContent( $audience=Revision::FOR_PUBLIC, User $user=null)
Get the content of the current revision.
Definition: WikiPage.php:816
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:138
displayPermissionsError(array $permErrors)
Display a permissions error page, like OutputPage::showPermissionsErrorPage(), but with the following...
Definition: EditPage.php:757
incrementConflictStats()
Definition: EditPage.php:3777
const FOR_THIS_USER
Definition: Revision.php:55
bool $nosummary
If true, hide the summary field.
Definition: EditPage.php:351
getPreloadedContent( $preload, $params=[])
Get the contents to be preloaded into the box, either set by an earlier setPreloadText() or by loadin...
Definition: EditPage.php:1467
$editFormTextBottom
Definition: EditPage.php:424
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:84
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:282
bool $bot
Definition: EditPage.php:404
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:1585
string $textbox2
Definition: EditPage.php:343
either a plain
Definition: hooks.txt:2043
bool $mTokenOk
Definition: EditPage.php:264
$editFormTextAfterContent
Definition: EditPage.php:425
showContentForm()
Subpage overridable method for printing the form for page content editing By default this simply outp...
Definition: EditPage.php:3398
bool $allowBlankSummary
Definition: EditPage.php:285
getPreviewText()
Get the rendered text for previewing.
Definition: EditPage.php:3895
bool $isConflict
Whether an edit conflict needs to be resolved.
Definition: EditPage.php:243
int $oldid
Revision ID the edit is based on, or 0 if it&#39;s the current revision.
Definition: EditPage.php:388
getContentObject( $def_content=null)
Definition: EditPage.php:1180
handleStatus(Status $status, $resultDetails)
Handle status, such as after attempt save.
Definition: EditPage.php:1623
string $summary
Definition: EditPage.php:346
processing should stop and the error should be shown to the user * false
Definition: hooks.txt:187
setHeaders()
Definition: EditPage.php:2504
WikiPage $page
Definition: EditPage.php:225
per default it will return the text for text based content
static matchSpamRegexInternal( $text, $regexes)
Definition: EditPage.php:2494
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:985
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:105
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:64
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Definition: router.php:42
div flags Integer display flags(NO_ACTION_LINK, NO_EXTRA_USER_LINKS) 'LogException' returning false will NOT prevent logging $e
Definition: hooks.txt:2159
showTextbox2()
Definition: EditPage.php:3438
bool $tooBig
Definition: EditPage.php:276
showHeaderCopyrightWarning()
Show the header copyright warning.
Definition: EditPage.php:3587
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:3642
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:3603
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:725
The edit page/HTML interface (split from Article) The actual database and text munging is still in Ar...
Definition: EditPage.php:45
Title $mTitle
Definition: EditPage.php:231
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:127
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
const AS_HOOK_ERROR_EXPECTED
Status: A hook function returned an error.
Definition: EditPage.php:69
getLocalURL( $query='', $query2=false)
Get a URL with no fragment or server name (relative URL) from a Title object.
Definition: Title.php:1993
static accesskey( $name)
Given the id of an interface element, constructs the appropriate accesskey attribute from the system ...
Definition: Linker.php:2075
static loadFromTimestamp( $db, $title, $timestamp)
Load the revision for the given title with the given timestamp.
Definition: Revision.php:295
getSummaryInputAttributes(array $inputAttrs=null)
Helper function for summary input functions, which returns the necessary attributes for the input...
Definition: EditPage.php:3258
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:1266
string $editintro
Definition: EditPage.php:398
Class for viewing MediaWiki article and history.
Definition: Article.php:38
null for the local wiki Added in
Definition: hooks.txt:1585
static getSkinNames()
Fetch the set of available skins.
Definition: Skin.php:57
bool $allowBlankArticle
Definition: EditPage.php:291
toEditText( $content)
Gets an editable textual representation of $content.
Definition: EditPage.php:2727
IContextSource $context
Definition: EditPage.php:449
$value
Article $mArticle
Definition: EditPage.php:223
null string $contentFormat
Definition: EditPage.php:410
const AS_BLOCKED_PAGE_FOR_USER
Status: User is blocked from editing this page.
Definition: EditPage.php:74
getWikiText( $shortContext=false, $longContext=false, $lang=null)
Get the error list as a wikitext formatted list.
Definition: Status.php:175
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:288
buildTextboxAttribs( $name, array $customAttribs, User $user)
Definition: EditPage.php:4503
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:111
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:279
const EDIT_MINOR
Definition: Defines.php:150
getEditPermissionErrors( $rigor='secure')
Definition: EditPage.php:713
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:217
const EDIT_UPDATE
Definition: Defines.php:149
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:1304
this hook is for auditing only $response
Definition: hooks.txt:780
showFormBeforeText()
Definition: EditPage.php:3361
null means default & $customAttribs
Definition: hooks.txt:1982
internalAttemptSave(&$result, $bot=false)
Attempt submission (no UI)
Definition: EditPage.php:1869
bool stdClass $lastDelete
Definition: EditPage.php:261
const AS_UNICODE_NOT_SUPPORTED
Status: edit rejected because browser doesn&#39;t support Unicode.
Definition: EditPage.php:191
target page
addPageProtectionWarningHeaders()
Definition: EditPage.php:4445
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:235
edit()
This is the function that gets called for "action=edit".
Definition: EditPage.php:580
getContextTitle()
Get the context title object.
Definition: EditPage.php:537
see documentation in includes Linker php for Linker::makeImageLink or false for current used if you return false $parser
Definition: hooks.txt:1799
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:2406
const DB_MASTER
Definition: defines.php:26
displayPreviewArea( $previewOutput, $isOnTop=false)
Definition: EditPage.php:3456
addEditNotices()
Definition: EditPage.php:4386
static formatHiddenCategories( $hiddencats)
Returns HTML for the "hidden categories on this page" list.
Definition: Linker.php:1992
isRedirect()
Tests if the article content represents a redirect.
Definition: WikiPage.php:630
null Title $mContextTitle
Definition: EditPage.php:234
static textarea( $name, $value='', array $attribs=[])
Convenience function to produce a <textarea> element.
Definition: Html.php:813
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:2217
int $editRevId
Revision ID of the latest revision of the page when editing was initiated on the client.
Definition: EditPage.php:370
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:1980
const AS_CONTENT_TOO_BIG
Status: Content too big (> $wgMaxArticleSize)
Definition: EditPage.php:79
The User object encapsulates all of the user-specific settings (user_id, name, rights, email address, options, last login time).
Definition: User.php:51
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 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
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 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
getContext()
Gets the context this Article is executed in.
Definition: Article.php:2237
addExplainConflictHeader(OutputPage $out)
Definition: EditPage.php:4490
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:1589
showIntro()
Show all applicable editing introductions.
Definition: EditPage.php:2569
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:1982
getArticle()
Definition: EditPage.php:500
$wgSpamRegex
Edits matching these regular expressions in body text will be recognised as spam and rejected automat...
bool $watchthis
Definition: EditPage.php:332
$previewTextAfterContent
Definition: EditPage.php:426
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 '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:1263
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
const DELETED_COMMENT
Definition: LogPage.php:35
static addCallableUpdate( $callable, $stage=self::POSTSEND, $dbw=null)
Add a callable update.
getParentRevId()
Get the edit&#39;s parent revision ID.
Definition: EditPage.php:1390
addLongPageWarningHeader()
Definition: EditPage.php:4415
getTemplates()
Definition: EditPage.php:4105
bool $save
Definition: EditPage.php:320
wfReadOnly()
Check whether the wiki is in read-only mode.
$wgLang
Definition: Setup.php:931
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:525
static openElement( $element, $attribs=null)
This opens an XML element.
Definition: Xml.php:108
TextConflictHelper null $editConflictHelper
Definition: EditPage.php:471
toEditContent( $text)
Turns the given text into a Content object by unserializing it.
Definition: EditPage.php:2755
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:147
const EDIT_FORCE_BOT
Definition: Defines.php:152
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:2027
static getPreviewLimitReport(ParserOutput $output=null)
Get the Limit report for page previews.
Definition: EditPage.php:3670
callable $editConflictHelperFactory
Factory function to create an edit conflict helper.
Definition: EditPage.php:466
getLastDelete()
Get the last log record of this page being deleted, if ever.
Definition: EditPage.php:3847
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:1420
getActionURL(Title $title)
Returns the URL to use in the form&#39;s action attribute.
Definition: EditPage.php:3811
static doWatchOrUnwatch( $watch, Title $title, User $user)
Watch or unwatch a page.
Definition: WatchAction.php:89
$editFormTextAfterTools
Definition: EditPage.php:423
const AS_CANNOT_USE_CUSTOM_MODEL
Status: when changing the content model is disallowed due to $wgContentHandlerUseDB being false...
Definition: EditPage.php:186
$editFormTextAfterWarn
Definition: EditPage.php:422
$res
Definition: database.txt:21
bool $recreate
Definition: EditPage.php:335
setPreloadedContent(Content $content)
Use this method before edit() to preload some content into the edit box.
Definition: EditPage.php:1452
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:54
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:94
static extractSectionTitle( $text)
Extract the section title from current section text, if any.
Definition: EditPage.php:3073
bool $isOldRev
Whether an old revision is edited.
Definition: EditPage.php:454
showHeader()
Definition: EditPage.php:3083
const EDIT_AUTOSUMMARY
Definition: Defines.php:154
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
const UNICODE_CHECK
Used for Unicode support checks.
Definition: EditPage.php:49
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:163
addContentModelChangeLogEntry(User $user, $oldModel, $newModel, $reason)
Definition: EditPage.php:2361
getTitle()
Get the title object of the article.
Definition: Article.php:221
const IGNORE_USER_RIGHTS
Definition: User.php:83
$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:1982
static tags( $element, $attribs, $contents)
Same as Xml::element(), but does not escape contents.
Definition: Xml.php:130
static closeElement( $element)
Returns "</$element>".
Definition: Html.php:316
string $edittime
Timestamp of the latest revision of the page when editing was initiated on the client.
Definition: EditPage.php:357
showSummaryInput( $isSubjectPreview, $summary="")
Definition: EditPage.php:3314
showEditForm( $formCallback=null)
Send the edit form and related headers to OutputPage.
Definition: EditPage.php:2778
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:277
initialiseForm()
Initialise form fields in the object Called on the first invocation, e.g.
Definition: EditPage.php:1141
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:1982
wasDeletedSinceLastEdit()
Check if a page was deleted while the user was editing it, before submit.
Definition: EditPage.php:3822
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:902
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:258
isPageExistingAndViewable( $title, User $user)
Verify if a given title exists and the given user is allowed to view it.
Definition: EditPage.php:1533
namespace and then decline to actually register it file or subcat img or subcat $title
Definition: hooks.txt:925
showFormAfterText()
Definition: EditPage.php:3370
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:3525
bool $isNew
New page or new section.
Definition: EditPage.php:246
$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:840
const EDITFORM_ID
HTML id and name for the beginning of the edit form.
Definition: EditPage.php:196
const NS_FILE
Definition: Defines.php:66
getCopywarn()
Get the copyright warning.
Definition: EditPage.php:3630
bool $allowSelfRedirect
Definition: EditPage.php:297
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don&#39;t need a full Title object...
Definition: SpecialPage.php:83
const AS_SUCCESS_NEW_ARTICLE
Status: Article successfully created.
Definition: EditPage.php:59
Show an error when the user tries to do something whilst blocked.
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:1766
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:157
const RAW
Definition: Revision.php:56
static getForTitle(Title $title)
Returns the appropriate ContentHandler singleton for the given title.
getPreviewParserOptions()
Get parser options for a preview.
Definition: EditPage.php:4058
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 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:3784
bool int $contentLength
Definition: EditPage.php:439
const NS_MEDIAWIKI
Definition: Defines.php:68
showTextbox1( $customAttribs=null, $textoverride=null)
Method to output wpTextbox1 The $textoverride method can be used by subclasses overriding showContent...
Definition: EditPage.php:3410
incrementEditFailureStats( $failureType)
Definition: EditPage.php:4049
const AS_END
Status: WikiPage::doEdit() was unsuccessful.
Definition: EditPage.php:142
getSummaryPreview( $isSubjectPreview, $summary="")
Definition: EditPage.php:3340
makeTemplatesOnThisPageList(array $templates)
Wrapper around TemplatesOnThisPageFormatter to make a "templates on this page" list.
Definition: EditPage.php:3049
showPreview( $text)
Append preview output to OutputPage.
Definition: EditPage.php:3504
string $textbox1
Page content input field.
Definition: EditPage.php:340
CONTENT_MODEL_JAVASCRIPT
Allow users to upload files.
const DELETED_USER
Definition: LogPage.php:36
const DELETED_TEXT
Definition: Revision.php:46
importFormData(&$request)
This function collects the form data and uses it to populate various member variables.
Definition: EditPage.php:912
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:787
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:589
ParserOutput $mParserOutput
Definition: EditPage.php:306
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:315
string $sectiontitle
Definition: EditPage.php:376
string null $unicodeCheck
What the user submitted in the &#39;wpUnicodeCheck&#39; field.
Definition: EditPage.php:459
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 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
bool $minoredit
Definition: EditPage.php:329
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:444
static closeElement( $element)
Shortcut to close an XML element.
Definition: Xml.php:117
int $parentRevId
Revision ID the edit is based on, adjusted when an edit conflict is resolved.
Definition: EditPage.php:395
doPreviewParse(Content $content)
Parse the page for a preview.
Definition: EditPage.php:4081
static matchSpamRegex( $text)
Check given input text against $wgSpamRegex, and return the text of the first match.
Definition: EditPage.php:2469
string $action
Definition: EditPage.php:237
newTextConflictHelper( $submitButtonLabel)
Definition: EditPage.php:4572
bool $deletedSinceEdit
Definition: EditPage.php:249
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:2693
static matchSummarySpamRegex( $text)
Check given input text against $wgSummarySpamRegex, and return the text of the first match...
Definition: EditPage.php:2483
importContentFormData(&$request)
Subpage overridable method for extracting the page content data from the posted form to be placed in ...
Definition: EditPage.php:1132
const EDIT_NEW
Definition: Defines.php:148
Revision bool null $mBaseRevision
A revision object corresponding to $this->editRevId.
Definition: EditPage.php:312
newSectionSummary(&$sectionanchor=null)
Return the summary to be used for a new section.
Definition: EditPage.php:1821
const AS_RATE_LIMITED
Status: rate limiter for action &#39;edit&#39; was tripped.
Definition: EditPage.php:99
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:2452
static hasDifferencesOutsideMainSlot(Revision $a, Revision $b)
Helper method for checking whether two revisions have differences that go beyond the main slot...
Definition: WikiPage.php:1526
addNewLineAtEnd( $wikitext)
Definition: EditPage.php:4514
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:1754
const AS_MAX_ARTICLE_SIZE_EXCEEDED
Status: article is too big (> $wgMaxArticleSize), after merging in the new section.
Definition: EditPage.php:137
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:1606
getCurrentContent()
Get the current content of the page.
Definition: EditPage.php:1406
updateWatchlist()
Register the change of watch status.
Definition: EditPage.php:2378
static hidden( $name, $value, array $attribs=[])
Convenience function to produce an input element with type=hidden.
Definition: Html.php:797
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
string $hookError
Definition: EditPage.php:303
showEditTools()
Inserts optional text shown below edit and upload forms.
Definition: EditPage.php:3618
bool $preview
Definition: EditPage.php:323
const AS_IMAGE_REDIRECT_ANON
Status: anonymous user is not allowed to upload (User::isAllowed(&#39;upload&#39;) == false) ...
Definition: EditPage.php:152
getCheckboxesDefinition( $checked)
Return an array of checkbox definitions.
Definition: EditPage.php:4156
showStandardInputs(&$tabindex=2)
Definition: EditPage.php:3716
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:132
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:1568
getRedirectTarget()
If this page is a redirect, get its target.
Definition: WikiPage.php:995
bool $mTriedSave
Definition: EditPage.php:270
const CONTENT_MODEL_CSS
Definition: Defines.php:233
$mPreloadContent
Definition: EditPage.php:427
getContext()
Definition: EditPage.php:508
showConflict()
Show an edit conflict.
Definition: EditPage.php:3766
addTalkPageText()
Definition: EditPage.php:4406
bool $hasPresetSummary
Has a summary been preset using GET parameter &summary= ?
Definition: EditPage.php:309
getEditConflictHelper()
Definition: EditPage.php:4557
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:202
bool $diff
Definition: EditPage.php:326
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:116
addHTML( $text)
Append $text to the body HTML.
setEditConflictHelperFactory(callable $factory)
Set a factory function to create an EditConflictHelper.
Definition: EditPage.php:4549
string $starttime
Timestamp from the first time the edit form was rendered.
Definition: EditPage.php:381
string $formtype
Definition: EditPage.php:252
string $section
Definition: EditPage.php:373
getSummaryInputWidget( $summary="", $labelText=null, $inputAttrs=null)
Builds a standard summary input with a label.
Definition: EditPage.php:3281
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:1473
static makeInternalOrExternalUrl( $name)
If url string starts with http, consider as external URL, else internal.
Definition: Skin.php:1205
formatStatusErrors(Status $status)
Wrap status errors in an errorbox for increased visibility.
Definition: EditPage.php:1801
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:516
bool $mTokenOkExceptSuffix
Definition: EditPage.php:267
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:419
wfArrayDiff2( $a, $b)
Like array_diff( $a, $b ) except that it works with two-dimensional arrays.
const AS_CONFLICT_DETECTED
Status: (non-resolvable) edit conflict.
Definition: EditPage.php:121
isSupportedContentModel( $modelId)
Returns if the given content model is editable.
Definition: EditPage.php:554
setApiEditOverride( $enableOverride)
Allow editing of content that supports API direct editing, but not general direct editing...
Definition: EditPage.php:565
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition: User.php:594
$suppressIntro
Definition: EditPage.php:433
getOriginalContent(User $user)
Get the content of the wanted revision, without section extraction.
Definition: EditPage.php:1365
static newFromId( $id, $flags=0)
Load a page revision from a given revision ID number.
Definition: Revision.php:118
bool $selfRedirect
Definition: EditPage.php:294
bool $incompleteForm
Definition: EditPage.php:273
bool $edit
Definition: EditPage.php:436
const AS_READ_ONLY_PAGE_LOGGED
Status: this logged in user is not allowed to edit this page.
Definition: EditPage.php:89
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:180
const NS_USER_TALK
Definition: Defines.php:63
getSubmitButtonLabel()
Get the message key of the label for the button to save the page.
Definition: EditPage.php:4246
showTextbox( $text, $name, $customAttribs=[])
Definition: EditPage.php:3442
const AS_SELF_REDIRECT
Status: user tried to create self-redirect (redirect to the same article) and wpIgnoreSelfRedirect ==...
Definition: EditPage.php:169
setContentModel( $model)
Set a proposed content model for the page for permissions checking.
Definition: Title.php:1076
$editFormTextBeforeContent
Definition: EditPage.php:421
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:2633
null array $changeTags
Definition: EditPage.php:413
setTextboxes( $yourtext, $storedversion)
isWrongCaseUserConfigPage()
Checks whether the user entered a skin name in uppercase, e.g.
Definition: EditPage.php:881
Show an error when the user hits a rate limit.
static getEditToolbar()
Allow extensions to provide a toolbar.
Definition: EditPage.php:4127
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:1542
string $contentModel
Definition: EditPage.php:407
return true to allow those checks to and false if checking is done & $user
Definition: hooks.txt:1473
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:1546
string $editFormPageTop
Before even the preview.
Definition: EditPage.php:419
const AS_CHANGE_TAG_ERROR
Status: an error relating to change tagging.
Definition: EditPage.php:175
guessSectionName( $text)
Turns section name wikitext into anchors for use in HTTP redirects.
Definition: EditPage.php:4528
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:476
$editFormTextTop
Definition: EditPage.php:420
static showLogExtract(&$out, $types=[], $page='', $user='', $param=[])
Show log extract.
$matches
null $scrolltop
Definition: EditPage.php:401
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition: Title.php:319