MediaWiki  master
EditPage.php
Go to the documentation of this file.
1 <?php
28 
44 class EditPage {
48  const UNICODE_CHECK = 'ℳ𝒲♥𝓊𝓃𝒾𝒸ℴ𝒹ℯ';
49 
53  const AS_SUCCESS_UPDATE = 200;
54 
59 
63  const AS_HOOK_ERROR = 210;
64 
69 
74 
78  const AS_CONTENT_TOO_BIG = 216;
79 
84 
89 
93  const AS_READ_ONLY_PAGE = 220;
94 
98  const AS_RATE_LIMITED = 221;
99 
105 
111 
115  const AS_BLANK_ARTICLE = 224;
116 
120  const AS_CONFLICT_DETECTED = 225;
121 
126  const AS_SUMMARY_NEEDED = 226;
127 
131  const AS_TEXTBOX_EMPTY = 228;
132 
137 
141  const AS_END = 231;
142 
146  const AS_SPAM_ERROR = 232;
147 
152 
157 
163 
168  const AS_SELF_REDIRECT = 236;
169 
174  const AS_CHANGE_TAG_ERROR = 237;
175 
179  const AS_PARSE_ERROR = 240;
180 
186 
191 
195  const EDITFORM_ID = 'editform';
196 
201  const POST_EDIT_COOKIE_KEY_PREFIX = 'PostEditRevision';
202 
217 
222  public $mArticle;
224  private $page;
225 
230  public $mTitle;
231 
233  private $mContextTitle = null;
234 
236  public $action = 'submit';
237 
239  public $isConflict = false;
240 
242  public $isNew = false;
243 
246 
248  public $formtype;
249 
251  public $firsttime;
252 
254  public $lastDelete;
255 
257  public $mTokenOk = false;
258 
260  public $mTokenOkExceptSuffix = false;
261 
263  public $mTriedSave = false;
264 
266  public $incompleteForm = false;
267 
269  public $tooBig = false;
270 
272  public $missingComment = false;
273 
275  public $missingSummary = false;
276 
278  public $allowBlankSummary = false;
279 
281  protected $blankArticle = false;
282 
284  protected $allowBlankArticle = false;
285 
287  protected $selfRedirect = false;
288 
290  protected $allowSelfRedirect = false;
291 
293  public $autoSumm = '';
294 
296  public $hookError = '';
297 
300 
302  public $hasPresetSummary = false;
303 
305  public $mBaseRevision = false;
306 
308  public $mShowSummaryField = true;
309 
310  # Form values
311 
313  public $save = false;
314 
316  public $preview = false;
317 
319  public $diff = false;
320 
322  public $minoredit = false;
323 
325  public $watchthis = false;
326 
328  public $recreate = false;
329 
331  public $textbox1 = '';
332 
334  public $textbox2 = '';
335 
337  public $summary = '';
338 
340  public $nosummary = false;
341 
343  public $edittime = '';
344 
346  private $editRevId = null;
347 
349  public $section = '';
350 
352  public $sectiontitle = '';
353 
355  public $starttime = '';
356 
358  public $oldid = 0;
359 
361  public $parentRevId = 0;
362 
364  public $editintro = '';
365 
367  public $scrolltop = null;
368 
370  public $bot = true;
371 
374 
376  public $contentFormat = null;
377 
379  private $changeTags = null;
380 
381  # Placeholders for text injection by hooks (must be HTML)
382  # extensions should take care to _append_ to the present value
383 
385  public $editFormPageTop = '';
386  public $editFormTextTop = '';
390  public $editFormTextBottom = '';
393  public $mPreloadContent = null;
394 
395  /* $didSave should be set to true whenever an article was successfully altered. */
396  public $didSave = false;
397  public $undidRev = 0;
398 
399  public $suppressIntro = false;
400 
402  protected $edit;
403 
405  protected $contentLength = false;
406 
410  private $enableApiEditOverride = false;
411 
415  protected $context;
416 
420  private $isOldRev = false;
421 
425  private $unicodeCheck;
426 
433 
438 
442  public function __construct( Article $article ) {
443  $this->mArticle = $article;
444  $this->page = $article->getPage(); // model object
445  $this->mTitle = $article->getTitle();
446  $this->context = $article->getContext();
447 
448  $this->contentModel = $this->mTitle->getContentModel();
449 
450  $handler = ContentHandler::getForModelID( $this->contentModel );
451  $this->contentFormat = $handler->getDefaultFormat();
452  $this->editConflictHelperFactory = [ $this, 'newTextConflictHelper' ];
453  }
454 
458  public function getArticle() {
459  return $this->mArticle;
460  }
461 
466  public function getContext() {
467  return $this->context;
468  }
469 
474  public function getTitle() {
475  return $this->mTitle;
476  }
477 
483  public function setContextTitle( $title ) {
484  $this->mContextTitle = $title;
485  }
486 
494  public function getContextTitle() {
495  if ( is_null( $this->mContextTitle ) ) {
496  wfDebugLog(
497  'GlobalTitleFail',
498  __METHOD__ . ' called by ' . wfGetAllCallers( 5 ) . ' with no title set.'
499  );
501  return $wgTitle;
502  } else {
503  return $this->mContextTitle;
504  }
505  }
506 
514  public function isSupportedContentModel( $modelId ) {
515  return $this->enableApiEditOverride === true ||
516  ContentHandler::getForModelID( $modelId )->supportsDirectEditing();
517  }
518 
525  public function setApiEditOverride( $enableOverride ) {
526  $this->enableApiEditOverride = $enableOverride;
527  }
528 
532  public function submit() {
533  wfDeprecated( __METHOD__, '1.29' );
534  $this->edit();
535  }
536 
548  public function edit() {
549  // Allow extensions to modify/prevent this form or submission
550  if ( !Hooks::run( 'AlternateEdit', [ $this ] ) ) {
551  return;
552  }
553 
554  wfDebug( __METHOD__ . ": enter\n" );
555 
556  $request = $this->context->getRequest();
557  // If they used redlink=1 and the page exists, redirect to the main article
558  if ( $request->getBool( 'redlink' ) && $this->mTitle->exists() ) {
559  $this->context->getOutput()->redirect( $this->mTitle->getFullURL() );
560  return;
561  }
562 
563  $this->importFormData( $request );
564  $this->firsttime = false;
565 
566  if ( wfReadOnly() && $this->save ) {
567  // Force preview
568  $this->save = false;
569  $this->preview = true;
570  }
571 
572  if ( $this->save ) {
573  $this->formtype = 'save';
574  } elseif ( $this->preview ) {
575  $this->formtype = 'preview';
576  } elseif ( $this->diff ) {
577  $this->formtype = 'diff';
578  } else { # First time through
579  $this->firsttime = true;
580  if ( $this->previewOnOpen() ) {
581  $this->formtype = 'preview';
582  } else {
583  $this->formtype = 'initial';
584  }
585  }
586 
587  $permErrors = $this->getEditPermissionErrors( $this->save ? 'secure' : 'full' );
588  if ( $permErrors ) {
589  wfDebug( __METHOD__ . ": User can't edit\n" );
590  // Auto-block user's IP if the account was "hard" blocked
591  if ( !wfReadOnly() ) {
593  $this->context->getUser()->spreadAnyEditBlock();
594  } );
595  }
596  $this->displayPermissionsError( $permErrors );
597 
598  return;
599  }
600 
601  $revision = $this->mArticle->getRevisionFetched();
602  // Disallow editing revisions with content models different from the current one
603  // Undo edits being an exception in order to allow reverting content model changes.
604  if ( $revision
605  && $revision->getContentModel() !== $this->contentModel
606  ) {
607  $prevRev = null;
608  if ( $this->undidRev ) {
609  $undidRevObj = Revision::newFromId( $this->undidRev );
610  $prevRev = $undidRevObj ? $undidRevObj->getPrevious() : null;
611  }
612  if ( !$this->undidRev
613  || !$prevRev
614  || $prevRev->getContentModel() !== $this->contentModel
615  ) {
616  $this->displayViewSourcePage(
617  $this->getContentObject(),
618  $this->context->msg(
619  'contentmodelediterror',
620  $revision->getContentModel(),
622  )->plain()
623  );
624  return;
625  }
626  }
627 
628  $this->isConflict = false;
629 
630  # Show applicable editing introductions
631  if ( $this->formtype == 'initial' || $this->firsttime ) {
632  $this->showIntro();
633  }
634 
635  # Attempt submission here. This will check for edit conflicts,
636  # and redundantly check for locked database, blocked IPs, etc.
637  # that edit() already checked just in case someone tries to sneak
638  # in the back door with a hand-edited submission URL.
639 
640  if ( 'save' == $this->formtype ) {
641  $resultDetails = null;
642  $status = $this->attemptSave( $resultDetails );
643  if ( !$this->handleStatus( $status, $resultDetails ) ) {
644  return;
645  }
646  }
647 
648  # First time through: get contents, set time for conflict
649  # checking, etc.
650  if ( 'initial' == $this->formtype || $this->firsttime ) {
651  if ( $this->initialiseForm() === false ) {
652  $this->noSuchSectionPage();
653  return;
654  }
655 
656  if ( !$this->mTitle->getArticleID() ) {
657  Hooks::run( 'EditFormPreloadText', [ &$this->textbox1, &$this->mTitle ] );
658  } else {
659  Hooks::run( 'EditFormInitialText', [ $this ] );
660  }
661 
662  }
663 
664  $this->showEditForm();
665  }
666 
671  protected function getEditPermissionErrors( $rigor = 'secure' ) {
672  $user = $this->context->getUser();
673  $permErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $user, $rigor );
674  # Can this title be created?
675  if ( !$this->mTitle->exists() ) {
676  $permErrors = array_merge(
677  $permErrors,
678  wfArrayDiff2(
679  $this->mTitle->getUserPermissionsErrors( 'create', $user, $rigor ),
680  $permErrors
681  )
682  );
683  }
684  # Ignore some permissions errors when a user is just previewing/viewing diffs
685  $remove = [];
686  foreach ( $permErrors as $error ) {
687  if ( ( $this->preview || $this->diff )
688  && (
689  $error[0] == 'blockedtext' ||
690  $error[0] == 'autoblockedtext' ||
691  $error[0] == 'systemblockedtext'
692  )
693  ) {
694  $remove[] = $error;
695  }
696  }
697  $permErrors = wfArrayDiff2( $permErrors, $remove );
698 
699  return $permErrors;
700  }
701 
715  protected function displayPermissionsError( array $permErrors ) {
716  $out = $this->context->getOutput();
717  if ( $this->context->getRequest()->getBool( 'redlink' ) ) {
718  // The edit page was reached via a red link.
719  // Redirect to the article page and let them click the edit tab if
720  // they really want a permission error.
721  $out->redirect( $this->mTitle->getFullURL() );
722  return;
723  }
724 
725  $content = $this->getContentObject();
726 
727  # Use the normal message if there's nothing to display
728  if ( $this->firsttime && ( !$content || $content->isEmpty() ) ) {
729  $action = $this->mTitle->exists() ? 'edit' :
730  ( $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage' );
731  throw new PermissionsError( $action, $permErrors );
732  }
733 
734  $this->displayViewSourcePage(
735  $content,
736  $out->formatPermissionsErrorMessage( $permErrors, 'edit' )
737  );
738  }
739 
745  protected function displayViewSourcePage( Content $content, $errorMessage = '' ) {
746  $out = $this->context->getOutput();
747  Hooks::run( 'EditPage::showReadOnlyForm:initial', [ $this, &$out ] );
748 
749  $out->setRobotPolicy( 'noindex,nofollow' );
750  $out->setPageTitle( $this->context->msg(
751  'viewsource-title',
752  $this->getContextTitle()->getPrefixedText()
753  ) );
754  $out->addBacklinkSubtitle( $this->getContextTitle() );
755  $out->addHTML( $this->editFormPageTop );
756  $out->addHTML( $this->editFormTextTop );
757 
758  if ( $errorMessage !== '' ) {
759  $out->addWikiText( $errorMessage );
760  $out->addHTML( "<hr />\n" );
761  }
762 
763  # If the user made changes, preserve them when showing the markup
764  # (This happens when a user is blocked during edit, for instance)
765  if ( !$this->firsttime ) {
766  $text = $this->textbox1;
767  $out->addWikiMsg( 'viewyourtext' );
768  } else {
769  try {
770  $text = $this->toEditText( $content );
771  } catch ( MWException $e ) {
772  # Serialize using the default format if the content model is not supported
773  # (e.g. for an old revision with a different model)
774  $text = $content->serialize();
775  }
776  $out->addWikiMsg( 'viewsourcetext' );
777  }
778 
779  $out->addHTML( $this->editFormTextBeforeContent );
780  $this->showTextbox( $text, 'wpTextbox1', [ 'readonly' ] );
781  $out->addHTML( $this->editFormTextAfterContent );
782 
783  $out->addHTML( $this->makeTemplatesOnThisPageList( $this->getTemplates() ) );
784 
785  $out->addModules( 'mediawiki.action.edit.collapsibleFooter' );
786 
787  $out->addHTML( $this->editFormTextBottom );
788  if ( $this->mTitle->exists() ) {
789  $out->returnToMain( null, $this->mTitle );
790  }
791  }
792 
798  protected function previewOnOpen() {
799  $config = $this->context->getConfig();
800  $previewOnOpenNamespaces = $config->get( 'PreviewOnOpenNamespaces' );
801  $request = $this->context->getRequest();
802  if ( $config->get( 'RawHtml' ) ) {
803  // If raw HTML is enabled, disable preview on open
804  // since it has to be posted with a token for
805  // security reasons
806  return false;
807  }
808  if ( $request->getVal( 'preview' ) == 'yes' ) {
809  // Explicit override from request
810  return true;
811  } elseif ( $request->getVal( 'preview' ) == 'no' ) {
812  // Explicit override from request
813  return false;
814  } elseif ( $this->section == 'new' ) {
815  // Nothing *to* preview for new sections
816  return false;
817  } elseif ( ( $request->getVal( 'preload' ) !== null || $this->mTitle->exists() )
818  && $this->context->getUser()->getOption( 'previewonfirst' )
819  ) {
820  // Standard preference behavior
821  return true;
822  } elseif ( !$this->mTitle->exists()
823  && isset( $previewOnOpenNamespaces[$this->mTitle->getNamespace()] )
824  && $previewOnOpenNamespaces[$this->mTitle->getNamespace()]
825  ) {
826  // Categories are special
827  return true;
828  } else {
829  return false;
830  }
831  }
832 
839  protected function isWrongCaseUserConfigPage() {
840  if ( $this->mTitle->isUserConfigPage() ) {
841  $name = $this->mTitle->getSkinFromConfigSubpage();
842  $skins = array_merge(
843  array_keys( Skin::getSkinNames() ),
844  [ 'common' ]
845  );
846  return !in_array( $name, $skins )
847  && in_array( strtolower( $name ), $skins );
848  } else {
849  return false;
850  }
851  }
852 
860  protected function isSectionEditSupported() {
861  $contentHandler = ContentHandler::getForTitle( $this->mTitle );
862  return $contentHandler->supportsSections();
863  }
864 
870  public function importFormData( &$request ) {
871  # Section edit can come from either the form or a link
872  $this->section = $request->getVal( 'wpSection', $request->getVal( 'section' ) );
873 
874  if ( $this->section !== null && $this->section !== '' && !$this->isSectionEditSupported() ) {
875  throw new ErrorPageError( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
876  }
877 
878  $this->isNew = !$this->mTitle->exists() || $this->section == 'new';
879 
880  if ( $request->wasPosted() ) {
881  # These fields need to be checked for encoding.
882  # Also remove trailing whitespace, but don't remove _initial_
883  # whitespace from the text boxes. This may be significant formatting.
884  $this->textbox1 = rtrim( $request->getText( 'wpTextbox1' ) );
885  if ( !$request->getCheck( 'wpTextbox2' ) ) {
886  // Skip this if wpTextbox2 has input, it indicates that we came
887  // from a conflict page with raw page text, not a custom form
888  // modified by subclasses
890  if ( $textbox1 !== null ) {
891  $this->textbox1 = $textbox1;
892  }
893  }
894 
895  $this->unicodeCheck = $request->getText( 'wpUnicodeCheck' );
896 
897  $this->summary = $request->getText( 'wpSummary' );
898 
899  # If the summary consists of a heading, e.g. '==Foobar==', extract the title from the
900  # header syntax, e.g. 'Foobar'. This is mainly an issue when we are using wpSummary for
901  # section titles.
902  $this->summary = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->summary );
903 
904  # Treat sectiontitle the same way as summary.
905  # Note that wpSectionTitle is not yet a part of the actual edit form, as wpSummary is
906  # currently doing double duty as both edit summary and section title. Right now this
907  # is just to allow API edits to work around this limitation, but this should be
908  # incorporated into the actual edit form when EditPage is rewritten (Bugs 18654, 26312).
909  $this->sectiontitle = $request->getText( 'wpSectionTitle' );
910  $this->sectiontitle = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->sectiontitle );
911 
912  $this->edittime = $request->getVal( 'wpEdittime' );
913  $this->editRevId = $request->getIntOrNull( 'editRevId' );
914  $this->starttime = $request->getVal( 'wpStarttime' );
915 
916  $undidRev = $request->getInt( 'wpUndidRevision' );
917  if ( $undidRev ) {
918  $this->undidRev = $undidRev;
919  }
920 
921  $this->scrolltop = $request->getIntOrNull( 'wpScrolltop' );
922 
923  if ( $this->textbox1 === '' && $request->getVal( 'wpTextbox1' ) === null ) {
924  // wpTextbox1 field is missing, possibly due to being "too big"
925  // according to some filter rules such as Suhosin's setting for
926  // suhosin.request.max_value_length (d'oh)
927  $this->incompleteForm = true;
928  } else {
929  // If we receive the last parameter of the request, we can fairly
930  // claim the POST request has not been truncated.
931 
932  // TODO: softened the check for cutover. Once we determine
933  // that it is safe, we should complete the transition by
934  // removing the "edittime" clause.
935  $this->incompleteForm = ( !$request->getVal( 'wpUltimateParam' )
936  && is_null( $this->edittime ) );
937  }
938  if ( $this->incompleteForm ) {
939  # If the form is incomplete, force to preview.
940  wfDebug( __METHOD__ . ": Form data appears to be incomplete\n" );
941  wfDebug( "POST DATA: " . var_export( $request->getPostValues(), true ) . "\n" );
942  $this->preview = true;
943  } else {
944  $this->preview = $request->getCheck( 'wpPreview' );
945  $this->diff = $request->getCheck( 'wpDiff' );
946 
947  // Remember whether a save was requested, so we can indicate
948  // if we forced preview due to session failure.
949  $this->mTriedSave = !$this->preview;
950 
951  if ( $this->tokenOk( $request ) ) {
952  # Some browsers will not report any submit button
953  # if the user hits enter in the comment box.
954  # The unmarked state will be assumed to be a save,
955  # if the form seems otherwise complete.
956  wfDebug( __METHOD__ . ": Passed token check.\n" );
957  } elseif ( $this->diff ) {
958  # Failed token check, but only requested "Show Changes".
959  wfDebug( __METHOD__ . ": Failed token check; Show Changes requested.\n" );
960  } else {
961  # Page might be a hack attempt posted from
962  # an external site. Preview instead of saving.
963  wfDebug( __METHOD__ . ": Failed token check; forcing preview\n" );
964  $this->preview = true;
965  }
966  }
967  $this->save = !$this->preview && !$this->diff;
968  if ( !preg_match( '/^\d{14}$/', $this->edittime ) ) {
969  $this->edittime = null;
970  }
971 
972  if ( !preg_match( '/^\d{14}$/', $this->starttime ) ) {
973  $this->starttime = null;
974  }
975 
976  $this->recreate = $request->getCheck( 'wpRecreate' );
977 
978  $this->minoredit = $request->getCheck( 'wpMinoredit' );
979  $this->watchthis = $request->getCheck( 'wpWatchthis' );
980 
981  $user = $this->context->getUser();
982  # Don't force edit summaries when a user is editing their own user or talk page
983  if ( ( $this->mTitle->mNamespace == NS_USER || $this->mTitle->mNamespace == NS_USER_TALK )
984  && $this->mTitle->getText() == $user->getName()
985  ) {
986  $this->allowBlankSummary = true;
987  } else {
988  $this->allowBlankSummary = $request->getBool( 'wpIgnoreBlankSummary' )
989  || !$user->getOption( 'forceeditsummary' );
990  }
991 
992  $this->autoSumm = $request->getText( 'wpAutoSummary' );
993 
994  $this->allowBlankArticle = $request->getBool( 'wpIgnoreBlankArticle' );
995  $this->allowSelfRedirect = $request->getBool( 'wpIgnoreSelfRedirect' );
996 
997  $changeTags = $request->getVal( 'wpChangeTags' );
998  if ( is_null( $changeTags ) || $changeTags === '' ) {
999  $this->changeTags = [];
1000  } else {
1001  $this->changeTags = array_filter( array_map( 'trim', explode( ',',
1002  $changeTags ) ) );
1003  }
1004  } else {
1005  # Not a posted form? Start with nothing.
1006  wfDebug( __METHOD__ . ": Not a posted form.\n" );
1007  $this->textbox1 = '';
1008  $this->summary = '';
1009  $this->sectiontitle = '';
1010  $this->edittime = '';
1011  $this->editRevId = null;
1012  $this->starttime = wfTimestampNow();
1013  $this->edit = false;
1014  $this->preview = false;
1015  $this->save = false;
1016  $this->diff = false;
1017  $this->minoredit = false;
1018  // Watch may be overridden by request parameters
1019  $this->watchthis = $request->getBool( 'watchthis', false );
1020  $this->recreate = false;
1021 
1022  // When creating a new section, we can preload a section title by passing it as the
1023  // preloadtitle parameter in the URL (T15100)
1024  if ( $this->section == 'new' && $request->getVal( 'preloadtitle' ) ) {
1025  $this->sectiontitle = $request->getVal( 'preloadtitle' );
1026  // Once wpSummary isn't being use for setting section titles, we should delete this.
1027  $this->summary = $request->getVal( 'preloadtitle' );
1028  } elseif ( $this->section != 'new' && $request->getVal( 'summary' ) ) {
1029  $this->summary = $request->getText( 'summary' );
1030  if ( $this->summary !== '' ) {
1031  $this->hasPresetSummary = true;
1032  }
1033  }
1034 
1035  if ( $request->getVal( 'minor' ) ) {
1036  $this->minoredit = true;
1037  }
1038  }
1039 
1040  $this->oldid = $request->getInt( 'oldid' );
1041  $this->parentRevId = $request->getInt( 'parentRevId' );
1042 
1043  $this->bot = $request->getBool( 'bot', true );
1044  $this->nosummary = $request->getBool( 'nosummary' );
1045 
1046  // May be overridden by revision.
1047  $this->contentModel = $request->getText( 'model', $this->contentModel );
1048  // May be overridden by revision.
1049  $this->contentFormat = $request->getText( 'format', $this->contentFormat );
1050 
1051  try {
1052  $handler = ContentHandler::getForModelID( $this->contentModel );
1053  } catch ( MWUnknownContentModelException $e ) {
1054  throw new ErrorPageError(
1055  'editpage-invalidcontentmodel-title',
1056  'editpage-invalidcontentmodel-text',
1057  [ wfEscapeWikiText( $this->contentModel ) ]
1058  );
1059  }
1060 
1061  if ( !$handler->isSupportedFormat( $this->contentFormat ) ) {
1062  throw new ErrorPageError(
1063  'editpage-notsupportedcontentformat-title',
1064  'editpage-notsupportedcontentformat-text',
1065  [
1066  wfEscapeWikiText( $this->contentFormat ),
1067  wfEscapeWikiText( ContentHandler::getLocalizedName( $this->contentModel ) )
1068  ]
1069  );
1070  }
1071 
1078  $this->editintro = $request->getText( 'editintro',
1079  // Custom edit intro for new sections
1080  $this->section === 'new' ? 'MediaWiki:addsection-editintro' : '' );
1081 
1082  // Allow extensions to modify form data
1083  Hooks::run( 'EditPage::importFormData', [ $this, $request ] );
1084  }
1085 
1095  protected function importContentFormData( &$request ) {
1096  return; // Don't do anything, EditPage already extracted wpTextbox1
1097  }
1098 
1104  public function initialiseForm() {
1105  $this->edittime = $this->page->getTimestamp();
1106  $this->editRevId = $this->page->getLatest();
1107 
1108  $content = $this->getContentObject( false ); # TODO: track content object?!
1109  if ( $content === false ) {
1110  return false;
1111  }
1112  $this->textbox1 = $this->toEditText( $content );
1113 
1114  $user = $this->context->getUser();
1115  // activate checkboxes if user wants them to be always active
1116  # Sort out the "watch" checkbox
1117  if ( $user->getOption( 'watchdefault' ) ) {
1118  # Watch all edits
1119  $this->watchthis = true;
1120  } elseif ( $user->getOption( 'watchcreations' ) && !$this->mTitle->exists() ) {
1121  # Watch creations
1122  $this->watchthis = true;
1123  } elseif ( $user->isWatched( $this->mTitle ) ) {
1124  # Already watched
1125  $this->watchthis = true;
1126  }
1127  if ( $user->getOption( 'minordefault' ) && !$this->isNew ) {
1128  $this->minoredit = true;
1129  }
1130  if ( $this->textbox1 === false ) {
1131  return false;
1132  }
1133  return true;
1134  }
1135 
1143  protected function getContentObject( $def_content = null ) {
1145 
1146  $content = false;
1147 
1148  $user = $this->context->getUser();
1149  $request = $this->context->getRequest();
1150  // For message page not locally set, use the i18n message.
1151  // For other non-existent articles, use preload text if any.
1152  if ( !$this->mTitle->exists() || $this->section == 'new' ) {
1153  if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && $this->section != 'new' ) {
1154  # If this is a system message, get the default text.
1155  $msg = $this->mTitle->getDefaultMessageText();
1156 
1157  $content = $this->toEditContent( $msg );
1158  }
1159  if ( $content === false ) {
1160  # If requested, preload some text.
1161  $preload = $request->getVal( 'preload',
1162  // Custom preload text for new sections
1163  $this->section === 'new' ? 'MediaWiki:addsection-preload' : '' );
1164  $params = $request->getArray( 'preloadparams', [] );
1165 
1166  $content = $this->getPreloadedContent( $preload, $params );
1167  }
1168  // For existing pages, get text based on "undo" or section parameters.
1169  } else {
1170  if ( $this->section != '' ) {
1171  // Get section edit text (returns $def_text for invalid sections)
1172  $orig = $this->getOriginalContent( $user );
1173  $content = $orig ? $orig->getSection( $this->section ) : null;
1174 
1175  if ( !$content ) {
1176  $content = $def_content;
1177  }
1178  } else {
1179  $undoafter = $request->getInt( 'undoafter' );
1180  $undo = $request->getInt( 'undo' );
1181 
1182  if ( $undo > 0 && $undoafter > 0 ) {
1183  $undorev = Revision::newFromId( $undo );
1184  $oldrev = Revision::newFromId( $undoafter );
1185 
1186  # Sanity check, make sure it's the right page,
1187  # the revisions exist and they were not deleted.
1188  # Otherwise, $content will be left as-is.
1189  if ( !is_null( $undorev ) && !is_null( $oldrev ) &&
1190  !$undorev->isDeleted( Revision::DELETED_TEXT ) &&
1191  !$oldrev->isDeleted( Revision::DELETED_TEXT )
1192  ) {
1193  $content = $this->page->getUndoContent( $undorev, $oldrev );
1194 
1195  if ( $content === false ) {
1196  # Warn the user that something went wrong
1197  $undoMsg = 'failure';
1198  } else {
1199  $oldContent = $this->page->getContent( Revision::RAW );
1200  $popts = ParserOptions::newFromUserAndLang( $user, $wgContLang );
1201  $newContent = $content->preSaveTransform( $this->mTitle, $user, $popts );
1202  if ( $newContent->getModel() !== $oldContent->getModel() ) {
1203  // The undo may change content
1204  // model if its reverting the top
1205  // edit. This can result in
1206  // mismatched content model/format.
1207  $this->contentModel = $newContent->getModel();
1208  $this->contentFormat = $oldrev->getContentFormat();
1209  }
1210 
1211  if ( $newContent->equals( $oldContent ) ) {
1212  # Tell the user that the undo results in no change,
1213  # i.e. the revisions were already undone.
1214  $undoMsg = 'nochange';
1215  $content = false;
1216  } else {
1217  # Inform the user of our success and set an automatic edit summary
1218  $undoMsg = 'success';
1219 
1220  # If we just undid one rev, use an autosummary
1221  $firstrev = $oldrev->getNext();
1222  if ( $firstrev && $firstrev->getId() == $undo ) {
1223  $userText = $undorev->getUserText();
1224  if ( $userText === '' ) {
1225  $undoSummary = $this->context->msg(
1226  'undo-summary-username-hidden',
1227  $undo
1228  )->inContentLanguage()->text();
1229  } else {
1230  $undoSummary = $this->context->msg(
1231  'undo-summary',
1232  $undo,
1233  $userText
1234  )->inContentLanguage()->text();
1235  }
1236  if ( $this->summary === '' ) {
1237  $this->summary = $undoSummary;
1238  } else {
1239  $this->summary = $undoSummary . $this->context->msg( 'colon-separator' )
1240  ->inContentLanguage()->text() . $this->summary;
1241  }
1242  $this->undidRev = $undo;
1243  }
1244  $this->formtype = 'diff';
1245  }
1246  }
1247  } else {
1248  // Failed basic sanity checks.
1249  // Older revisions may have been removed since the link
1250  // was created, or we may simply have got bogus input.
1251  $undoMsg = 'norev';
1252  }
1253 
1254  $out = $this->context->getOutput();
1255  // Messages: undo-success, undo-failure, undo-norev, undo-nochange
1256  $class = ( $undoMsg == 'success' ? '' : 'error ' ) . "mw-undo-{$undoMsg}";
1257  $this->editFormPageTop .= $out->parse( "<div class=\"{$class}\">" .
1258  $this->context->msg( 'undo-' . $undoMsg )->plain() . '</div>', true, /* interface */true );
1259  }
1260 
1261  if ( $content === false ) {
1262  $content = $this->getOriginalContent( $user );
1263  }
1264  }
1265  }
1266 
1267  return $content;
1268  }
1269 
1285  private function getOriginalContent( User $user ) {
1286  if ( $this->section == 'new' ) {
1287  return $this->getCurrentContent();
1288  }
1289  $revision = $this->mArticle->getRevisionFetched();
1290  if ( $revision === null ) {
1291  $handler = ContentHandler::getForModelID( $this->contentModel );
1292  return $handler->makeEmptyContent();
1293  }
1294  $content = $revision->getContent( Revision::FOR_THIS_USER, $user );
1295  return $content;
1296  }
1297 
1310  public function getParentRevId() {
1311  if ( $this->parentRevId ) {
1312  return $this->parentRevId;
1313  } else {
1314  return $this->mArticle->getRevIdFetched();
1315  }
1316  }
1317 
1326  protected function getCurrentContent() {
1327  $rev = $this->page->getRevision();
1328  $content = $rev ? $rev->getContent( Revision::RAW ) : null;
1329 
1330  if ( $content === false || $content === null ) {
1331  $handler = ContentHandler::getForModelID( $this->contentModel );
1332  return $handler->makeEmptyContent();
1333  } elseif ( !$this->undidRev ) {
1334  // Content models should always be the same since we error
1335  // out if they are different before this point (in ->edit()).
1336  // The exception being, during an undo, the current revision might
1337  // differ from the prior revision.
1338  $logger = LoggerFactory::getInstance( 'editpage' );
1339  if ( $this->contentModel !== $rev->getContentModel() ) {
1340  $logger->warning( "Overriding content model from current edit {prev} to {new}", [
1341  'prev' => $this->contentModel,
1342  'new' => $rev->getContentModel(),
1343  'title' => $this->getTitle()->getPrefixedDBkey(),
1344  'method' => __METHOD__
1345  ] );
1346  $this->contentModel = $rev->getContentModel();
1347  }
1348 
1349  // Given that the content models should match, the current selected
1350  // format should be supported.
1351  if ( !$content->isSupportedFormat( $this->contentFormat ) ) {
1352  $logger->warning( "Current revision content format unsupported. Overriding {prev} to {new}", [
1353 
1354  'prev' => $this->contentFormat,
1355  'new' => $rev->getContentFormat(),
1356  'title' => $this->getTitle()->getPrefixedDBkey(),
1357  'method' => __METHOD__
1358  ] );
1359  $this->contentFormat = $rev->getContentFormat();
1360  }
1361  }
1362  return $content;
1363  }
1364 
1372  public function setPreloadedContent( Content $content ) {
1373  $this->mPreloadContent = $content;
1374  }
1375 
1387  protected function getPreloadedContent( $preload, $params = [] ) {
1388  if ( !empty( $this->mPreloadContent ) ) {
1389  return $this->mPreloadContent;
1390  }
1391 
1392  $handler = ContentHandler::getForModelID( $this->contentModel );
1393 
1394  if ( $preload === '' ) {
1395  return $handler->makeEmptyContent();
1396  }
1397 
1398  $user = $this->context->getUser();
1399  $title = Title::newFromText( $preload );
1400  # Check for existence to avoid getting MediaWiki:Noarticletext
1401  if ( $title === null || !$title->exists() || !$title->userCan( 'read', $user ) ) {
1402  // TODO: somehow show a warning to the user!
1403  return $handler->makeEmptyContent();
1404  }
1405 
1407  if ( $page->isRedirect() ) {
1409  # Same as before
1410  if ( $title === null || !$title->exists() || !$title->userCan( 'read', $user ) ) {
1411  // TODO: somehow show a warning to the user!
1412  return $handler->makeEmptyContent();
1413  }
1415  }
1416 
1417  $parserOptions = ParserOptions::newFromUser( $user );
1418  $content = $page->getContent( Revision::RAW );
1419 
1420  if ( !$content ) {
1421  // TODO: somehow show a warning to the user!
1422  return $handler->makeEmptyContent();
1423  }
1424 
1425  if ( $content->getModel() !== $handler->getModelID() ) {
1426  $converted = $content->convert( $handler->getModelID() );
1427 
1428  if ( !$converted ) {
1429  // TODO: somehow show a warning to the user!
1430  wfDebug( "Attempt to preload incompatible content: " .
1431  "can't convert " . $content->getModel() .
1432  " to " . $handler->getModelID() );
1433 
1434  return $handler->makeEmptyContent();
1435  }
1436 
1437  $content = $converted;
1438  }
1439 
1440  return $content->preloadTransform( $title, $parserOptions, $params );
1441  }
1442 
1450  public function tokenOk( &$request ) {
1451  $token = $request->getVal( 'wpEditToken' );
1452  $user = $this->context->getUser();
1453  $this->mTokenOk = $user->matchEditToken( $token );
1454  $this->mTokenOkExceptSuffix = $user->matchEditTokenNoSuffix( $token );
1455  return $this->mTokenOk;
1456  }
1457 
1472  protected function setPostEditCookie( $statusValue ) {
1473  $revisionId = $this->page->getLatest();
1474  $postEditKey = self::POST_EDIT_COOKIE_KEY_PREFIX . $revisionId;
1475 
1476  $val = 'saved';
1477  if ( $statusValue == self::AS_SUCCESS_NEW_ARTICLE ) {
1478  $val = 'created';
1479  } elseif ( $this->oldid ) {
1480  $val = 'restored';
1481  }
1482 
1483  $response = $this->context->getRequest()->response();
1484  $response->setCookie( $postEditKey, $val, time() + self::POST_EDIT_COOKIE_DURATION );
1485  }
1486 
1493  public function attemptSave( &$resultDetails = false ) {
1494  # Allow bots to exempt some edits from bot flagging
1495  $bot = $this->context->getUser()->isAllowed( 'bot' ) && $this->bot;
1496  $status = $this->internalAttemptSave( $resultDetails, $bot );
1497 
1498  Hooks::run( 'EditPage::attemptSave:after', [ $this, $status, $resultDetails ] );
1499 
1500  return $status;
1501  }
1502 
1506  private function incrementResolvedConflicts() {
1507  if ( $this->context->getRequest()->getText( 'mode' ) !== 'conflict' ) {
1508  return;
1509  }
1510 
1511  $this->getEditConflictHelper()->incrementResolvedStats();
1512  }
1513 
1523  private function handleStatus( Status $status, $resultDetails ) {
1528  if ( $status->value == self::AS_SUCCESS_UPDATE
1529  || $status->value == self::AS_SUCCESS_NEW_ARTICLE
1530  ) {
1531  $this->incrementResolvedConflicts();
1532 
1533  $this->didSave = true;
1534  if ( !$resultDetails['nullEdit'] ) {
1535  $this->setPostEditCookie( $status->value );
1536  }
1537  }
1538 
1539  $out = $this->context->getOutput();
1540 
1541  // "wpExtraQueryRedirect" is a hidden input to modify
1542  // after save URL and is not used by actual edit form
1543  $request = $this->context->getRequest();
1544  $extraQueryRedirect = $request->getVal( 'wpExtraQueryRedirect' );
1545 
1546  switch ( $status->value ) {
1547  case self::AS_HOOK_ERROR_EXPECTED:
1548  case self::AS_CONTENT_TOO_BIG:
1549  case self::AS_ARTICLE_WAS_DELETED:
1550  case self::AS_CONFLICT_DETECTED:
1551  case self::AS_SUMMARY_NEEDED:
1552  case self::AS_TEXTBOX_EMPTY:
1553  case self::AS_MAX_ARTICLE_SIZE_EXCEEDED:
1554  case self::AS_END:
1555  case self::AS_BLANK_ARTICLE:
1556  case self::AS_SELF_REDIRECT:
1557  return true;
1558 
1559  case self::AS_HOOK_ERROR:
1560  return false;
1561 
1562  case self::AS_CANNOT_USE_CUSTOM_MODEL:
1563  case self::AS_PARSE_ERROR:
1564  case self::AS_UNICODE_NOT_SUPPORTED:
1565  $out->addWikiText( '<div class="error">' . "\n" . $status->getWikiText() . '</div>' );
1566  return true;
1567 
1568  case self::AS_SUCCESS_NEW_ARTICLE:
1569  $query = $resultDetails['redirect'] ? 'redirect=no' : '';
1570  if ( $extraQueryRedirect ) {
1571  if ( $query === '' ) {
1572  $query = $extraQueryRedirect;
1573  } else {
1574  $query = $query . '&' . $extraQueryRedirect;
1575  }
1576  }
1577  $anchor = isset( $resultDetails['sectionanchor'] ) ? $resultDetails['sectionanchor'] : '';
1578  $out->redirect( $this->mTitle->getFullURL( $query ) . $anchor );
1579  return false;
1580 
1581  case self::AS_SUCCESS_UPDATE:
1582  $extraQuery = '';
1583  $sectionanchor = $resultDetails['sectionanchor'];
1584 
1585  // Give extensions a chance to modify URL query on update
1586  Hooks::run(
1587  'ArticleUpdateBeforeRedirect',
1588  [ $this->mArticle, &$sectionanchor, &$extraQuery ]
1589  );
1590 
1591  if ( $resultDetails['redirect'] ) {
1592  if ( $extraQuery == '' ) {
1593  $extraQuery = 'redirect=no';
1594  } else {
1595  $extraQuery = 'redirect=no&' . $extraQuery;
1596  }
1597  }
1598  if ( $extraQueryRedirect ) {
1599  if ( $extraQuery === '' ) {
1600  $extraQuery = $extraQueryRedirect;
1601  } else {
1602  $extraQuery = $extraQuery . '&' . $extraQueryRedirect;
1603  }
1604  }
1605 
1606  $out->redirect( $this->mTitle->getFullURL( $extraQuery ) . $sectionanchor );
1607  return false;
1608 
1609  case self::AS_SPAM_ERROR:
1610  $this->spamPageWithContent( $resultDetails['spam'] );
1611  return false;
1612 
1613  case self::AS_BLOCKED_PAGE_FOR_USER:
1614  throw new UserBlockedError( $this->context->getUser()->getBlock() );
1615 
1616  case self::AS_IMAGE_REDIRECT_ANON:
1617  case self::AS_IMAGE_REDIRECT_LOGGED:
1618  throw new PermissionsError( 'upload' );
1619 
1620  case self::AS_READ_ONLY_PAGE_ANON:
1621  case self::AS_READ_ONLY_PAGE_LOGGED:
1622  throw new PermissionsError( 'edit' );
1623 
1624  case self::AS_READ_ONLY_PAGE:
1625  throw new ReadOnlyError;
1626 
1627  case self::AS_RATE_LIMITED:
1628  throw new ThrottledError();
1629 
1630  case self::AS_NO_CREATE_PERMISSION:
1631  $permission = $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage';
1632  throw new PermissionsError( $permission );
1633 
1634  case self::AS_NO_CHANGE_CONTENT_MODEL:
1635  throw new PermissionsError( 'editcontentmodel' );
1636 
1637  default:
1638  // We don't recognize $status->value. The only way that can happen
1639  // is if an extension hook aborted from inside ArticleSave.
1640  // Render the status object into $this->hookError
1641  // FIXME this sucks, we should just use the Status object throughout
1642  $this->hookError = '<div class="error">' ."\n" . $status->getWikiText() .
1643  '</div>';
1644  return true;
1645  }
1646  }
1647 
1657  protected function runPostMergeFilters( Content $content, Status $status, User $user ) {
1658  // Run old style post-section-merge edit filter
1659  if ( $this->hookError != '' ) {
1660  # ...or the hook could be expecting us to produce an error
1661  $status->fatal( 'hookaborted' );
1662  $status->value = self::AS_HOOK_ERROR_EXPECTED;
1663  return false;
1664  }
1665 
1666  // Run new style post-section-merge edit filter
1667  if ( !Hooks::run( 'EditFilterMergedContent',
1668  [ $this->context, $content, $status, $this->summary,
1669  $user, $this->minoredit ] )
1670  ) {
1671  # Error messages etc. could be handled within the hook...
1672  if ( $status->isGood() ) {
1673  $status->fatal( 'hookaborted' );
1674  // Not setting $this->hookError here is a hack to allow the hook
1675  // to cause a return to the edit page without $this->hookError
1676  // being set. This is used by ConfirmEdit to display a captcha
1677  // without any error message cruft.
1678  } else {
1679  $this->hookError = $this->formatStatusErrors( $status );
1680  }
1681  // Use the existing $status->value if the hook set it
1682  if ( !$status->value ) {
1683  $status->value = self::AS_HOOK_ERROR;
1684  }
1685  return false;
1686  } elseif ( !$status->isOK() ) {
1687  # ...or the hook could be expecting us to produce an error
1688  // FIXME this sucks, we should just use the Status object throughout
1689  $this->hookError = $this->formatStatusErrors( $status );
1690  $status->fatal( 'hookaborted' );
1691  $status->value = self::AS_HOOK_ERROR_EXPECTED;
1692  return false;
1693  }
1694 
1695  return true;
1696  }
1697 
1704  private function formatStatusErrors( Status $status ) {
1705  $errmsg = $status->getWikiText(
1706  'edit-error-short',
1707  'edit-error-long',
1708  $this->context->getLanguage()
1709  );
1710  return <<<ERROR
1711 <div class="errorbox">
1712 {$errmsg}
1713 </div>
1714 <br clear="all" />
1715 ERROR;
1716  }
1717 
1724  private function newSectionSummary( &$sectionanchor = null ) {
1725  global $wgParser;
1726 
1727  if ( $this->sectiontitle !== '' ) {
1728  $sectionanchor = $this->guessSectionName( $this->sectiontitle );
1729  // If no edit summary was specified, create one automatically from the section
1730  // title and have it link to the new section. Otherwise, respect the summary as
1731  // passed.
1732  if ( $this->summary === '' ) {
1733  $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
1734  return $this->context->msg( 'newsectionsummary' )
1735  ->rawParams( $cleanSectionTitle )->inContentLanguage()->text();
1736  }
1737  } elseif ( $this->summary !== '' ) {
1738  $sectionanchor = $this->guessSectionName( $this->summary );
1739  # This is a new section, so create a link to the new section
1740  # in the revision summary.
1741  $cleanSummary = $wgParser->stripSectionName( $this->summary );
1742  return $this->context->msg( 'newsectionsummary' )
1743  ->rawParams( $cleanSummary )->inContentLanguage()->text();
1744  }
1745  return $this->summary;
1746  }
1747 
1772  public function internalAttemptSave( &$result, $bot = false ) {
1774  $user = $this->context->getUser();
1775 
1776  if ( !Hooks::run( 'EditPage::attemptSave', [ $this ] ) ) {
1777  wfDebug( "Hook 'EditPage::attemptSave' aborted article saving\n" );
1778  $status->fatal( 'hookaborted' );
1779  $status->value = self::AS_HOOK_ERROR;
1780  return $status;
1781  }
1782 
1783  if ( $this->unicodeCheck !== self::UNICODE_CHECK ) {
1784  $status->fatal( 'unicode-support-fail' );
1785  $status->value = self::AS_UNICODE_NOT_SUPPORTED;
1786  return $status;
1787  }
1788 
1789  $request = $this->context->getRequest();
1790  $spam = $request->getText( 'wpAntispam' );
1791  if ( $spam !== '' ) {
1792  wfDebugLog(
1793  'SimpleAntiSpam',
1794  $user->getName() .
1795  ' editing "' .
1796  $this->mTitle->getPrefixedText() .
1797  '" submitted bogus field "' .
1798  $spam .
1799  '"'
1800  );
1801  $status->fatal( 'spamprotectionmatch', false );
1802  $status->value = self::AS_SPAM_ERROR;
1803  return $status;
1804  }
1805 
1806  try {
1807  # Construct Content object
1808  $textbox_content = $this->toEditContent( $this->textbox1 );
1809  } catch ( MWContentSerializationException $ex ) {
1810  $status->fatal(
1811  'content-failed-to-parse',
1812  $this->contentModel,
1813  $this->contentFormat,
1814  $ex->getMessage()
1815  );
1816  $status->value = self::AS_PARSE_ERROR;
1817  return $status;
1818  }
1819 
1820  # Check image redirect
1821  if ( $this->mTitle->getNamespace() == NS_FILE &&
1822  $textbox_content->isRedirect() &&
1823  !$user->isAllowed( 'upload' )
1824  ) {
1825  $code = $user->isAnon() ? self::AS_IMAGE_REDIRECT_ANON : self::AS_IMAGE_REDIRECT_LOGGED;
1826  $status->setResult( false, $code );
1827 
1828  return $status;
1829  }
1830 
1831  # Check for spam
1832  $match = self::matchSummarySpamRegex( $this->summary );
1833  if ( $match === false && $this->section == 'new' ) {
1834  # $wgSpamRegex is enforced on this new heading/summary because, unlike
1835  # regular summaries, it is added to the actual wikitext.
1836  if ( $this->sectiontitle !== '' ) {
1837  # This branch is taken when the API is used with the 'sectiontitle' parameter.
1838  $match = self::matchSpamRegex( $this->sectiontitle );
1839  } else {
1840  # This branch is taken when the "Add Topic" user interface is used, or the API
1841  # is used with the 'summary' parameter.
1842  $match = self::matchSpamRegex( $this->summary );
1843  }
1844  }
1845  if ( $match === false ) {
1846  $match = self::matchSpamRegex( $this->textbox1 );
1847  }
1848  if ( $match !== false ) {
1849  $result['spam'] = $match;
1850  $ip = $request->getIP();
1851  $pdbk = $this->mTitle->getPrefixedDBkey();
1852  $match = str_replace( "\n", '', $match );
1853  wfDebugLog( 'SpamRegex', "$ip spam regex hit [[$pdbk]]: \"$match\"" );
1854  $status->fatal( 'spamprotectionmatch', $match );
1855  $status->value = self::AS_SPAM_ERROR;
1856  return $status;
1857  }
1858  if ( !Hooks::run(
1859  'EditFilter',
1860  [ $this, $this->textbox1, $this->section, &$this->hookError, $this->summary ] )
1861  ) {
1862  # Error messages etc. could be handled within the hook...
1863  $status->fatal( 'hookaborted' );
1864  $status->value = self::AS_HOOK_ERROR;
1865  return $status;
1866  } elseif ( $this->hookError != '' ) {
1867  # ...or the hook could be expecting us to produce an error
1868  $status->fatal( 'hookaborted' );
1869  $status->value = self::AS_HOOK_ERROR_EXPECTED;
1870  return $status;
1871  }
1872 
1873  if ( $user->isBlockedFrom( $this->mTitle, false ) ) {
1874  // Auto-block user's IP if the account was "hard" blocked
1875  if ( !wfReadOnly() ) {
1876  $user->spreadAnyEditBlock();
1877  }
1878  # Check block state against master, thus 'false'.
1879  $status->setResult( false, self::AS_BLOCKED_PAGE_FOR_USER );
1880  return $status;
1881  }
1882 
1883  $this->contentLength = strlen( $this->textbox1 );
1884  $config = $this->context->getConfig();
1885  $maxArticleSize = $config->get( 'MaxArticleSize' );
1886  if ( $this->contentLength > $maxArticleSize * 1024 ) {
1887  // Error will be displayed by showEditForm()
1888  $this->tooBig = true;
1889  $status->setResult( false, self::AS_CONTENT_TOO_BIG );
1890  return $status;
1891  }
1892 
1893  if ( !$user->isAllowed( 'edit' ) ) {
1894  if ( $user->isAnon() ) {
1895  $status->setResult( false, self::AS_READ_ONLY_PAGE_ANON );
1896  return $status;
1897  } else {
1898  $status->fatal( 'readonlytext' );
1899  $status->value = self::AS_READ_ONLY_PAGE_LOGGED;
1900  return $status;
1901  }
1902  }
1903 
1904  $changingContentModel = false;
1905  if ( $this->contentModel !== $this->mTitle->getContentModel() ) {
1906  if ( !$config->get( 'ContentHandlerUseDB' ) ) {
1907  $status->fatal( 'editpage-cannot-use-custom-model' );
1908  $status->value = self::AS_CANNOT_USE_CUSTOM_MODEL;
1909  return $status;
1910  } elseif ( !$user->isAllowed( 'editcontentmodel' ) ) {
1911  $status->setResult( false, self::AS_NO_CHANGE_CONTENT_MODEL );
1912  return $status;
1913  }
1914  // Make sure the user can edit the page under the new content model too
1915  $titleWithNewContentModel = clone $this->mTitle;
1916  $titleWithNewContentModel->setContentModel( $this->contentModel );
1917  if ( !$titleWithNewContentModel->userCan( 'editcontentmodel', $user )
1918  || !$titleWithNewContentModel->userCan( 'edit', $user )
1919  ) {
1920  $status->setResult( false, self::AS_NO_CHANGE_CONTENT_MODEL );
1921  return $status;
1922  }
1923 
1924  $changingContentModel = true;
1925  $oldContentModel = $this->mTitle->getContentModel();
1926  }
1927 
1928  if ( $this->changeTags ) {
1929  $changeTagsStatus = ChangeTags::canAddTagsAccompanyingChange(
1930  $this->changeTags, $user );
1931  if ( !$changeTagsStatus->isOK() ) {
1932  $changeTagsStatus->value = self::AS_CHANGE_TAG_ERROR;
1933  return $changeTagsStatus;
1934  }
1935  }
1936 
1937  if ( wfReadOnly() ) {
1938  $status->fatal( 'readonlytext' );
1939  $status->value = self::AS_READ_ONLY_PAGE;
1940  return $status;
1941  }
1942  if ( $user->pingLimiter() || $user->pingLimiter( 'linkpurge', 0 )
1943  || ( $changingContentModel && $user->pingLimiter( 'editcontentmodel' ) )
1944  ) {
1945  $status->fatal( 'actionthrottledtext' );
1946  $status->value = self::AS_RATE_LIMITED;
1947  return $status;
1948  }
1949 
1950  # If the article has been deleted while editing, don't save it without
1951  # confirmation
1952  if ( $this->wasDeletedSinceLastEdit() && !$this->recreate ) {
1953  $status->setResult( false, self::AS_ARTICLE_WAS_DELETED );
1954  return $status;
1955  }
1956 
1957  # Load the page data from the master. If anything changes in the meantime,
1958  # we detect it by using page_latest like a token in a 1 try compare-and-swap.
1959  $this->page->loadPageData( 'fromdbmaster' );
1960  $new = !$this->page->exists();
1961 
1962  if ( $new ) {
1963  // Late check for create permission, just in case *PARANOIA*
1964  if ( !$this->mTitle->userCan( 'create', $user ) ) {
1965  $status->fatal( 'nocreatetext' );
1966  $status->value = self::AS_NO_CREATE_PERMISSION;
1967  wfDebug( __METHOD__ . ": no create permission\n" );
1968  return $status;
1969  }
1970 
1971  // Don't save a new page if it's blank or if it's a MediaWiki:
1972  // message with content equivalent to default (allow empty pages
1973  // in this case to disable messages, see T52124)
1974  $defaultMessageText = $this->mTitle->getDefaultMessageText();
1975  if ( $this->mTitle->getNamespace() === NS_MEDIAWIKI && $defaultMessageText !== false ) {
1976  $defaultText = $defaultMessageText;
1977  } else {
1978  $defaultText = '';
1979  }
1980 
1981  if ( !$this->allowBlankArticle && $this->textbox1 === $defaultText ) {
1982  $this->blankArticle = true;
1983  $status->fatal( 'blankarticle' );
1984  $status->setResult( false, self::AS_BLANK_ARTICLE );
1985  return $status;
1986  }
1987 
1988  if ( !$this->runPostMergeFilters( $textbox_content, $status, $user ) ) {
1989  return $status;
1990  }
1991 
1992  $content = $textbox_content;
1993 
1994  $result['sectionanchor'] = '';
1995  if ( $this->section == 'new' ) {
1996  if ( $this->sectiontitle !== '' ) {
1997  // Insert the section title above the content.
1998  $content = $content->addSectionHeader( $this->sectiontitle );
1999  } elseif ( $this->summary !== '' ) {
2000  // Insert the section title above the content.
2001  $content = $content->addSectionHeader( $this->summary );
2002  }
2003  $this->summary = $this->newSectionSummary( $result['sectionanchor'] );
2004  }
2005 
2006  $status->value = self::AS_SUCCESS_NEW_ARTICLE;
2007 
2008  } else { # not $new
2009 
2010  # Article exists. Check for edit conflict.
2011 
2012  $this->page->clear(); # Force reload of dates, etc.
2013  $timestamp = $this->page->getTimestamp();
2014  $latest = $this->page->getLatest();
2015 
2016  wfDebug( "timestamp: {$timestamp}, edittime: {$this->edittime}\n" );
2017 
2018  // Check editRevId if set, which handles same-second timestamp collisions
2019  if ( $timestamp != $this->edittime
2020  || ( $this->editRevId !== null && $this->editRevId != $latest )
2021  ) {
2022  $this->isConflict = true;
2023  if ( $this->section == 'new' ) {
2024  if ( $this->page->getUserText() == $user->getName() &&
2025  $this->page->getComment() == $this->newSectionSummary()
2026  ) {
2027  // Probably a duplicate submission of a new comment.
2028  // This can happen when CDN resends a request after
2029  // a timeout but the first one actually went through.
2030  wfDebug( __METHOD__
2031  . ": duplicate new section submission; trigger edit conflict!\n" );
2032  } else {
2033  // New comment; suppress conflict.
2034  $this->isConflict = false;
2035  wfDebug( __METHOD__ . ": conflict suppressed; new section\n" );
2036  }
2037  } elseif ( $this->section == ''
2039  DB_MASTER, $this->mTitle->getArticleID(),
2040  $user->getId(), $this->edittime
2041  )
2042  ) {
2043  # Suppress edit conflict with self, except for section edits where merging is required.
2044  wfDebug( __METHOD__ . ": Suppressing edit conflict, same user.\n" );
2045  $this->isConflict = false;
2046  }
2047  }
2048 
2049  // If sectiontitle is set, use it, otherwise use the summary as the section title.
2050  if ( $this->sectiontitle !== '' ) {
2051  $sectionTitle = $this->sectiontitle;
2052  } else {
2053  $sectionTitle = $this->summary;
2054  }
2055 
2056  $content = null;
2057 
2058  if ( $this->isConflict ) {
2059  wfDebug( __METHOD__
2060  . ": conflict! getting section '{$this->section}' for time '{$this->edittime}'"
2061  . " (id '{$this->editRevId}') (article time '{$timestamp}')\n" );
2062  // @TODO: replaceSectionAtRev() with base ID (not prior current) for ?oldid=X case
2063  // ...or disable section editing for non-current revisions (not exposed anyway).
2064  if ( $this->editRevId !== null ) {
2065  $content = $this->page->replaceSectionAtRev(
2066  $this->section,
2067  $textbox_content,
2068  $sectionTitle,
2069  $this->editRevId
2070  );
2071  } else {
2072  $content = $this->page->replaceSectionContent(
2073  $this->section,
2074  $textbox_content,
2075  $sectionTitle,
2076  $this->edittime
2077  );
2078  }
2079  } else {
2080  wfDebug( __METHOD__ . ": getting section '{$this->section}'\n" );
2081  $content = $this->page->replaceSectionContent(
2082  $this->section,
2083  $textbox_content,
2084  $sectionTitle
2085  );
2086  }
2087 
2088  if ( is_null( $content ) ) {
2089  wfDebug( __METHOD__ . ": activating conflict; section replace failed.\n" );
2090  $this->isConflict = true;
2091  $content = $textbox_content; // do not try to merge here!
2092  } elseif ( $this->isConflict ) {
2093  # Attempt merge
2094  if ( $this->mergeChangesIntoContent( $content ) ) {
2095  // Successful merge! Maybe we should tell the user the good news?
2096  $this->isConflict = false;
2097  wfDebug( __METHOD__ . ": Suppressing edit conflict, successful merge.\n" );
2098  } else {
2099  $this->section = '';
2100  $this->textbox1 = ContentHandler::getContentText( $content );
2101  wfDebug( __METHOD__ . ": Keeping edit conflict, failed merge.\n" );
2102  }
2103  }
2104 
2105  if ( $this->isConflict ) {
2106  $status->setResult( false, self::AS_CONFLICT_DETECTED );
2107  return $status;
2108  }
2109 
2110  if ( !$this->runPostMergeFilters( $content, $status, $user ) ) {
2111  return $status;
2112  }
2113 
2114  if ( $this->section == 'new' ) {
2115  // Handle the user preference to force summaries here
2116  if ( !$this->allowBlankSummary && trim( $this->summary ) == '' ) {
2117  $this->missingSummary = true;
2118  $status->fatal( 'missingsummary' ); // or 'missingcommentheader' if $section == 'new'. Blegh
2119  $status->value = self::AS_SUMMARY_NEEDED;
2120  return $status;
2121  }
2122 
2123  // Do not allow the user to post an empty comment
2124  if ( $this->textbox1 == '' ) {
2125  $this->missingComment = true;
2126  $status->fatal( 'missingcommenttext' );
2127  $status->value = self::AS_TEXTBOX_EMPTY;
2128  return $status;
2129  }
2130  } elseif ( !$this->allowBlankSummary
2131  && !$content->equals( $this->getOriginalContent( $user ) )
2132  && !$content->isRedirect()
2133  && md5( $this->summary ) == $this->autoSumm
2134  ) {
2135  $this->missingSummary = true;
2136  $status->fatal( 'missingsummary' );
2137  $status->value = self::AS_SUMMARY_NEEDED;
2138  return $status;
2139  }
2140 
2141  # All's well
2142  $sectionanchor = '';
2143  if ( $this->section == 'new' ) {
2144  $this->summary = $this->newSectionSummary( $sectionanchor );
2145  } elseif ( $this->section != '' ) {
2146  # Try to get a section anchor from the section source, redirect
2147  # to edited section if header found.
2148  # XXX: Might be better to integrate this into Article::replaceSectionAtRev
2149  # for duplicate heading checking and maybe parsing.
2150  $hasmatch = preg_match( "/^ *([=]{1,6})(.*?)(\\1) *\\n/i", $this->textbox1, $matches );
2151  # We can't deal with anchors, includes, html etc in the header for now,
2152  # headline would need to be parsed to improve this.
2153  if ( $hasmatch && strlen( $matches[2] ) > 0 ) {
2154  $sectionanchor = $this->guessSectionName( $matches[2] );
2155  }
2156  }
2157  $result['sectionanchor'] = $sectionanchor;
2158 
2159  // Save errors may fall down to the edit form, but we've now
2160  // merged the section into full text. Clear the section field
2161  // so that later submission of conflict forms won't try to
2162  // replace that into a duplicated mess.
2163  $this->textbox1 = $this->toEditText( $content );
2164  $this->section = '';
2165 
2166  $status->value = self::AS_SUCCESS_UPDATE;
2167  }
2168 
2169  if ( !$this->allowSelfRedirect
2170  && $content->isRedirect()
2171  && $content->getRedirectTarget()->equals( $this->getTitle() )
2172  ) {
2173  // If the page already redirects to itself, don't warn.
2174  $currentTarget = $this->getCurrentContent()->getRedirectTarget();
2175  if ( !$currentTarget || !$currentTarget->equals( $this->getTitle() ) ) {
2176  $this->selfRedirect = true;
2177  $status->fatal( 'selfredirect' );
2178  $status->value = self::AS_SELF_REDIRECT;
2179  return $status;
2180  }
2181  }
2182 
2183  // Check for length errors again now that the section is merged in
2184  $this->contentLength = strlen( $this->toEditText( $content ) );
2185  if ( $this->contentLength > $maxArticleSize * 1024 ) {
2186  $this->tooBig = true;
2187  $status->setResult( false, self::AS_MAX_ARTICLE_SIZE_EXCEEDED );
2188  return $status;
2189  }
2190 
2191  $flags = EDIT_AUTOSUMMARY |
2192  ( $new ? EDIT_NEW : EDIT_UPDATE ) |
2193  ( ( $this->minoredit && !$this->isNew ) ? EDIT_MINOR : 0 ) |
2194  ( $bot ? EDIT_FORCE_BOT : 0 );
2195 
2196  $doEditStatus = $this->page->doEditContent(
2197  $content,
2198  $this->summary,
2199  $flags,
2200  false,
2201  $user,
2202  $content->getDefaultFormat(),
2205  );
2206 
2207  if ( !$doEditStatus->isOK() ) {
2208  // Failure from doEdit()
2209  // Show the edit conflict page for certain recognized errors from doEdit(),
2210  // but don't show it for errors from extension hooks
2211  $errors = $doEditStatus->getErrorsArray();
2212  if ( in_array( $errors[0][0],
2213  [ 'edit-gone-missing', 'edit-conflict', 'edit-already-exists' ] )
2214  ) {
2215  $this->isConflict = true;
2216  // Destroys data doEdit() put in $status->value but who cares
2217  $doEditStatus->value = self::AS_END;
2218  }
2219  return $doEditStatus;
2220  }
2221 
2222  $result['nullEdit'] = $doEditStatus->hasMessage( 'edit-no-change' );
2223  if ( $result['nullEdit'] ) {
2224  // We don't know if it was a null edit until now, so increment here
2225  $user->pingLimiter( 'linkpurge' );
2226  }
2227  $result['redirect'] = $content->isRedirect();
2228 
2229  $this->updateWatchlist();
2230 
2231  // If the content model changed, add a log entry
2232  if ( $changingContentModel ) {
2234  $user,
2235  $new ? false : $oldContentModel,
2236  $this->contentModel,
2237  $this->summary
2238  );
2239  }
2240 
2241  return $status;
2242  }
2243 
2250  protected function addContentModelChangeLogEntry( User $user, $oldModel, $newModel, $reason ) {
2251  $new = $oldModel === false;
2252  $log = new ManualLogEntry( 'contentmodel', $new ? 'new' : 'change' );
2253  $log->setPerformer( $user );
2254  $log->setTarget( $this->mTitle );
2255  $log->setComment( $reason );
2256  $log->setParameters( [
2257  '4::oldmodel' => $oldModel,
2258  '5::newmodel' => $newModel
2259  ] );
2260  $logid = $log->insert();
2261  $log->publish( $logid );
2262  }
2263 
2267  protected function updateWatchlist() {
2268  $user = $this->context->getUser();
2269  if ( !$user->isLoggedIn() ) {
2270  return;
2271  }
2272 
2274  $watch = $this->watchthis;
2275  // Do this in its own transaction to reduce contention...
2276  DeferredUpdates::addCallableUpdate( function () use ( $user, $title, $watch ) {
2277  if ( $watch == $user->isWatched( $title, User::IGNORE_USER_RIGHTS ) ) {
2278  return; // nothing to change
2279  }
2281  } );
2282  }
2283 
2295  private function mergeChangesIntoContent( &$editContent ) {
2296  $db = wfGetDB( DB_MASTER );
2297 
2298  // This is the revision the editor started from
2299  $baseRevision = $this->getBaseRevision();
2300  $baseContent = $baseRevision ? $baseRevision->getContent() : null;
2301 
2302  if ( is_null( $baseContent ) ) {
2303  return false;
2304  }
2305 
2306  // The current state, we want to merge updates into it
2307  $currentRevision = Revision::loadFromTitle( $db, $this->mTitle );
2308  $currentContent = $currentRevision ? $currentRevision->getContent() : null;
2309 
2310  if ( is_null( $currentContent ) ) {
2311  return false;
2312  }
2313 
2314  $handler = ContentHandler::getForModelID( $baseContent->getModel() );
2315 
2316  $result = $handler->merge3( $baseContent, $editContent, $currentContent );
2317 
2318  if ( $result ) {
2319  $editContent = $result;
2320  // Update parentRevId to what we just merged.
2321  $this->parentRevId = $currentRevision->getId();
2322  return true;
2323  }
2324 
2325  return false;
2326  }
2327 
2333  public function getBaseRevision() {
2334  if ( !$this->mBaseRevision ) {
2335  $db = wfGetDB( DB_MASTER );
2336  $this->mBaseRevision = $this->editRevId
2337  ? Revision::newFromId( $this->editRevId, Revision::READ_LATEST )
2338  : Revision::loadFromTimestamp( $db, $this->mTitle, $this->edittime );
2339  }
2340  return $this->mBaseRevision;
2341  }
2342 
2350  public static function matchSpamRegex( $text ) {
2351  global $wgSpamRegex;
2352  // For back compatibility, $wgSpamRegex may be a single string or an array of regexes.
2353  $regexes = (array)$wgSpamRegex;
2354  return self::matchSpamRegexInternal( $text, $regexes );
2355  }
2356 
2364  public static function matchSummarySpamRegex( $text ) {
2365  global $wgSummarySpamRegex;
2366  $regexes = (array)$wgSummarySpamRegex;
2367  return self::matchSpamRegexInternal( $text, $regexes );
2368  }
2369 
2375  protected static function matchSpamRegexInternal( $text, $regexes ) {
2376  foreach ( $regexes as $regex ) {
2377  $matches = [];
2378  if ( preg_match( $regex, $text, $matches ) ) {
2379  return $matches[0];
2380  }
2381  }
2382  return false;
2383  }
2384 
2385  public function setHeaders() {
2386  $out = $this->context->getOutput();
2387 
2388  $out->addModules( 'mediawiki.action.edit' );
2389  $out->addModuleStyles( 'mediawiki.action.edit.styles' );
2390  $out->addModuleStyles( 'mediawiki.editfont.styles' );
2391 
2392  $user = $this->context->getUser();
2393  if ( $user->getOption( 'showtoolbar' ) ) {
2394  // The addition of default buttons is handled by getEditToolbar() which
2395  // has its own dependency on this module. The call here ensures the module
2396  // is loaded in time (it has position "top") for other modules to register
2397  // buttons (e.g. extensions, gadgets, user scripts).
2398  $out->addModules( 'mediawiki.toolbar' );
2399  }
2400 
2401  if ( $user->getOption( 'uselivepreview' ) ) {
2402  $out->addModules( 'mediawiki.action.edit.preview' );
2403  }
2404 
2405  if ( $user->getOption( 'useeditwarning' ) ) {
2406  $out->addModules( 'mediawiki.action.edit.editWarning' );
2407  }
2408 
2409  # Enabled article-related sidebar, toplinks, etc.
2410  $out->setArticleRelated( true );
2411 
2412  $contextTitle = $this->getContextTitle();
2413  if ( $this->isConflict ) {
2414  $msg = 'editconflict';
2415  } elseif ( $contextTitle->exists() && $this->section != '' ) {
2416  $msg = $this->section == 'new' ? 'editingcomment' : 'editingsection';
2417  } else {
2418  $msg = $contextTitle->exists()
2419  || ( $contextTitle->getNamespace() == NS_MEDIAWIKI
2420  && $contextTitle->getDefaultMessageText() !== false
2421  )
2422  ? 'editing'
2423  : 'creating';
2424  }
2425 
2426  # Use the title defined by DISPLAYTITLE magic word when present
2427  # NOTE: getDisplayTitle() returns HTML while getPrefixedText() returns plain text.
2428  # setPageTitle() treats the input as wikitext, which should be safe in either case.
2429  $displayTitle = isset( $this->mParserOutput ) ? $this->mParserOutput->getDisplayTitle() : false;
2430  if ( $displayTitle === false ) {
2431  $displayTitle = $contextTitle->getPrefixedText();
2432  }
2433  $out->setPageTitle( $this->context->msg( $msg, $displayTitle ) );
2434 
2435  $config = $this->context->getConfig();
2436 
2437  # Transmit the name of the message to JavaScript for live preview
2438  # Keep Resources.php/mediawiki.action.edit.preview in sync with the possible keys
2439  $out->addJsConfigVars( [
2440  'wgEditMessage' => $msg,
2441  'wgAjaxEditStash' => $config->get( 'AjaxEditStash' ),
2442  ] );
2443 
2444  // Add whether to use 'save' or 'publish' messages to JavaScript for post-edit, other
2445  // editors, etc.
2446  $out->addJsConfigVars(
2447  'wgEditSubmitButtonLabelPublish',
2448  $config->get( 'EditSubmitButtonLabelPublish' )
2449  );
2450  }
2451 
2455  protected function showIntro() {
2456  if ( $this->suppressIntro ) {
2457  return;
2458  }
2459 
2460  $out = $this->context->getOutput();
2461  $namespace = $this->mTitle->getNamespace();
2462 
2463  if ( $namespace == NS_MEDIAWIKI ) {
2464  # Show a warning if editing an interface message
2465  $out->wrapWikiMsg( "<div class='mw-editinginterface'>\n$1\n</div>", 'editinginterface' );
2466  # If this is a default message (but not css, json, or js),
2467  # show a hint that it is translatable on translatewiki.net
2468  if (
2469  !$this->mTitle->hasContentModel( CONTENT_MODEL_CSS )
2470  && !$this->mTitle->hasContentModel( CONTENT_MODEL_JSON )
2471  && !$this->mTitle->hasContentModel( CONTENT_MODEL_JAVASCRIPT )
2472  ) {
2473  $defaultMessageText = $this->mTitle->getDefaultMessageText();
2474  if ( $defaultMessageText !== false ) {
2475  $out->wrapWikiMsg( "<div class='mw-translateinterface'>\n$1\n</div>",
2476  'translateinterface' );
2477  }
2478  }
2479  } elseif ( $namespace == NS_FILE ) {
2480  # Show a hint to shared repo
2481  $file = wfFindFile( $this->mTitle );
2482  if ( $file && !$file->isLocal() ) {
2483  $descUrl = $file->getDescriptionUrl();
2484  # there must be a description url to show a hint to shared repo
2485  if ( $descUrl ) {
2486  if ( !$this->mTitle->exists() ) {
2487  $out->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-create\">\n$1\n</div>", [
2488  'sharedupload-desc-create', $file->getRepo()->getDisplayName(), $descUrl
2489  ] );
2490  } else {
2491  $out->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-edit\">\n$1\n</div>", [
2492  'sharedupload-desc-edit', $file->getRepo()->getDisplayName(), $descUrl
2493  ] );
2494  }
2495  }
2496  }
2497  }
2498 
2499  # Show a warning message when someone creates/edits a user (talk) page but the user does not exist
2500  # Show log extract when the user is currently blocked
2501  if ( $namespace == NS_USER || $namespace == NS_USER_TALK ) {
2502  $username = explode( '/', $this->mTitle->getText(), 2 )[0];
2503  $user = User::newFromName( $username, false /* allow IP users */ );
2504  $ip = User::isIP( $username );
2505  $block = Block::newFromTarget( $user, $user );
2506  if ( !( $user && $user->isLoggedIn() ) && !$ip ) { # User does not exist
2507  $out->wrapWikiMsg( "<div class=\"mw-userpage-userdoesnotexist error\">\n$1\n</div>",
2508  [ 'userpage-userdoesnotexist', wfEscapeWikiText( $username ) ] );
2509  } elseif ( !is_null( $block ) && $block->getType() != Block::TYPE_AUTO ) {
2510  # Show log extract if the user is currently blocked
2512  $out,
2513  'block',
2514  MWNamespace::getCanonicalName( NS_USER ) . ':' . $block->getTarget(),
2515  '',
2516  [
2517  'lim' => 1,
2518  'showIfEmpty' => false,
2519  'msgKey' => [
2520  'blocked-notice-logextract',
2521  $user->getName() # Support GENDER in notice
2522  ]
2523  ]
2524  );
2525  }
2526  }
2527  # Try to add a custom edit intro, or use the standard one if this is not possible.
2528  if ( !$this->showCustomIntro() && !$this->mTitle->exists() ) {
2530  $this->context->msg( 'helppage' )->inContentLanguage()->text()
2531  ) );
2532  if ( $this->context->getUser()->isLoggedIn() ) {
2533  $out->wrapWikiMsg(
2534  // Suppress the external link icon, consider the help url an internal one
2535  "<div class=\"mw-newarticletext plainlinks\">\n$1\n</div>",
2536  [
2537  'newarticletext',
2538  $helpLink
2539  ]
2540  );
2541  } else {
2542  $out->wrapWikiMsg(
2543  // Suppress the external link icon, consider the help url an internal one
2544  "<div class=\"mw-newarticletextanon plainlinks\">\n$1\n</div>",
2545  [
2546  'newarticletextanon',
2547  $helpLink
2548  ]
2549  );
2550  }
2551  }
2552  # Give a notice if the user is editing a deleted/moved page...
2553  if ( !$this->mTitle->exists() ) {
2554  $dbr = wfGetDB( DB_REPLICA );
2555 
2556  LogEventsList::showLogExtract( $out, [ 'delete', 'move' ], $this->mTitle,
2557  '',
2558  [
2559  'lim' => 10,
2560  'conds' => [ 'log_action != ' . $dbr->addQuotes( 'revision' ) ],
2561  'showIfEmpty' => false,
2562  'msgKey' => [ 'recreate-moveddeleted-warn' ]
2563  ]
2564  );
2565  }
2566  }
2567 
2573  protected function showCustomIntro() {
2574  if ( $this->editintro ) {
2575  $title = Title::newFromText( $this->editintro );
2576  if ( $title instanceof Title && $title->exists() && $title->userCan( 'read' ) ) {
2577  // Added using template syntax, to take <noinclude>'s into account.
2578  $this->context->getOutput()->addWikiTextTitleTidy(
2579  '<div class="mw-editintro">{{:' . $title->getFullText() . '}}</div>',
2581  );
2582  return true;
2583  }
2584  }
2585  return false;
2586  }
2587 
2606  protected function toEditText( $content ) {
2607  if ( $content === null || $content === false || is_string( $content ) ) {
2608  return $content;
2609  }
2610 
2611  if ( !$this->isSupportedContentModel( $content->getModel() ) ) {
2612  throw new MWException( 'This content model is not supported: ' . $content->getModel() );
2613  }
2614 
2615  return $content->serialize( $this->contentFormat );
2616  }
2617 
2634  protected function toEditContent( $text ) {
2635  if ( $text === false || $text === null ) {
2636  return $text;
2637  }
2638 
2639  $content = ContentHandler::makeContent( $text, $this->getTitle(),
2640  $this->contentModel, $this->contentFormat );
2641 
2642  if ( !$this->isSupportedContentModel( $content->getModel() ) ) {
2643  throw new MWException( 'This content model is not supported: ' . $content->getModel() );
2644  }
2645 
2646  return $content;
2647  }
2648 
2657  public function showEditForm( $formCallback = null ) {
2658  # need to parse the preview early so that we know which templates are used,
2659  # otherwise users with "show preview after edit box" will get a blank list
2660  # we parse this near the beginning so that setHeaders can do the title
2661  # setting work instead of leaving it in getPreviewText
2662  $previewOutput = '';
2663  if ( $this->formtype == 'preview' ) {
2664  $previewOutput = $this->getPreviewText();
2665  }
2666 
2667  $out = $this->context->getOutput();
2668 
2669  // Avoid PHP 7.1 warning of passing $this by reference
2670  $editPage = $this;
2671  Hooks::run( 'EditPage::showEditForm:initial', [ &$editPage, &$out ] );
2672 
2673  $this->setHeaders();
2674 
2675  $this->addTalkPageText();
2676  $this->addEditNotices();
2677 
2678  if ( !$this->isConflict &&
2679  $this->section != '' &&
2680  !$this->isSectionEditSupported() ) {
2681  // We use $this->section to much before this and getVal('wgSection') directly in other places
2682  // at this point we can't reset $this->section to '' to fallback to non-section editing.
2683  // Someone is welcome to try refactoring though
2684  $out->showErrorPage( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
2685  return;
2686  }
2687 
2688  $this->showHeader();
2689 
2690  $out->addHTML( $this->editFormPageTop );
2691 
2692  $user = $this->context->getUser();
2693  if ( $user->getOption( 'previewontop' ) ) {
2694  $this->displayPreviewArea( $previewOutput, true );
2695  }
2696 
2697  $out->addHTML( $this->editFormTextTop );
2698 
2699  $showToolbar = true;
2700  if ( $this->wasDeletedSinceLastEdit() ) {
2701  if ( $this->formtype == 'save' ) {
2702  // Hide the toolbar and edit area, user can click preview to get it back
2703  // Add an confirmation checkbox and explanation.
2704  $showToolbar = false;
2705  } else {
2706  $out->wrapWikiMsg( "<div class='error mw-deleted-while-editing'>\n$1\n</div>",
2707  'deletedwhileediting' );
2708  }
2709  }
2710 
2711  // @todo add EditForm plugin interface and use it here!
2712  // search for textarea1 and textarea2, and allow EditForm to override all uses.
2713  $out->addHTML( Html::openElement(
2714  'form',
2715  [
2716  'class' => 'mw-editform',
2717  'id' => self::EDITFORM_ID,
2718  'name' => self::EDITFORM_ID,
2719  'method' => 'post',
2720  'action' => $this->getActionURL( $this->getContextTitle() ),
2721  'enctype' => 'multipart/form-data'
2722  ]
2723  ) );
2724 
2725  if ( is_callable( $formCallback ) ) {
2726  wfWarn( 'The $formCallback parameter to ' . __METHOD__ . 'is deprecated' );
2727  call_user_func_array( $formCallback, [ &$out ] );
2728  }
2729 
2730  // Add a check for Unicode support
2731  $out->addHTML( Html::hidden( 'wpUnicodeCheck', self::UNICODE_CHECK ) );
2732 
2733  // Add an empty field to trip up spambots
2734  $out->addHTML(
2735  Xml::openElement( 'div', [ 'id' => 'antispam-container', 'style' => 'display: none;' ] )
2736  . Html::rawElement(
2737  'label',
2738  [ 'for' => 'wpAntispam' ],
2739  $this->context->msg( 'simpleantispam-label' )->parse()
2740  )
2741  . Xml::element(
2742  'input',
2743  [
2744  'type' => 'text',
2745  'name' => 'wpAntispam',
2746  'id' => 'wpAntispam',
2747  'value' => ''
2748  ]
2749  )
2750  . Xml::closeElement( 'div' )
2751  );
2752 
2753  // Avoid PHP 7.1 warning of passing $this by reference
2754  $editPage = $this;
2755  Hooks::run( 'EditPage::showEditForm:fields', [ &$editPage, &$out ] );
2756 
2757  // Put these up at the top to ensure they aren't lost on early form submission
2758  $this->showFormBeforeText();
2759 
2760  if ( $this->wasDeletedSinceLastEdit() && 'save' == $this->formtype ) {
2761  $username = $this->lastDelete->user_name;
2762  $comment = CommentStore::getStore()
2763  ->getComment( 'log_comment', $this->lastDelete )->text;
2764 
2765  // It is better to not parse the comment at all than to have templates expanded in the middle
2766  // TODO: can the checkLabel be moved outside of the div so that wrapWikiMsg could be used?
2767  $key = $comment === ''
2768  ? 'confirmrecreate-noreason'
2769  : 'confirmrecreate';
2770  $out->addHTML(
2771  '<div class="mw-confirm-recreate">' .
2772  $this->context->msg( $key, $username, "<nowiki>$comment</nowiki>" )->parse() .
2773  Xml::checkLabel( $this->context->msg( 'recreate' )->text(), 'wpRecreate', 'wpRecreate', false,
2774  [ 'title' => Linker::titleAttrib( 'recreate' ), 'tabindex' => 1, 'id' => 'wpRecreate' ]
2775  ) .
2776  '</div>'
2777  );
2778  }
2779 
2780  # When the summary is hidden, also hide them on preview/show changes
2781  if ( $this->nosummary ) {
2782  $out->addHTML( Html::hidden( 'nosummary', true ) );
2783  }
2784 
2785  # If a blank edit summary was previously provided, and the appropriate
2786  # user preference is active, pass a hidden tag as wpIgnoreBlankSummary. This will stop the
2787  # user being bounced back more than once in the event that a summary
2788  # is not required.
2789  # ####
2790  # For a bit more sophisticated detection of blank summaries, hash the
2791  # automatic one and pass that in the hidden field wpAutoSummary.
2792  if ( $this->missingSummary || ( $this->section == 'new' && $this->nosummary ) ) {
2793  $out->addHTML( Html::hidden( 'wpIgnoreBlankSummary', true ) );
2794  }
2795 
2796  if ( $this->undidRev ) {
2797  $out->addHTML( Html::hidden( 'wpUndidRevision', $this->undidRev ) );
2798  }
2799 
2800  if ( $this->selfRedirect ) {
2801  $out->addHTML( Html::hidden( 'wpIgnoreSelfRedirect', true ) );
2802  }
2803 
2804  if ( $this->hasPresetSummary ) {
2805  // If a summary has been preset using &summary= we don't want to prompt for
2806  // a different summary. Only prompt for a summary if the summary is blanked.
2807  // (T19416)
2808  $this->autoSumm = md5( '' );
2809  }
2810 
2811  $autosumm = $this->autoSumm ? $this->autoSumm : md5( $this->summary );
2812  $out->addHTML( Html::hidden( 'wpAutoSummary', $autosumm ) );
2813 
2814  $out->addHTML( Html::hidden( 'oldid', $this->oldid ) );
2815  $out->addHTML( Html::hidden( 'parentRevId', $this->getParentRevId() ) );
2816 
2817  $out->addHTML( Html::hidden( 'format', $this->contentFormat ) );
2818  $out->addHTML( Html::hidden( 'model', $this->contentModel ) );
2819 
2820  $out->enableOOUI();
2821 
2822  if ( $this->section == 'new' ) {
2823  $this->showSummaryInput( true, $this->summary );
2824  $out->addHTML( $this->getSummaryPreview( true, $this->summary ) );
2825  }
2826 
2827  $out->addHTML( $this->editFormTextBeforeContent );
2828  if ( $this->isConflict ) {
2829  // In an edit conflict, we turn textbox2 into the user's text,
2830  // and textbox1 into the stored version
2831  $this->textbox2 = $this->textbox1;
2832 
2833  $content = $this->getCurrentContent();
2834  $this->textbox1 = $this->toEditText( $content );
2835 
2837  $editConflictHelper->setTextboxes( $this->textbox2, $this->textbox1 );
2838  $editConflictHelper->setContentModel( $this->contentModel );
2839  $editConflictHelper->setContentFormat( $this->contentFormat );
2841  }
2842 
2843  if ( !$this->mTitle->isUserConfigPage() && $showToolbar && $user->getOption( 'showtoolbar' ) ) {
2844  $out->addHTML( self::getEditToolbar( $this->mTitle ) );
2845  }
2846 
2847  if ( $this->blankArticle ) {
2848  $out->addHTML( Html::hidden( 'wpIgnoreBlankArticle', true ) );
2849  }
2850 
2851  if ( $this->isConflict ) {
2852  // In an edit conflict bypass the overridable content form method
2853  // and fallback to the raw wpTextbox1 since editconflicts can't be
2854  // resolved between page source edits and custom ui edits using the
2855  // custom edit ui.
2856  $conflictTextBoxAttribs = [];
2857  if ( $this->wasDeletedSinceLastEdit() ) {
2858  $conflictTextBoxAttribs['style'] = 'display:none;';
2859  } elseif ( $this->isOldRev ) {
2860  $conflictTextBoxAttribs['class'] = 'mw-textarea-oldrev';
2861  }
2862 
2863  $out->addHTML( $editConflictHelper->getEditConflictMainTextBox( $conflictTextBoxAttribs ) );
2865  } else {
2866  $this->showContentForm();
2867  }
2868 
2869  $out->addHTML( $this->editFormTextAfterContent );
2870 
2871  $this->showStandardInputs();
2872 
2873  $this->showFormAfterText();
2874 
2875  $this->showTosSummary();
2876 
2877  $this->showEditTools();
2878 
2879  $out->addHTML( $this->editFormTextAfterTools . "\n" );
2880 
2881  $out->addHTML( $this->makeTemplatesOnThisPageList( $this->getTemplates() ) );
2882 
2883  $out->addHTML( Html::rawElement( 'div', [ 'class' => 'hiddencats' ],
2884  Linker::formatHiddenCategories( $this->page->getHiddenCategories() ) ) );
2885 
2886  $out->addHTML( Html::rawElement( 'div', [ 'class' => 'limitreport' ],
2887  self::getPreviewLimitReport( $this->mParserOutput ) ) );
2888 
2889  $out->addModules( 'mediawiki.action.edit.collapsibleFooter' );
2890 
2891  if ( $this->isConflict ) {
2892  try {
2893  $this->showConflict();
2894  } catch ( MWContentSerializationException $ex ) {
2895  // this can't really happen, but be nice if it does.
2896  $msg = $this->context->msg(
2897  'content-failed-to-parse',
2898  $this->contentModel,
2899  $this->contentFormat,
2900  $ex->getMessage()
2901  );
2902  $out->addWikiText( '<div class="error">' . $msg->text() . '</div>' );
2903  }
2904  }
2905 
2906  // Set a hidden field so JS knows what edit form mode we are in
2907  if ( $this->isConflict ) {
2908  $mode = 'conflict';
2909  } elseif ( $this->preview ) {
2910  $mode = 'preview';
2911  } elseif ( $this->diff ) {
2912  $mode = 'diff';
2913  } else {
2914  $mode = 'text';
2915  }
2916  $out->addHTML( Html::hidden( 'mode', $mode, [ 'id' => 'mw-edit-mode' ] ) );
2917 
2918  // Marker for detecting truncated form data. This must be the last
2919  // parameter sent in order to be of use, so do not move me.
2920  $out->addHTML( Html::hidden( 'wpUltimateParam', true ) );
2921  $out->addHTML( $this->editFormTextBottom . "\n</form>\n" );
2922 
2923  if ( !$user->getOption( 'previewontop' ) ) {
2924  $this->displayPreviewArea( $previewOutput, false );
2925  }
2926  }
2927 
2935  public function makeTemplatesOnThisPageList( array $templates ) {
2936  $templateListFormatter = new TemplatesOnThisPageFormatter(
2937  $this->context, MediaWikiServices::getInstance()->getLinkRenderer()
2938  );
2939 
2940  // preview if preview, else section if section, else false
2941  $type = false;
2942  if ( $this->preview ) {
2943  $type = 'preview';
2944  } elseif ( $this->section != '' ) {
2945  $type = 'section';
2946  }
2947 
2948  return Html::rawElement( 'div', [ 'class' => 'templatesUsed' ],
2949  $templateListFormatter->format( $templates, $type )
2950  );
2951  }
2952 
2959  public static function extractSectionTitle( $text ) {
2960  preg_match( "/^(=+)(.+)\\1\\s*(\n|$)/i", $text, $matches );
2961  if ( !empty( $matches[2] ) ) {
2962  global $wgParser;
2963  return $wgParser->stripSectionName( trim( $matches[2] ) );
2964  } else {
2965  return false;
2966  }
2967  }
2968 
2969  protected function showHeader() {
2970  $out = $this->context->getOutput();
2971  $user = $this->context->getUser();
2972  if ( $this->isConflict ) {
2973  $this->addExplainConflictHeader( $out );
2974  $this->editRevId = $this->page->getLatest();
2975  } else {
2976  if ( $this->section != '' && $this->section != 'new' ) {
2977  if ( !$this->summary && !$this->preview && !$this->diff ) {
2978  $sectionTitle = self::extractSectionTitle( $this->textbox1 ); // FIXME: use Content object
2979  if ( $sectionTitle !== false ) {
2980  $this->summary = "/* $sectionTitle */ ";
2981  }
2982  }
2983  }
2984 
2985  $buttonLabel = $this->context->msg( $this->getSubmitButtonLabel() )->text();
2986 
2987  if ( $this->missingComment ) {
2988  $out->wrapWikiMsg( "<div id='mw-missingcommenttext'>\n$1\n</div>", 'missingcommenttext' );
2989  }
2990 
2991  if ( $this->missingSummary && $this->section != 'new' ) {
2992  $out->wrapWikiMsg(
2993  "<div id='mw-missingsummary'>\n$1\n</div>",
2994  [ 'missingsummary', $buttonLabel ]
2995  );
2996  }
2997 
2998  if ( $this->missingSummary && $this->section == 'new' ) {
2999  $out->wrapWikiMsg(
3000  "<div id='mw-missingcommentheader'>\n$1\n</div>",
3001  [ 'missingcommentheader', $buttonLabel ]
3002  );
3003  }
3004 
3005  if ( $this->blankArticle ) {
3006  $out->wrapWikiMsg(
3007  "<div id='mw-blankarticle'>\n$1\n</div>",
3008  [ 'blankarticle', $buttonLabel ]
3009  );
3010  }
3011 
3012  if ( $this->selfRedirect ) {
3013  $out->wrapWikiMsg(
3014  "<div id='mw-selfredirect'>\n$1\n</div>",
3015  [ 'selfredirect', $buttonLabel ]
3016  );
3017  }
3018 
3019  if ( $this->hookError !== '' ) {
3020  $out->addWikiText( $this->hookError );
3021  }
3022 
3023  if ( $this->section != 'new' ) {
3024  $revision = $this->mArticle->getRevisionFetched();
3025  if ( $revision ) {
3026  // Let sysop know that this will make private content public if saved
3027 
3028  if ( !$revision->userCan( Revision::DELETED_TEXT, $user ) ) {
3029  $out->wrapWikiMsg(
3030  "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
3031  'rev-deleted-text-permission'
3032  );
3033  } elseif ( $revision->isDeleted( Revision::DELETED_TEXT ) ) {
3034  $out->wrapWikiMsg(
3035  "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
3036  'rev-deleted-text-view'
3037  );
3038  }
3039 
3040  if ( !$revision->isCurrent() ) {
3041  $this->mArticle->setOldSubtitle( $revision->getId() );
3042  $out->addWikiMsg( 'editingold' );
3043  $this->isOldRev = true;
3044  }
3045  } elseif ( $this->mTitle->exists() ) {
3046  // Something went wrong
3047 
3048  $out->wrapWikiMsg( "<div class='errorbox'>\n$1\n</div>\n",
3049  [ 'missing-revision', $this->oldid ] );
3050  }
3051  }
3052  }
3053 
3054  if ( wfReadOnly() ) {
3055  $out->wrapWikiMsg(
3056  "<div id=\"mw-read-only-warning\">\n$1\n</div>",
3057  [ 'readonlywarning', wfReadOnlyReason() ]
3058  );
3059  } elseif ( $user->isAnon() ) {
3060  if ( $this->formtype != 'preview' ) {
3061  $out->wrapWikiMsg(
3062  "<div id='mw-anon-edit-warning' class='warningbox'>\n$1\n</div>",
3063  [ 'anoneditwarning',
3064  // Log-in link
3065  SpecialPage::getTitleFor( 'Userlogin' )->getFullURL( [
3066  'returnto' => $this->getTitle()->getPrefixedDBkey()
3067  ] ),
3068  // Sign-up link
3069  SpecialPage::getTitleFor( 'CreateAccount' )->getFullURL( [
3070  'returnto' => $this->getTitle()->getPrefixedDBkey()
3071  ] )
3072  ]
3073  );
3074  } else {
3075  $out->wrapWikiMsg( "<div id=\"mw-anon-preview-warning\" class=\"warningbox\">\n$1</div>",
3076  'anonpreviewwarning'
3077  );
3078  }
3079  } else {
3080  if ( $this->mTitle->isUserConfigPage() ) {
3081  # Check the skin exists
3082  if ( $this->isWrongCaseUserConfigPage() ) {
3083  $out->wrapWikiMsg(
3084  "<div class='error' id='mw-userinvalidconfigtitle'>\n$1\n</div>",
3085  [ 'userinvalidconfigtitle', $this->mTitle->getSkinFromConfigSubpage() ]
3086  );
3087  }
3088  if ( $this->getTitle()->isSubpageOf( $user->getUserPage() ) ) {
3089  $isUserCssConfig = $this->mTitle->isUserCssConfigPage();
3090  $isUserJsonConfig = $this->mTitle->isUserJsonConfigPage();
3091  $isUserJsConfig = $this->mTitle->isUserJsConfigPage();
3092 
3093  $warning = $isUserCssConfig
3094  ? 'usercssispublic'
3095  : ( $isUserJsonConfig ? 'userjsonispublic' : 'userjsispublic' );
3096 
3097  $out->wrapWikiMsg( '<div class="mw-userconfigpublic">$1</div>', $warning );
3098 
3099  if ( $this->formtype !== 'preview' ) {
3100  $config = $this->context->getConfig();
3101  if ( $isUserCssConfig && $config->get( 'AllowUserCss' ) ) {
3102  $out->wrapWikiMsg(
3103  "<div id='mw-usercssyoucanpreview'>\n$1\n</div>",
3104  [ 'usercssyoucanpreview' ]
3105  );
3106  } elseif ( $isUserJsonConfig /* No comparable 'AllowUserJson' */ ) {
3107  $out->wrapWikiMsg(
3108  "<div id='mw-userjsonyoucanpreview'>\n$1\n</div>",
3109  [ 'userjsonyoucanpreview' ]
3110  );
3111  } elseif ( $isUserJsConfig && $config->get( 'AllowUserJs' ) ) {
3112  $out->wrapWikiMsg(
3113  "<div id='mw-userjsyoucanpreview'>\n$1\n</div>",
3114  [ 'userjsyoucanpreview' ]
3115  );
3116  }
3117  }
3118  }
3119  }
3120  }
3121 
3123 
3124  $this->addLongPageWarningHeader();
3125 
3126  # Add header copyright warning
3127  $this->showHeaderCopyrightWarning();
3128  }
3129 
3137  private function getSummaryInputAttributes( array $inputAttrs = null ) {
3138  $conf = $this->context->getConfig();
3139  $oldCommentSchema = $conf->get( 'CommentTableSchemaMigrationStage' ) === MIGRATION_OLD;
3140  // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
3141  // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
3142  // Unicode codepoints (or 255 UTF-8 bytes for old schema).
3143  return ( is_array( $inputAttrs ) ? $inputAttrs : [] ) + [
3144  'id' => 'wpSummary',
3145  'name' => 'wpSummary',
3146  'maxlength' => $oldCommentSchema ? 200 : CommentStore::COMMENT_CHARACTER_LIMIT,
3147  'tabindex' => 1,
3148  'size' => 60,
3149  'spellcheck' => 'true',
3150  ];
3151  }
3152 
3162  function getSummaryInputWidget( $summary = "", $labelText = null, $inputAttrs = null ) {
3163  $inputAttrs = OOUI\Element::configFromHtmlAttributes(
3164  $this->getSummaryInputAttributes( $inputAttrs )
3165  );
3166  $inputAttrs += [
3167  'title' => Linker::titleAttrib( 'summary' ),
3168  'accessKey' => Linker::accesskey( 'summary' ),
3169  ];
3170 
3171  // For compatibility with old scripts and extensions, we want the legacy 'id' on the `<input>`
3172  $inputAttrs['inputId'] = $inputAttrs['id'];
3173  $inputAttrs['id'] = 'wpSummaryWidget';
3174 
3175  return new OOUI\FieldLayout(
3176  new OOUI\TextInputWidget( [
3177  'value' => $summary,
3178  'infusable' => true,
3179  ] + $inputAttrs ),
3180  [
3181  'label' => new OOUI\HtmlSnippet( $labelText ),
3182  'align' => 'top',
3183  'id' => 'wpSummaryLabel',
3184  'classes' => [ $this->missingSummary ? 'mw-summarymissed' : 'mw-summary' ],
3185  ]
3186  );
3187  }
3188 
3195  protected function showSummaryInput( $isSubjectPreview, $summary = "" ) {
3196  # Add a class if 'missingsummary' is triggered to allow styling of the summary line
3197  $summaryClass = $this->missingSummary ? 'mw-summarymissed' : 'mw-summary';
3198  if ( $isSubjectPreview ) {
3199  if ( $this->nosummary ) {
3200  return;
3201  }
3202  } else {
3203  if ( !$this->mShowSummaryField ) {
3204  return;
3205  }
3206  }
3207 
3208  $labelText = $this->context->msg( $isSubjectPreview ? 'subject' : 'summary' )->parse();
3209  $this->context->getOutput()->addHTML( $this->getSummaryInputWidget(
3210  $summary,
3211  $labelText,
3212  [ 'class' => $summaryClass ]
3213  ) );
3214  }
3215 
3223  protected function getSummaryPreview( $isSubjectPreview, $summary = "" ) {
3224  // avoid spaces in preview, gets always trimmed on save
3225  $summary = trim( $summary );
3226  if ( !$summary || ( !$this->preview && !$this->diff ) ) {
3227  return "";
3228  }
3229 
3230  global $wgParser;
3231 
3232  if ( $isSubjectPreview ) {
3233  $summary = $this->context->msg( 'newsectionsummary' )
3234  ->rawParams( $wgParser->stripSectionName( $summary ) )
3235  ->inContentLanguage()->text();
3236  }
3237 
3238  $message = $isSubjectPreview ? 'subject-preview' : 'summary-preview';
3239 
3240  $summary = $this->context->msg( $message )->parse()
3241  . Linker::commentBlock( $summary, $this->mTitle, $isSubjectPreview );
3242  return Xml::tags( 'div', [ 'class' => 'mw-summary-preview' ], $summary );
3243  }
3244 
3245  protected function showFormBeforeText() {
3246  $out = $this->context->getOutput();
3247  $out->addHTML( Html::hidden( 'wpSection', $this->section ) );
3248  $out->addHTML( Html::hidden( 'wpStarttime', $this->starttime ) );
3249  $out->addHTML( Html::hidden( 'wpEdittime', $this->edittime ) );
3250  $out->addHTML( Html::hidden( 'editRevId', $this->editRevId ) );
3251  $out->addHTML( Html::hidden( 'wpScrolltop', $this->scrolltop, [ 'id' => 'wpScrolltop' ] ) );
3252  }
3253 
3254  protected function showFormAfterText() {
3267  $this->context->getOutput()->addHTML(
3268  "\n" .
3269  Html::hidden( "wpEditToken", $this->context->getUser()->getEditToken() ) .
3270  "\n"
3271  );
3272  }
3273 
3282  protected function showContentForm() {
3283  $this->showTextbox1();
3284  }
3285 
3294  protected function showTextbox1( $customAttribs = null, $textoverride = null ) {
3295  if ( $this->wasDeletedSinceLastEdit() && $this->formtype == 'save' ) {
3296  $attribs = [ 'style' => 'display:none;' ];
3297  } else {
3298  $builder = new TextboxBuilder();
3299  $classes = $builder->getTextboxProtectionCSSClasses( $this->getTitle() );
3300 
3301  # Is an old revision being edited?
3302  if ( $this->isOldRev ) {
3303  $classes[] = 'mw-textarea-oldrev';
3304  }
3305 
3306  $attribs = [ 'tabindex' => 1 ];
3307 
3308  if ( is_array( $customAttribs ) ) {
3310  }
3311 
3312  $attribs = $builder->mergeClassesIntoAttributes( $classes, $attribs );
3313  }
3314 
3315  $this->showTextbox(
3316  $textoverride !== null ? $textoverride : $this->textbox1,
3317  'wpTextbox1',
3318  $attribs
3319  );
3320  }
3321 
3322  protected function showTextbox2() {
3323  $this->showTextbox( $this->textbox2, 'wpTextbox2', [ 'tabindex' => 6, 'readonly' ] );
3324  }
3325 
3326  protected function showTextbox( $text, $name, $customAttribs = [] ) {
3327  $builder = new TextboxBuilder();
3328  $attribs = $builder->buildTextboxAttribs(
3329  $name,
3331  $this->context->getUser(),
3333  );
3334 
3335  $this->context->getOutput()->addHTML(
3336  Html::textarea( $name, $builder->addNewLineAtEnd( $text ), $attribs )
3337  );
3338  }
3339 
3340  protected function displayPreviewArea( $previewOutput, $isOnTop = false ) {
3341  $classes = [];
3342  if ( $isOnTop ) {
3343  $classes[] = 'ontop';
3344  }
3345 
3346  $attribs = [ 'id' => 'wikiPreview', 'class' => implode( ' ', $classes ) ];
3347 
3348  if ( $this->formtype != 'preview' ) {
3349  $attribs['style'] = 'display: none;';
3350  }
3351 
3352  $out = $this->context->getOutput();
3353  $out->addHTML( Xml::openElement( 'div', $attribs ) );
3354 
3355  if ( $this->formtype == 'preview' ) {
3356  $this->showPreview( $previewOutput );
3357  } else {
3358  // Empty content container for LivePreview
3359  $pageViewLang = $this->mTitle->getPageViewLanguage();
3360  $attribs = [ 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(),
3361  'class' => 'mw-content-' . $pageViewLang->getDir() ];
3362  $out->addHTML( Html::rawElement( 'div', $attribs ) );
3363  }
3364 
3365  $out->addHTML( '</div>' );
3366 
3367  if ( $this->formtype == 'diff' ) {
3368  try {
3369  $this->showDiff();
3370  } catch ( MWContentSerializationException $ex ) {
3371  $msg = $this->context->msg(
3372  'content-failed-to-parse',
3373  $this->contentModel,
3374  $this->contentFormat,
3375  $ex->getMessage()
3376  );
3377  $out->addWikiText( '<div class="error">' . $msg->text() . '</div>' );
3378  }
3379  }
3380  }
3381 
3388  protected function showPreview( $text ) {
3389  if ( $this->mArticle instanceof CategoryPage ) {
3390  $this->mArticle->openShowCategory();
3391  }
3392  # This hook seems slightly odd here, but makes things more
3393  # consistent for extensions.
3394  $out = $this->context->getOutput();
3395  Hooks::run( 'OutputPageBeforeHTML', [ &$out, &$text ] );
3396  $out->addHTML( $text );
3397  if ( $this->mArticle instanceof CategoryPage ) {
3398  $this->mArticle->closeShowCategory();
3399  }
3400  }
3401 
3409  public function showDiff() {
3411 
3412  $oldtitlemsg = 'currentrev';
3413  # if message does not exist, show diff against the preloaded default
3414  if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && !$this->mTitle->exists() ) {
3415  $oldtext = $this->mTitle->getDefaultMessageText();
3416  if ( $oldtext !== false ) {
3417  $oldtitlemsg = 'defaultmessagetext';
3418  $oldContent = $this->toEditContent( $oldtext );
3419  } else {
3420  $oldContent = null;
3421  }
3422  } else {
3423  $oldContent = $this->getCurrentContent();
3424  }
3425 
3426  $textboxContent = $this->toEditContent( $this->textbox1 );
3427  if ( $this->editRevId !== null ) {
3428  $newContent = $this->page->replaceSectionAtRev(
3429  $this->section, $textboxContent, $this->summary, $this->editRevId
3430  );
3431  } else {
3432  $newContent = $this->page->replaceSectionContent(
3433  $this->section, $textboxContent, $this->summary, $this->edittime
3434  );
3435  }
3436 
3437  if ( $newContent ) {
3438  Hooks::run( 'EditPageGetDiffContent', [ $this, &$newContent ] );
3439 
3440  $user = $this->context->getUser();
3441  $popts = ParserOptions::newFromUserAndLang( $user, $wgContLang );
3442  $newContent = $newContent->preSaveTransform( $this->mTitle, $user, $popts );
3443  }
3444 
3445  if ( ( $oldContent && !$oldContent->isEmpty() ) || ( $newContent && !$newContent->isEmpty() ) ) {
3446  $oldtitle = $this->context->msg( $oldtitlemsg )->parse();
3447  $newtitle = $this->context->msg( 'yourtext' )->parse();
3448 
3449  if ( !$oldContent ) {
3450  $oldContent = $newContent->getContentHandler()->makeEmptyContent();
3451  }
3452 
3453  if ( !$newContent ) {
3454  $newContent = $oldContent->getContentHandler()->makeEmptyContent();
3455  }
3456 
3457  $de = $oldContent->getContentHandler()->createDifferenceEngine( $this->context );
3458  $de->setContent( $oldContent, $newContent );
3459 
3460  $difftext = $de->getDiff( $oldtitle, $newtitle );
3461  $de->showDiffStyle();
3462  } else {
3463  $difftext = '';
3464  }
3465 
3466  $this->context->getOutput()->addHTML( '<div id="wikiDiff">' . $difftext . '</div>' );
3467  }
3468 
3472  protected function showHeaderCopyrightWarning() {
3473  $msg = 'editpage-head-copy-warn';
3474  if ( !$this->context->msg( $msg )->isDisabled() ) {
3475  $this->context->getOutput()->wrapWikiMsg( "<div class='editpage-head-copywarn'>\n$1\n</div>",
3476  'editpage-head-copy-warn' );
3477  }
3478  }
3479 
3488  protected function showTosSummary() {
3489  $msg = 'editpage-tos-summary';
3490  Hooks::run( 'EditPageTosSummary', [ $this->mTitle, &$msg ] );
3491  if ( !$this->context->msg( $msg )->isDisabled() ) {
3492  $out = $this->context->getOutput();
3493  $out->addHTML( '<div class="mw-tos-summary">' );
3494  $out->addWikiMsg( $msg );
3495  $out->addHTML( '</div>' );
3496  }
3497  }
3498 
3503  protected function showEditTools() {
3504  $this->context->getOutput()->addHTML( '<div class="mw-editTools">' .
3505  $this->context->msg( 'edittools' )->inContentLanguage()->parse() .
3506  '</div>' );
3507  }
3508 
3515  protected function getCopywarn() {
3516  return self::getCopyrightWarning( $this->mTitle );
3517  }
3518 
3527  public static function getCopyrightWarning( $title, $format = 'plain', $langcode = null ) {
3528  global $wgRightsText;
3529  if ( $wgRightsText ) {
3530  $copywarnMsg = [ 'copyrightwarning',
3531  '[[' . wfMessage( 'copyrightpage' )->inContentLanguage()->text() . ']]',
3532  $wgRightsText ];
3533  } else {
3534  $copywarnMsg = [ 'copyrightwarning2',
3535  '[[' . wfMessage( 'copyrightpage' )->inContentLanguage()->text() . ']]' ];
3536  }
3537  // Allow for site and per-namespace customization of contribution/copyright notice.
3538  Hooks::run( 'EditPageCopyrightWarning', [ $title, &$copywarnMsg ] );
3539 
3540  $msg = call_user_func_array( 'wfMessage', $copywarnMsg )->title( $title );
3541  if ( $langcode ) {
3542  $msg->inLanguage( $langcode );
3543  }
3544  return "<div id=\"editpage-copywarn\">\n" .
3545  $msg->$format() . "\n</div>";
3546  }
3547 
3555  public static function getPreviewLimitReport( $output ) {
3556  global $wgLang;
3557 
3558  if ( !$output || !$output->getLimitReportData() ) {
3559  return '';
3560  }
3561 
3562  $limitReport = Html::rawElement( 'div', [ 'class' => 'mw-limitReportExplanation' ],
3563  wfMessage( 'limitreport-title' )->parseAsBlock()
3564  );
3565 
3566  // Show/hide animation doesn't work correctly on a table, so wrap it in a div.
3567  $limitReport .= Html::openElement( 'div', [ 'class' => 'preview-limit-report-wrapper' ] );
3568 
3569  $limitReport .= Html::openElement( 'table', [
3570  'class' => 'preview-limit-report wikitable'
3571  ] ) .
3572  Html::openElement( 'tbody' );
3573 
3574  foreach ( $output->getLimitReportData() as $key => $value ) {
3575  if ( Hooks::run( 'ParserLimitReportFormat',
3576  [ $key, &$value, &$limitReport, true, true ]
3577  ) ) {
3578  $keyMsg = wfMessage( $key );
3579  $valueMsg = wfMessage( [ "$key-value-html", "$key-value" ] );
3580  if ( !$valueMsg->exists() ) {
3581  $valueMsg = new RawMessage( '$1' );
3582  }
3583  if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) {
3584  $limitReport .= Html::openElement( 'tr' ) .
3585  Html::rawElement( 'th', null, $keyMsg->parse() ) .
3586  Html::rawElement( 'td', null,
3587  $wgLang->formatNum( $valueMsg->params( $value )->parse() )
3588  ) .
3589  Html::closeElement( 'tr' );
3590  }
3591  }
3592  }
3593 
3594  $limitReport .= Html::closeElement( 'tbody' ) .
3595  Html::closeElement( 'table' ) .
3596  Html::closeElement( 'div' );
3597 
3598  return $limitReport;
3599  }
3600 
3601  protected function showStandardInputs( &$tabindex = 2 ) {
3602  $out = $this->context->getOutput();
3603  $out->addHTML( "<div class='editOptions'>\n" );
3604 
3605  if ( $this->section != 'new' ) {
3606  $this->showSummaryInput( false, $this->summary );
3607  $out->addHTML( $this->getSummaryPreview( false, $this->summary ) );
3608  }
3609 
3610  $checkboxes = $this->getCheckboxesWidget(
3611  $tabindex,
3612  [ 'minor' => $this->minoredit, 'watch' => $this->watchthis ]
3613  );
3614  $checkboxesHTML = new OOUI\HorizontalLayout( [ 'items' => $checkboxes ] );
3615 
3616  $out->addHTML( "<div class='editCheckboxes'>" . $checkboxesHTML . "</div>\n" );
3617 
3618  // Show copyright warning.
3619  $out->addWikiText( $this->getCopywarn() );
3620  $out->addHTML( $this->editFormTextAfterWarn );
3621 
3622  $out->addHTML( "<div class='editButtons'>\n" );
3623  $out->addHTML( implode( "\n", $this->getEditButtons( $tabindex ) ) . "\n" );
3624 
3625  $cancel = $this->getCancelLink();
3626 
3627  $message = $this->context->msg( 'edithelppage' )->inContentLanguage()->text();
3628  $edithelpurl = Skin::makeInternalOrExternalUrl( $message );
3629  $edithelp =
3631  $this->context->msg( 'edithelp' )->text(),
3632  [ 'target' => 'helpwindow', 'href' => $edithelpurl ],
3633  [ 'mw-ui-quiet' ]
3634  ) .
3635  $this->context->msg( 'word-separator' )->escaped() .
3636  $this->context->msg( 'newwindow' )->parse();
3637 
3638  $out->addHTML( " <span class='cancelLink'>{$cancel}</span>\n" );
3639  $out->addHTML( " <span class='editHelp'>{$edithelp}</span>\n" );
3640  $out->addHTML( "</div><!-- editButtons -->\n" );
3641 
3642  Hooks::run( 'EditPage::showStandardInputs:options', [ $this, $out, &$tabindex ] );
3643 
3644  $out->addHTML( "</div><!-- editOptions -->\n" );
3645  }
3646 
3651  protected function showConflict() {
3652  $out = $this->context->getOutput();
3653  // Avoid PHP 7.1 warning of passing $this by reference
3654  $editPage = $this;
3655  if ( Hooks::run( 'EditPageBeforeConflictDiff', [ &$editPage, &$out ] ) ) {
3656  $this->incrementConflictStats();
3657 
3658  $this->getEditConflictHelper()->showEditFormTextAfterFooters();
3659  }
3660  }
3661 
3662  protected function incrementConflictStats() {
3663  $this->getEditConflictHelper()->incrementConflictStats();
3664  }
3665 
3669  public function getCancelLink() {
3670  $cancelParams = [];
3671  if ( !$this->isConflict && $this->oldid > 0 ) {
3672  $cancelParams['oldid'] = $this->oldid;
3673  } elseif ( $this->getContextTitle()->isRedirect() ) {
3674  $cancelParams['redirect'] = 'no';
3675  }
3676 
3677  return new OOUI\ButtonWidget( [
3678  'id' => 'mw-editform-cancel',
3679  'href' => $this->getContextTitle()->getLinkURL( $cancelParams ),
3680  'label' => new OOUI\HtmlSnippet( $this->context->msg( 'cancel' )->parse() ),
3681  'framed' => false,
3682  'infusable' => true,
3683  'flags' => 'destructive',
3684  ] );
3685  }
3686 
3696  protected function getActionURL( Title $title ) {
3697  return $title->getLocalURL( [ 'action' => $this->action ] );
3698  }
3699 
3707  protected function wasDeletedSinceLastEdit() {
3708  if ( $this->deletedSinceEdit !== null ) {
3709  return $this->deletedSinceEdit;
3710  }
3711 
3712  $this->deletedSinceEdit = false;
3713 
3714  if ( !$this->mTitle->exists() && $this->mTitle->isDeletedQuick() ) {
3715  $this->lastDelete = $this->getLastDelete();
3716  if ( $this->lastDelete ) {
3717  $deleteTime = wfTimestamp( TS_MW, $this->lastDelete->log_timestamp );
3718  if ( $deleteTime > $this->starttime ) {
3719  $this->deletedSinceEdit = true;
3720  }
3721  }
3722  }
3723 
3724  return $this->deletedSinceEdit;
3725  }
3726 
3730  protected function getLastDelete() {
3731  $dbr = wfGetDB( DB_REPLICA );
3732  $commentQuery = CommentStore::getStore()->getJoin( 'log_comment' );
3733  $actorQuery = ActorMigration::newMigration()->getJoin( 'log_user' );
3734  $data = $dbr->selectRow(
3735  array_merge( [ 'logging' ], $commentQuery['tables'], $actorQuery['tables'], [ 'user' ] ),
3736  [
3737  'log_type',
3738  'log_action',
3739  'log_timestamp',
3740  'log_namespace',
3741  'log_title',
3742  'log_params',
3743  'log_deleted',
3744  'user_name'
3745  ] + $commentQuery['fields'] + $actorQuery['fields'],
3746  [
3747  'log_namespace' => $this->mTitle->getNamespace(),
3748  'log_title' => $this->mTitle->getDBkey(),
3749  'log_type' => 'delete',
3750  'log_action' => 'delete',
3751  ],
3752  __METHOD__,
3753  [ 'LIMIT' => 1, 'ORDER BY' => 'log_timestamp DESC' ],
3754  [
3755  'user' => [ 'JOIN', 'user_id=' . $actorQuery['fields']['log_user'] ],
3756  ] + $commentQuery['joins'] + $actorQuery['joins']
3757  );
3758  // Quick paranoid permission checks...
3759  if ( is_object( $data ) ) {
3760  if ( $data->log_deleted & LogPage::DELETED_USER ) {
3761  $data->user_name = $this->context->msg( 'rev-deleted-user' )->escaped();
3762  }
3763 
3764  if ( $data->log_deleted & LogPage::DELETED_COMMENT ) {
3765  $data->log_comment_text = $this->context->msg( 'rev-deleted-comment' )->escaped();
3766  $data->log_comment_data = null;
3767  }
3768  }
3769 
3770  return $data;
3771  }
3772 
3778  public function getPreviewText() {
3779  $out = $this->context->getOutput();
3780  $config = $this->context->getConfig();
3781 
3782  if ( $config->get( 'RawHtml' ) && !$this->mTokenOk ) {
3783  // Could be an offsite preview attempt. This is very unsafe if
3784  // HTML is enabled, as it could be an attack.
3785  $parsedNote = '';
3786  if ( $this->textbox1 !== '' ) {
3787  // Do not put big scary notice, if previewing the empty
3788  // string, which happens when you initially edit
3789  // a category page, due to automatic preview-on-open.
3790  $parsedNote = $out->parse( "<div class='previewnote'>" .
3791  $this->context->msg( 'session_fail_preview_html' )->text() . "</div>",
3792  true, /* interface */true );
3793  }
3794  $this->incrementEditFailureStats( 'session_loss' );
3795  return $parsedNote;
3796  }
3797 
3798  $note = '';
3799 
3800  try {
3801  $content = $this->toEditContent( $this->textbox1 );
3802 
3803  $previewHTML = '';
3804  if ( !Hooks::run(
3805  'AlternateEditPreview',
3806  [ $this, &$content, &$previewHTML, &$this->mParserOutput ] )
3807  ) {
3808  return $previewHTML;
3809  }
3810 
3811  # provide a anchor link to the editform
3812  $continueEditing = '<span class="mw-continue-editing">' .
3813  '[[#' . self::EDITFORM_ID . '|' .
3814  $this->context->getLanguage()->getArrow() . ' ' .
3815  $this->context->msg( 'continue-editing' )->text() . ']]</span>';
3816  if ( $this->mTriedSave && !$this->mTokenOk ) {
3817  if ( $this->mTokenOkExceptSuffix ) {
3818  $note = $this->context->msg( 'token_suffix_mismatch' )->plain();
3819  $this->incrementEditFailureStats( 'bad_token' );
3820  } else {
3821  $note = $this->context->msg( 'session_fail_preview' )->plain();
3822  $this->incrementEditFailureStats( 'session_loss' );
3823  }
3824  } elseif ( $this->incompleteForm ) {
3825  $note = $this->context->msg( 'edit_form_incomplete' )->plain();
3826  if ( $this->mTriedSave ) {
3827  $this->incrementEditFailureStats( 'incomplete_form' );
3828  }
3829  } else {
3830  $note = $this->context->msg( 'previewnote' )->plain() . ' ' . $continueEditing;
3831  }
3832 
3833  # don't parse non-wikitext pages, show message about preview
3834  if ( $this->mTitle->isUserConfigPage() || $this->mTitle->isSiteConfigPage() ) {
3835  if ( $this->mTitle->isUserConfigPage() ) {
3836  $level = 'user';
3837  } elseif ( $this->mTitle->isSiteConfigPage() ) {
3838  $level = 'site';
3839  } else {
3840  $level = false;
3841  }
3842 
3843  if ( $content->getModel() == CONTENT_MODEL_CSS ) {
3844  $format = 'css';
3845  if ( $level === 'user' && !$config->get( 'AllowUserCss' ) ) {
3846  $format = false;
3847  }
3848  } elseif ( $content->getModel() == CONTENT_MODEL_JSON ) {
3849  $format = 'json';
3850  if ( $level === 'user' /* No comparable 'AllowUserJson' */ ) {
3851  $format = false;
3852  }
3853  } elseif ( $content->getModel() == CONTENT_MODEL_JAVASCRIPT ) {
3854  $format = 'js';
3855  if ( $level === 'user' && !$config->get( 'AllowUserJs' ) ) {
3856  $format = false;
3857  }
3858  } else {
3859  $format = false;
3860  }
3861 
3862  # Used messages to make sure grep find them:
3863  # Messages: usercsspreview, userjsonpreview, userjspreview,
3864  # sitecsspreview, sitejsonpreview, sitejspreview
3865  if ( $level && $format ) {
3866  $note = "<div id='mw-{$level}{$format}preview'>" .
3867  $this->context->msg( "{$level}{$format}preview" )->text() .
3868  ' ' . $continueEditing . "</div>";
3869  }
3870  }
3871 
3872  # If we're adding a comment, we need to show the
3873  # summary as the headline
3874  if ( $this->section === "new" && $this->summary !== "" ) {
3875  $content = $content->addSectionHeader( $this->summary );
3876  }
3877 
3878  $hook_args = [ $this, &$content ];
3879  Hooks::run( 'EditPageGetPreviewContent', $hook_args );
3880 
3881  $parserResult = $this->doPreviewParse( $content );
3882  $parserOutput = $parserResult['parserOutput'];
3883  $previewHTML = $parserResult['html'];
3884  $this->mParserOutput = $parserOutput;
3885  $out->addParserOutputMetadata( $parserOutput );
3886  if ( $out->userCanPreview() ) {
3887  $out->addContentOverride( $this->getTitle(), $content );
3888  }
3889 
3890  if ( count( $parserOutput->getWarnings() ) ) {
3891  $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
3892  }
3893 
3894  } catch ( MWContentSerializationException $ex ) {
3895  $m = $this->context->msg(
3896  'content-failed-to-parse',
3897  $this->contentModel,
3898  $this->contentFormat,
3899  $ex->getMessage()
3900  );
3901  $note .= "\n\n" . $m->parse();
3902  $previewHTML = '';
3903  }
3904 
3905  if ( $this->isConflict ) {
3906  $conflict = '<h2 id="mw-previewconflict">'
3907  . $this->context->msg( 'previewconflict' )->escaped() . "</h2>\n";
3908  } else {
3909  $conflict = '<hr />';
3910  }
3911 
3912  $previewhead = "<div class='previewnote'>\n" .
3913  '<h2 id="mw-previewheader">' . $this->context->msg( 'preview' )->escaped() . "</h2>" .
3914  $out->parse( $note, true, /* interface */true ) . $conflict . "</div>\n";
3915 
3916  $pageViewLang = $this->mTitle->getPageViewLanguage();
3917  $attribs = [ 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(),
3918  'class' => 'mw-content-' . $pageViewLang->getDir() ];
3919  $previewHTML = Html::rawElement( 'div', $attribs, $previewHTML );
3920 
3921  return $previewhead . $previewHTML . $this->previewTextAfterContent;
3922  }
3923 
3924  private function incrementEditFailureStats( $failureType ) {
3925  $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
3926  $stats->increment( 'edit.failures.' . $failureType );
3927  }
3928 
3933  protected function getPreviewParserOptions() {
3934  $parserOptions = $this->page->makeParserOptions( $this->context );
3935  $parserOptions->setIsPreview( true );
3936  $parserOptions->setIsSectionPreview( !is_null( $this->section ) && $this->section !== '' );
3937  $parserOptions->enableLimitReport();
3938  return $parserOptions;
3939  }
3940 
3950  protected function doPreviewParse( Content $content ) {
3951  $user = $this->context->getUser();
3952  $parserOptions = $this->getPreviewParserOptions();
3953  $pstContent = $content->preSaveTransform( $this->mTitle, $user, $parserOptions );
3954  $scopedCallback = $parserOptions->setupFakeRevision(
3955  $this->mTitle, $pstContent, $user );
3956  $parserOutput = $pstContent->getParserOutput( $this->mTitle, null, $parserOptions );
3957  ScopedCallback::consume( $scopedCallback );
3958  return [
3959  'parserOutput' => $parserOutput,
3960  'html' => $parserOutput->getText( [
3961  'enableSectionEditLinks' => false
3962  ] )
3963  ];
3964  }
3965 
3969  public function getTemplates() {
3970  if ( $this->preview || $this->section != '' ) {
3971  $templates = [];
3972  if ( !isset( $this->mParserOutput ) ) {
3973  return $templates;
3974  }
3975  foreach ( $this->mParserOutput->getTemplates() as $ns => $template ) {
3976  foreach ( array_keys( $template ) as $dbk ) {
3977  $templates[] = Title::makeTitle( $ns, $dbk );
3978  }
3979  }
3980  return $templates;
3981  } else {
3982  return $this->mTitle->getTemplateLinksFrom();
3983  }
3984  }
3985 
3993  public static function getEditToolbar( $title = null ) {
3996 
3997  $imagesAvailable = $wgEnableUploads || count( $wgForeignFileRepos );
3998  $showSignature = true;
3999  if ( $title ) {
4000  $showSignature = MWNamespace::wantSignatures( $title->getNamespace() );
4001  }
4002 
4012  $toolarray = [
4013  [
4014  'id' => 'mw-editbutton-bold',
4015  'open' => '\'\'\'',
4016  'close' => '\'\'\'',
4017  'sample' => wfMessage( 'bold_sample' )->text(),
4018  'tip' => wfMessage( 'bold_tip' )->text(),
4019  ],
4020  [
4021  'id' => 'mw-editbutton-italic',
4022  'open' => '\'\'',
4023  'close' => '\'\'',
4024  'sample' => wfMessage( 'italic_sample' )->text(),
4025  'tip' => wfMessage( 'italic_tip' )->text(),
4026  ],
4027  [
4028  'id' => 'mw-editbutton-link',
4029  'open' => '[[',
4030  'close' => ']]',
4031  'sample' => wfMessage( 'link_sample' )->text(),
4032  'tip' => wfMessage( 'link_tip' )->text(),
4033  ],
4034  [
4035  'id' => 'mw-editbutton-extlink',
4036  'open' => '[',
4037  'close' => ']',
4038  'sample' => wfMessage( 'extlink_sample' )->text(),
4039  'tip' => wfMessage( 'extlink_tip' )->text(),
4040  ],
4041  [
4042  'id' => 'mw-editbutton-headline',
4043  'open' => "\n== ",
4044  'close' => " ==\n",
4045  'sample' => wfMessage( 'headline_sample' )->text(),
4046  'tip' => wfMessage( 'headline_tip' )->text(),
4047  ],
4048  $imagesAvailable ? [
4049  'id' => 'mw-editbutton-image',
4050  'open' => '[[' . $wgContLang->getNsText( NS_FILE ) . ':',
4051  'close' => ']]',
4052  'sample' => wfMessage( 'image_sample' )->text(),
4053  'tip' => wfMessage( 'image_tip' )->text(),
4054  ] : false,
4055  $imagesAvailable ? [
4056  'id' => 'mw-editbutton-media',
4057  'open' => '[[' . $wgContLang->getNsText( NS_MEDIA ) . ':',
4058  'close' => ']]',
4059  'sample' => wfMessage( 'media_sample' )->text(),
4060  'tip' => wfMessage( 'media_tip' )->text(),
4061  ] : false,
4062  [
4063  'id' => 'mw-editbutton-nowiki',
4064  'open' => "<nowiki>",
4065  'close' => "</nowiki>",
4066  'sample' => wfMessage( 'nowiki_sample' )->text(),
4067  'tip' => wfMessage( 'nowiki_tip' )->text(),
4068  ],
4069  $showSignature ? [
4070  'id' => 'mw-editbutton-signature',
4071  'open' => wfMessage( 'sig-text', '~~~~' )->inContentLanguage()->text(),
4072  'close' => '',
4073  'sample' => '',
4074  'tip' => wfMessage( 'sig_tip' )->text(),
4075  ] : false,
4076  [
4077  'id' => 'mw-editbutton-hr',
4078  'open' => "\n----\n",
4079  'close' => '',
4080  'sample' => '',
4081  'tip' => wfMessage( 'hr_tip' )->text(),
4082  ]
4083  ];
4084 
4085  $script = 'mw.loader.using("mediawiki.toolbar", function () {';
4086  foreach ( $toolarray as $tool ) {
4087  if ( !$tool ) {
4088  continue;
4089  }
4090 
4091  $params = [
4092  // Images are defined in ResourceLoaderEditToolbarModule
4093  false,
4094  // Note that we use the tip both for the ALT tag and the TITLE tag of the image.
4095  // Older browsers show a "speedtip" type message only for ALT.
4096  // Ideally these should be different, realistically they
4097  // probably don't need to be.
4098  $tool['tip'],
4099  $tool['open'],
4100  $tool['close'],
4101  $tool['sample'],
4102  $tool['id'],
4103  ];
4104 
4105  $script .= Xml::encodeJsCall(
4106  'mw.toolbar.addButton',
4107  $params,
4109  );
4110  }
4111 
4112  $script .= '});';
4113 
4114  $nonce = $wgOut->getCSPNonce();
4115  $wgOut->addScript( ResourceLoader::makeInlineScript( $script, $nonce ) );
4116 
4117  $toolbar = '<div id="toolbar"></div>';
4118 
4119  if ( Hooks::run( 'EditPageBeforeEditToolbar', [ &$toolbar ] ) ) {
4120  // Only add the old toolbar cruft to the page payload if the toolbar has not
4121  // been over-written by a hook caller
4122  $wgOut->addScript( ResourceLoader::makeInlineScript( $script, $nonce ) );
4123  };
4124 
4125  return $toolbar;
4126  }
4127 
4146  public function getCheckboxesDefinition( $checked ) {
4147  $checkboxes = [];
4148 
4149  $user = $this->context->getUser();
4150  // don't show the minor edit checkbox if it's a new page or section
4151  if ( !$this->isNew && $user->isAllowed( 'minoredit' ) ) {
4152  $checkboxes['wpMinoredit'] = [
4153  'id' => 'wpMinoredit',
4154  'label-message' => 'minoredit',
4155  // Uses messages: tooltip-minoredit, accesskey-minoredit
4156  'tooltip' => 'minoredit',
4157  'label-id' => 'mw-editpage-minoredit',
4158  'legacy-name' => 'minor',
4159  'default' => $checked['minor'],
4160  ];
4161  }
4162 
4163  if ( $user->isLoggedIn() ) {
4164  $checkboxes['wpWatchthis'] = [
4165  'id' => 'wpWatchthis',
4166  'label-message' => 'watchthis',
4167  // Uses messages: tooltip-watch, accesskey-watch
4168  'tooltip' => 'watch',
4169  'label-id' => 'mw-editpage-watch',
4170  'legacy-name' => 'watch',
4171  'default' => $checked['watch'],
4172  ];
4173  }
4174 
4175  $editPage = $this;
4176  Hooks::run( 'EditPageGetCheckboxesDefinition', [ $editPage, &$checkboxes ] );
4177 
4178  return $checkboxes;
4179  }
4180 
4191  public function getCheckboxesWidget( &$tabindex, $checked ) {
4192  $checkboxes = [];
4193  $checkboxesDef = $this->getCheckboxesDefinition( $checked );
4194 
4195  foreach ( $checkboxesDef as $name => $options ) {
4196  $legacyName = isset( $options['legacy-name'] ) ? $options['legacy-name'] : $name;
4197 
4198  $title = null;
4199  $accesskey = null;
4200  if ( isset( $options['tooltip'] ) ) {
4201  $accesskey = $this->context->msg( "accesskey-{$options['tooltip']}" )->text();
4202  $title = Linker::titleAttrib( $options['tooltip'] );
4203  }
4204  if ( isset( $options['title-message'] ) ) {
4205  $title = $this->context->msg( $options['title-message'] )->text();
4206  }
4207 
4208  $checkboxes[ $legacyName ] = new OOUI\FieldLayout(
4209  new OOUI\CheckboxInputWidget( [
4210  'tabIndex' => ++$tabindex,
4211  'accessKey' => $accesskey,
4212  'id' => $options['id'] . 'Widget',
4213  'inputId' => $options['id'],
4214  'name' => $name,
4215  'selected' => $options['default'],
4216  'infusable' => true,
4217  ] ),
4218  [
4219  'align' => 'inline',
4220  'label' => new OOUI\HtmlSnippet( $this->context->msg( $options['label-message'] )->parse() ),
4221  'title' => $title,
4222  'id' => isset( $options['label-id'] ) ? $options['label-id'] : null,
4223  ]
4224  );
4225  }
4226 
4227  // Backwards-compatibility hack to run the EditPageBeforeEditChecks hook. It's important,
4228  // people have used it for the weirdest things completely unrelated to checkboxes...
4229  // And if we're gonna run it, might as well allow its legacy checkboxes to be shown.
4230  $legacyCheckboxes = [];
4231  if ( !$this->isNew ) {
4232  $legacyCheckboxes['minor'] = '';
4233  }
4234  $legacyCheckboxes['watch'] = '';
4235  // Copy new-style checkboxes into an old-style structure
4236  foreach ( $checkboxes as $name => $oouiLayout ) {
4237  $legacyCheckboxes[$name] = (string)$oouiLayout;
4238  }
4239  // Avoid PHP 7.1 warning of passing $this by reference
4240  $ep = $this;
4241  Hooks::run( 'EditPageBeforeEditChecks', [ &$ep, &$legacyCheckboxes, &$tabindex ], '1.29' );
4242  // Copy back any additional old-style checkboxes into the new-style structure
4243  foreach ( $legacyCheckboxes as $name => $html ) {
4244  if ( $html && !isset( $checkboxes[$name] ) ) {
4245  $checkboxes[$name] = new OOUI\Widget( [ 'content' => new OOUI\HtmlSnippet( $html ) ] );
4246  }
4247  }
4248 
4249  return $checkboxes;
4250  }
4251 
4258  protected function getSubmitButtonLabel() {
4259  $labelAsPublish =
4260  $this->context->getConfig()->get( 'EditSubmitButtonLabelPublish' );
4261 
4262  // Can't use $this->isNew as that's also true if we're adding a new section to an extant page
4263  $newPage = !$this->mTitle->exists();
4264 
4265  if ( $labelAsPublish ) {
4266  $buttonLabelKey = $newPage ? 'publishpage' : 'publishchanges';
4267  } else {
4268  $buttonLabelKey = $newPage ? 'savearticle' : 'savechanges';
4269  }
4270 
4271  return $buttonLabelKey;
4272  }
4273 
4282  public function getEditButtons( &$tabindex ) {
4283  $buttons = [];
4284 
4285  $labelAsPublish =
4286  $this->context->getConfig()->get( 'EditSubmitButtonLabelPublish' );
4287 
4288  $buttonLabel = $this->context->msg( $this->getSubmitButtonLabel() )->text();
4289  $buttonTooltip = $labelAsPublish ? 'publish' : 'save';
4290 
4291  $buttons['save'] = new OOUI\ButtonInputWidget( [
4292  'name' => 'wpSave',
4293  'tabIndex' => ++$tabindex,
4294  'id' => 'wpSaveWidget',
4295  'inputId' => 'wpSave',
4296  // Support: IE 6 – Use <input>, otherwise it can't distinguish which button was clicked
4297  'useInputTag' => true,
4298  'flags' => [ 'progressive', 'primary' ],
4299  'label' => $buttonLabel,
4300  'infusable' => true,
4301  'type' => 'submit',
4302  // Messages used: tooltip-save, tooltip-publish
4303  'title' => Linker::titleAttrib( $buttonTooltip ),
4304  // Messages used: accesskey-save, accesskey-publish
4305  'accessKey' => Linker::accesskey( $buttonTooltip ),
4306  ] );
4307 
4308  $buttons['preview'] = new OOUI\ButtonInputWidget( [
4309  'name' => 'wpPreview',
4310  'tabIndex' => ++$tabindex,
4311  'id' => 'wpPreviewWidget',
4312  'inputId' => 'wpPreview',
4313  // Support: IE 6 – Use <input>, otherwise it can't distinguish which button was clicked
4314  'useInputTag' => true,
4315  'label' => $this->context->msg( 'showpreview' )->text(),
4316  'infusable' => true,
4317  'type' => 'submit',
4318  // Message used: tooltip-preview
4319  'title' => Linker::titleAttrib( 'preview' ),
4320  // Message used: accesskey-preview
4321  'accessKey' => Linker::accesskey( 'preview' ),
4322  ] );
4323 
4324  $buttons['diff'] = new OOUI\ButtonInputWidget( [
4325  'name' => 'wpDiff',
4326  'tabIndex' => ++$tabindex,
4327  'id' => 'wpDiffWidget',
4328  'inputId' => 'wpDiff',
4329  // Support: IE 6 – Use <input>, otherwise it can't distinguish which button was clicked
4330  'useInputTag' => true,
4331  'label' => $this->context->msg( 'showdiff' )->text(),
4332  'infusable' => true,
4333  'type' => 'submit',
4334  // Message used: tooltip-diff
4335  'title' => Linker::titleAttrib( 'diff' ),
4336  // Message used: accesskey-diff
4337  'accessKey' => Linker::accesskey( 'diff' ),
4338  ] );
4339 
4340  // Avoid PHP 7.1 warning of passing $this by reference
4341  $editPage = $this;
4342  Hooks::run( 'EditPageBeforeEditButtons', [ &$editPage, &$buttons, &$tabindex ] );
4343 
4344  return $buttons;
4345  }
4346 
4351  public function noSuchSectionPage() {
4352  $out = $this->context->getOutput();
4353  $out->prepareErrorPage( $this->context->msg( 'nosuchsectiontitle' ) );
4354 
4355  $res = $this->context->msg( 'nosuchsectiontext', $this->section )->parseAsBlock();
4356 
4357  // Avoid PHP 7.1 warning of passing $this by reference
4358  $editPage = $this;
4359  Hooks::run( 'EditPageNoSuchSection', [ &$editPage, &$res ] );
4360  $out->addHTML( $res );
4361 
4362  $out->returnToMain( false, $this->mTitle );
4363  }
4364 
4370  public function spamPageWithContent( $match = false ) {
4371  $this->textbox2 = $this->textbox1;
4372 
4373  if ( is_array( $match ) ) {
4374  $match = $this->context->getLanguage()->listToText( $match );
4375  }
4376  $out = $this->context->getOutput();
4377  $out->prepareErrorPage( $this->context->msg( 'spamprotectiontitle' ) );
4378 
4379  $out->addHTML( '<div id="spamprotected">' );
4380  $out->addWikiMsg( 'spamprotectiontext' );
4381  if ( $match ) {
4382  $out->addWikiMsg( 'spamprotectionmatch', wfEscapeWikiText( $match ) );
4383  }
4384  $out->addHTML( '</div>' );
4385 
4386  $out->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
4387  $this->showDiff();
4388 
4389  $out->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
4390  $this->showTextbox2();
4391 
4392  $out->addReturnTo( $this->getContextTitle(), [ 'action' => 'edit' ] );
4393  }
4394 
4405  protected function safeUnicodeInput( $request, $field ) {
4406  return rtrim( $request->getText( $field ) );
4407  }
4408 
4418  protected function safeUnicodeOutput( $text ) {
4419  return $text;
4420  }
4421 
4425  protected function addEditNotices() {
4426  $out = $this->context->getOutput();
4427  $editNotices = $this->mTitle->getEditNotices( $this->oldid );
4428  if ( count( $editNotices ) ) {
4429  $out->addHTML( implode( "\n", $editNotices ) );
4430  } else {
4431  $msg = $this->context->msg( 'editnotice-notext' );
4432  if ( !$msg->isDisabled() ) {
4433  $out->addHTML(
4434  '<div class="mw-editnotice-notext">'
4435  . $msg->parseAsBlock()
4436  . '</div>'
4437  );
4438  }
4439  }
4440  }
4441 
4445  protected function addTalkPageText() {
4446  if ( $this->mTitle->isTalkPage() ) {
4447  $this->context->getOutput()->addWikiMsg( 'talkpagetext' );
4448  }
4449  }
4450 
4454  protected function addLongPageWarningHeader() {
4455  if ( $this->contentLength === false ) {
4456  $this->contentLength = strlen( $this->textbox1 );
4457  }
4458 
4459  $out = $this->context->getOutput();
4460  $lang = $this->context->getLanguage();
4461  $maxArticleSize = $this->context->getConfig()->get( 'MaxArticleSize' );
4462  if ( $this->tooBig || $this->contentLength > $maxArticleSize * 1024 ) {
4463  $out->wrapWikiMsg( "<div class='error' id='mw-edit-longpageerror'>\n$1\n</div>",
4464  [
4465  'longpageerror',
4466  $lang->formatNum( round( $this->contentLength / 1024, 3 ) ),
4467  $lang->formatNum( $maxArticleSize )
4468  ]
4469  );
4470  } else {
4471  if ( !$this->context->msg( 'longpage-hint' )->isDisabled() ) {
4472  $out->wrapWikiMsg( "<div id='mw-edit-longpage-hint'>\n$1\n</div>",
4473  [
4474  'longpage-hint',
4475  $lang->formatSize( strlen( $this->textbox1 ) ),
4476  strlen( $this->textbox1 )
4477  ]
4478  );
4479  }
4480  }
4481  }
4482 
4486  protected function addPageProtectionWarningHeaders() {
4487  $out = $this->context->getOutput();
4488  if ( $this->mTitle->isProtected( 'edit' ) &&
4489  MWNamespace::getRestrictionLevels( $this->mTitle->getNamespace() ) !== [ '' ]
4490  ) {
4491  # Is the title semi-protected?
4492  if ( $this->mTitle->isSemiProtected() ) {
4493  $noticeMsg = 'semiprotectedpagewarning';
4494  } else {
4495  # Then it must be protected based on static groups (regular)
4496  $noticeMsg = 'protectedpagewarning';
4497  }
4498  LogEventsList::showLogExtract( $out, 'protect', $this->mTitle, '',
4499  [ 'lim' => 1, 'msgKey' => [ $noticeMsg ] ] );
4500  }
4501  if ( $this->mTitle->isCascadeProtected() ) {
4502  # Is this page under cascading protection from some source pages?
4503 
4504  list( $cascadeSources, /* $restrictions */ ) = $this->mTitle->getCascadeProtectionSources();
4505  $notice = "<div class='mw-cascadeprotectedwarning'>\n$1\n";
4506  $cascadeSourcesCount = count( $cascadeSources );
4507  if ( $cascadeSourcesCount > 0 ) {
4508  # Explain, and list the titles responsible
4509  foreach ( $cascadeSources as $page ) {
4510  $notice .= '* [[:' . $page->getPrefixedText() . "]]\n";
4511  }
4512  }
4513  $notice .= '</div>';
4514  $out->wrapWikiMsg( $notice, [ 'cascadeprotectedwarning', $cascadeSourcesCount ] );
4515  }
4516  if ( !$this->mTitle->exists() && $this->mTitle->getRestrictions( 'create' ) ) {
4517  LogEventsList::showLogExtract( $out, 'protect', $this->mTitle, '',
4518  [ 'lim' => 1,
4519  'showIfEmpty' => false,
4520  'msgKey' => [ 'titleprotectedwarning' ],
4521  'wrap' => "<div class=\"mw-titleprotectedwarning\">\n$1</div>" ] );
4522  }
4523  }
4524 
4529  protected function addExplainConflictHeader( OutputPage $out ) {
4530  $out->addHTML(
4531  $this->getEditConflictHelper()->getExplainHeader()
4532  );
4533  }
4534 
4543  return ( new TextboxBuilder() )->buildTextboxAttribs(
4544  $name, $customAttribs, $user, $this->mTitle
4545  );
4546  }
4547 
4553  protected function addNewLineAtEnd( $wikitext ) {
4554  return ( new TextboxBuilder() )->addNewLineAtEnd( $wikitext );
4555  }
4556 
4567  private function guessSectionName( $text ) {
4568  global $wgParser;
4569 
4570  // Detect Microsoft browsers
4571  $userAgent = $this->context->getRequest()->getHeader( 'User-Agent' );
4572  if ( $userAgent && preg_match( '/MSIE|Edge/', $userAgent ) ) {
4573  // ...and redirect them to legacy encoding, if available
4574  return $wgParser->guessLegacySectionNameFromWikiText( $text );
4575  }
4576  // Meanwhile, real browsers get real anchors
4577  return $wgParser->guessSectionNameFromWikiText( $text );
4578  }
4579 
4586  public function setEditConflictHelperFactory( callable $factory ) {
4587  $this->editConflictHelperFactory = $factory;
4588  $this->editConflictHelper = null;
4589  }
4590 
4594  private function getEditConflictHelper() {
4595  if ( !$this->editConflictHelper ) {
4596  $this->editConflictHelper = call_user_func(
4597  $this->editConflictHelperFactory,
4598  $this->getSubmitButtonLabel()
4599  );
4600  }
4601 
4603  }
4604 
4609  private function newTextConflictHelper( $submitButtonLabel ) {
4610  return new TextConflictHelper(
4611  $this->getTitle(),
4612  $this->getContext()->getOutput(),
4613  MediaWikiServices::getInstance()->getStatsdDataFactory(),
4614  $submitButtonLabel
4615  );
4616  }
4617 }
string $autoSumm
Definition: EditPage.php:293
static newFromName($name, $validate= 'valid')
Static factory method for creation from username.
Definition: User.php:585
Using a hook running we can avoid having all this option specific stuff in our mainline code Using the function array $article
Definition: hooks.txt:77
Helps EditPage build textboxes.
static factory(Title $title)
Create a WikiPage object of the appropriate class for the given title.
Definition: WikiPage.php:115
displayPermissionsError(array $permErrors)
Display a permissions error page, like OutputPage::showPermissionsErrorPage(), but with the following...
Definition: EditPage.php:715
$wgForeignFileRepos
incrementConflictStats()
Definition: EditPage.php:3662
const FOR_THIS_USER
Definition: Revision.php:56
getEditConflictMainTextBox($customAttribs=[])
HTML to build the textbox1 on edit conflicts.
bool $nosummary
Definition: EditPage.php:340
static closeElement($element)
Returns "".
Definition: Html.php:309
$editFormTextBottom
Definition: EditPage.php:390
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return an< a > element with HTML attributes $attribs and contents $html will be returned If you return $ret will be returned and may include noclasses & $html
Definition: hooks.txt:2019
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition: deferred.txt:11
const AS_READ_ONLY_PAGE_ANON
Status: this anonymous user is not allowed to edit this page.
Definition: EditPage.php:83
wfGetDB($db, $groups=[], $wiki=false)
Get a Database object.
addHTML($text)
Append $text to the body HTML.
bool $missingSummary
Definition: EditPage.php:275
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that probably a stub it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output $out
Definition: hooks.txt:790
the array() calling protocol came about after MediaWiki 1.4rc1.
bool $bot
Definition: EditPage.php:370
null for the local wiki Added should default to null in handler for backwards compatibility add a value to it if you want to add a cookie that have to vary cache options can modify $query
Definition: hooks.txt:1623
string $textbox2
Definition: EditPage.php:334
either a plain
Definition: hooks.txt:2080
bool $mTokenOk
Definition: EditPage.php:257
magic word the default is to use $key to get the and $key value or $key value text $key value html to format the value $key
Definition: hooks.txt:2610
$editFormTextAfterContent
Definition: EditPage.php:391
showContentForm()
Subpage overridable method for printing the form for page content editing By default this simply outp...
Definition: EditPage.php:3282
bool $allowBlankSummary
Definition: EditPage.php:278
getPreviewText()
Get the rendered text for previewing.
Definition: EditPage.php:3778
serialize($format=null)
Convenience method for serializing this Content object.
bool $isConflict
Definition: EditPage.php:239
int $oldid
Definition: EditPage.php:358
static element($element, $attribs=null, $contents= '', $allowShortTag=true)
Format an XML element with given attributes and, optionally, text content.
Definition: Xml.php:39
handleStatus(Status $status, $resultDetails)
Handle status, such as after attempt save.
Definition: EditPage.php:1523
string $summary
Definition: EditPage.php:337
setHeaders()
Definition: EditPage.php:2385
WikiPage $page
Definition: EditPage.php:224
per default it will return the text for text based content
Show an error when the wiki is locked/read-only and the user tries to do something that requires writ...
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
const AS_ARTICLE_WAS_DELETED
Status: article was deleted while editing and param wpRecreate == false or form was not posted...
Definition: EditPage.php:104
static getTitleFor($name, $subpage=false, $fragment= '')
Get a localised Title object for a specified special page name If you don't need a full Title object...
Definition: SpecialPage.php:82
getCheckboxesDefinition($checked)
Return an array of checkbox definitions.
Definition: EditPage.php:4146
Handles formatting for the "templates used on this page" lists.
const AS_HOOK_ERROR
Status: Article update aborted by a hook function.
Definition: EditPage.php:63
static getForModelID($modelId)
Returns the ContentHandler singleton for the given model ID.
div flags Integer display flags(NO_ACTION_LINK, NO_EXTRA_USER_LINKS) 'LogException'returning false will NOT prevent logging $e
Definition: hooks.txt:2195
safeUnicodeInput($request, $field)
Filter an input field through a Unicode de-armoring process if it came from an old browser with known...
Definition: EditPage.php:4405
showTextbox2()
Definition: EditPage.php:3322
bool $tooBig
Definition: EditPage.php:269
$wgParser
Definition: Setup.php:905
static rawElement($element, $attribs=[], $contents= '')
Returns an HTML element in a string.
Definition: Html.php:209
showHeaderCopyrightWarning()
Show the header copyright warning.
Definition: EditPage.php:3472
getPage()
Get the WikiPage object of this instance.
Definition: Article.php:191
getWikiText($shortContext=false, $longContext=false, $lang=null)
Get the error list as a wikitext formatted list.
Definition: Status.php:177
globals txt Globals are evil The original MediaWiki code relied on globals for processing context far too often MediaWiki development since then has been a story of slowly moving context out of global variables and into objects Storing processing context in object member variables allows those objects to be reused in a much more flexible way Consider the elegance of
database rows
Definition: globals.txt:10
getSummaryInputWidget($summary="", $labelText=null, $inputAttrs=null)
Builds a standard summary input with a label.
Definition: EditPage.php:3162
if(!isset($args[0])) $lang
processing should stop and the error should be shown to the user * false
Definition: hooks.txt:187
showTosSummary()
Give a chance for site and per-namespace customizations of terms of service summary link that might e...
Definition: EditPage.php:3488
Special handling for category description pages, showing pages, subcategories and file that belong to...
static configuration should be added through ResourceLoaderGetConfigVars instead can be used to get the real title after the basic globals have been set but before ordinary actions take place $output
Definition: hooks.txt:2252
incrementEditFailureStats($failureType)
Definition: EditPage.php:3924
The edit page/HTML interface (split from Article) The actual database and text munging is still in Ar...
Definition: EditPage.php:44
Title $mTitle
Definition: EditPage.php:230
static hidden($name, $value, array $attribs=[])
Convenience function to produce an input element with type=hidden.
Definition: Html.php:790
setContextTitle($title)
Set the context Title object.
Definition: EditPage.php:483
const AS_SUMMARY_NEEDED
Status: no edit summary given and the user has forceeditsummary set and the user is not editing in hi...
Definition: EditPage.php:126
const AS_HOOK_ERROR_EXPECTED
Status: A hook function returned an error.
Definition: EditPage.php:68
getSummaryInputAttributes(array $inputAttrs=null)
Helper function for summary input functions, which returns the neccessary attributes for the input...
Definition: EditPage.php:3137
getEditButtons(&$tabindex)
Returns an array of html code of the following buttons: save, diff and preview.
Definition: EditPage.php:4282
static newFromUserAndLang(User $user, Language $lang)
Get a ParserOptions object from a given user and language.
also included in $newHeader if any indicating whether we should show just the diff
Definition: hooks.txt:1288
string $editintro
Definition: EditPage.php:364
Class for viewing MediaWiki article and history.
Definition: Article.php:35
null for the local wiki Added in
Definition: hooks.txt:1623
static getSkinNames()
Fetch the set of available skins.
Definition: Skin.php:55
bool $allowBlankArticle
Definition: EditPage.php:284
This code would result in ircNotify being run twice when an article is and once for brion Hooks can return three possible true was required This is the default since MediaWiki *some string
Definition: hooks.txt:175
IContextSource $context
Definition: EditPage.php:415
$value
Article $mArticle
Definition: EditPage.php:222
null string $contentFormat
Definition: EditPage.php:376
const AS_BLOCKED_PAGE_FOR_USER
Status: User is blocked from editing this page.
Definition: EditPage.php:73
guessSectionName($text)
Turns section name wikitext into anchors for use in HTTP redirects.
Definition: EditPage.php:4567
bool $blankArticle
Definition: EditPage.php:281
setPostEditCookie($statusValue)
Sets post-edit cookie indicating the user just saved a particular revision.
Definition: EditPage.php:1472
Helper for displaying edit conflicts in text content models to users.
isGood()
Returns whether the operation completed and didn'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('create') == f...
Definition: EditPage.php:110
The First
Definition: primes.txt:1
static getPreviewLimitReport($output)
Get the Limit report for page previews.
Definition: EditPage.php:3555
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency MediaWikiServices
Definition: injection.txt:23
spamPageWithContent($match=false)
Show "your edit contains spam" page with your diff and text.
Definition: EditPage.php:4370
bool $missingComment
Definition: EditPage.php:272
const EDIT_MINOR
Definition: Defines.php:155
const COMMENT_CHARACTER_LIMIT
Maximum length of a comment in UTF-8 characters.
const POST_EDIT_COOKIE_DURATION
Duration of PostEdit cookie, in seconds.
Definition: EditPage.php:216
const EDIT_UPDATE
Definition: Defines.php:154
static newFromText($text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition: Title.php:273
this hook is for auditing only $response
Definition: hooks.txt:790
showFormBeforeText()
Definition: EditPage.php:3245
null means default & $customAttribs
Definition: hooks.txt:2019
internalAttemptSave(&$result, $bot=false)
Attempt submission (no UI)
Definition: EditPage.php:1772
bool stdClass $lastDelete
Definition: EditPage.php:254
const AS_UNICODE_NOT_SUPPORTED
Status: edit rejected because browser doesn't support Unicode.
Definition: EditPage.php:190
target page
when a variable name is used in a it is silently declared as a new local masking the global
Definition: design.txt:93
wfExpandUrl($url, $defaultProto=PROTO_CURRENT)
Expand a potentially local URL to a fully-qualified URL.
addPageProtectionWarningHeaders()
Definition: EditPage.php:4486
getCheckboxesWidget(&$tabindex, $checked)
Returns an array of checkboxes for the edit form, including 'minor' and 'watch' checkboxes and any ot...
Definition: EditPage.php:4191
static newFromUser($user)
Get a ParserOptions object from a given user.
const CONTENT_MODEL_JSON
Definition: Defines.php:240
edit()
This is the function that gets called for "action=edit".
Definition: EditPage.php:548
getContextTitle()
Get the context title object.
Definition: EditPage.php:494
getContentObject($def_content=null)
Definition: EditPage.php:1143
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:2295
const DB_MASTER
Definition: defines.php:26
static getRestrictionLevels($index, User $user=null)
Determine which restriction levels it makes sense to use in a namespace, optionally filtered by a use...
addEditNotices()
Definition: EditPage.php:4425
getEditPermissionErrors($rigor= 'secure')
Definition: EditPage.php:671
isRedirect()
Tests if the article content represents a redirect.
Definition: WikiPage.php:544
null Title $mContextTitle
Definition: EditPage.php:233
wfArrayDiff2($a, $b)
Like array_diff( $a, $b ) except that it works with two-dimensional arrays.
wfDebug($text, $dest= 'all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
getEditFormHtmlBeforeContent()
Content to go in the edit form before textbox1.
static showLogExtract(&$out, $types=[], $page= '', $user= '', $param=[])
Show log extract.
static matchSummarySpamRegex($text)
Check given input text against $wgSummarySpamRegex, and return the text of the first match...
Definition: EditPage.php:2364
int $editRevId
Definition: EditPage.php:346
this class mediates it Skin Encapsulates a look and feel for the wiki All of the functions that render HTML and make choices about how to render it are here and are called from various other places when and is meant to be subclassed with other skins that may override some of its functions The User object contains a reference to a and so rather than having a global skin object we just rely on the global User and get the skin with $wgUser and also has some character encoding functions and other locale stuff The current user interface language is instantiated as $wgLang
Definition: design.txt:56
The index of the header message $result[1]=The index of the body text message $result[2 through n]=Parameters passed to body text message.Please note the header message cannot receive/use parameters. 'ImportHandleLogItemXMLTag':When parsing a XML tag in a log item.Return false to stop further processing of the tag $reader:XMLReader object $logInfo:Array of information 'ImportHandlePageXMLTag':When parsing a XML tag in a page.Return false to stop further processing of the tag $reader:XMLReader object &$pageInfo:Array of information 'ImportHandleRevisionXMLTag':When parsing a XML tag in a page revision.Return false to stop further processing of the tag $reader:XMLReader object $pageInfo:Array of page information $revisionInfo:Array of revision information 'ImportHandleToplevelXMLTag':When parsing a top level XML tag.Return false to stop further processing of the tag $reader:XMLReader object 'ImportHandleUnknownUser':When a user doesn't exist locally, this hook is called to give extensions an opportunity to auto-create it.If the auto-creation is successful, return false.$name:User name 'ImportHandleUploadXMLTag':When parsing a XML tag in a file upload.Return false to stop further processing of the tag $reader:XMLReader object $revisionInfo:Array of information 'ImportLogInterwikiLink':Hook to change the interwiki link used in log entries and edit summaries for transwiki imports.&$fullInterwikiPrefix:Interwiki prefix, may contain colons.&$pageTitle:String that contains page title. 'ImportSources':Called when reading from the $wgImportSources configuration variable.Can be used to lazy-load the import sources list.&$importSources:The value of $wgImportSources.Modify as necessary.See the comment in DefaultSettings.php for the detail of how to structure this array. 'InfoAction':When building information to display on the action=info page.$context:IContextSource object &$pageInfo:Array of information 'InitializeArticleMaybeRedirect':MediaWiki check to see if title is a redirect.&$title:Title object for the current page &$request:WebRequest &$ignoreRedirect:boolean to skip redirect check &$target:Title/string of redirect target &$article:Article object 'InternalParseBeforeLinks':during Parser's internalParse method before links but after nowiki/noinclude/includeonly/onlyinclude and other processings.&$parser:Parser object &$text:string containing partially parsed text &$stripState:Parser's internal StripState object 'InternalParseBeforeSanitize':during Parser's internalParse method just before the parser removes unwanted/dangerous HTML tags and after nowiki/noinclude/includeonly/onlyinclude and other processings.Ideal for syntax-extensions after template/parser function execution which respect nowiki and HTML-comments.&$parser:Parser object &$text:string containing partially parsed text &$stripState:Parser's internal StripState object 'InterwikiLoadPrefix':When resolving if a given prefix is an interwiki or not.Return true without providing an interwiki to continue interwiki search.$prefix:interwiki prefix we are looking for.&$iwData:output array describing the interwiki with keys iw_url, iw_local, iw_trans and optionally iw_api and iw_wikiid. 'InvalidateEmailComplete':Called after a user's email has been invalidated successfully.$user:user(object) whose email is being invalidated 'IRCLineURL':When constructing the URL to use in an IRC notification.Callee may modify $url and $query, URL will be constructed as $url.$query &$url:URL to index.php &$query:Query string $rc:RecentChange object that triggered url generation 'IsFileCacheable':Override the result of Article::isFileCacheable()(if true) &$article:article(object) being checked 'IsTrustedProxy':Override the result of IP::isTrustedProxy() &$ip:IP being check &$result:Change this value to override the result of IP::isTrustedProxy() 'IsUploadAllowedFromUrl':Override the result of UploadFromUrl::isAllowedUrl() $url:URL used to upload from &$allowed:Boolean indicating if uploading is allowed for given URL 'isValidEmailAddr':Override the result of Sanitizer::validateEmail(), for instance to return false if the domain name doesn't match your organization.$addr:The e-mail address entered by the user &$result:Set this and return false to override the internal checks 'isValidPassword':Override the result of User::isValidPassword() $password:The password entered by the user &$result:Set this and return false to override the internal checks $user:User the password is being validated for 'Language::getMessagesFileName':$code:The language code or the language we're looking for a messages file for &$file:The messages file path, you can override this to change the location. 'LanguageGetMagic':DEPRECATED!Use $magicWords in a file listed in $wgExtensionMessagesFiles instead.Use this to define synonyms of magic words depending of the language &$magicExtensions:associative array of magic words synonyms $lang:language code(string) 'LanguageGetNamespaces':Provide custom ordering for namespaces or remove namespaces.Do not use this hook to add namespaces.Use CanonicalNamespaces for that.&$namespaces:Array of namespaces indexed by their numbers 'LanguageGetSpecialPageAliases':DEPRECATED!Use $specialPageAliases in a file listed in $wgExtensionMessagesFiles instead.Use to define aliases of special pages names depending of the language &$specialPageAliases:associative array of magic words synonyms $lang:language code(string) 'LanguageGetTranslatedLanguageNames':Provide translated language names.&$names:array of language code=> language name $code:language of the preferred translations 'LanguageLinks':Manipulate a page's language links.This is called in various places to allow extensions to define the effective language links for a page.$title:The page's Title.&$links:Array with elements of the form"language:title"in the order that they will be output.&$linkFlags:Associative array mapping prefixed links to arrays of flags.Currently unused, but planned to provide support for marking individual language links in the UI, e.g.for featured articles. 'LanguageSelector':Hook to change the language selector available on a page.$out:The output page.$cssClassName:CSS class name of the language selector. 'LinkBegin':DEPRECATED!Use HtmlPageLinkRendererBegin instead.Used when generating internal and interwiki links in Linker::link(), before processing starts.Return false to skip default processing and return $ret.See documentation for Linker::link() for details on the expected meanings of parameters.$skin:the Skin object $target:the Title that the link is pointing to &$html:the contents that the< a > tag should have(raw HTML) $result
Definition: hooks.txt:2017
const AS_CONTENT_TOO_BIG
Status: Content too big (> $wgMaxArticleSize)
Definition: EditPage.php:78
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that probably a stub it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping $template
Definition: hooks.txt:790
safeUnicodeOutput($text)
Filter an output field through a Unicode armoring process if it is going to an old browser with known...
Definition: EditPage.php:4418
The User object encapsulates all of the user-specific settings (user_id, name, rights, email address, options, last login time).
Definition: User.php:46
$wgEnableUploads
Uploads have to be specially set up to be secure.
getContext()
Gets the context this Article is executed in.
Definition: Article.php:2023
addExplainConflictHeader(OutputPage $out)
Definition: EditPage.php:4529
attemptSave(&$resultDetails=false)
Attempt submission.
Definition: EditPage.php:1493
showIntro()
Show all applicable editing introductions.
Definition: EditPage.php:2455
wfTimestamp($outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return true
Definition: hooks.txt:2019
static getLocalizedName($name, Language $lang=null)
Returns the localized name for a given content model.
getArticle()
Definition: EditPage.php:458
bool $watchthis
Definition: EditPage.php:325
$previewTextAfterContent
Definition: EditPage.php:392
static closeElement($element)
Shortcut to close an XML element.
Definition: Xml.php:118
const DELETED_COMMENT
Definition: LogPage.php:35
wfDebugLog($logGroup, $text, $dest= 'all', array $context=[])
Send a line to a supplementary debug log file, if configured, or main debug log if not...
fatal($message)
Add an error and set OK to false, indicating that the operation as a whole was fatal.
getContent($audience=Revision::FOR_PUBLIC, User $user=null)
Get the content of the current revision.
Definition: WikiPage.php:718
static openElement($element, $attribs=[])
Identical to rawElement(), but has no third parameter and omits the end tag (and the self-closing '/'...
Definition: Html.php:251
getParentRevId()
Get the edit's parent revision ID.
Definition: EditPage.php:1310
addLongPageWarningHeader()
Definition: EditPage.php:4454
getTemplates()
Definition: EditPage.php:3969
wfEscapeWikiText($text)
Escapes the given text so that it may be output using addWikiText() without any linking, formatting, etc.
bool $save
Definition: EditPage.php:313
wfReadOnly()
Check whether the wiki is in read-only mode.
static newMigration()
Static constructor.
static getCopyrightWarning($title, $format= 'plain', $langcode=null)
Get the copyright warning, by default returns wikitext.
Definition: EditPage.php:3527
static textarea($name, $value= '', array $attribs=[])
Convenience function to produce a