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