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 
411  public $contentFormat = null;
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 = '';
428  public $mPreloadContent = null;
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  // Auto-block user's IP if the account was "hard" blocked
626  if ( !wfReadOnly() ) {
628  $this->context->getUser()->spreadAnyEditBlock();
629  } );
630  }
631  }
632  $this->displayPermissionsError( $permErrors );
633 
634  return;
635  }
636 
637  $revision = $this->mArticle->getRevisionFetched();
638  // Disallow editing revisions with content models different from the current one
639  // Undo edits being an exception in order to allow reverting content model changes.
640  if ( $revision
641  && $revision->getContentModel() !== $this->contentModel
642  ) {
643  $prevRev = null;
644  if ( $this->undidRev ) {
645  $undidRevObj = Revision::newFromId( $this->undidRev );
646  $prevRev = $undidRevObj ? $undidRevObj->getPrevious() : null;
647  }
648  if ( !$this->undidRev
649  || !$prevRev
650  || $prevRev->getContentModel() !== $this->contentModel
651  ) {
652  $this->displayViewSourcePage(
653  $this->getContentObject(),
654  $this->context->msg(
655  'contentmodelediterror',
656  $revision->getContentModel(),
658  )->plain()
659  );
660  return;
661  }
662  }
663 
664  $this->isConflict = false;
665 
666  # Show applicable editing introductions
667  if ( $this->formtype == 'initial' || $this->firsttime ) {
668  $this->showIntro();
669  }
670 
671  # Attempt submission here. This will check for edit conflicts,
672  # and redundantly check for locked database, blocked IPs, etc.
673  # that edit() already checked just in case someone tries to sneak
674  # in the back door with a hand-edited submission URL.
675 
676  if ( $this->formtype == 'save' ) {
677  $resultDetails = null;
678  $status = $this->attemptSave( $resultDetails );
679  if ( !$this->handleStatus( $status, $resultDetails ) ) {
680  return;
681  }
682  }
683 
684  # First time through: get contents, set time for conflict
685  # checking, etc.
686  if ( $this->formtype == 'initial' || $this->firsttime ) {
687  if ( $this->initialiseForm() === false ) {
688  return;
689  }
690 
691  if ( !$this->mTitle->getArticleID() ) {
692  Hooks::run( 'EditFormPreloadText', [ &$this->textbox1, &$this->mTitle ] );
693  } else {
694  Hooks::run( 'EditFormInitialText', [ $this ] );
695  }
696 
697  }
698 
699  $this->showEditForm();
700  }
701 
706  protected function getEditPermissionErrors( $rigor = 'secure' ) {
707  $user = $this->context->getUser();
708  $permErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $user, $rigor );
709  # Can this title be created?
710  if ( !$this->mTitle->exists() ) {
711  $permErrors = array_merge(
712  $permErrors,
713  wfArrayDiff2(
714  $this->mTitle->getUserPermissionsErrors( 'create', $user, $rigor ),
715  $permErrors
716  )
717  );
718  }
719  # Ignore some permissions errors when a user is just previewing/viewing diffs
720  $remove = [];
721  foreach ( $permErrors as $error ) {
722  if ( ( $this->preview || $this->diff )
723  && (
724  $error[0] == 'blockedtext' ||
725  $error[0] == 'autoblockedtext' ||
726  $error[0] == 'systemblockedtext'
727  )
728  ) {
729  $remove[] = $error;
730  }
731  }
732  $permErrors = wfArrayDiff2( $permErrors, $remove );
733 
734  return $permErrors;
735  }
736 
750  protected function displayPermissionsError( array $permErrors ) {
751  $out = $this->context->getOutput();
752  if ( $this->context->getRequest()->getBool( 'redlink' ) ) {
753  // The edit page was reached via a red link.
754  // Redirect to the article page and let them click the edit tab if
755  // they really want a permission error.
756  $out->redirect( $this->mTitle->getFullURL() );
757  return;
758  }
759 
760  $content = $this->getContentObject();
761 
762  # Use the normal message if there's nothing to display
763  if ( $this->firsttime && ( !$content || $content->isEmpty() ) ) {
764  $action = $this->mTitle->exists() ? 'edit' :
765  ( $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage' );
766  throw new PermissionsError( $action, $permErrors );
767  }
768 
769  $this->displayViewSourcePage(
770  $content,
771  $out->formatPermissionsErrorMessage( $permErrors, 'edit' )
772  );
773  }
774 
780  protected function displayViewSourcePage( Content $content, $errorMessage = '' ) {
781  $out = $this->context->getOutput();
782  Hooks::run( 'EditPage::showReadOnlyForm:initial', [ $this, &$out ] );
783 
784  $out->setRobotPolicy( 'noindex,nofollow' );
785  $out->setPageTitle( $this->context->msg(
786  'viewsource-title',
787  $this->getContextTitle()->getPrefixedText()
788  ) );
789  $out->addBacklinkSubtitle( $this->getContextTitle() );
790  $out->addHTML( $this->editFormPageTop );
791  $out->addHTML( $this->editFormTextTop );
792 
793  if ( $errorMessage !== '' ) {
794  $out->addWikiTextAsInterface( $errorMessage );
795  $out->addHTML( "<hr />\n" );
796  }
797 
798  # If the user made changes, preserve them when showing the markup
799  # (This happens when a user is blocked during edit, for instance)
800  if ( !$this->firsttime ) {
801  $text = $this->textbox1;
802  $out->addWikiMsg( 'viewyourtext' );
803  } else {
804  try {
805  $text = $this->toEditText( $content );
806  } catch ( MWException $e ) {
807  # Serialize using the default format if the content model is not supported
808  # (e.g. for an old revision with a different model)
809  $text = $content->serialize();
810  }
811  $out->addWikiMsg( 'viewsourcetext' );
812  }
813 
814  $out->addHTML( $this->editFormTextBeforeContent );
815  $this->showTextbox( $text, 'wpTextbox1', [ 'readonly' ] );
816  $out->addHTML( $this->editFormTextAfterContent );
817 
818  $out->addHTML( $this->makeTemplatesOnThisPageList( $this->getTemplates() ) );
819 
820  $out->addModules( 'mediawiki.action.edit.collapsibleFooter' );
821 
822  $out->addHTML( $this->editFormTextBottom );
823  if ( $this->mTitle->exists() ) {
824  $out->returnToMain( null, $this->mTitle );
825  }
826  }
827 
833  protected function previewOnOpen() {
834  $config = $this->context->getConfig();
835  $previewOnOpenNamespaces = $config->get( 'PreviewOnOpenNamespaces' );
836  $request = $this->context->getRequest();
837  if ( $config->get( 'RawHtml' ) ) {
838  // If raw HTML is enabled, disable preview on open
839  // since it has to be posted with a token for
840  // security reasons
841  return false;
842  }
843  if ( $request->getVal( 'preview' ) == 'yes' ) {
844  // Explicit override from request
845  return true;
846  } elseif ( $request->getVal( 'preview' ) == 'no' ) {
847  // Explicit override from request
848  return false;
849  } elseif ( $this->section == 'new' ) {
850  // Nothing *to* preview for new sections
851  return false;
852  } elseif ( ( $request->getCheck( 'preload' ) || $this->mTitle->exists() )
853  && $this->context->getUser()->getOption( 'previewonfirst' )
854  ) {
855  // Standard preference behavior
856  return true;
857  } elseif ( !$this->mTitle->exists()
858  && isset( $previewOnOpenNamespaces[$this->mTitle->getNamespace()] )
859  && $previewOnOpenNamespaces[$this->mTitle->getNamespace()]
860  ) {
861  // Categories are special
862  return true;
863  } else {
864  return false;
865  }
866  }
867 
874  protected function isWrongCaseUserConfigPage() {
875  if ( $this->mTitle->isUserConfigPage() ) {
876  $name = $this->mTitle->getSkinFromConfigSubpage();
877  $skins = array_merge(
878  array_keys( Skin::getSkinNames() ),
879  [ 'common' ]
880  );
881  return !in_array( $name, $skins )
882  && in_array( strtolower( $name ), $skins );
883  } else {
884  return false;
885  }
886  }
887 
895  protected function isSectionEditSupported() {
896  $contentHandler = ContentHandler::getForTitle( $this->mTitle );
897  return $contentHandler->supportsSections();
898  }
899 
905  public function importFormData( &$request ) {
906  # Section edit can come from either the form or a link
907  $this->section = $request->getVal( 'wpSection', $request->getVal( 'section' ) );
908 
909  if ( $this->section !== null && $this->section !== '' && !$this->isSectionEditSupported() ) {
910  throw new ErrorPageError( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
911  }
912 
913  $this->isNew = !$this->mTitle->exists() || $this->section == 'new';
914 
915  if ( $request->wasPosted() ) {
916  # These fields need to be checked for encoding.
917  # Also remove trailing whitespace, but don't remove _initial_
918  # whitespace from the text boxes. This may be significant formatting.
919  $this->textbox1 = rtrim( $request->getText( 'wpTextbox1' ) );
920  if ( !$request->getCheck( 'wpTextbox2' ) ) {
921  // Skip this if wpTextbox2 has input, it indicates that we came
922  // from a conflict page with raw page text, not a custom form
923  // modified by subclasses
924  $textbox1 = $this->importContentFormData( $request );
925  if ( $textbox1 !== null ) {
926  $this->textbox1 = $textbox1;
927  }
928  }
929 
930  $this->unicodeCheck = $request->getText( 'wpUnicodeCheck' );
931 
932  $this->summary = $request->getText( 'wpSummary' );
933 
934  # If the summary consists of a heading, e.g. '==Foobar==', extract the title from the
935  # header syntax, e.g. 'Foobar'. This is mainly an issue when we are using wpSummary for
936  # section titles.
937  $this->summary = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->summary );
938 
939  # Treat sectiontitle the same way as summary.
940  # Note that wpSectionTitle is not yet a part of the actual edit form, as wpSummary is
941  # currently doing double duty as both edit summary and section title. Right now this
942  # is just to allow API edits to work around this limitation, but this should be
943  # incorporated into the actual edit form when EditPage is rewritten (T20654, T28312).
944  $this->sectiontitle = $request->getText( 'wpSectionTitle' );
945  $this->sectiontitle = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->sectiontitle );
946 
947  $this->edittime = $request->getVal( 'wpEdittime' );
948  $this->editRevId = $request->getIntOrNull( 'editRevId' );
949  $this->starttime = $request->getVal( 'wpStarttime' );
950 
951  $undidRev = $request->getInt( 'wpUndidRevision' );
952  if ( $undidRev ) {
953  $this->undidRev = $undidRev;
954  }
955 
956  $this->scrolltop = $request->getIntOrNull( 'wpScrolltop' );
957 
958  if ( $this->textbox1 === '' && !$request->getCheck( 'wpTextbox1' ) ) {
959  // wpTextbox1 field is missing, possibly due to being "too big"
960  // according to some filter rules such as Suhosin's setting for
961  // suhosin.request.max_value_length (d'oh)
962  $this->incompleteForm = true;
963  } else {
964  // If we receive the last parameter of the request, we can fairly
965  // claim the POST request has not been truncated.
966  $this->incompleteForm = !$request->getVal( 'wpUltimateParam' );
967  }
968  if ( $this->incompleteForm ) {
969  # If the form is incomplete, force to preview.
970  wfDebug( __METHOD__ . ": Form data appears to be incomplete\n" );
971  wfDebug( "POST DATA: " . var_export( $request->getPostValues(), true ) . "\n" );
972  $this->preview = true;
973  } else {
974  $this->preview = $request->getCheck( 'wpPreview' );
975  $this->diff = $request->getCheck( 'wpDiff' );
976 
977  // Remember whether a save was requested, so we can indicate
978  // if we forced preview due to session failure.
979  $this->mTriedSave = !$this->preview;
980 
981  if ( $this->tokenOk( $request ) ) {
982  # Some browsers will not report any submit button
983  # if the user hits enter in the comment box.
984  # The unmarked state will be assumed to be a save,
985  # if the form seems otherwise complete.
986  wfDebug( __METHOD__ . ": Passed token check.\n" );
987  } elseif ( $this->diff ) {
988  # Failed token check, but only requested "Show Changes".
989  wfDebug( __METHOD__ . ": Failed token check; Show Changes requested.\n" );
990  } else {
991  # Page might be a hack attempt posted from
992  # an external site. Preview instead of saving.
993  wfDebug( __METHOD__ . ": Failed token check; forcing preview\n" );
994  $this->preview = true;
995  }
996  }
997  $this->save = !$this->preview && !$this->diff;
998  if ( !preg_match( '/^\d{14}$/', $this->edittime ) ) {
999  $this->edittime = null;
1000  }
1001 
1002  if ( !preg_match( '/^\d{14}$/', $this->starttime ) ) {
1003  $this->starttime = null;
1004  }
1005 
1006  $this->recreate = $request->getCheck( 'wpRecreate' );
1007 
1008  $this->minoredit = $request->getCheck( 'wpMinoredit' );
1009  $this->watchthis = $request->getCheck( 'wpWatchthis' );
1010 
1011  $user = $this->context->getUser();
1012  # Don't force edit summaries when a user is editing their own user or talk page
1013  if ( ( $this->mTitle->mNamespace == NS_USER || $this->mTitle->mNamespace == NS_USER_TALK )
1014  && $this->mTitle->getText() == $user->getName()
1015  ) {
1016  $this->allowBlankSummary = true;
1017  } else {
1018  $this->allowBlankSummary = $request->getBool( 'wpIgnoreBlankSummary' )
1019  || !$user->getOption( 'forceeditsummary' );
1020  }
1021 
1022  $this->autoSumm = $request->getText( 'wpAutoSummary' );
1023 
1024  $this->allowBlankArticle = $request->getBool( 'wpIgnoreBlankArticle' );
1025  $this->allowSelfRedirect = $request->getBool( 'wpIgnoreSelfRedirect' );
1026 
1027  $changeTags = $request->getVal( 'wpChangeTags' );
1028  if ( is_null( $changeTags ) || $changeTags === '' ) {
1029  $this->changeTags = [];
1030  } else {
1031  $this->changeTags = array_filter( array_map( 'trim', explode( ',',
1032  $changeTags ) ) );
1033  }
1034  } else {
1035  # Not a posted form? Start with nothing.
1036  wfDebug( __METHOD__ . ": Not a posted form.\n" );
1037  $this->textbox1 = '';
1038  $this->summary = '';
1039  $this->sectiontitle = '';
1040  $this->edittime = '';
1041  $this->editRevId = null;
1042  $this->starttime = wfTimestampNow();
1043  $this->edit = false;
1044  $this->preview = false;
1045  $this->save = false;
1046  $this->diff = false;
1047  $this->minoredit = false;
1048  // Watch may be overridden by request parameters
1049  $this->watchthis = $request->getBool( 'watchthis', false );
1050  $this->recreate = false;
1051 
1052  // When creating a new section, we can preload a section title by passing it as the
1053  // preloadtitle parameter in the URL (T15100)
1054  if ( $this->section == 'new' && $request->getVal( 'preloadtitle' ) ) {
1055  $this->sectiontitle = $request->getVal( 'preloadtitle' );
1056  // Once wpSummary isn't being use for setting section titles, we should delete this.
1057  $this->summary = $request->getVal( 'preloadtitle' );
1058  } elseif ( $this->section != 'new' && $request->getVal( 'summary' ) !== '' ) {
1059  $this->summary = $request->getText( 'summary' );
1060  if ( $this->summary !== '' ) {
1061  $this->hasPresetSummary = true;
1062  }
1063  }
1064 
1065  if ( $request->getVal( 'minor' ) ) {
1066  $this->minoredit = true;
1067  }
1068  }
1069 
1070  $this->oldid = $request->getInt( 'oldid' );
1071  $this->parentRevId = $request->getInt( 'parentRevId' );
1072 
1073  $this->bot = $request->getBool( 'bot', true );
1074  $this->nosummary = $request->getBool( 'nosummary' );
1075 
1076  // May be overridden by revision.
1077  $this->contentModel = $request->getText( 'model', $this->contentModel );
1078  // May be overridden by revision.
1079  $this->contentFormat = $request->getText( 'format', $this->contentFormat );
1080 
1081  try {
1082  $handler = ContentHandler::getForModelID( $this->contentModel );
1083  } catch ( MWUnknownContentModelException $e ) {
1084  throw new ErrorPageError(
1085  'editpage-invalidcontentmodel-title',
1086  'editpage-invalidcontentmodel-text',
1087  [ wfEscapeWikiText( $this->contentModel ) ]
1088  );
1089  }
1090 
1091  if ( !$handler->isSupportedFormat( $this->contentFormat ) ) {
1092  throw new ErrorPageError(
1093  'editpage-notsupportedcontentformat-title',
1094  'editpage-notsupportedcontentformat-text',
1095  [
1096  wfEscapeWikiText( $this->contentFormat ),
1097  wfEscapeWikiText( ContentHandler::getLocalizedName( $this->contentModel ) )
1098  ]
1099  );
1100  }
1101 
1108  $this->editintro = $request->getText( 'editintro',
1109  // Custom edit intro for new sections
1110  $this->section === 'new' ? 'MediaWiki:addsection-editintro' : '' );
1111 
1112  // Allow extensions to modify form data
1113  Hooks::run( 'EditPage::importFormData', [ $this, $request ] );
1114  }
1115 
1125  protected function importContentFormData( &$request ) {
1126  return null; // Don't do anything, EditPage already extracted wpTextbox1
1127  }
1128 
1134  public function initialiseForm() {
1135  $this->edittime = $this->page->getTimestamp();
1136  $this->editRevId = $this->page->getLatest();
1137 
1138  $content = $this->getContentObject( false ); # TODO: track content object?!
1139  if ( $content === false ) {
1140  $out = $this->context->getOutput();
1141  if ( $out->getRedirect() === '' ) { // mcrundo hack redirects, don't override it
1142  $this->noSuchSectionPage();
1143  }
1144  return false;
1145  }
1146 
1147  if ( !$this->isSupportedContentModel( $content->getModel() ) ) {
1148  $modelMsg = $this->getContext()->msg( 'content-model-' . $content->getModel() );
1149  $modelName = $modelMsg->exists() ? $modelMsg->text() : $content->getModel();
1150 
1151  $out = $this->context->getOutput();
1152  $out->showErrorPage(
1153  'modeleditnotsupported-title',
1154  'modeleditnotsupported-text',
1155  [ $modelName ]
1156  );
1157  return false;
1158  }
1159 
1160  $this->textbox1 = $this->toEditText( $content );
1161 
1162  $user = $this->context->getUser();
1163  // activate checkboxes if user wants them to be always active
1164  # Sort out the "watch" checkbox
1165  if ( $user->getOption( 'watchdefault' ) ) {
1166  # Watch all edits
1167  $this->watchthis = true;
1168  } elseif ( $user->getOption( 'watchcreations' ) && !$this->mTitle->exists() ) {
1169  # Watch creations
1170  $this->watchthis = true;
1171  } elseif ( $user->isWatched( $this->mTitle ) ) {
1172  # Already watched
1173  $this->watchthis = true;
1174  }
1175  if ( $user->getOption( 'minordefault' ) && !$this->isNew ) {
1176  $this->minoredit = true;
1177  }
1178  if ( $this->textbox1 === false ) {
1179  return false;
1180  }
1181  return true;
1182  }
1183 
1191  protected function getContentObject( $def_content = null ) {
1192  global $wgDisableAnonTalk;
1193 
1194  $content = false;
1195 
1196  $user = $this->context->getUser();
1197  $request = $this->context->getRequest();
1198  // For message page not locally set, use the i18n message.
1199  // For other non-existent articles, use preload text if any.
1200  if ( !$this->mTitle->exists() || $this->section == 'new' ) {
1201  if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && $this->section != 'new' ) {
1202  # If this is a system message, get the default text.
1203  $msg = $this->mTitle->getDefaultMessageText();
1204 
1205  $content = $this->toEditContent( $msg );
1206  }
1207  if ( $content === false ) {
1208  # If requested, preload some text.
1209  $preload = $request->getVal( 'preload',
1210  // Custom preload text for new sections
1211  $this->section === 'new' ? 'MediaWiki:addsection-preload' : '' );
1212  $params = $request->getArray( 'preloadparams', [] );
1213 
1214  $content = $this->getPreloadedContent( $preload, $params );
1215  }
1216  // For existing pages, get text based on "undo" or section parameters.
1217  } elseif ( $this->section != '' ) {
1218  // Get section edit text (returns $def_text for invalid sections)
1219  $orig = $this->getOriginalContent( $user );
1220  $content = $orig ? $orig->getSection( $this->section ) : null;
1221 
1222  if ( !$content ) {
1223  $content = $def_content;
1224  }
1225  } else {
1226  $undoafter = $request->getInt( 'undoafter' );
1227  $undo = $request->getInt( 'undo' );
1228 
1229  if ( $undo > 0 && $undoafter > 0 ) {
1230  $undorev = Revision::newFromId( $undo );
1231  $oldrev = Revision::newFromId( $undoafter );
1232  $undoMsg = null;
1233 
1234  # Sanity check, make sure it's the right page,
1235  # the revisions exist and they were not deleted.
1236  # Otherwise, $content will be left as-is.
1237  if ( !is_null( $undorev ) && !is_null( $oldrev ) &&
1238  !$undorev->isDeleted( RevisionRecord::DELETED_TEXT ) &&
1239  !$oldrev->isDeleted( RevisionRecord::DELETED_TEXT )
1240  ) {
1241  if ( WikiPage::hasDifferencesOutsideMainSlot( $undorev, $oldrev )
1242  || !$this->isSupportedContentModel( $oldrev->getContentModel() )
1243  ) {
1244  // Hack for undo while EditPage can't handle multi-slot editing
1245  $this->context->getOutput()->redirect( $this->mTitle->getFullURL( [
1246  'action' => 'mcrundo',
1247  'undo' => $undo,
1248  'undoafter' => $undoafter,
1249  ] ) );
1250  return false;
1251  } else {
1252  $content = $this->page->getUndoContent( $undorev, $oldrev );
1253 
1254  if ( $content === false ) {
1255  # Warn the user that something went wrong
1256  $undoMsg = 'failure';
1257  }
1258  }
1259 
1260  if ( $undoMsg === null ) {
1261  $oldContent = $this->page->getContent( RevisionRecord::RAW );
1263  $user, MediaWikiServices::getInstance()->getContentLanguage() );
1264  $newContent = $content->preSaveTransform( $this->mTitle, $user, $popts );
1265  if ( $newContent->getModel() !== $oldContent->getModel() ) {
1266  // The undo may change content
1267  // model if its reverting the top
1268  // edit. This can result in
1269  // mismatched content model/format.
1270  $this->contentModel = $newContent->getModel();
1271  $this->contentFormat = $oldrev->getContentFormat();
1272  }
1273 
1274  if ( $newContent->equals( $oldContent ) ) {
1275  # Tell the user that the undo results in no change,
1276  # i.e. the revisions were already undone.
1277  $undoMsg = 'nochange';
1278  $content = false;
1279  } else {
1280  # Inform the user of our success and set an automatic edit summary
1281  $undoMsg = 'success';
1282 
1283  # If we just undid one rev, use an autosummary
1284  $firstrev = $oldrev->getNext();
1285  if ( $firstrev && $firstrev->getId() == $undo ) {
1286  $userText = $undorev->getUserText();
1287  if ( $userText === '' ) {
1288  $undoSummary = $this->context->msg(
1289  'undo-summary-username-hidden',
1290  $undo
1291  )->inContentLanguage()->text();
1292  } else {
1293  $undoMessage = ( $undorev->getUser() === 0 && $wgDisableAnonTalk ) ?
1294  'undo-summary-anon' :
1295  'undo-summary';
1296  $undoSummary = $this->context->msg(
1297  $undoMessage,
1298  $undo,
1299  $userText
1300  )->inContentLanguage()->text();
1301  }
1302  if ( $this->summary === '' ) {
1303  $this->summary = $undoSummary;
1304  } else {
1305  $this->summary = $undoSummary . $this->context->msg( 'colon-separator' )
1306  ->inContentLanguage()->text() . $this->summary;
1307  }
1308  $this->undidRev = $undo;
1309  }
1310  $this->formtype = 'diff';
1311  }
1312  }
1313  } else {
1314  // Failed basic sanity checks.
1315  // Older revisions may have been removed since the link
1316  // was created, or we may simply have got bogus input.
1317  $undoMsg = 'norev';
1318  }
1319 
1320  $out = $this->context->getOutput();
1321  // Messages: undo-success, undo-failure, undo-main-slot-only, undo-norev,
1322  // undo-nochange.
1323  $class = ( $undoMsg == 'success' ? '' : 'error ' ) . "mw-undo-{$undoMsg}";
1324  $this->editFormPageTop .= Html::rawElement(
1325  'div', [ 'class' => $class ],
1326  $out->parseAsInterface(
1327  $this->context->msg( 'undo-' . $undoMsg )->plain()
1328  )
1329  );
1330  }
1331 
1332  if ( $content === false ) {
1333  // Hack for restoring old revisions while EditPage
1334  // can't handle multi-slot editing.
1335 
1336  $curRevision = $this->page->getRevision();
1337  $oldRevision = $this->mArticle->getRevisionFetched();
1338 
1339  if ( $curRevision
1340  && $oldRevision
1341  && $curRevision->getId() !== $oldRevision->getId()
1342  && ( WikiPage::hasDifferencesOutsideMainSlot( $oldRevision, $curRevision )
1343  || !$this->isSupportedContentModel( $oldRevision->getContentModel() ) )
1344  ) {
1345  $this->context->getOutput()->redirect(
1346  $this->mTitle->getFullURL(
1347  [
1348  'action' => 'mcrrestore',
1349  'restore' => $oldRevision->getId(),
1350  ]
1351  )
1352  );
1353 
1354  return false;
1355  }
1356  }
1357 
1358  if ( $content === false ) {
1359  $content = $this->getOriginalContent( $user );
1360  }
1361  }
1362 
1363  return $content;
1364  }
1365 
1381  private function getOriginalContent( User $user ) {
1382  if ( $this->section == 'new' ) {
1383  return $this->getCurrentContent();
1384  }
1385  $revision = $this->mArticle->getRevisionFetched();
1386  if ( $revision === null ) {
1387  $handler = ContentHandler::getForModelID( $this->contentModel );
1388  return $handler->makeEmptyContent();
1389  }
1390  $content = $revision->getContent( RevisionRecord::FOR_THIS_USER, $user );
1391  return $content;
1392  }
1393 
1406  public function getParentRevId() {
1407  if ( $this->parentRevId ) {
1408  return $this->parentRevId;
1409  } else {
1410  return $this->mArticle->getRevIdFetched();
1411  }
1412  }
1413 
1422  protected function getCurrentContent() {
1423  $rev = $this->page->getRevision();
1424  $content = $rev ? $rev->getContent( RevisionRecord::RAW ) : null;
1425 
1426  if ( $content === false || $content === null ) {
1427  $handler = ContentHandler::getForModelID( $this->contentModel );
1428  return $handler->makeEmptyContent();
1429  } elseif ( !$this->undidRev ) {
1430  // Content models should always be the same since we error
1431  // out if they are different before this point (in ->edit()).
1432  // The exception being, during an undo, the current revision might
1433  // differ from the prior revision.
1434  $logger = LoggerFactory::getInstance( 'editpage' );
1435  if ( $this->contentModel !== $rev->getContentModel() ) {
1436  $logger->warning( "Overriding content model from current edit {prev} to {new}", [
1437  'prev' => $this->contentModel,
1438  'new' => $rev->getContentModel(),
1439  'title' => $this->getTitle()->getPrefixedDBkey(),
1440  'method' => __METHOD__
1441  ] );
1442  $this->contentModel = $rev->getContentModel();
1443  }
1444 
1445  // Given that the content models should match, the current selected
1446  // format should be supported.
1447  if ( !$content->isSupportedFormat( $this->contentFormat ) ) {
1448  $logger->warning( "Current revision content format unsupported. Overriding {prev} to {new}", [
1449 
1450  'prev' => $this->contentFormat,
1451  'new' => $rev->getContentFormat(),
1452  'title' => $this->getTitle()->getPrefixedDBkey(),
1453  'method' => __METHOD__
1454  ] );
1455  $this->contentFormat = $rev->getContentFormat();
1456  }
1457  }
1458  return $content;
1459  }
1460 
1468  public function setPreloadedContent( Content $content ) {
1469  $this->mPreloadContent = $content;
1470  }
1471 
1483  protected function getPreloadedContent( $preload, $params = [] ) {
1484  if ( !empty( $this->mPreloadContent ) ) {
1485  return $this->mPreloadContent;
1486  }
1487 
1488  $handler = ContentHandler::getForModelID( $this->contentModel );
1489 
1490  if ( $preload === '' ) {
1491  return $handler->makeEmptyContent();
1492  }
1493 
1494  $user = $this->context->getUser();
1495  $title = Title::newFromText( $preload );
1496 
1497  # Check for existence to avoid getting MediaWiki:Noarticletext
1498  if ( !$this->isPageExistingAndViewable( $title, $user ) ) {
1499  // TODO: somehow show a warning to the user!
1500  return $handler->makeEmptyContent();
1501  }
1502 
1504  if ( $page->isRedirect() ) {
1506  # Same as before
1507  if ( !$this->isPageExistingAndViewable( $title, $user ) ) {
1508  // TODO: somehow show a warning to the user!
1509  return $handler->makeEmptyContent();
1510  }
1512  }
1513 
1514  $parserOptions = ParserOptions::newFromUser( $user );
1515  $content = $page->getContent( RevisionRecord::RAW );
1516 
1517  if ( !$content ) {
1518  // TODO: somehow show a warning to the user!
1519  return $handler->makeEmptyContent();
1520  }
1521 
1522  if ( $content->getModel() !== $handler->getModelID() ) {
1523  $converted = $content->convert( $handler->getModelID() );
1524 
1525  if ( !$converted ) {
1526  // TODO: somehow show a warning to the user!
1527  wfDebug( "Attempt to preload incompatible content: " .
1528  "can't convert " . $content->getModel() .
1529  " to " . $handler->getModelID() );
1530 
1531  return $handler->makeEmptyContent();
1532  }
1533 
1534  $content = $converted;
1535  }
1536 
1537  return $content->preloadTransform( $title, $parserOptions, $params );
1538  }
1539 
1549  private function isPageExistingAndViewable( $title, User $user ) {
1550  $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
1551 
1552  return $title && $title->exists() && $permissionManager->userCan( 'read', $user, $title );
1553  }
1554 
1562  public function tokenOk( &$request ) {
1563  $token = $request->getVal( 'wpEditToken' );
1564  $user = $this->context->getUser();
1565  $this->mTokenOk = $user->matchEditToken( $token );
1566  $this->mTokenOkExceptSuffix = $user->matchEditTokenNoSuffix( $token );
1567  return $this->mTokenOk;
1568  }
1569 
1584  protected function setPostEditCookie( $statusValue ) {
1585  $revisionId = $this->page->getLatest();
1586  $postEditKey = self::POST_EDIT_COOKIE_KEY_PREFIX . $revisionId;
1587 
1588  $val = 'saved';
1589  if ( $statusValue == self::AS_SUCCESS_NEW_ARTICLE ) {
1590  $val = 'created';
1591  } elseif ( $this->oldid ) {
1592  $val = 'restored';
1593  }
1594 
1595  $response = $this->context->getRequest()->response();
1596  $response->setCookie( $postEditKey, $val, time() + self::POST_EDIT_COOKIE_DURATION );
1597  }
1598 
1605  public function attemptSave( &$resultDetails = false ) {
1606  // TODO: MCR: treat $this->minoredit like $this->bot and check isAllowed( 'minoredit' )!
1607  // Also, add $this->autopatrol like $this->bot and check isAllowed( 'autopatrol' )!
1608  // This is needed since PageUpdater no longer checks these rights!
1609 
1610  // Allow bots to exempt some edits from bot flagging
1611  $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
1612  $bot = $permissionManager->userHasRight( $this->context->getUser(), 'bot' ) && $this->bot;
1613  $status = $this->internalAttemptSave( $resultDetails, $bot );
1614 
1615  Hooks::run( 'EditPage::attemptSave:after', [ $this, $status, $resultDetails ] );
1616 
1617  return $status;
1618  }
1619 
1623  private function incrementResolvedConflicts() {
1624  if ( $this->context->getRequest()->getText( 'mode' ) !== 'conflict' ) {
1625  return;
1626  }
1627 
1628  $this->getEditConflictHelper()->incrementResolvedStats();
1629  }
1630 
1640  private function handleStatus( Status $status, $resultDetails ) {
1645  if ( $status->value == self::AS_SUCCESS_UPDATE
1646  || $status->value == self::AS_SUCCESS_NEW_ARTICLE
1647  ) {
1648  $this->incrementResolvedConflicts();
1649 
1650  $this->didSave = true;
1651  if ( !$resultDetails['nullEdit'] ) {
1652  $this->setPostEditCookie( $status->value );
1653  }
1654  }
1655 
1656  $out = $this->context->getOutput();
1657 
1658  // "wpExtraQueryRedirect" is a hidden input to modify
1659  // after save URL and is not used by actual edit form
1660  $request = $this->context->getRequest();
1661  $extraQueryRedirect = $request->getVal( 'wpExtraQueryRedirect' );
1662 
1663  switch ( $status->value ) {
1664  case self::AS_HOOK_ERROR_EXPECTED:
1665  case self::AS_CONTENT_TOO_BIG:
1666  case self::AS_ARTICLE_WAS_DELETED:
1667  case self::AS_CONFLICT_DETECTED:
1668  case self::AS_SUMMARY_NEEDED:
1669  case self::AS_TEXTBOX_EMPTY:
1670  case self::AS_MAX_ARTICLE_SIZE_EXCEEDED:
1671  case self::AS_END:
1672  case self::AS_BLANK_ARTICLE:
1673  case self::AS_SELF_REDIRECT:
1674  return true;
1675 
1676  case self::AS_HOOK_ERROR:
1677  return false;
1678 
1679  case self::AS_CANNOT_USE_CUSTOM_MODEL:
1680  case self::AS_PARSE_ERROR:
1681  case self::AS_UNICODE_NOT_SUPPORTED:
1682  $out->wrapWikiTextAsInterface( 'error',
1683  $status->getWikiText( false, false, $this->context->getLanguage() )
1684  );
1685  return true;
1686 
1687  case self::AS_SUCCESS_NEW_ARTICLE:
1688  $query = $resultDetails['redirect'] ? 'redirect=no' : '';
1689  if ( $extraQueryRedirect ) {
1690  if ( $query !== '' ) {
1691  $query .= '&';
1692  }
1693  $query .= $extraQueryRedirect;
1694  }
1695  $anchor = $resultDetails['sectionanchor'] ?? '';
1696  $out->redirect( $this->mTitle->getFullURL( $query ) . $anchor );
1697  return false;
1698 
1699  case self::AS_SUCCESS_UPDATE:
1700  $extraQuery = '';
1701  $sectionanchor = $resultDetails['sectionanchor'];
1702 
1703  // Give extensions a chance to modify URL query on update
1704  Hooks::run(
1705  'ArticleUpdateBeforeRedirect',
1706  [ $this->mArticle, &$sectionanchor, &$extraQuery ]
1707  );
1708 
1709  if ( $resultDetails['redirect'] ) {
1710  if ( $extraQuery !== '' ) {
1711  $extraQuery = '&' . $extraQuery;
1712  }
1713  $extraQuery = 'redirect=no' . $extraQuery;
1714  }
1715  if ( $extraQueryRedirect ) {
1716  if ( $extraQuery !== '' ) {
1717  $extraQuery .= '&';
1718  }
1719  $extraQuery .= $extraQueryRedirect;
1720  }
1721 
1722  $out->redirect( $this->mTitle->getFullURL( $extraQuery ) . $sectionanchor );
1723  return false;
1724 
1725  case self::AS_SPAM_ERROR:
1726  $this->spamPageWithContent( $resultDetails['spam'] );
1727  return false;
1728 
1729  case self::AS_BLOCKED_PAGE_FOR_USER:
1730  throw new UserBlockedError( $this->context->getUser()->getBlock() );
1731 
1732  case self::AS_IMAGE_REDIRECT_ANON:
1733  case self::AS_IMAGE_REDIRECT_LOGGED:
1734  throw new PermissionsError( 'upload' );
1735 
1736  case self::AS_READ_ONLY_PAGE_ANON:
1737  case self::AS_READ_ONLY_PAGE_LOGGED:
1738  throw new PermissionsError( 'edit' );
1739 
1740  case self::AS_READ_ONLY_PAGE:
1741  throw new ReadOnlyError;
1742 
1743  case self::AS_RATE_LIMITED:
1744  throw new ThrottledError();
1745 
1746  case self::AS_NO_CREATE_PERMISSION:
1747  $permission = $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage';
1748  throw new PermissionsError( $permission );
1749 
1750  case self::AS_NO_CHANGE_CONTENT_MODEL:
1751  throw new PermissionsError( 'editcontentmodel' );
1752 
1753  default:
1754  // We don't recognize $status->value. The only way that can happen
1755  // is if an extension hook aborted from inside ArticleSave.
1756  // Render the status object into $this->hookError
1757  // FIXME this sucks, we should just use the Status object throughout
1758  $this->hookError = '<div class="error">' . "\n" .
1759  $status->getWikiText( false, false, $this->context->getLanguage() ) .
1760  '</div>';
1761  return true;
1762  }
1763  }
1764 
1774  protected function runPostMergeFilters( Content $content, Status $status, User $user ) {
1775  // Run old style post-section-merge edit filter
1776  if ( $this->hookError != '' ) {
1777  # ...or the hook could be expecting us to produce an error
1778  $status->fatal( 'hookaborted' );
1779  $status->value = self::AS_HOOK_ERROR_EXPECTED;
1780  return false;
1781  }
1782 
1783  // Run new style post-section-merge edit filter
1784  if ( !Hooks::run( 'EditFilterMergedContent',
1785  [ $this->context, $content, $status, $this->summary,
1786  $user, $this->minoredit ] )
1787  ) {
1788  # Error messages etc. could be handled within the hook...
1789  if ( $status->isGood() ) {
1790  $status->fatal( 'hookaborted' );
1791  // Not setting $this->hookError here is a hack to allow the hook
1792  // to cause a return to the edit page without $this->hookError
1793  // being set. This is used by ConfirmEdit to display a captcha
1794  // without any error message cruft.
1795  } else {
1796  $this->hookError = $this->formatStatusErrors( $status );
1797  }
1798  // Use the existing $status->value if the hook set it
1799  if ( !$status->value ) {
1800  $status->value = self::AS_HOOK_ERROR;
1801  }
1802  return false;
1803  } elseif ( !$status->isOK() ) {
1804  # ...or the hook could be expecting us to produce an error
1805  // FIXME this sucks, we should just use the Status object throughout
1806  if ( !$status->getErrors() ) {
1807  // Provide a fallback error message if none was set
1808  $status->fatal( 'hookaborted' );
1809  }
1810  $this->hookError = $this->formatStatusErrors( $status );
1811  $status->value = self::AS_HOOK_ERROR_EXPECTED;
1812  return false;
1813  }
1814 
1815  return true;
1816  }
1817 
1824  private function formatStatusErrors( Status $status ) {
1825  $errmsg = $status->getWikiText(
1826  'edit-error-short',
1827  'edit-error-long',
1828  $this->context->getLanguage()
1829  );
1830  return <<<ERROR
1831 <div class="errorbox">
1832 {$errmsg}
1833 </div>
1834 <br clear="all" />
1835 ERROR;
1836  }
1837 
1844  private function newSectionSummary( &$sectionanchor = null ) {
1845  if ( $this->sectiontitle !== '' ) {
1846  $sectionanchor = $this->guessSectionName( $this->sectiontitle );
1847  // If no edit summary was specified, create one automatically from the section
1848  // title and have it link to the new section. Otherwise, respect the summary as
1849  // passed.
1850  if ( $this->summary === '' ) {
1851  $cleanSectionTitle = MediaWikiServices::getInstance()->getParser()
1852  ->stripSectionName( $this->sectiontitle );
1853  return $this->context->msg( 'newsectionsummary' )
1854  ->plaintextParams( $cleanSectionTitle )->inContentLanguage()->text();
1855  }
1856  } elseif ( $this->summary !== '' ) {
1857  $sectionanchor = $this->guessSectionName( $this->summary );
1858  # This is a new section, so create a link to the new section
1859  # in the revision summary.
1860  $cleanSummary = MediaWikiServices::getInstance()->getParser()
1861  ->stripSectionName( $this->summary );
1862  return $this->context->msg( 'newsectionsummary' )
1863  ->plaintextParams( $cleanSummary )->inContentLanguage()->text();
1864  }
1865  return $this->summary;
1866  }
1867 
1892  public function internalAttemptSave( &$result, $bot = false ) {
1893  $status = Status::newGood();
1894  $user = $this->context->getUser();
1895  $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
1896 
1897  if ( !Hooks::run( 'EditPage::attemptSave', [ $this ] ) ) {
1898  wfDebug( "Hook 'EditPage::attemptSave' aborted article saving\n" );
1899  $status->fatal( 'hookaborted' );
1900  $status->value = self::AS_HOOK_ERROR;
1901  return $status;
1902  }
1903 
1904  if ( $this->unicodeCheck !== self::UNICODE_CHECK ) {
1905  $status->fatal( 'unicode-support-fail' );
1906  $status->value = self::AS_UNICODE_NOT_SUPPORTED;
1907  return $status;
1908  }
1909 
1910  $request = $this->context->getRequest();
1911  $spam = $request->getText( 'wpAntispam' );
1912  if ( $spam !== '' ) {
1913  wfDebugLog(
1914  'SimpleAntiSpam',
1915  $user->getName() .
1916  ' editing "' .
1917  $this->mTitle->getPrefixedText() .
1918  '" submitted bogus field "' .
1919  $spam .
1920  '"'
1921  );
1922  $status->fatal( 'spamprotectionmatch', false );
1923  $status->value = self::AS_SPAM_ERROR;
1924  return $status;
1925  }
1926 
1927  try {
1928  # Construct Content object
1929  $textbox_content = $this->toEditContent( $this->textbox1 );
1930  } catch ( MWContentSerializationException $ex ) {
1931  $status->fatal(
1932  'content-failed-to-parse',
1933  $this->contentModel,
1934  $this->contentFormat,
1935  $ex->getMessage()
1936  );
1937  $status->value = self::AS_PARSE_ERROR;
1938  return $status;
1939  }
1940 
1941  # Check image redirect
1942  if ( $this->mTitle->getNamespace() == NS_FILE &&
1943  $textbox_content->isRedirect() &&
1944  !$permissionManager->userHasRight( $user, 'upload' )
1945  ) {
1946  $code = $user->isAnon() ? self::AS_IMAGE_REDIRECT_ANON : self::AS_IMAGE_REDIRECT_LOGGED;
1947  $status->setResult( false, $code );
1948 
1949  return $status;
1950  }
1951 
1952  # Check for spam
1953  $match = self::matchSummarySpamRegex( $this->summary );
1954  if ( $match === false && $this->section == 'new' ) {
1955  # $wgSpamRegex is enforced on this new heading/summary because, unlike
1956  # regular summaries, it is added to the actual wikitext.
1957  if ( $this->sectiontitle !== '' ) {
1958  # This branch is taken when the API is used with the 'sectiontitle' parameter.
1959  $match = self::matchSpamRegex( $this->sectiontitle );
1960  } else {
1961  # This branch is taken when the "Add Topic" user interface is used, or the API
1962  # is used with the 'summary' parameter.
1963  $match = self::matchSpamRegex( $this->summary );
1964  }
1965  }
1966  if ( $match === false ) {
1967  $match = self::matchSpamRegex( $this->textbox1 );
1968  }
1969  if ( $match !== false ) {
1970  $result['spam'] = $match;
1971  $ip = $request->getIP();
1972  $pdbk = $this->mTitle->getPrefixedDBkey();
1973  $match = str_replace( "\n", '', $match );
1974  wfDebugLog( 'SpamRegex', "$ip spam regex hit [[$pdbk]]: \"$match\"" );
1975  $status->fatal( 'spamprotectionmatch', $match );
1976  $status->value = self::AS_SPAM_ERROR;
1977  return $status;
1978  }
1979  if ( !Hooks::run(
1980  'EditFilter',
1981  [ $this, $this->textbox1, $this->section, &$this->hookError, $this->summary ] )
1982  ) {
1983  # Error messages etc. could be handled within the hook...
1984  $status->fatal( 'hookaborted' );
1985  $status->value = self::AS_HOOK_ERROR;
1986  return $status;
1987  } elseif ( $this->hookError != '' ) {
1988  # ...or the hook could be expecting us to produce an error
1989  $status->fatal( 'hookaborted' );
1990  $status->value = self::AS_HOOK_ERROR_EXPECTED;
1991  return $status;
1992  }
1993 
1994  if ( $permissionManager->isBlockedFrom( $user, $this->mTitle ) ) {
1995  // Auto-block user's IP if the account was "hard" blocked
1996  if ( !wfReadOnly() ) {
1997  $user->spreadAnyEditBlock();
1998  }
1999  # Check block state against master, thus 'false'.
2000  $status->setResult( false, self::AS_BLOCKED_PAGE_FOR_USER );
2001  return $status;
2002  }
2003 
2004  $this->contentLength = strlen( $this->textbox1 );
2005  $config = $this->context->getConfig();
2006  $maxArticleSize = $config->get( 'MaxArticleSize' );
2007  if ( $this->contentLength > $maxArticleSize * 1024 ) {
2008  // Error will be displayed by showEditForm()
2009  $this->tooBig = true;
2010  $status->setResult( false, self::AS_CONTENT_TOO_BIG );
2011  return $status;
2012  }
2013 
2014  if ( !$permissionManager->userHasRight( $user, 'edit' ) ) {
2015  if ( $user->isAnon() ) {
2016  $status->setResult( false, self::AS_READ_ONLY_PAGE_ANON );
2017  return $status;
2018  } else {
2019  $status->fatal( 'readonlytext' );
2020  $status->value = self::AS_READ_ONLY_PAGE_LOGGED;
2021  return $status;
2022  }
2023  }
2024 
2025  $changingContentModel = false;
2026  if ( $this->contentModel !== $this->mTitle->getContentModel() ) {
2027  if ( !$config->get( 'ContentHandlerUseDB' ) ) {
2028  $status->fatal( 'editpage-cannot-use-custom-model' );
2029  $status->value = self::AS_CANNOT_USE_CUSTOM_MODEL;
2030  return $status;
2031  } elseif ( !$permissionManager->userHasRight( $user, 'editcontentmodel' ) ) {
2032  $status->setResult( false, self::AS_NO_CHANGE_CONTENT_MODEL );
2033  return $status;
2034  }
2035  // Make sure the user can edit the page under the new content model too
2036  $titleWithNewContentModel = clone $this->mTitle;
2037  $titleWithNewContentModel->setContentModel( $this->contentModel );
2038 
2039  $canEditModel = $permissionManager->userCan(
2040  'editcontentmodel',
2041  $user,
2042  $titleWithNewContentModel
2043  );
2044 
2045  if (
2046  !$canEditModel
2047  || !$permissionManager->userCan( 'edit', $user, $titleWithNewContentModel )
2048  ) {
2049  $status->setResult( false, self::AS_NO_CHANGE_CONTENT_MODEL );
2050 
2051  return $status;
2052  }
2053 
2054  $changingContentModel = true;
2055  $oldContentModel = $this->mTitle->getContentModel();
2056  }
2057 
2058  if ( $this->changeTags ) {
2059  $changeTagsStatus = ChangeTags::canAddTagsAccompanyingChange(
2060  $this->changeTags, $user );
2061  if ( !$changeTagsStatus->isOK() ) {
2062  $changeTagsStatus->value = self::AS_CHANGE_TAG_ERROR;
2063  return $changeTagsStatus;
2064  }
2065  }
2066 
2067  if ( wfReadOnly() ) {
2068  $status->fatal( 'readonlytext' );
2069  $status->value = self::AS_READ_ONLY_PAGE;
2070  return $status;
2071  }
2072  if ( $user->pingLimiter() || $user->pingLimiter( 'linkpurge', 0 )
2073  || ( $changingContentModel && $user->pingLimiter( 'editcontentmodel' ) )
2074  ) {
2075  $status->fatal( 'actionthrottledtext' );
2076  $status->value = self::AS_RATE_LIMITED;
2077  return $status;
2078  }
2079 
2080  # If the article has been deleted while editing, don't save it without
2081  # confirmation
2082  if ( $this->wasDeletedSinceLastEdit() && !$this->recreate ) {
2083  $status->setResult( false, self::AS_ARTICLE_WAS_DELETED );
2084  return $status;
2085  }
2086 
2087  # Load the page data from the master. If anything changes in the meantime,
2088  # we detect it by using page_latest like a token in a 1 try compare-and-swap.
2089  $this->page->loadPageData( 'fromdbmaster' );
2090  $new = !$this->page->exists();
2091 
2092  if ( $new ) {
2093  // Late check for create permission, just in case *PARANOIA*
2094  if ( !$permissionManager->userCan( 'create', $user, $this->mTitle ) ) {
2095  $status->fatal( 'nocreatetext' );
2096  $status->value = self::AS_NO_CREATE_PERMISSION;
2097  wfDebug( __METHOD__ . ": no create permission\n" );
2098  return $status;
2099  }
2100 
2101  // Don't save a new page if it's blank or if it's a MediaWiki:
2102  // message with content equivalent to default (allow empty pages
2103  // in this case to disable messages, see T52124)
2104  $defaultMessageText = $this->mTitle->getDefaultMessageText();
2105  if ( $this->mTitle->getNamespace() === NS_MEDIAWIKI && $defaultMessageText !== false ) {
2106  $defaultText = $defaultMessageText;
2107  } else {
2108  $defaultText = '';
2109  }
2110 
2111  if ( !$this->allowBlankArticle && $this->textbox1 === $defaultText ) {
2112  $this->blankArticle = true;
2113  $status->fatal( 'blankarticle' );
2114  $status->setResult( false, self::AS_BLANK_ARTICLE );
2115  return $status;
2116  }
2117 
2118  if ( !$this->runPostMergeFilters( $textbox_content, $status, $user ) ) {
2119  return $status;
2120  }
2121 
2122  $content = $textbox_content;
2123 
2124  $result['sectionanchor'] = '';
2125  if ( $this->section == 'new' ) {
2126  if ( $this->sectiontitle !== '' ) {
2127  // Insert the section title above the content.
2128  $content = $content->addSectionHeader( $this->sectiontitle );
2129  } elseif ( $this->summary !== '' ) {
2130  // Insert the section title above the content.
2131  $content = $content->addSectionHeader( $this->summary );
2132  }
2133  $this->summary = $this->newSectionSummary( $result['sectionanchor'] );
2134  }
2135 
2136  $status->value = self::AS_SUCCESS_NEW_ARTICLE;
2137 
2138  } else { # not $new
2139 
2140  # Article exists. Check for edit conflict.
2141 
2142  $this->page->clear(); # Force reload of dates, etc.
2143  $timestamp = $this->page->getTimestamp();
2144  $latest = $this->page->getLatest();
2145 
2146  wfDebug( "timestamp: {$timestamp}, edittime: {$this->edittime}\n" );
2147 
2148  // An edit conflict is detected if the current revision is different from the
2149  // revision that was current when editing was initiated on the client.
2150  // This is checked based on the timestamp and revision ID.
2151  // TODO: the timestamp based check can probably go away now.
2152  if ( $timestamp != $this->edittime
2153  || ( $this->editRevId !== null && $this->editRevId != $latest )
2154  ) {
2155  $this->isConflict = true;
2156  if ( $this->section == 'new' ) {
2157  if ( $this->page->getUserText() == $user->getName() &&
2158  $this->page->getComment() == $this->newSectionSummary()
2159  ) {
2160  // Probably a duplicate submission of a new comment.
2161  // This can happen when CDN resends a request after
2162  // a timeout but the first one actually went through.
2163  wfDebug( __METHOD__
2164  . ": duplicate new section submission; trigger edit conflict!\n" );
2165  } else {
2166  // New comment; suppress conflict.
2167  $this->isConflict = false;
2168  wfDebug( __METHOD__ . ": conflict suppressed; new section\n" );
2169  }
2170  } elseif ( $this->section == ''
2172  DB_MASTER, $this->mTitle->getArticleID(),
2173  $user->getId(), $this->edittime
2174  )
2175  ) {
2176  # Suppress edit conflict with self, except for section edits where merging is required.
2177  wfDebug( __METHOD__ . ": Suppressing edit conflict, same user.\n" );
2178  $this->isConflict = false;
2179  }
2180  }
2181 
2182  // If sectiontitle is set, use it, otherwise use the summary as the section title.
2183  if ( $this->sectiontitle !== '' ) {
2184  $sectionTitle = $this->sectiontitle;
2185  } else {
2186  $sectionTitle = $this->summary;
2187  }
2188 
2189  $content = null;
2190 
2191  if ( $this->isConflict ) {
2192  wfDebug( __METHOD__
2193  . ": conflict! getting section '{$this->section}' for time '{$this->edittime}'"
2194  . " (id '{$this->editRevId}') (article time '{$timestamp}')\n" );
2195  // @TODO: replaceSectionAtRev() with base ID (not prior current) for ?oldid=X case
2196  // ...or disable section editing for non-current revisions (not exposed anyway).
2197  if ( $this->editRevId !== null ) {
2198  $content = $this->page->replaceSectionAtRev(
2199  $this->section,
2200  $textbox_content,
2201  $sectionTitle,
2202  $this->editRevId
2203  );
2204  } else {
2205  $content = $this->page->replaceSectionContent(
2206  $this->section,
2207  $textbox_content,
2208  $sectionTitle,
2209  $this->edittime
2210  );
2211  }
2212  } else {
2213  wfDebug( __METHOD__ . ": getting section '{$this->section}'\n" );
2214  $content = $this->page->replaceSectionContent(
2215  $this->section,
2216  $textbox_content,
2217  $sectionTitle
2218  );
2219  }
2220 
2221  if ( is_null( $content ) ) {
2222  wfDebug( __METHOD__ . ": activating conflict; section replace failed.\n" );
2223  $this->isConflict = true;
2224  $content = $textbox_content; // do not try to merge here!
2225  } elseif ( $this->isConflict ) {
2226  # Attempt merge
2227  if ( $this->mergeChangesIntoContent( $content ) ) {
2228  // Successful merge! Maybe we should tell the user the good news?
2229  $this->isConflict = false;
2230  wfDebug( __METHOD__ . ": Suppressing edit conflict, successful merge.\n" );
2231  } else {
2232  $this->section = '';
2233  $this->textbox1 = ContentHandler::getContentText( $content );
2234  wfDebug( __METHOD__ . ": Keeping edit conflict, failed merge.\n" );
2235  }
2236  }
2237 
2238  if ( $this->isConflict ) {
2239  $status->setResult( false, self::AS_CONFLICT_DETECTED );
2240  return $status;
2241  }
2242 
2243  if ( !$this->runPostMergeFilters( $content, $status, $user ) ) {
2244  return $status;
2245  }
2246 
2247  if ( $this->section == 'new' ) {
2248  // Handle the user preference to force summaries here
2249  if ( !$this->allowBlankSummary && trim( $this->summary ) == '' ) {
2250  $this->missingSummary = true;
2251  $status->fatal( 'missingsummary' ); // or 'missingcommentheader' if $section == 'new'. Blegh
2252  $status->value = self::AS_SUMMARY_NEEDED;
2253  return $status;
2254  }
2255 
2256  // Do not allow the user to post an empty comment
2257  if ( $this->textbox1 == '' ) {
2258  $this->missingComment = true;
2259  $status->fatal( 'missingcommenttext' );
2260  $status->value = self::AS_TEXTBOX_EMPTY;
2261  return $status;
2262  }
2263  } elseif ( !$this->allowBlankSummary
2264  && !$content->equals( $this->getOriginalContent( $user ) )
2265  && !$content->isRedirect()
2266  && md5( $this->summary ) == $this->autoSumm
2267  ) {
2268  $this->missingSummary = true;
2269  $status->fatal( 'missingsummary' );
2270  $status->value = self::AS_SUMMARY_NEEDED;
2271  return $status;
2272  }
2273 
2274  # All's well
2275  $sectionanchor = '';
2276  if ( $this->section == 'new' ) {
2277  $this->summary = $this->newSectionSummary( $sectionanchor );
2278  } elseif ( $this->section != '' ) {
2279  # Try to get a section anchor from the section source, redirect
2280  # to edited section if header found.
2281  # XXX: Might be better to integrate this into Article::replaceSectionAtRev
2282  # for duplicate heading checking and maybe parsing.
2283  $hasmatch = preg_match( "/^ *([=]{1,6})(.*?)(\\1) *\\n/i", $this->textbox1, $matches );
2284  # We can't deal with anchors, includes, html etc in the header for now,
2285  # headline would need to be parsed to improve this.
2286  if ( $hasmatch && strlen( $matches[2] ) > 0 ) {
2287  $sectionanchor = $this->guessSectionName( $matches[2] );
2288  }
2289  }
2290  $result['sectionanchor'] = $sectionanchor;
2291 
2292  // Save errors may fall down to the edit form, but we've now
2293  // merged the section into full text. Clear the section field
2294  // so that later submission of conflict forms won't try to
2295  // replace that into a duplicated mess.
2296  $this->textbox1 = $this->toEditText( $content );
2297  $this->section = '';
2298 
2299  $status->value = self::AS_SUCCESS_UPDATE;
2300  }
2301 
2302  if ( !$this->allowSelfRedirect
2303  && $content->isRedirect()
2304  && $content->getRedirectTarget()->equals( $this->getTitle() )
2305  ) {
2306  // If the page already redirects to itself, don't warn.
2307  $currentTarget = $this->getCurrentContent()->getRedirectTarget();
2308  if ( !$currentTarget || !$currentTarget->equals( $this->getTitle() ) ) {
2309  $this->selfRedirect = true;
2310  $status->fatal( 'selfredirect' );
2311  $status->value = self::AS_SELF_REDIRECT;
2312  return $status;
2313  }
2314  }
2315 
2316  // Check for length errors again now that the section is merged in
2317  $this->contentLength = strlen( $this->toEditText( $content ) );
2318  if ( $this->contentLength > $maxArticleSize * 1024 ) {
2319  $this->tooBig = true;
2320  $status->setResult( false, self::AS_MAX_ARTICLE_SIZE_EXCEEDED );
2321  return $status;
2322  }
2323 
2324  $flags = EDIT_AUTOSUMMARY |
2325  ( $new ? EDIT_NEW : EDIT_UPDATE ) |
2326  ( ( $this->minoredit && !$this->isNew ) ? EDIT_MINOR : 0 ) |
2327  ( $bot ? EDIT_FORCE_BOT : 0 );
2328 
2329  $doEditStatus = $this->page->doEditContent(
2330  $content,
2331  $this->summary,
2332  $flags,
2333  false,
2334  $user,
2335  $content->getDefaultFormat(),
2338  );
2339 
2340  if ( !$doEditStatus->isOK() ) {
2341  // Failure from doEdit()
2342  // Show the edit conflict page for certain recognized errors from doEdit(),
2343  // but don't show it for errors from extension hooks
2344  $errors = $doEditStatus->getErrorsArray();
2345  if ( in_array( $errors[0][0],
2346  [ 'edit-gone-missing', 'edit-conflict', 'edit-already-exists' ] )
2347  ) {
2348  $this->isConflict = true;
2349  // Destroys data doEdit() put in $status->value but who cares
2350  $doEditStatus->value = self::AS_END;
2351  }
2352  return $doEditStatus;
2353  }
2354 
2355  $result['nullEdit'] = $doEditStatus->hasMessage( 'edit-no-change' );
2356  if ( $result['nullEdit'] ) {
2357  // We don't know if it was a null edit until now, so increment here
2358  $user->pingLimiter( 'linkpurge' );
2359  }
2360  $result['redirect'] = $content->isRedirect();
2361 
2362  $this->updateWatchlist();
2363 
2364  // If the content model changed, add a log entry
2365  if ( $changingContentModel ) {
2367  $user,
2368  $new ? false : $oldContentModel,
2369  $this->contentModel,
2370  $this->summary
2371  );
2372  }
2373 
2374  return $status;
2375  }
2376 
2383  protected function addContentModelChangeLogEntry( User $user, $oldModel, $newModel, $reason ) {
2384  $new = $oldModel === false;
2385  $log = new ManualLogEntry( 'contentmodel', $new ? 'new' : 'change' );
2386  $log->setPerformer( $user );
2387  $log->setTarget( $this->mTitle );
2388  $log->setComment( $reason );
2389  $log->setParameters( [
2390  '4::oldmodel' => $oldModel,
2391  '5::newmodel' => $newModel
2392  ] );
2393  $logid = $log->insert();
2394  $log->publish( $logid );
2395  }
2396 
2400  protected function updateWatchlist() {
2401  $user = $this->context->getUser();
2402  if ( !$user->isLoggedIn() ) {
2403  return;
2404  }
2405 
2407  $watch = $this->watchthis;
2408  // Do this in its own transaction to reduce contention...
2409  DeferredUpdates::addCallableUpdate( function () use ( $user, $title, $watch ) {
2410  if ( $watch == $user->isWatched( $title, User::IGNORE_USER_RIGHTS ) ) {
2411  return; // nothing to change
2412  }
2413  WatchAction::doWatchOrUnwatch( $watch, $title, $user );
2414  } );
2415  }
2416 
2428  private function mergeChangesIntoContent( &$editContent ) {
2429  $db = wfGetDB( DB_MASTER );
2430 
2431  // This is the revision that was current at the time editing was initiated on the client,
2432  // even if the edit was based on an old revision.
2433  $baseRevision = $this->getBaseRevision();
2434  $baseContent = $baseRevision ? $baseRevision->getContent() : null;
2435 
2436  if ( is_null( $baseContent ) ) {
2437  return false;
2438  }
2439 
2440  // The current state, we want to merge updates into it
2441  $currentRevision = Revision::loadFromTitle( $db, $this->mTitle );
2442  $currentContent = $currentRevision ? $currentRevision->getContent() : null;
2443 
2444  if ( is_null( $currentContent ) ) {
2445  return false;
2446  }
2447 
2448  $handler = ContentHandler::getForModelID( $baseContent->getModel() );
2449 
2450  $result = $handler->merge3( $baseContent, $editContent, $currentContent );
2451 
2452  if ( $result ) {
2453  $editContent = $result;
2454  // Update parentRevId to what we just merged.
2455  $this->parentRevId = $currentRevision->getId();
2456  return true;
2457  }
2458 
2459  return false;
2460  }
2461 
2474  public function getBaseRevision() {
2475  if ( !$this->mBaseRevision ) {
2476  $db = wfGetDB( DB_MASTER );
2477  $this->mBaseRevision = $this->editRevId
2478  ? Revision::newFromId( $this->editRevId, Revision::READ_LATEST )
2479  : Revision::loadFromTimestamp( $db, $this->mTitle, $this->edittime );
2480  }
2481  return $this->mBaseRevision;
2482  }
2483 
2491  public static function matchSpamRegex( $text ) {
2492  global $wgSpamRegex;
2493  // For back compatibility, $wgSpamRegex may be a single string or an array of regexes.
2494  $regexes = (array)$wgSpamRegex;
2495  return self::matchSpamRegexInternal( $text, $regexes );
2496  }
2497 
2505  public static function matchSummarySpamRegex( $text ) {
2506  global $wgSummarySpamRegex;
2507  $regexes = (array)$wgSummarySpamRegex;
2508  return self::matchSpamRegexInternal( $text, $regexes );
2509  }
2510 
2516  protected static function matchSpamRegexInternal( $text, $regexes ) {
2517  foreach ( $regexes as $regex ) {
2518  $matches = [];
2519  if ( preg_match( $regex, $text, $matches ) ) {
2520  return $matches[0];
2521  }
2522  }
2523  return false;
2524  }
2525 
2526  public function setHeaders() {
2527  $out = $this->context->getOutput();
2528 
2529  $out->addModules( 'mediawiki.action.edit' );
2530  $out->addModuleStyles( 'mediawiki.action.edit.styles' );
2531  $out->addModuleStyles( 'mediawiki.editfont.styles' );
2532 
2533  $user = $this->context->getUser();
2534 
2535  if ( $user->getOption( 'uselivepreview' ) ) {
2536  $out->addModules( 'mediawiki.action.edit.preview' );
2537  }
2538 
2539  if ( $user->getOption( 'useeditwarning' ) ) {
2540  $out->addModules( 'mediawiki.action.edit.editWarning' );
2541  }
2542 
2543  # Enabled article-related sidebar, toplinks, etc.
2544  $out->setArticleRelated( true );
2545 
2546  $contextTitle = $this->getContextTitle();
2547  if ( $this->isConflict ) {
2548  $msg = 'editconflict';
2549  } elseif ( $contextTitle->exists() && $this->section != '' ) {
2550  $msg = $this->section == 'new' ? 'editingcomment' : 'editingsection';
2551  } else {
2552  $msg = $contextTitle->exists()
2553  || ( $contextTitle->getNamespace() == NS_MEDIAWIKI
2554  && $contextTitle->getDefaultMessageText() !== false
2555  )
2556  ? 'editing'
2557  : 'creating';
2558  }
2559 
2560  # Use the title defined by DISPLAYTITLE magic word when present
2561  # NOTE: getDisplayTitle() returns HTML while getPrefixedText() returns plain text.
2562  # setPageTitle() treats the input as wikitext, which should be safe in either case.
2563  $displayTitle = isset( $this->mParserOutput ) ? $this->mParserOutput->getDisplayTitle() : false;
2564  if ( $displayTitle === false ) {
2565  $displayTitle = $contextTitle->getPrefixedText();
2566  } else {
2567  $out->setDisplayTitle( $displayTitle );
2568  }
2569  $out->setPageTitle( $this->context->msg( $msg, $displayTitle ) );
2570 
2571  $config = $this->context->getConfig();
2572 
2573  # Transmit the name of the message to JavaScript for live preview
2574  # Keep Resources.php/mediawiki.action.edit.preview in sync with the possible keys
2575  $out->addJsConfigVars( [
2576  'wgEditMessage' => $msg,
2577  'wgAjaxEditStash' => $config->get( 'AjaxEditStash' ),
2578  ] );
2579 
2580  // Add whether to use 'save' or 'publish' messages to JavaScript for post-edit, other
2581  // editors, etc.
2582  $out->addJsConfigVars(
2583  'wgEditSubmitButtonLabelPublish',
2584  $config->get( 'EditSubmitButtonLabelPublish' )
2585  );
2586  }
2587 
2591  protected function showIntro() {
2592  if ( $this->suppressIntro ) {
2593  return;
2594  }
2595 
2596  $out = $this->context->getOutput();
2597  $namespace = $this->mTitle->getNamespace();
2598 
2599  if ( $namespace == NS_MEDIAWIKI ) {
2600  # Show a warning if editing an interface message
2601  $out->wrapWikiMsg( "<div class='mw-editinginterface'>\n$1\n</div>", 'editinginterface' );
2602  # If this is a default message (but not css, json, or js),
2603  # show a hint that it is translatable on translatewiki.net
2604  if (
2605  !$this->mTitle->hasContentModel( CONTENT_MODEL_CSS )
2606  && !$this->mTitle->hasContentModel( CONTENT_MODEL_JSON )
2607  && !$this->mTitle->hasContentModel( CONTENT_MODEL_JAVASCRIPT )
2608  ) {
2609  $defaultMessageText = $this->mTitle->getDefaultMessageText();
2610  if ( $defaultMessageText !== false ) {
2611  $out->wrapWikiMsg( "<div class='mw-translateinterface'>\n$1\n</div>",
2612  'translateinterface' );
2613  }
2614  }
2615  } elseif ( $namespace == NS_FILE ) {
2616  # Show a hint to shared repo
2617  $file = MediaWikiServices::getInstance()->getRepoGroup()->findFile( $this->mTitle );
2618  if ( $file && !$file->isLocal() ) {
2619  $descUrl = $file->getDescriptionUrl();
2620  # there must be a description url to show a hint to shared repo
2621  if ( $descUrl ) {
2622  if ( !$this->mTitle->exists() ) {
2623  $out->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-create\">\n$1\n</div>", [
2624  'sharedupload-desc-create', $file->getRepo()->getDisplayName(), $descUrl
2625  ] );
2626  } else {
2627  $out->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-edit\">\n$1\n</div>", [
2628  'sharedupload-desc-edit', $file->getRepo()->getDisplayName(), $descUrl
2629  ] );
2630  }
2631  }
2632  }
2633  }
2634 
2635  # Show a warning message when someone creates/edits a user (talk) page but the user does not exist
2636  # Show log extract when the user is currently blocked
2637  if ( $namespace == NS_USER || $namespace == NS_USER_TALK ) {
2638  $username = explode( '/', $this->mTitle->getText(), 2 )[0];
2639  $user = User::newFromName( $username, false /* allow IP users */ );
2640  $ip = User::isIP( $username );
2641  $block = DatabaseBlock::newFromTarget( $user, $user );
2642  if ( !( $user && $user->isLoggedIn() ) && !$ip ) { # User does not exist
2643  $out->wrapWikiMsg( "<div class=\"mw-userpage-userdoesnotexist error\">\n$1\n</div>",
2644  [ 'userpage-userdoesnotexist', wfEscapeWikiText( $username ) ] );
2645  } elseif (
2646  !is_null( $block ) &&
2647  $block->getType() != DatabaseBlock::TYPE_AUTO &&
2648  ( $block->isSitewide() || $user->isBlockedFrom( $this->mTitle ) )
2649  ) {
2650  // Show log extract if the user is sitewide blocked or is partially
2651  // blocked and not allowed to edit their user page or user talk page
2653  $out,
2654  'block',
2655  MediaWikiServices::getInstance()->getNamespaceInfo()->
2656  getCanonicalName( NS_USER ) . ':' . $block->getTarget(),
2657  '',
2658  [
2659  'lim' => 1,
2660  'showIfEmpty' => false,
2661  'msgKey' => [
2662  'blocked-notice-logextract',
2663  $user->getName() # Support GENDER in notice
2664  ]
2665  ]
2666  );
2667  }
2668  }
2669  # Try to add a custom edit intro, or use the standard one if this is not possible.
2670  if ( !$this->showCustomIntro() && !$this->mTitle->exists() ) {
2672  $this->context->msg( 'helppage' )->inContentLanguage()->text()
2673  ) );
2674  if ( $this->context->getUser()->isLoggedIn() ) {
2675  $out->wrapWikiMsg(
2676  // Suppress the external link icon, consider the help url an internal one
2677  "<div class=\"mw-newarticletext plainlinks\">\n$1\n</div>",
2678  [
2679  'newarticletext',
2680  $helpLink
2681  ]
2682  );
2683  } else {
2684  $out->wrapWikiMsg(
2685  // Suppress the external link icon, consider the help url an internal one
2686  "<div class=\"mw-newarticletextanon plainlinks\">\n$1\n</div>",
2687  [
2688  'newarticletextanon',
2689  $helpLink
2690  ]
2691  );
2692  }
2693  }
2694  # Give a notice if the user is editing a deleted/moved page...
2695  if ( !$this->mTitle->exists() ) {
2696  $dbr = wfGetDB( DB_REPLICA );
2697 
2698  LogEventsList::showLogExtract( $out, [ 'delete', 'move' ], $this->mTitle,
2699  '',
2700  [
2701  'lim' => 10,
2702  'conds' => [ 'log_action != ' . $dbr->addQuotes( 'revision' ) ],
2703  'showIfEmpty' => false,
2704  'msgKey' => [ 'recreate-moveddeleted-warn' ]
2705  ]
2706  );
2707  }
2708  }
2709 
2715  protected function showCustomIntro() {
2716  if ( $this->editintro ) {
2717  $title = Title::newFromText( $this->editintro );
2718  if ( $this->isPageExistingAndViewable( $title, $this->context->getUser() ) ) {
2719  // Added using template syntax, to take <noinclude>'s into account.
2720  $this->context->getOutput()->addWikiTextAsContent(
2721  '<div class="mw-editintro">{{:' . $title->getFullText() . '}}</div>',
2722  /*linestart*/true,
2724  );
2725  return true;
2726  }
2727  }
2728  return false;
2729  }
2730 
2749  protected function toEditText( $content ) {
2750  if ( $content === null || $content === false || is_string( $content ) ) {
2751  return $content;
2752  }
2753 
2754  if ( !$this->isSupportedContentModel( $content->getModel() ) ) {
2755  throw new MWException( 'This content model is not supported: ' . $content->getModel() );
2756  }
2757 
2758  return $content->serialize( $this->contentFormat );
2759  }
2760 
2777  protected function toEditContent( $text ) {
2778  if ( $text === false || $text === null ) {
2779  return $text;
2780  }
2781 
2782  $content = ContentHandler::makeContent( $text, $this->getTitle(),
2783  $this->contentModel, $this->contentFormat );
2784 
2785  if ( !$this->isSupportedContentModel( $content->getModel() ) ) {
2786  throw new MWException( 'This content model is not supported: ' . $content->getModel() );
2787  }
2788 
2789  return $content;
2790  }
2791 
2800  public function showEditForm( $formCallback = null ) {
2801  # need to parse the preview early so that we know which templates are used,
2802  # otherwise users with "show preview after edit box" will get a blank list
2803  # we parse this near the beginning so that setHeaders can do the title
2804  # setting work instead of leaving it in getPreviewText
2805  $previewOutput = '';
2806  if ( $this->formtype == 'preview' ) {
2807  $previewOutput = $this->getPreviewText();
2808  }
2809 
2810  $out = $this->context->getOutput();
2811 
2812  // Avoid PHP 7.1 warning of passing $this by reference
2813  $editPage = $this;
2814  Hooks::run( 'EditPage::showEditForm:initial', [ &$editPage, &$out ] );
2815 
2816  $this->setHeaders();
2817 
2818  $this->addTalkPageText();
2819  $this->addEditNotices();
2820 
2821  if ( !$this->isConflict &&
2822  $this->section != '' &&
2823  !$this->isSectionEditSupported() ) {
2824  // We use $this->section to much before this and getVal('wgSection') directly in other places
2825  // at this point we can't reset $this->section to '' to fallback to non-section editing.
2826  // Someone is welcome to try refactoring though
2827  $out->showErrorPage( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
2828  return;
2829  }
2830 
2831  $this->showHeader();
2832 
2833  $out->addHTML( $this->editFormPageTop );
2834 
2835  $user = $this->context->getUser();
2836  if ( $user->getOption( 'previewontop' ) ) {
2837  $this->displayPreviewArea( $previewOutput, true );
2838  }
2839 
2840  $out->addHTML( $this->editFormTextTop );
2841 
2842  if ( $this->wasDeletedSinceLastEdit() && $this->formtype !== 'save' ) {
2843  $out->wrapWikiMsg( "<div class='error mw-deleted-while-editing'>\n$1\n</div>",
2844  'deletedwhileediting' );
2845  }
2846 
2847  // @todo add EditForm plugin interface and use it here!
2848  // search for textarea1 and textarea2, and allow EditForm to override all uses.
2849  $out->addHTML( Html::openElement(
2850  'form',
2851  [
2852  'class' => 'mw-editform',
2853  'id' => self::EDITFORM_ID,
2854  'name' => self::EDITFORM_ID,
2855  'method' => 'post',
2856  'action' => $this->getActionURL( $this->getContextTitle() ),
2857  'enctype' => 'multipart/form-data'
2858  ]
2859  ) );
2860 
2861  if ( is_callable( $formCallback ) ) {
2862  wfWarn( 'The $formCallback parameter to ' . __METHOD__ . 'is deprecated' );
2863  call_user_func_array( $formCallback, [ &$out ] );
2864  }
2865 
2866  // Add a check for Unicode support
2867  $out->addHTML( Html::hidden( 'wpUnicodeCheck', self::UNICODE_CHECK ) );
2868 
2869  // Add an empty field to trip up spambots
2870  $out->addHTML(
2871  Xml::openElement( 'div', [ 'id' => 'antispam-container', 'style' => 'display: none;' ] )
2872  . Html::rawElement(
2873  'label',
2874  [ 'for' => 'wpAntispam' ],
2875  $this->context->msg( 'simpleantispam-label' )->parse()
2876  )
2877  . Xml::element(
2878  'input',
2879  [
2880  'type' => 'text',
2881  'name' => 'wpAntispam',
2882  'id' => 'wpAntispam',
2883  'value' => ''
2884  ]
2885  )
2886  . Xml::closeElement( 'div' )
2887  );
2888 
2889  // Avoid PHP 7.1 warning of passing $this by reference
2890  $editPage = $this;
2891  Hooks::run( 'EditPage::showEditForm:fields', [ &$editPage, &$out ] );
2892 
2893  // Put these up at the top to ensure they aren't lost on early form submission
2894  $this->showFormBeforeText();
2895 
2896  if ( $this->wasDeletedSinceLastEdit() && $this->formtype == 'save' ) {
2897  $username = $this->lastDelete->user_name;
2898  $comment = CommentStore::getStore()
2899  ->getComment( 'log_comment', $this->lastDelete )->text;
2900 
2901  // It is better to not parse the comment at all than to have templates expanded in the middle
2902  // TODO: can the checkLabel be moved outside of the div so that wrapWikiMsg could be used?
2903  $key = $comment === ''
2904  ? 'confirmrecreate-noreason'
2905  : 'confirmrecreate';
2906  $out->addHTML(
2907  '<div class="mw-confirm-recreate">' .
2908  $this->context->msg( $key, $username, "<nowiki>$comment</nowiki>" )->parse() .
2909  Xml::checkLabel( $this->context->msg( 'recreate' )->text(), 'wpRecreate', 'wpRecreate', false,
2910  [ 'title' => Linker::titleAttrib( 'recreate' ), 'tabindex' => 1, 'id' => 'wpRecreate' ]
2911  ) .
2912  '</div>'
2913  );
2914  }
2915 
2916  # When the summary is hidden, also hide them on preview/show changes
2917  if ( $this->nosummary ) {
2918  $out->addHTML( Html::hidden( 'nosummary', true ) );
2919  }
2920 
2921  # If a blank edit summary was previously provided, and the appropriate
2922  # user preference is active, pass a hidden tag as wpIgnoreBlankSummary. This will stop the
2923  # user being bounced back more than once in the event that a summary
2924  # is not required.
2925  # ####
2926  # For a bit more sophisticated detection of blank summaries, hash the
2927  # automatic one and pass that in the hidden field wpAutoSummary.
2928  if ( $this->missingSummary || ( $this->section == 'new' && $this->nosummary ) ) {
2929  $out->addHTML( Html::hidden( 'wpIgnoreBlankSummary', true ) );
2930  }
2931 
2932  if ( $this->undidRev ) {
2933  $out->addHTML( Html::hidden( 'wpUndidRevision', $this->undidRev ) );
2934  }
2935 
2936  if ( $this->selfRedirect ) {
2937  $out->addHTML( Html::hidden( 'wpIgnoreSelfRedirect', true ) );
2938  }
2939 
2940  if ( $this->hasPresetSummary ) {
2941  // If a summary has been preset using &summary= we don't want to prompt for
2942  // a different summary. Only prompt for a summary if the summary is blanked.
2943  // (T19416)
2944  $this->autoSumm = md5( '' );
2945  }
2946 
2947  $autosumm = $this->autoSumm !== '' ? $this->autoSumm : md5( $this->summary );
2948  $out->addHTML( Html::hidden( 'wpAutoSummary', $autosumm ) );
2949 
2950  $out->addHTML( Html::hidden( 'oldid', $this->oldid ) );
2951  $out->addHTML( Html::hidden( 'parentRevId', $this->getParentRevId() ) );
2952 
2953  $out->addHTML( Html::hidden( 'format', $this->contentFormat ) );
2954  $out->addHTML( Html::hidden( 'model', $this->contentModel ) );
2955 
2956  $out->enableOOUI();
2957 
2958  if ( $this->section == 'new' ) {
2959  $this->showSummaryInput( true, $this->summary );
2960  $out->addHTML( $this->getSummaryPreview( true, $this->summary ) );
2961  }
2962 
2963  $out->addHTML( $this->editFormTextBeforeContent );
2964  if ( $this->isConflict ) {
2965  // In an edit conflict, we turn textbox2 into the user's text,
2966  // and textbox1 into the stored version
2967  $this->textbox2 = $this->textbox1;
2968 
2969  $content = $this->getCurrentContent();
2970  $this->textbox1 = $this->toEditText( $content );
2971 
2973  $editConflictHelper->setTextboxes( $this->textbox2, $this->textbox1 );
2974  $editConflictHelper->setContentModel( $this->contentModel );
2975  $editConflictHelper->setContentFormat( $this->contentFormat );
2977  }
2978 
2979  if ( !$this->mTitle->isUserConfigPage() ) {
2980  $out->addHTML( self::getEditToolbar() );
2981  }
2982 
2983  if ( $this->blankArticle ) {
2984  $out->addHTML( Html::hidden( 'wpIgnoreBlankArticle', true ) );
2985  }
2986 
2987  if ( $this->isConflict ) {
2988  // In an edit conflict bypass the overridable content form method
2989  // and fallback to the raw wpTextbox1 since editconflicts can't be
2990  // resolved between page source edits and custom ui edits using the
2991  // custom edit ui.
2992  $conflictTextBoxAttribs = [];
2993  if ( $this->wasDeletedSinceLastEdit() ) {
2994  $conflictTextBoxAttribs['style'] = 'display:none;';
2995  } elseif ( $this->isOldRev ) {
2996  $conflictTextBoxAttribs['class'] = 'mw-textarea-oldrev';
2997  }
2998 
2999  $out->addHTML( $editConflictHelper->getEditConflictMainTextBox( $conflictTextBoxAttribs ) );
3001  } else {
3002  $this->showContentForm();
3003  }
3004 
3005  $out->addHTML( $this->editFormTextAfterContent );
3006 
3007  $this->showStandardInputs();
3008 
3009  $this->showFormAfterText();
3010 
3011  $this->showTosSummary();
3012 
3013  $this->showEditTools();
3014 
3015  $out->addHTML( $this->editFormTextAfterTools . "\n" );
3016 
3017  $out->addHTML( $this->makeTemplatesOnThisPageList( $this->getTemplates() ) );
3018 
3019  $out->addHTML( Html::rawElement( 'div', [ 'class' => 'hiddencats' ],
3020  Linker::formatHiddenCategories( $this->page->getHiddenCategories() ) ) );
3021 
3022  $out->addHTML( Html::rawElement( 'div', [ 'class' => 'limitreport' ],
3023  self::getPreviewLimitReport( $this->mParserOutput ) ) );
3024 
3025  $out->addModules( 'mediawiki.action.edit.collapsibleFooter' );
3026 
3027  if ( $this->isConflict ) {
3028  try {
3029  $this->showConflict();
3030  } catch ( MWContentSerializationException $ex ) {
3031  // this can't really happen, but be nice if it does.
3032  $msg = $this->context->msg(
3033  'content-failed-to-parse',
3034  $this->contentModel,
3035  $this->contentFormat,
3036  $ex->getMessage()
3037  );
3038  $out->wrapWikiTextAsInterface( 'error', $msg->plain() );
3039  }
3040  }
3041 
3042  // Set a hidden field so JS knows what edit form mode we are in
3043  if ( $this->isConflict ) {
3044  $mode = 'conflict';
3045  } elseif ( $this->preview ) {
3046  $mode = 'preview';
3047  } elseif ( $this->diff ) {
3048  $mode = 'diff';
3049  } else {
3050  $mode = 'text';
3051  }
3052  $out->addHTML( Html::hidden( 'mode', $mode, [ 'id' => 'mw-edit-mode' ] ) );
3053 
3054  // Marker for detecting truncated form data. This must be the last
3055  // parameter sent in order to be of use, so do not move me.
3056  $out->addHTML( Html::hidden( 'wpUltimateParam', true ) );
3057  $out->addHTML( $this->editFormTextBottom . "\n</form>\n" );
3058 
3059  if ( !$user->getOption( 'previewontop' ) ) {
3060  $this->displayPreviewArea( $previewOutput, false );
3061  }
3062  }
3063 
3071  public function makeTemplatesOnThisPageList( array $templates ) {
3072  $templateListFormatter = new TemplatesOnThisPageFormatter(
3073  $this->context, MediaWikiServices::getInstance()->getLinkRenderer()
3074  );
3075 
3076  // preview if preview, else section if section, else false
3077  $type = false;
3078  if ( $this->preview ) {
3079  $type = 'preview';
3080  } elseif ( $this->section != '' ) {
3081  $type = 'section';
3082  }
3083 
3084  return Html::rawElement( 'div', [ 'class' => 'templatesUsed' ],
3085  $templateListFormatter->format( $templates, $type )
3086  );
3087  }
3088 
3095  public static function extractSectionTitle( $text ) {
3096  preg_match( "/^(=+)(.+)\\1\\s*(\n|$)/i", $text, $matches );
3097  if ( !empty( $matches[2] ) ) {
3098  return MediaWikiServices::getInstance()->getParser()
3099  ->stripSectionName( trim( $matches[2] ) );
3100  } else {
3101  return false;
3102  }
3103  }
3104 
3105  protected function showHeader() {
3106  $out = $this->context->getOutput();
3107  $user = $this->context->getUser();
3108  if ( $this->isConflict ) {
3109  $this->addExplainConflictHeader( $out );
3110  $this->editRevId = $this->page->getLatest();
3111  } else {
3112  if ( $this->section != '' && $this->section != 'new' && !$this->summary &&
3113  !$this->preview && !$this->diff
3114  ) {
3115  $sectionTitle = self::extractSectionTitle( $this->textbox1 ); // FIXME: use Content object
3116  if ( $sectionTitle !== false ) {
3117  $this->summary = "/* $sectionTitle */ ";
3118  }
3119  }
3120 
3121  $buttonLabel = $this->context->msg( $this->getSubmitButtonLabel() )->text();
3122 
3123  if ( $this->missingComment ) {
3124  $out->wrapWikiMsg( "<div id='mw-missingcommenttext'>\n$1\n</div>", 'missingcommenttext' );
3125  }
3126 
3127  if ( $this->missingSummary && $this->section != 'new' ) {
3128  $out->wrapWikiMsg(
3129  "<div id='mw-missingsummary'>\n$1\n</div>",
3130  [ 'missingsummary', $buttonLabel ]
3131  );
3132  }
3133 
3134  if ( $this->missingSummary && $this->section == 'new' ) {
3135  $out->wrapWikiMsg(
3136  "<div id='mw-missingcommentheader'>\n$1\n</div>",
3137  [ 'missingcommentheader', $buttonLabel ]
3138  );
3139  }
3140 
3141  if ( $this->blankArticle ) {
3142  $out->wrapWikiMsg(
3143  "<div id='mw-blankarticle'>\n$1\n</div>",
3144  [ 'blankarticle', $buttonLabel ]
3145  );
3146  }
3147 
3148  if ( $this->selfRedirect ) {
3149  $out->wrapWikiMsg(
3150  "<div id='mw-selfredirect'>\n$1\n</div>",
3151  [ 'selfredirect', $buttonLabel ]
3152  );
3153  }
3154 
3155  if ( $this->hookError !== '' ) {
3156  $out->addWikiTextAsInterface( $this->hookError );
3157  }
3158 
3159  if ( $this->section != 'new' ) {
3160  $revision = $this->mArticle->getRevisionFetched();
3161  if ( $revision ) {
3162  // Let sysop know that this will make private content public if saved
3163 
3164  if ( !$revision->userCan( RevisionRecord::DELETED_TEXT, $user ) ) {
3165  $out->wrapWikiMsg(
3166  "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
3167  'rev-deleted-text-permission'
3168  );
3169  } elseif ( $revision->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
3170  $out->wrapWikiMsg(
3171  "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
3172  'rev-deleted-text-view'
3173  );
3174  }
3175 
3176  if ( !$revision->isCurrent() ) {
3177  $this->mArticle->setOldSubtitle( $revision->getId() );
3178  $out->wrapWikiMsg(
3179  Html::warningBox( "\n$1\n" ),
3180  'editingold'
3181  );
3182  $this->isOldRev = true;
3183  }
3184  } elseif ( $this->mTitle->exists() ) {
3185  // Something went wrong
3186 
3187  $out->wrapWikiMsg( "<div class='errorbox'>\n$1\n</div>\n",
3188  [ 'missing-revision', $this->oldid ] );
3189  }
3190  }
3191  }
3192 
3193  if ( wfReadOnly() ) {
3194  $out->wrapWikiMsg(
3195  "<div id=\"mw-read-only-warning\">\n$1\n</div>",
3196  [ 'readonlywarning', wfReadOnlyReason() ]
3197  );
3198  } elseif ( $user->isAnon() ) {
3199  if ( $this->formtype != 'preview' ) {
3200  $returntoquery = array_diff_key(
3201  $this->context->getRequest()->getValues(),
3202  [ 'title' => true, 'returnto' => true, 'returntoquery' => true ]
3203  );
3204  $out->wrapWikiMsg(
3205  "<div id='mw-anon-edit-warning' class='warningbox'>\n$1\n</div>",
3206  [ 'anoneditwarning',
3207  // Log-in link
3208  SpecialPage::getTitleFor( 'Userlogin' )->getFullURL( [
3209  'returnto' => $this->getTitle()->getPrefixedDBkey(),
3210  'returntoquery' => wfArrayToCgi( $returntoquery ),
3211  ] ),
3212  // Sign-up link
3213  SpecialPage::getTitleFor( 'CreateAccount' )->getFullURL( [
3214  'returnto' => $this->getTitle()->getPrefixedDBkey(),
3215  'returntoquery' => wfArrayToCgi( $returntoquery ),
3216  ] )
3217  ]
3218  );
3219  } else {
3220  $out->wrapWikiMsg( "<div id=\"mw-anon-preview-warning\" class=\"warningbox\">\n$1</div>",
3221  'anonpreviewwarning'
3222  );
3223  }
3224  } elseif ( $this->mTitle->isUserConfigPage() ) {
3225  # Check the skin exists
3226  if ( $this->isWrongCaseUserConfigPage() ) {
3227  $out->wrapWikiMsg(
3228  "<div class='error' id='mw-userinvalidconfigtitle'>\n$1\n</div>",
3229  [ 'userinvalidconfigtitle', $this->mTitle->getSkinFromConfigSubpage() ]
3230  );
3231  }
3232  if ( $this->getTitle()->isSubpageOf( $user->getUserPage() ) ) {
3233  $isUserCssConfig = $this->mTitle->isUserCssConfigPage();
3234  $isUserJsonConfig = $this->mTitle->isUserJsonConfigPage();
3235  $isUserJsConfig = $this->mTitle->isUserJsConfigPage();
3236 
3237  $warning = $isUserCssConfig
3238  ? 'usercssispublic'
3239  : ( $isUserJsonConfig ? 'userjsonispublic' : 'userjsispublic' );
3240 
3241  $out->wrapWikiMsg( '<div class="mw-userconfigpublic">$1</div>', $warning );
3242 
3243  if ( $this->formtype !== 'preview' ) {
3244  $config = $this->context->getConfig();
3245  if ( $isUserCssConfig && $config->get( 'AllowUserCss' ) ) {
3246  $out->wrapWikiMsg(
3247  "<div id='mw-usercssyoucanpreview'>\n$1\n</div>",
3248  [ 'usercssyoucanpreview' ]
3249  );
3250  } elseif ( $isUserJsonConfig /* No comparable 'AllowUserJson' */ ) {
3251  $out->wrapWikiMsg(
3252  "<div id='mw-userjsonyoucanpreview'>\n$1\n</div>",
3253  [ 'userjsonyoucanpreview' ]
3254  );
3255  } elseif ( $isUserJsConfig && $config->get( 'AllowUserJs' ) ) {
3256  $out->wrapWikiMsg(
3257  "<div id='mw-userjsyoucanpreview'>\n$1\n</div>",
3258  [ 'userjsyoucanpreview' ]
3259  );
3260  }
3261  }
3262  }
3263  }
3264 
3266 
3267  $this->addLongPageWarningHeader();
3268 
3269  # Add header copyright warning
3270  $this->showHeaderCopyrightWarning();
3271  }
3272 
3280  private function getSummaryInputAttributes( array $inputAttrs = null ) {
3281  // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
3282  // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
3283  // Unicode codepoints.
3284  return ( is_array( $inputAttrs ) ? $inputAttrs : [] ) + [
3285  'id' => 'wpSummary',
3286  'name' => 'wpSummary',
3288  'tabindex' => 1,
3289  'size' => 60,
3290  'spellcheck' => 'true',
3291  ];
3292  }
3293 
3303  function getSummaryInputWidget( $summary = "", $labelText = null, $inputAttrs = null ) {
3304  $inputAttrs = OOUI\Element::configFromHtmlAttributes(
3305  $this->getSummaryInputAttributes( $inputAttrs )
3306  );
3307  $inputAttrs += [
3308  'title' => Linker::titleAttrib( 'summary' ),
3309  'accessKey' => Linker::accesskey( 'summary' ),
3310  ];
3311 
3312  // For compatibility with old scripts and extensions, we want the legacy 'id' on the `<input>`
3313  $inputAttrs['inputId'] = $inputAttrs['id'];
3314  $inputAttrs['id'] = 'wpSummaryWidget';
3315 
3316  return new OOUI\FieldLayout(
3317  new OOUI\TextInputWidget( [
3318  'value' => $summary,
3319  'infusable' => true,
3320  ] + $inputAttrs ),
3321  [
3322  'label' => new OOUI\HtmlSnippet( $labelText ),
3323  'align' => 'top',
3324  'id' => 'wpSummaryLabel',
3325  'classes' => [ $this->missingSummary ? 'mw-summarymissed' : 'mw-summary' ],
3326  ]
3327  );
3328  }
3329 
3336  protected function showSummaryInput( $isSubjectPreview, $summary = "" ) {
3337  # Add a class if 'missingsummary' is triggered to allow styling of the summary line
3338  $summaryClass = $this->missingSummary ? 'mw-summarymissed' : 'mw-summary';
3339  if ( $isSubjectPreview ) {
3340  if ( $this->nosummary ) {
3341  return;
3342  }
3343  } elseif ( !$this->mShowSummaryField ) {
3344  return;
3345  }
3346 
3347  $labelText = $this->context->msg( $isSubjectPreview ? 'subject' : 'summary' )->parse();
3348  $this->context->getOutput()->addHTML( $this->getSummaryInputWidget(
3349  $summary,
3350  $labelText,
3351  [ 'class' => $summaryClass ]
3352  ) );
3353  }
3354 
3362  protected function getSummaryPreview( $isSubjectPreview, $summary = "" ) {
3363  // avoid spaces in preview, gets always trimmed on save
3364  $summary = trim( $summary );
3365  if ( !$summary || ( !$this->preview && !$this->diff ) ) {
3366  return "";
3367  }
3368 
3369  if ( $isSubjectPreview ) {
3370  $summary = $this->context->msg( 'newsectionsummary' )
3371  ->rawParams( MediaWikiServices::getInstance()->getParser()
3372  ->stripSectionName( $summary ) )
3373  ->inContentLanguage()->text();
3374  }
3375 
3376  $message = $isSubjectPreview ? 'subject-preview' : 'summary-preview';
3377 
3378  $summary = $this->context->msg( $message )->parse()
3379  . Linker::commentBlock( $summary, $this->mTitle, $isSubjectPreview );
3380  return Xml::tags( 'div', [ 'class' => 'mw-summary-preview' ], $summary );
3381  }
3382 
3383  protected function showFormBeforeText() {
3384  $out = $this->context->getOutput();
3385  $out->addHTML( Html::hidden( 'wpSection', $this->section ) );
3386  $out->addHTML( Html::hidden( 'wpStarttime', $this->starttime ) );
3387  $out->addHTML( Html::hidden( 'wpEdittime', $this->edittime ) );
3388  $out->addHTML( Html::hidden( 'editRevId', $this->editRevId ) );
3389  $out->addHTML( Html::hidden( 'wpScrolltop', $this->scrolltop, [ 'id' => 'wpScrolltop' ] ) );
3390  }
3391 
3392  protected function showFormAfterText() {
3405  $this->context->getOutput()->addHTML(
3406  "\n" .
3407  Html::hidden( "wpEditToken", $this->context->getUser()->getEditToken() ) .
3408  "\n"
3409  );
3410  }
3411 
3420  protected function showContentForm() {
3421  $this->showTextbox1();
3422  }
3423 
3432  protected function showTextbox1( $customAttribs = null, $textoverride = null ) {
3433  if ( $this->wasDeletedSinceLastEdit() && $this->formtype == 'save' ) {
3434  $attribs = [ 'style' => 'display:none;' ];
3435  } else {
3436  $builder = new TextboxBuilder();
3437  $classes = $builder->getTextboxProtectionCSSClasses( $this->getTitle() );
3438 
3439  # Is an old revision being edited?
3440  if ( $this->isOldRev ) {
3441  $classes[] = 'mw-textarea-oldrev';
3442  }
3443 
3444  $attribs = [ 'tabindex' => 1 ];
3445 
3446  if ( is_array( $customAttribs ) ) {
3447  $attribs += $customAttribs;
3448  }
3449 
3450  $attribs = $builder->mergeClassesIntoAttributes( $classes, $attribs );
3451  }
3452 
3453  $this->showTextbox(
3454  $textoverride ?? $this->textbox1,
3455  'wpTextbox1',
3456  $attribs
3457  );
3458  }
3459 
3460  protected function showTextbox2() {
3461  $this->showTextbox( $this->textbox2, 'wpTextbox2', [ 'tabindex' => 6, 'readonly' ] );
3462  }
3463 
3464  protected function showTextbox( $text, $name, $customAttribs = [] ) {
3465  $builder = new TextboxBuilder();
3466  $attribs = $builder->buildTextboxAttribs(
3467  $name,
3468  $customAttribs,
3469  $this->context->getUser(),
3471  );
3472 
3473  $this->context->getOutput()->addHTML(
3474  Html::textarea( $name, $builder->addNewLineAtEnd( $text ), $attribs )
3475  );
3476  }
3477 
3478  protected function displayPreviewArea( $previewOutput, $isOnTop = false ) {
3479  $classes = [];
3480  if ( $isOnTop ) {
3481  $classes[] = 'ontop';
3482  }
3483 
3484  $attribs = [ 'id' => 'wikiPreview', 'class' => implode( ' ', $classes ) ];
3485 
3486  if ( $this->formtype != 'preview' ) {
3487  $attribs['style'] = 'display: none;';
3488  }
3489 
3490  $out = $this->context->getOutput();
3491  $out->addHTML( Xml::openElement( 'div', $attribs ) );
3492 
3493  if ( $this->formtype == 'preview' ) {
3494  $this->showPreview( $previewOutput );
3495  } else {
3496  // Empty content container for LivePreview
3497  $pageViewLang = $this->mTitle->getPageViewLanguage();
3498  $attribs = [ 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(),
3499  'class' => 'mw-content-' . $pageViewLang->getDir() ];
3500  $out->addHTML( Html::rawElement( 'div', $attribs ) );
3501  }
3502 
3503  $out->addHTML( '</div>' );
3504 
3505  if ( $this->formtype == 'diff' ) {
3506  try {
3507  $this->showDiff();
3508  } catch ( MWContentSerializationException $ex ) {
3509  $msg = $this->context->msg(
3510  'content-failed-to-parse',
3511  $this->contentModel,
3512  $this->contentFormat,
3513  $ex->getMessage()
3514  );
3515  $out->wrapWikiTextAsInterface( 'error', $msg->plain() );
3516  }
3517  }
3518  }
3519 
3526  protected function showPreview( $text ) {
3527  if ( $this->mArticle instanceof CategoryPage ) {
3528  $this->mArticle->openShowCategory();
3529  }
3530  # This hook seems slightly odd here, but makes things more
3531  # consistent for extensions.
3532  $out = $this->context->getOutput();
3533  Hooks::run( 'OutputPageBeforeHTML', [ &$out, &$text ] );
3534  $out->addHTML( $text );
3535  if ( $this->mArticle instanceof CategoryPage ) {
3536  $this->mArticle->closeShowCategory();
3537  }
3538  }
3539 
3547  public function showDiff() {
3548  $oldtitlemsg = 'currentrev';
3549  # if message does not exist, show diff against the preloaded default
3550  if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && !$this->mTitle->exists() ) {
3551  $oldtext = $this->mTitle->getDefaultMessageText();
3552  if ( $oldtext !== false ) {
3553  $oldtitlemsg = 'defaultmessagetext';
3554  $oldContent = $this->toEditContent( $oldtext );
3555  } else {
3556  $oldContent = null;
3557  }
3558  } else {
3559  $oldContent = $this->getCurrentContent();
3560  }
3561 
3562  $textboxContent = $this->toEditContent( $this->textbox1 );
3563  if ( $this->editRevId !== null ) {
3564  $newContent = $this->page->replaceSectionAtRev(
3565  $this->section, $textboxContent, $this->summary, $this->editRevId
3566  );
3567  } else {
3568  $newContent = $this->page->replaceSectionContent(
3569  $this->section, $textboxContent, $this->summary, $this->edittime
3570  );
3571  }
3572 
3573  if ( $newContent ) {
3574  Hooks::run( 'EditPageGetDiffContent', [ $this, &$newContent ] );
3575 
3576  $user = $this->context->getUser();
3577  $popts = ParserOptions::newFromUserAndLang( $user,
3578  MediaWikiServices::getInstance()->getContentLanguage() );
3579  $newContent = $newContent->preSaveTransform( $this->mTitle, $user, $popts );
3580  }
3581 
3582  if ( ( $oldContent && !$oldContent->isEmpty() ) || ( $newContent && !$newContent->isEmpty() ) ) {
3583  $oldtitle = $this->context->msg( $oldtitlemsg )->parse();
3584  $newtitle = $this->context->msg( 'yourtext' )->parse();
3585 
3586  if ( !$oldContent ) {
3587  $oldContent = $newContent->getContentHandler()->makeEmptyContent();
3588  }
3589 
3590  if ( !$newContent ) {
3591  $newContent = $oldContent->getContentHandler()->makeEmptyContent();
3592  }
3593 
3594  $de = $oldContent->getContentHandler()->createDifferenceEngine( $this->context );
3595  $de->setContent( $oldContent, $newContent );
3596 
3597  $difftext = $de->getDiff( $oldtitle, $newtitle );
3598  $de->showDiffStyle();
3599  } else {
3600  $difftext = '';
3601  }
3602 
3603  $this->context->getOutput()->addHTML( '<div id="wikiDiff">' . $difftext . '</div>' );
3604  }
3605 
3609  protected function showHeaderCopyrightWarning() {
3610  $msg = 'editpage-head-copy-warn';
3611  if ( !$this->context->msg( $msg )->isDisabled() ) {
3612  $this->context->getOutput()->wrapWikiMsg( "<div class='editpage-head-copywarn'>\n$1\n</div>",
3613  'editpage-head-copy-warn' );
3614  }
3615  }
3616 
3625  protected function showTosSummary() {
3626  $msg = 'editpage-tos-summary';
3627  Hooks::run( 'EditPageTosSummary', [ $this->mTitle, &$msg ] );
3628  if ( !$this->context->msg( $msg )->isDisabled() ) {
3629  $out = $this->context->getOutput();
3630  $out->addHTML( '<div class="mw-tos-summary">' );
3631  $out->addWikiMsg( $msg );
3632  $out->addHTML( '</div>' );
3633  }
3634  }
3635 
3640  protected function showEditTools() {
3641  $this->context->getOutput()->addHTML( '<div class="mw-editTools">' .
3642  $this->context->msg( 'edittools' )->inContentLanguage()->parse() .
3643  '</div>' );
3644  }
3645 
3652  protected function getCopywarn() {
3653  return self::getCopyrightWarning( $this->mTitle );
3654  }
3655 
3664  public static function getCopyrightWarning( $title, $format = 'plain', $langcode = null ) {
3665  global $wgRightsText;
3666  if ( $wgRightsText ) {
3667  $copywarnMsg = [ 'copyrightwarning',
3668  '[[' . wfMessage( 'copyrightpage' )->inContentLanguage()->text() . ']]',
3669  $wgRightsText ];
3670  } else {
3671  $copywarnMsg = [ 'copyrightwarning2',
3672  '[[' . wfMessage( 'copyrightpage' )->inContentLanguage()->text() . ']]' ];
3673  }
3674  // Allow for site and per-namespace customization of contribution/copyright notice.
3675  Hooks::run( 'EditPageCopyrightWarning', [ $title, &$copywarnMsg ] );
3676 
3677  $msg = wfMessage( ...$copywarnMsg )->title( $title );
3678  if ( $langcode ) {
3679  $msg->inLanguage( $langcode );
3680  }
3681  return "<div id=\"editpage-copywarn\">\n" .
3682  $msg->$format() . "\n</div>";
3683  }
3684 
3692  public static function getPreviewLimitReport( ParserOutput $output = null ) {
3693  global $wgLang;
3694 
3695  if ( !$output || !$output->getLimitReportData() ) {
3696  return '';
3697  }
3698 
3699  $limitReport = Html::rawElement( 'div', [ 'class' => 'mw-limitReportExplanation' ],
3700  wfMessage( 'limitreport-title' )->parseAsBlock()
3701  );
3702 
3703  // Show/hide animation doesn't work correctly on a table, so wrap it in a div.
3704  $limitReport .= Html::openElement( 'div', [ 'class' => 'preview-limit-report-wrapper' ] );
3705 
3706  $limitReport .= Html::openElement( 'table', [
3707  'class' => 'preview-limit-report wikitable'
3708  ] ) .
3709  Html::openElement( 'tbody' );
3710 
3711  foreach ( $output->getLimitReportData() as $key => $value ) {
3712  if ( Hooks::run( 'ParserLimitReportFormat',
3713  [ $key, &$value, &$limitReport, true, true ]
3714  ) ) {
3715  $keyMsg = wfMessage( $key );
3716  $valueMsg = wfMessage( [ "$key-value-html", "$key-value" ] );
3717  if ( !$valueMsg->exists() ) {
3718  $valueMsg = new RawMessage( '$1' );
3719  }
3720  if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) {
3721  $limitReport .= Html::openElement( 'tr' ) .
3722  Html::rawElement( 'th', null, $keyMsg->parse() ) .
3723  Html::rawElement( 'td', null,
3724  $wgLang->formatNum( $valueMsg->params( $value )->parse() )
3725  ) .
3726  Html::closeElement( 'tr' );
3727  }
3728  }
3729  }
3730 
3731  $limitReport .= Html::closeElement( 'tbody' ) .
3732  Html::closeElement( 'table' ) .
3733  Html::closeElement( 'div' );
3734 
3735  return $limitReport;
3736  }
3737 
3738  protected function showStandardInputs( &$tabindex = 2 ) {
3739  $out = $this->context->getOutput();
3740  $out->addHTML( "<div class='editOptions'>\n" );
3741 
3742  if ( $this->section != 'new' ) {
3743  $this->showSummaryInput( false, $this->summary );
3744  $out->addHTML( $this->getSummaryPreview( false, $this->summary ) );
3745  }
3746 
3747  $checkboxes = $this->getCheckboxesWidget(
3748  $tabindex,
3749  [ 'minor' => $this->minoredit, 'watch' => $this->watchthis ]
3750  );
3751  $checkboxesHTML = new OOUI\HorizontalLayout( [ 'items' => $checkboxes ] );
3752 
3753  $out->addHTML( "<div class='editCheckboxes'>" . $checkboxesHTML . "</div>\n" );
3754 
3755  // Show copyright warning.
3756  $out->addWikiTextAsInterface( $this->getCopywarn() );
3757  $out->addHTML( $this->editFormTextAfterWarn );
3758 
3759  $out->addHTML( "<div class='editButtons'>\n" );
3760  $out->addHTML( implode( "\n", $this->getEditButtons( $tabindex ) ) . "\n" );
3761 
3762  $cancel = $this->getCancelLink();
3763 
3764  $message = $this->context->msg( 'edithelppage' )->inContentLanguage()->text();
3765  $edithelpurl = Skin::makeInternalOrExternalUrl( $message );
3766  $edithelp =
3768  $this->context->msg( 'edithelp' )->text(),
3769  [ 'target' => 'helpwindow', 'href' => $edithelpurl ],
3770  [ 'mw-ui-quiet' ]
3771  ) .
3772  $this->context->msg( 'word-separator' )->escaped() .
3773  $this->context->msg( 'newwindow' )->parse();
3774 
3775  $out->addHTML( " <span class='cancelLink'>{$cancel}</span>\n" );
3776  $out->addHTML( " <span class='editHelp'>{$edithelp}</span>\n" );
3777  $out->addHTML( "</div><!-- editButtons -->\n" );
3778 
3779  Hooks::run( 'EditPage::showStandardInputs:options', [ $this, $out, &$tabindex ] );
3780 
3781  $out->addHTML( "</div><!-- editOptions -->\n" );
3782  }
3783 
3788  protected function showConflict() {
3789  $out = $this->context->getOutput();
3790  // Avoid PHP 7.1 warning of passing $this by reference
3791  $editPage = $this;
3792  if ( Hooks::run( 'EditPageBeforeConflictDiff', [ &$editPage, &$out ] ) ) {
3793  $this->incrementConflictStats();
3794 
3795  $this->getEditConflictHelper()->showEditFormTextAfterFooters();
3796  }
3797  }
3798 
3799  protected function incrementConflictStats() {
3800  $this->getEditConflictHelper()->incrementConflictStats();
3801  }
3802 
3806  public function getCancelLink() {
3807  $cancelParams = [];
3808  if ( !$this->isConflict && $this->oldid > 0 ) {
3809  $cancelParams['oldid'] = $this->oldid;
3810  } elseif ( $this->getContextTitle()->isRedirect() ) {
3811  $cancelParams['redirect'] = 'no';
3812  }
3813 
3814  return new OOUI\ButtonWidget( [
3815  'id' => 'mw-editform-cancel',
3816  'href' => $this->getContextTitle()->getLinkURL( $cancelParams ),
3817  'label' => new OOUI\HtmlSnippet( $this->context->msg( 'cancel' )->parse() ),
3818  'framed' => false,
3819  'infusable' => true,
3820  'flags' => 'destructive',
3821  ] );
3822  }
3823 
3833  protected function getActionURL( Title $title ) {
3834  return $title->getLocalURL( [ 'action' => $this->action ] );
3835  }
3836 
3844  protected function wasDeletedSinceLastEdit() {
3845  if ( $this->deletedSinceEdit !== null ) {
3846  return $this->deletedSinceEdit;
3847  }
3848 
3849  $this->deletedSinceEdit = false;
3850 
3851  if ( !$this->mTitle->exists() && $this->mTitle->isDeletedQuick() ) {
3852  $this->lastDelete = $this->getLastDelete();
3853  if ( $this->lastDelete ) {
3854  $deleteTime = wfTimestamp( TS_MW, $this->lastDelete->log_timestamp );
3855  if ( $deleteTime > $this->starttime ) {
3856  $this->deletedSinceEdit = true;
3857  }
3858  }
3859  }
3860 
3861  return $this->deletedSinceEdit;
3862  }
3863 
3869  protected function getLastDelete() {
3870  $dbr = wfGetDB( DB_REPLICA );
3871  $commentQuery = CommentStore::getStore()->getJoin( 'log_comment' );
3872  $actorQuery = ActorMigration::newMigration()->getJoin( 'log_user' );
3873  $data = $dbr->selectRow(
3874  array_merge( [ 'logging' ], $commentQuery['tables'], $actorQuery['tables'], [ 'user' ] ),
3875  [
3876  'log_type',
3877  'log_action',
3878  'log_timestamp',
3879  'log_namespace',
3880  'log_title',
3881  'log_params',
3882  'log_deleted',
3883  'user_name'
3884  ] + $commentQuery['fields'] + $actorQuery['fields'],
3885  [
3886  'log_namespace' => $this->mTitle->getNamespace(),
3887  'log_title' => $this->mTitle->getDBkey(),
3888  'log_type' => 'delete',
3889  'log_action' => 'delete',
3890  ],
3891  __METHOD__,
3892  [ 'LIMIT' => 1, 'ORDER BY' => 'log_timestamp DESC' ],
3893  [
3894  'user' => [ 'JOIN', 'user_id=' . $actorQuery['fields']['log_user'] ],
3895  ] + $commentQuery['joins'] + $actorQuery['joins']
3896  );
3897  // Quick paranoid permission checks...
3898  if ( is_object( $data ) ) {
3899  if ( $data->log_deleted & LogPage::DELETED_USER ) {
3900  $data->user_name = $this->context->msg( 'rev-deleted-user' )->escaped();
3901  }
3902 
3903  if ( $data->log_deleted & LogPage::DELETED_COMMENT ) {
3904  $data->log_comment_text = $this->context->msg( 'rev-deleted-comment' )->escaped();
3905  $data->log_comment_data = null;
3906  }
3907  }
3908 
3909  return $data;
3910  }
3911 
3917  public function getPreviewText() {
3918  $out = $this->context->getOutput();
3919  $config = $this->context->getConfig();
3920 
3921  if ( $config->get( 'RawHtml' ) && !$this->mTokenOk ) {
3922  // Could be an offsite preview attempt. This is very unsafe if
3923  // HTML is enabled, as it could be an attack.
3924  $parsedNote = '';
3925  if ( $this->textbox1 !== '' ) {
3926  // Do not put big scary notice, if previewing the empty
3927  // string, which happens when you initially edit
3928  // a category page, due to automatic preview-on-open.
3929  $parsedNote = Html::rawElement( 'div', [ 'class' => 'previewnote' ],
3930  $out->parseAsInterface(
3931  $this->context->msg( 'session_fail_preview_html' )->plain()
3932  ) );
3933  }
3934  $this->incrementEditFailureStats( 'session_loss' );
3935  return $parsedNote;
3936  }
3937 
3938  $note = '';
3939 
3940  try {
3941  $content = $this->toEditContent( $this->textbox1 );
3942 
3943  $previewHTML = '';
3944  if ( !Hooks::run(
3945  'AlternateEditPreview',
3946  [ $this, &$content, &$previewHTML, &$this->mParserOutput ] )
3947  ) {
3948  return $previewHTML;
3949  }
3950 
3951  # provide a anchor link to the editform
3952  $continueEditing = '<span class="mw-continue-editing">' .
3953  '[[#' . self::EDITFORM_ID . '|' .
3954  $this->context->getLanguage()->getArrow() . ' ' .
3955  $this->context->msg( 'continue-editing' )->text() . ']]</span>';
3956  if ( $this->mTriedSave && !$this->mTokenOk ) {
3957  if ( $this->mTokenOkExceptSuffix ) {
3958  $note = $this->context->msg( 'token_suffix_mismatch' )->plain();
3959  $this->incrementEditFailureStats( 'bad_token' );
3960  } else {
3961  $note = $this->context->msg( 'session_fail_preview' )->plain();
3962  $this->incrementEditFailureStats( 'session_loss' );
3963  }
3964  } elseif ( $this->incompleteForm ) {
3965  $note = $this->context->msg( 'edit_form_incomplete' )->plain();
3966  if ( $this->mTriedSave ) {
3967  $this->incrementEditFailureStats( 'incomplete_form' );
3968  }
3969  } else {
3970  $note = $this->context->msg( 'previewnote' )->plain() . ' ' . $continueEditing;
3971  }
3972 
3973  # don't parse non-wikitext pages, show message about preview
3974  if ( $this->mTitle->isUserConfigPage() || $this->mTitle->isSiteConfigPage() ) {
3975  if ( $this->mTitle->isUserConfigPage() ) {
3976  $level = 'user';
3977  } elseif ( $this->mTitle->isSiteConfigPage() ) {
3978  $level = 'site';
3979  } else {
3980  $level = false;
3981  }
3982 
3983  if ( $content->getModel() == CONTENT_MODEL_CSS ) {
3984  $format = 'css';
3985  if ( $level === 'user' && !$config->get( 'AllowUserCss' ) ) {
3986  $format = false;
3987  }
3988  } elseif ( $content->getModel() == CONTENT_MODEL_JSON ) {
3989  $format = 'json';
3990  if ( $level === 'user' /* No comparable 'AllowUserJson' */ ) {
3991  $format = false;
3992  }
3993  } elseif ( $content->getModel() == CONTENT_MODEL_JAVASCRIPT ) {
3994  $format = 'js';
3995  if ( $level === 'user' && !$config->get( 'AllowUserJs' ) ) {
3996  $format = false;
3997  }
3998  } else {
3999  $format = false;
4000  }
4001 
4002  # Used messages to make sure grep find them:
4003  # Messages: usercsspreview, userjsonpreview, userjspreview,
4004  # sitecsspreview, sitejsonpreview, sitejspreview
4005  if ( $level && $format ) {
4006  $note = "<div id='mw-{$level}{$format}preview'>" .
4007  $this->context->msg( "{$level}{$format}preview" )->plain() .
4008  ' ' . $continueEditing . "</div>";
4009  }
4010  }
4011 
4012  # If we're adding a comment, we need to show the
4013  # summary as the headline
4014  if ( $this->section === "new" && $this->summary !== "" ) {
4015  $content = $content->addSectionHeader( $this->summary );
4016  }
4017 
4018  $hook_args = [ $this, &$content ];
4019  Hooks::run( 'EditPageGetPreviewContent', $hook_args );
4020 
4021  $parserResult = $this->doPreviewParse( $content );
4022  $parserOutput = $parserResult['parserOutput'];
4023  $previewHTML = $parserResult['html'];
4024  $this->mParserOutput = $parserOutput;
4025  $out->addParserOutputMetadata( $parserOutput );
4026  if ( $out->userCanPreview() ) {
4027  $out->addContentOverride( $this->getTitle(), $content );
4028  }
4029 
4030  if ( count( $parserOutput->getWarnings() ) ) {
4031  $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
4032  }
4033 
4034  } catch ( MWContentSerializationException $ex ) {
4035  $m = $this->context->msg(
4036  'content-failed-to-parse',
4037  $this->contentModel,
4038  $this->contentFormat,
4039  $ex->getMessage()
4040  );
4041  $note .= "\n\n" . $m->plain(); # gets parsed down below
4042  $previewHTML = '';
4043  }
4044 
4045  if ( $this->isConflict ) {
4046  $conflict = Html::rawElement(
4047  'div', [ 'id' => 'mw-previewconflict', 'class' => 'warningbox' ],
4048  $this->context->msg( 'previewconflict' )->escaped()
4049  );
4050  } else {
4051  $conflict = '';
4052  }
4053 
4054  $previewhead = Html::rawElement(
4055  'div', [ 'class' => 'previewnote' ],
4057  'h2', [ 'id' => 'mw-previewheader' ],
4058  $this->context->msg( 'preview' )->escaped()
4059  ) .
4060  Html::rawElement( 'div', [ 'class' => 'warningbox' ],
4061  $out->parseAsInterface( $note )
4062  ) . $conflict
4063  );
4064 
4065  $pageViewLang = $this->mTitle->getPageViewLanguage();
4066  $attribs = [ 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(),
4067  'class' => 'mw-content-' . $pageViewLang->getDir() ];
4068  $previewHTML = Html::rawElement( 'div', $attribs, $previewHTML );
4069 
4070  return $previewhead . $previewHTML . $this->previewTextAfterContent;
4071  }
4072 
4073  private function incrementEditFailureStats( $failureType ) {
4074  $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
4075  $stats->increment( 'edit.failures.' . $failureType );
4076  }
4077 
4082  protected function getPreviewParserOptions() {
4083  $parserOptions = $this->page->makeParserOptions( $this->context );
4084  $parserOptions->setIsPreview( true );
4085  $parserOptions->setIsSectionPreview( !is_null( $this->section ) && $this->section !== '' );
4086  $parserOptions->enableLimitReport();
4087 
4088  // XXX: we could call $parserOptions->setCurrentRevisionCallback here to force the
4089  // current revision to be null during PST, until setupFakeRevision is called on
4090  // the ParserOptions. Currently, we rely on Parser::getRevisionObject() to ignore
4091  // existing revisions in preview mode.
4092 
4093  return $parserOptions;
4094  }
4095 
4105  protected function doPreviewParse( Content $content ) {
4106  $user = $this->context->getUser();
4107  $parserOptions = $this->getPreviewParserOptions();
4108 
4109  // NOTE: preSaveTransform doesn't have a fake revision to operate on.
4110  // Parser::getRevisionObject() will return null in preview mode,
4111  // causing the context user to be used for {{subst:REVISIONUSER}}.
4112  // XXX: Alternatively, we could also call setupFakeRevision() a second time:
4113  // once before PST with $content, and then after PST with $pstContent.
4114  $pstContent = $content->preSaveTransform( $this->mTitle, $user, $parserOptions );
4115  $scopedCallback = $parserOptions->setupFakeRevision( $this->mTitle, $pstContent, $user );
4116  $parserOutput = $pstContent->getParserOutput( $this->mTitle, null, $parserOptions );
4117  ScopedCallback::consume( $scopedCallback );
4118  return [
4119  'parserOutput' => $parserOutput,
4120  'html' => $parserOutput->getText( [
4121  'enableSectionEditLinks' => false
4122  ] )
4123  ];
4124  }
4125 
4129  public function getTemplates() {
4130  if ( $this->preview || $this->section != '' ) {
4131  $templates = [];
4132  if ( !isset( $this->mParserOutput ) ) {
4133  return $templates;
4134  }
4135  foreach ( $this->mParserOutput->getTemplates() as $ns => $template ) {
4136  foreach ( array_keys( $template ) as $dbk ) {
4137  $templates[] = Title::makeTitle( $ns, $dbk );
4138  }
4139  }
4140  return $templates;
4141  } else {
4142  return $this->mTitle->getTemplateLinksFrom();
4143  }
4144  }
4145 
4151  public static function getEditToolbar() {
4152  $startingToolbar = '<div id="toolbar"></div>';
4153  $toolbar = $startingToolbar;
4154 
4155  if ( !Hooks::run( 'EditPageBeforeEditToolbar', [ &$toolbar ] ) ) {
4156  return null;
4157  }
4158  // Don't add a pointless `<div>` to the page unless a hook caller populated it
4159  return ( $toolbar === $startingToolbar ) ? null : $toolbar;
4160  }
4161 
4180  public function getCheckboxesDefinition( $checked ) {
4181  $checkboxes = [];
4182 
4183  $user = $this->context->getUser();
4184  // don't show the minor edit checkbox if it's a new page or section
4185  $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
4186  if ( !$this->isNew && $permissionManager->userHasRight( $user, 'minoredit' ) ) {
4187  $checkboxes['wpMinoredit'] = [
4188  'id' => 'wpMinoredit',
4189  'label-message' => 'minoredit',
4190  // Uses messages: tooltip-minoredit, accesskey-minoredit
4191  'tooltip' => 'minoredit',
4192  'label-id' => 'mw-editpage-minoredit',
4193  'legacy-name' => 'minor',
4194  'default' => $checked['minor'],
4195  ];
4196  }
4197 
4198  if ( $user->isLoggedIn() ) {
4199  $checkboxes['wpWatchthis'] = [
4200  'id' => 'wpWatchthis',
4201  'label-message' => 'watchthis',
4202  // Uses messages: tooltip-watch, accesskey-watch
4203  'tooltip' => 'watch',
4204  'label-id' => 'mw-editpage-watch',
4205  'legacy-name' => 'watch',
4206  'default' => $checked['watch'],
4207  ];
4208  }
4209 
4210  $editPage = $this;
4211  Hooks::run( 'EditPageGetCheckboxesDefinition', [ $editPage, &$checkboxes ] );
4212 
4213  return $checkboxes;
4214  }
4215 
4226  public function getCheckboxesWidget( &$tabindex, $checked ) {
4227  $checkboxes = [];
4228  $checkboxesDef = $this->getCheckboxesDefinition( $checked );
4229 
4230  foreach ( $checkboxesDef as $name => $options ) {
4231  $legacyName = $options['legacy-name'] ?? $name;
4232 
4233  $title = null;
4234  $accesskey = null;
4235  if ( isset( $options['tooltip'] ) ) {
4236  $accesskey = $this->context->msg( "accesskey-{$options['tooltip']}" )->text();
4237  $title = Linker::titleAttrib( $options['tooltip'] );
4238  }
4239  if ( isset( $options['title-message'] ) ) {
4240  $title = $this->context->msg( $options['title-message'] )->text();
4241  }
4242 
4243  $checkboxes[ $legacyName ] = new OOUI\FieldLayout(
4244  new OOUI\CheckboxInputWidget( [
4245  'tabIndex' => ++$tabindex,
4246  'accessKey' => $accesskey,
4247  'id' => $options['id'] . 'Widget',
4248  'inputId' => $options['id'],
4249  'name' => $name,
4250  'selected' => $options['default'],
4251  'infusable' => true,
4252  ] ),
4253  [
4254  'align' => 'inline',
4255  'label' => new OOUI\HtmlSnippet( $this->context->msg( $options['label-message'] )->parse() ),
4256  'title' => $title,
4257  'id' => $options['label-id'] ?? null,
4258  ]
4259  );
4260  }
4261 
4262  return $checkboxes;
4263  }
4264 
4271  protected function getSubmitButtonLabel() {
4272  $labelAsPublish =
4273  $this->context->getConfig()->get( 'EditSubmitButtonLabelPublish' );
4274 
4275  // Can't use $this->isNew as that's also true if we're adding a new section to an extant page
4276  $newPage = !$this->mTitle->exists();
4277 
4278  if ( $labelAsPublish ) {
4279  $buttonLabelKey = $newPage ? 'publishpage' : 'publishchanges';
4280  } else {
4281  $buttonLabelKey = $newPage ? 'savearticle' : 'savechanges';
4282  }
4283 
4284  return $buttonLabelKey;
4285  }
4286 
4295  public function getEditButtons( &$tabindex ) {
4296  $buttons = [];
4297 
4298  $labelAsPublish =
4299  $this->context->getConfig()->get( 'EditSubmitButtonLabelPublish' );
4300 
4301  $buttonLabel = $this->context->msg( $this->getSubmitButtonLabel() )->text();
4302  $buttonTooltip = $labelAsPublish ? 'publish' : 'save';
4303 
4304  $buttons['save'] = new OOUI\ButtonInputWidget( [
4305  'name' => 'wpSave',
4306  'tabIndex' => ++$tabindex,
4307  'id' => 'wpSaveWidget',
4308  'inputId' => 'wpSave',
4309  // Support: IE 6 – Use <input>, otherwise it can't distinguish which button was clicked
4310  'useInputTag' => true,
4311  'flags' => [ 'progressive', 'primary' ],
4312  'label' => $buttonLabel,
4313  'infusable' => true,
4314  'type' => 'submit',
4315  // Messages used: tooltip-save, tooltip-publish
4316  'title' => Linker::titleAttrib( $buttonTooltip ),
4317  // Messages used: accesskey-save, accesskey-publish
4318  'accessKey' => Linker::accesskey( $buttonTooltip ),
4319  ] );
4320 
4321  $buttons['preview'] = new OOUI\ButtonInputWidget( [
4322  'name' => 'wpPreview',
4323  'tabIndex' => ++$tabindex,
4324  'id' => 'wpPreviewWidget',
4325  'inputId' => 'wpPreview',
4326  // Support: IE 6 – Use <input>, otherwise it can't distinguish which button was clicked
4327  'useInputTag' => true,
4328  'label' => $this->context->msg( 'showpreview' )->text(),
4329  'infusable' => true,
4330  'type' => 'submit',
4331  // Message used: tooltip-preview
4332  'title' => Linker::titleAttrib( 'preview' ),
4333  // Message used: accesskey-preview
4334  'accessKey' => Linker::accesskey( 'preview' ),
4335  ] );
4336 
4337  $buttons['diff'] = new OOUI\ButtonInputWidget( [
4338  'name' => 'wpDiff',
4339  'tabIndex' => ++$tabindex,
4340  'id' => 'wpDiffWidget',
4341  'inputId' => 'wpDiff',
4342  // Support: IE 6 – Use <input>, otherwise it can't distinguish which button was clicked
4343  'useInputTag' => true,
4344  'label' => $this->context->msg( 'showdiff' )->text(),
4345  'infusable' => true,
4346  'type' => 'submit',
4347  // Message used: tooltip-diff
4348  'title' => Linker::titleAttrib( 'diff' ),
4349  // Message used: accesskey-diff
4350  'accessKey' => Linker::accesskey( 'diff' ),
4351  ] );
4352 
4353  // Avoid PHP 7.1 warning of passing $this by reference
4354  $editPage = $this;
4355  Hooks::run( 'EditPageBeforeEditButtons', [ &$editPage, &$buttons, &$tabindex ] );
4356 
4357  return $buttons;
4358  }
4359 
4364  public function noSuchSectionPage() {
4365  $out = $this->context->getOutput();
4366  $out->prepareErrorPage( $this->context->msg( 'nosuchsectiontitle' ) );
4367 
4368  $res = $this->context->msg( 'nosuchsectiontext', $this->section )->parseAsBlock();
4369 
4370  // Avoid PHP 7.1 warning of passing $this by reference
4371  $editPage = $this;
4372  Hooks::run( 'EditPageNoSuchSection', [ &$editPage, &$res ] );
4373  $out->addHTML( $res );
4374 
4375  $out->returnToMain( false, $this->mTitle );
4376  }
4377 
4383  public function spamPageWithContent( $match = false ) {
4384  $this->textbox2 = $this->textbox1;
4385 
4386  if ( is_array( $match ) ) {
4387  $match = $this->context->getLanguage()->listToText( $match );
4388  }
4389  $out = $this->context->getOutput();
4390  $out->prepareErrorPage( $this->context->msg( 'spamprotectiontitle' ) );
4391 
4392  $out->addHTML( '<div id="spamprotected">' );
4393  $out->addWikiMsg( 'spamprotectiontext' );
4394  if ( $match ) {
4395  $out->addWikiMsg( 'spamprotectionmatch', wfEscapeWikiText( $match ) );
4396  }
4397  $out->addHTML( '</div>' );
4398 
4399  $out->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
4400  $this->showDiff();
4401 
4402  $out->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
4403  $this->showTextbox2();
4404 
4405  $out->addReturnTo( $this->getContextTitle(), [ 'action' => 'edit' ] );
4406  }
4407 
4411  protected function addEditNotices() {
4412  $out = $this->context->getOutput();
4413  $editNotices = $this->mTitle->getEditNotices( $this->oldid );
4414  if ( count( $editNotices ) ) {
4415  $out->addHTML( implode( "\n", $editNotices ) );
4416  } else {
4417  $msg = $this->context->msg( 'editnotice-notext' );
4418  if ( !$msg->isDisabled() ) {
4419  $out->addHTML(
4420  '<div class="mw-editnotice-notext">'
4421  . $msg->parseAsBlock()
4422  . '</div>'
4423  );
4424  }
4425  }
4426  }
4427 
4431  protected function addTalkPageText() {
4432  if ( $this->mTitle->isTalkPage() ) {
4433  $this->context->getOutput()->addWikiMsg( 'talkpagetext' );
4434  }
4435  }
4436 
4440  protected function addLongPageWarningHeader() {
4441  if ( $this->contentLength === false ) {
4442  $this->contentLength = strlen( $this->textbox1 );
4443  }
4444 
4445  $out = $this->context->getOutput();
4446  $lang = $this->context->getLanguage();
4447  $maxArticleSize = $this->context->getConfig()->get( 'MaxArticleSize' );
4448  if ( $this->tooBig || $this->contentLength > $maxArticleSize * 1024 ) {
4449  $out->wrapWikiMsg( "<div class='error' id='mw-edit-longpageerror'>\n$1\n</div>",
4450  [
4451  'longpageerror',
4452  $lang->formatNum( round( $this->contentLength / 1024, 3 ) ),
4453  $lang->formatNum( $maxArticleSize )
4454  ]
4455  );
4456  } elseif ( !$this->context->msg( 'longpage-hint' )->isDisabled() ) {
4457  $out->wrapWikiMsg( "<div id='mw-edit-longpage-hint'>\n$1\n</div>",
4458  [
4459  'longpage-hint',
4460  $lang->formatSize( strlen( $this->textbox1 ) ),
4461  strlen( $this->textbox1 )
4462  ]
4463  );
4464  }
4465  }
4466 
4470  protected function addPageProtectionWarningHeaders() {
4471  $out = $this->context->getOutput();
4472  if ( $this->mTitle->isProtected( 'edit' ) &&
4473  MediaWikiServices::getInstance()->getPermissionManager()->getNamespaceRestrictionLevels(
4474  $this->getTitle()->getNamespace()
4475  ) !== [ '' ]
4476  ) {
4477  # Is the title semi-protected?
4478  if ( $this->mTitle->isSemiProtected() ) {
4479  $noticeMsg = 'semiprotectedpagewarning';
4480  } else {
4481  # Then it must be protected based on static groups (regular)
4482  $noticeMsg = 'protectedpagewarning';
4483  }
4484  LogEventsList::showLogExtract( $out, 'protect', $this->mTitle, '',
4485  [ 'lim' => 1, 'msgKey' => [ $noticeMsg ] ] );
4486  }
4487  if ( $this->mTitle->isCascadeProtected() ) {
4488  # Is this page under cascading protection from some source pages?
4489 
4490  list( $cascadeSources, /* $restrictions */ ) = $this->mTitle->getCascadeProtectionSources();
4491  $notice = "<div class='mw-cascadeprotectedwarning'>\n$1\n";
4492  $cascadeSourcesCount = count( $cascadeSources );
4493  if ( $cascadeSourcesCount > 0 ) {
4494  # Explain, and list the titles responsible
4495  foreach ( $cascadeSources as $page ) {
4496  $notice .= '* [[:' . $page->getPrefixedText() . "]]\n";
4497  }
4498  }
4499  $notice .= '</div>';
4500  $out->wrapWikiMsg( $notice, [ 'cascadeprotectedwarning', $cascadeSourcesCount ] );
4501  }
4502  if ( !$this->mTitle->exists() && $this->mTitle->getRestrictions( 'create' ) ) {
4503  LogEventsList::showLogExtract( $out, 'protect', $this->mTitle, '',
4504  [ 'lim' => 1,
4505  'showIfEmpty' => false,
4506  'msgKey' => [ 'titleprotectedwarning' ],
4507  'wrap' => "<div class=\"mw-titleprotectedwarning\">\n$1</div>" ] );
4508  }
4509  }
4510 
4515  protected function addExplainConflictHeader( OutputPage $out ) {
4516  $out->addHTML(
4517  $this->getEditConflictHelper()->getExplainHeader()
4518  );
4519  }
4520 
4528  protected function buildTextboxAttribs( $name, array $customAttribs, User $user ) {
4529  return ( new TextboxBuilder() )->buildTextboxAttribs(
4530  $name, $customAttribs, $user, $this->mTitle
4531  );
4532  }
4533 
4539  protected function addNewLineAtEnd( $wikitext ) {
4540  return ( new TextboxBuilder() )->addNewLineAtEnd( $wikitext );
4541  }
4542 
4553  private function guessSectionName( $text ) {
4554  // Detect Microsoft browsers
4555  $userAgent = $this->context->getRequest()->getHeader( 'User-Agent' );
4556  $parser = MediaWikiServices::getInstance()->getParser();
4557  if ( $userAgent && preg_match( '/MSIE|Edge/', $userAgent ) ) {
4558  // ...and redirect them to legacy encoding, if available
4559  return $parser->guessLegacySectionNameFromWikiText( $text );
4560  }
4561  // Meanwhile, real browsers get real anchors
4562  $name = $parser->guessSectionNameFromWikiText( $text );
4563  // With one little caveat: per T216029, fragments in HTTP redirects need to be urlencoded,
4564  // otherwise Chrome double-escapes the rest of the URL.
4565  return '#' . urlencode( mb_substr( $name, 1 ) );
4566  }
4567 
4574  public function setEditConflictHelperFactory( callable $factory ) {
4575  $this->editConflictHelperFactory = $factory;
4576  $this->editConflictHelper = null;
4577  }
4578 
4582  private function getEditConflictHelper() {
4583  if ( !$this->editConflictHelper ) {
4584  $this->editConflictHelper = call_user_func(
4585  $this->editConflictHelperFactory,
4586  $this->getSubmitButtonLabel()
4587  );
4588  }
4589 
4591  }
4592 
4597  private function newTextConflictHelper( $submitButtonLabel ) {
4598  return new TextConflictHelper(
4599  $this->getTitle(),
4600  $this->getContext()->getOutput(),
4601  MediaWikiServices::getInstance()->getStatsdDataFactory(),
4602  $submitButtonLabel
4603  );
4604  }
4605 }
string $autoSumm
Definition: EditPage.php:301
Helps EditPage build textboxes.
static factory(Title $title)
Create a WikiPage object of the appropriate class for the given title.
Definition: WikiPage.php:142
displayPermissionsError(array $permErrors)
Display a permissions error page, like OutputPage::showPermissionsErrorPage(), but with the following...
Definition: EditPage.php:750
incrementConflictStats()
Definition: EditPage.php:3799
bool $nosummary
If true, hide the summary field.
Definition: EditPage.php:352
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition: router.php:42
getPreloadedContent( $preload, $params=[])
Get the contents to be preloaded into the box, either set by an earlier setPreloadText() or by loadin...
Definition: EditPage.php:1483
$editFormTextBottom
Definition: EditPage.php:425
$response
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking, formatting, etc.
spamPageWithContent( $match=false)
Show "your edit contains spam" page with your diff and text.
Definition: EditPage.php:4383
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
string $textbox2
Definition: EditPage.php:344
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:3420
bool $allowBlankSummary
Definition: EditPage.php:286
fatal( $message,... $parameters)
Add an error and set OK to false, indicating that the operation as a whole was fatal.
getPreviewText()
Get the rendered text for previewing.
Definition: EditPage.php:3917
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:1191
handleStatus(Status $status, $resultDetails)
Handle status, such as after attempt save.
Definition: EditPage.php:1640
string $summary
Definition: EditPage.php:347
setHeaders()
Definition: EditPage.php:2526
WikiPage $page
Definition: EditPage.php:226
static matchSpamRegexInternal( $text, $regexes)
Definition: EditPage.php:2516
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:891
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
showTextbox2()
Definition: EditPage.php:3460
bool $tooBig
Definition: EditPage.php:277
showHeaderCopyrightWarning()
Show the header copyright warning.
Definition: EditPage.php:3609
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
if(!isset( $args[0])) $lang
static getCopyrightWarning( $title, $format='plain', $langcode=null)
Get the copyright warning, by default returns wikitext.
Definition: EditPage.php:3664
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:3625
An IContextSource implementation which will inherit context from another source but allow individual ...
The edit page/HTML interface (split from Article) The actual database and text munging is still in Ar...
Definition: EditPage.php: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:2177
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:296
getSummaryInputAttributes(array $inputAttrs=null)
Helper function for summary input functions, which returns the necessary attributes for the input...
Definition: EditPage.php:3280
getEditButtons(&$tabindex)
Returns an array of html code of the following buttons: save, diff and preview.
Definition: EditPage.php:4295
static newFromUserAndLang(User $user, Language $lang)
Get a ParserOptions object from a given user and language.
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:2749
IContextSource $context
Definition: EditPage.php:450
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:176
bool $blankArticle
Definition: EditPage.php:289
buildTextboxAttribs( $name, array $customAttribs, User $user)
Definition: EditPage.php:4528
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
getEditConflictMainTextBox(array $customAttribs=[])
HTML to build the textbox1 on edit conflicts.
bool $missingComment
Definition: EditPage.php:280
const EDIT_MINOR
Definition: Defines.php:134
getEditPermissionErrors( $rigor='secure')
Definition: EditPage.php:706
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:1102
showFormBeforeText()
Definition: EditPage.php:3383
internalAttemptSave(&$result, $bot=false)
Attempt submission (no UI)
Definition: EditPage.php:1892
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
addPageProtectionWarningHeaders()
Definition: EditPage.php:4470
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:4226
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
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:2428
const DB_MASTER
Definition: defines.php:26
displayPreviewArea( $previewOutput, $isOnTop=false)
Definition: EditPage.php:3478
addEditNotices()
Definition: EditPage.php:4411
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:634
null Title $mContextTitle
Definition: EditPage.php:235
static textarea( $name, $value='', array $attribs=[])
Convenience function to produce a <textarea> element.
Definition: Html.php:818
getEditFormHtmlBeforeContent()
Content to go in the edit form before textbox1.
int $editRevId
Revision ID of the latest revision of the page when editing was initiated on the client.
Definition: EditPage.php:371
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
getContext()
Gets the context this Article is executed in.
Definition: Article.php:2267
addExplainConflictHeader(OutputPage $out)
Definition: EditPage.php:4515
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:1605
showIntro()
Show all applicable editing introductions.
Definition: EditPage.php:2591
setWikiPage(WikiPage $wikiPage)
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
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:1406
addLongPageWarningHeader()
Definition: EditPage.php:4440
getTemplates()
Definition: EditPage.php:4129
bool $save
Definition: EditPage.php:321
wfReadOnly()
Check whether the wiki is in read-only mode.
$wgLang
Definition: Setup.php:857
static newMigration()
Static constructor.
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:2777
static getForModelID( $modelId)
Returns the ContentHandler singleton for the given model ID.
const AS_SPAM_ERROR
Status: summary contained spam according to one of the regexes in $wgSummarySpamRegex.
Definition: EditPage.php: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:3692
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:3869
getActionURL(Title $title)
Returns the URL to use in the form&#39;s action attribute.
Definition: EditPage.php:3833
static doWatchOrUnwatch( $watch, Title $title, User $user)
Watch or unwatch a page.
Definition: WatchAction.php:91
$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
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:1468
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
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:3095
bool $isOldRev
Whether an old revision is edited.
Definition: EditPage.php:455
showHeader()
Definition: EditPage.php:3105
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:2383
getTitle()
Get the title object of the article.
Definition: Article.php:221
const IGNORE_USER_RIGHTS
Definition: User.php:83
static tags( $element, $attribs, $contents)
Same as Xml::element(), but does not escape contents.
Definition: Xml.php:130
static closeElement( $element)
Returns "</$element>".
Definition: Html.php:315
string $edittime
Timestamp of the latest revision of the page when editing was initiated on the client.
Definition: EditPage.php:358
showSummaryInput( $isSubjectPreview, $summary="")
Definition: EditPage.php:3336
showEditForm( $formCallback=null)
Send the edit form and related headers to OutputPage.
Definition: EditPage.php:2800
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:278
initialiseForm()
Initialise form fields in the object Called on the first invocation, e.g.
Definition: EditPage.php:1134
wasDeletedSinceLastEdit()
Check if a page was deleted while the user was editing it, before submit.
Definition: EditPage.php:3844
isSectionEditSupported()
Returns whether section editing is supported for the current page.
Definition: EditPage.php:895
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:1549
showFormAfterText()
Definition: EditPage.php:3392
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:3547
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:833
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:3652
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.
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
$wgDisableAnonTalk
Disable links to talk pages of anonymous users (IPs) in listings on special pages like page history...
static getForTitle(Title $title)
Returns the appropriate ContentHandler singleton for the given title.
getPreviewParserOptions()
Get parser options for a preview.
Definition: EditPage.php:4082
getCancelLink()
Definition: EditPage.php:3806
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:3432
incrementEditFailureStats( $failureType)
Definition: EditPage.php:4073
const AS_END
Status: WikiPage::doEdit() was unsuccessful.
Definition: EditPage.php:143
getSummaryPreview( $isSubjectPreview, $summary="")
Definition: EditPage.php:3362
makeTemplatesOnThisPageList(array $templates)
Wrapper around TemplatesOnThisPageFormatter to make a "templates on this page" list.
Definition: EditPage.php:3071
showPreview( $text)
Append preview output to OutputPage.
Definition: EditPage.php:3526
string $textbox1
Page content input field.
Definition: EditPage.php:341
static warningBox( $html, $className='')
Return a warning box.
Definition: Html.php:726
CONTENT_MODEL_JAVASCRIPT
Allow users to upload files.
const DELETED_USER
Definition: LogPage.php:36
importFormData(&$request)
This function collects the form data and uses it to populate various member variables.
Definition: EditPage.php:905
noSuchSectionPage()
Creates a basic error page which informs the user that they have attempted to edit a nonexistent sect...
Definition: EditPage.php:4364
$wgSummarySpamRegex
Same as the above except for edit summaries.
displayViewSourcePage(Content $content, $errorMessage='')
Display a read-only View Source page.
Definition: EditPage.php:780
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:586
ParserOutput $mParserOutput
Definition: EditPage.php:307
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
bool $minoredit
Definition: EditPage.php:330
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:4105
static matchSpamRegex( $text)
Check given input text against $wgSpamRegex, and return the text of the first match.
Definition: EditPage.php:2491
string $action
Definition: EditPage.php:238
newTextConflictHelper( $submitButtonLabel)
Definition: EditPage.php:4597
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:2715
static matchSummarySpamRegex( $text)
Check given input text against $wgSummarySpamRegex, and return the text of the first match...
Definition: EditPage.php:2505
importContentFormData(&$request)
Subpage overridable method for extracting the page content data from the posted form to be placed in ...
Definition: EditPage.php:1125
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:1844
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:2474
static hasDifferencesOutsideMainSlot(Revision $a, Revision $b)
Helper method for checking whether two revisions have differences that go beyond the main slot...
Definition: WikiPage.php:1537
addNewLineAtEnd( $wikitext)
Definition: EditPage.php:4539
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:1774
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:1623
getCurrentContent()
Get the current content of the page.
Definition: EditPage.php:1422
updateWatchlist()
Register the change of watch status.
Definition: EditPage.php:2400
static hidden( $name, $value, array $attribs=[])
Convenience function to produce an input element with type=hidden.
Definition: Html.php:802
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
string $hookError
Definition: EditPage.php:304
showEditTools()
Inserts optional text shown below edit and upload forms.
Definition: EditPage.php:3640
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:4180
showStandardInputs(&$tabindex=2)
Definition: EditPage.php:3738
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:820
Show an error when a user tries to do something they do not have the necessary permissions for...
static element( $element, $attribs=null, $contents='', $allowShortTag=true)
Format an XML element with given attributes and, optionally, text content.
Definition: Xml.php:41
getErrors()
Get the list of errors.
setPostEditCookie( $statusValue)
Sets post-edit cookie indicating the user just saved a particular revision.
Definition: EditPage.php:1584
getRedirectTarget()
If this page is a redirect, get its target.
Definition: WikiPage.php:999
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:3788
addTalkPageText()
Definition: EditPage.php:4431
bool $hasPresetSummary
Has a summary been preset using GET parameter &summary= ?
Definition: EditPage.php:310
getEditConflictHelper()
Definition: EditPage.php:4582
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
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:4574
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:3303
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:1230
$content
Definition: router.php:78
formatStatusErrors(Status $status)
Wrap status errors in an errorbox for increased visibility.
Definition: EditPage.php:1824
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:58
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
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
setApiEditOverride( $enableOverride)
Allow editing of content that supports API direct editing, but not general direct editing...
Definition: EditPage.php:566
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition: User.php:515
$suppressIntro
Definition: EditPage.php:434
getOriginalContent(User $user)
Get the content of the wanted revision, without section extraction.
Definition: EditPage.php:1381
static newFromId( $id, $flags=0)
Load a page revision from a given revision ID number.
Definition: Revision.php:119
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.
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:4271
showTextbox( $text, $name, $customAttribs=[])
Definition: EditPage.php:3464
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:1102
$editFormTextBeforeContent
Definition: EditPage.php:422
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:874
Show an error when the user hits a rate limit.
return true
Definition: router.php:92
static getEditToolbar()
Allow extensions to provide a toolbar.
Definition: EditPage.php:4151
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
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:1562
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:4553
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:319