MediaWiki  1.28.1
EditPage.php
Go to the documentation of this file.
1 <?php
26 
42 class EditPage {
46  const AS_SUCCESS_UPDATE = 200;
47 
52 
56  const AS_HOOK_ERROR = 210;
57 
62 
67 
71  const AS_CONTENT_TOO_BIG = 216;
72 
77 
82 
86  const AS_READ_ONLY_PAGE = 220;
87 
91  const AS_RATE_LIMITED = 221;
92 
98 
104 
108  const AS_BLANK_ARTICLE = 224;
109 
113  const AS_CONFLICT_DETECTED = 225;
114 
119  const AS_SUMMARY_NEEDED = 226;
120 
124  const AS_TEXTBOX_EMPTY = 228;
125 
130 
134  const AS_END = 231;
135 
139  const AS_SPAM_ERROR = 232;
140 
145 
150 
156 
161  const AS_SELF_REDIRECT = 236;
162 
167  const AS_CHANGE_TAG_ERROR = 237;
168 
172  const AS_PARSE_ERROR = 240;
173 
179 
183  const EDITFORM_ID = 'editform';
184 
189  const POST_EDIT_COOKIE_KEY_PREFIX = 'PostEditRevision';
190 
205 
207  public $mArticle;
209  private $page;
210 
212  public $mTitle;
213 
215  private $mContextTitle = null;
216 
218  public $action = 'submit';
219 
221  public $isConflict = false;
222 
224  public $isCssJsSubpage = false;
225 
227  public $isCssSubpage = false;
228 
230  public $isJsSubpage = false;
231 
233  public $isWrongCaseCssJsPage = false;
234 
236  public $isNew = false;
237 
240 
242  public $formtype;
243 
245  public $firsttime;
246 
248  public $lastDelete;
249 
251  public $mTokenOk = false;
252 
254  public $mTokenOkExceptSuffix = false;
255 
257  public $mTriedSave = false;
258 
260  public $incompleteForm = false;
261 
263  public $tooBig = false;
264 
266  public $missingComment = false;
267 
269  public $missingSummary = false;
270 
272  public $allowBlankSummary = false;
273 
275  protected $blankArticle = false;
276 
278  protected $allowBlankArticle = false;
279 
281  protected $selfRedirect = false;
282 
284  protected $allowSelfRedirect = false;
285 
287  public $autoSumm = '';
288 
290  public $hookError = '';
291 
294 
296  public $hasPresetSummary = false;
297 
299  public $mBaseRevision = false;
300 
302  public $mShowSummaryField = true;
303 
304  # Form values
305 
307  public $save = false;
308 
310  public $preview = false;
311 
313  public $diff = false;
314 
316  public $minoredit = false;
317 
319  public $watchthis = false;
320 
322  public $recreate = false;
323 
325  public $textbox1 = '';
326 
328  public $textbox2 = '';
329 
331  public $summary = '';
332 
334  public $nosummary = false;
335 
337  public $edittime = '';
338 
340  private $editRevId = null;
341 
343  public $section = '';
344 
346  public $sectiontitle = '';
347 
349  public $starttime = '';
350 
352  public $oldid = 0;
353 
355  public $parentRevId = 0;
356 
358  public $editintro = '';
359 
361  public $scrolltop = null;
362 
364  public $bot = true;
365 
367  public $contentModel = null;
368 
370  public $contentFormat = null;
371 
373  private $changeTags = null;
374 
375  # Placeholders for text injection by hooks (must be HTML)
376  # extensions should take care to _append_ to the present value
377 
379  public $editFormPageTop = '';
380  public $editFormTextTop = '';
384  public $editFormTextBottom = '';
387  public $mPreloadContent = null;
388 
389  /* $didSave should be set to true whenever an article was successfully altered. */
390  public $didSave = false;
391  public $undidRev = 0;
392 
393  public $suppressIntro = false;
394 
396  protected $edit;
397 
399  protected $contentLength = false;
400 
404  private $enableApiEditOverride = false;
405 
409  protected $context;
410 
414  public function __construct( Article $article ) {
415  $this->mArticle = $article;
416  $this->page = $article->getPage(); // model object
417  $this->mTitle = $article->getTitle();
418  $this->context = $article->getContext();
419 
420  $this->contentModel = $this->mTitle->getContentModel();
421 
422  $handler = ContentHandler::getForModelID( $this->contentModel );
423  $this->contentFormat = $handler->getDefaultFormat();
424  }
425 
429  public function getArticle() {
430  return $this->mArticle;
431  }
432 
437  public function getContext() {
438  return $this->context;
439  }
440 
445  public function getTitle() {
446  return $this->mTitle;
447  }
448 
454  public function setContextTitle( $title ) {
455  $this->mContextTitle = $title;
456  }
457 
465  public function getContextTitle() {
466  if ( is_null( $this->mContextTitle ) ) {
468  return $wgTitle;
469  } else {
470  return $this->mContextTitle;
471  }
472  }
473 
481  public function isSupportedContentModel( $modelId ) {
482  return $this->enableApiEditOverride === true ||
483  ContentHandler::getForModelID( $modelId )->supportsDirectEditing();
484  }
485 
492  public function setApiEditOverride( $enableOverride ) {
493  $this->enableApiEditOverride = $enableOverride;
494  }
495 
496  function submit() {
497  $this->edit();
498  }
499 
511  function edit() {
513  // Allow extensions to modify/prevent this form or submission
514  if ( !Hooks::run( 'AlternateEdit', [ $this ] ) ) {
515  return;
516  }
517 
518  wfDebug( __METHOD__ . ": enter\n" );
519 
520  // If they used redlink=1 and the page exists, redirect to the main article
521  if ( $wgRequest->getBool( 'redlink' ) && $this->mTitle->exists() ) {
522  $wgOut->redirect( $this->mTitle->getFullURL() );
523  return;
524  }
525 
526  $this->importFormData( $wgRequest );
527  $this->firsttime = false;
528 
529  if ( wfReadOnly() && $this->save ) {
530  // Force preview
531  $this->save = false;
532  $this->preview = true;
533  }
534 
535  if ( $this->save ) {
536  $this->formtype = 'save';
537  } elseif ( $this->preview ) {
538  $this->formtype = 'preview';
539  } elseif ( $this->diff ) {
540  $this->formtype = 'diff';
541  } else { # First time through
542  $this->firsttime = true;
543  if ( $this->previewOnOpen() ) {
544  $this->formtype = 'preview';
545  } else {
546  $this->formtype = 'initial';
547  }
548  }
549 
550  $permErrors = $this->getEditPermissionErrors( $this->save ? 'secure' : 'full' );
551  if ( $permErrors ) {
552  wfDebug( __METHOD__ . ": User can't edit\n" );
553  // Auto-block user's IP if the account was "hard" blocked
554  if ( !wfReadOnly() ) {
555  $user = $wgUser;
556  DeferredUpdates::addCallableUpdate( function () use ( $user ) {
557  $user->spreadAnyEditBlock();
558  } );
559  }
560  $this->displayPermissionsError( $permErrors );
561 
562  return;
563  }
564 
565  $revision = $this->mArticle->getRevisionFetched();
566  // Disallow editing revisions with content models different from the current one
567  // Undo edits being an exception in order to allow reverting content model changes.
568  if ( $revision
569  && $revision->getContentModel() !== $this->contentModel
570  ) {
571  $prevRev = null;
572  if ( $this->undidRev ) {
573  $undidRevObj = Revision::newFromId( $this->undidRev );
574  $prevRev = $undidRevObj ? $undidRevObj->getPrevious() : null;
575  }
576  if ( !$this->undidRev
577  || !$prevRev
578  || $prevRev->getContentModel() !== $this->contentModel
579  ) {
580  $this->displayViewSourcePage(
581  $this->getContentObject(),
582  $this->context->msg(
583  'contentmodelediterror',
584  $revision->getContentModel(),
586  )->plain()
587  );
588  return;
589  }
590  }
591 
592  $this->isConflict = false;
593  // css / js subpages of user pages get a special treatment
594  $this->isCssJsSubpage = $this->mTitle->isCssJsSubpage();
595  $this->isCssSubpage = $this->mTitle->isCssSubpage();
596  $this->isJsSubpage = $this->mTitle->isJsSubpage();
597  // @todo FIXME: Silly assignment.
598  $this->isWrongCaseCssJsPage = $this->isWrongCaseCssJsPage();
599 
600  # Show applicable editing introductions
601  if ( $this->formtype == 'initial' || $this->firsttime ) {
602  $this->showIntro();
603  }
604 
605  # Attempt submission here. This will check for edit conflicts,
606  # and redundantly check for locked database, blocked IPs, etc.
607  # that edit() already checked just in case someone tries to sneak
608  # in the back door with a hand-edited submission URL.
609 
610  if ( 'save' == $this->formtype ) {
611  $resultDetails = null;
612  $status = $this->attemptSave( $resultDetails );
613  if ( !$this->handleStatus( $status, $resultDetails ) ) {
614  return;
615  }
616  }
617 
618  # First time through: get contents, set time for conflict
619  # checking, etc.
620  if ( 'initial' == $this->formtype || $this->firsttime ) {
621  if ( $this->initialiseForm() === false ) {
622  $this->noSuchSectionPage();
623  return;
624  }
625 
626  if ( !$this->mTitle->getArticleID() ) {
627  Hooks::run( 'EditFormPreloadText', [ &$this->textbox1, &$this->mTitle ] );
628  } else {
629  Hooks::run( 'EditFormInitialText', [ $this ] );
630  }
631 
632  }
633 
634  $this->showEditForm();
635  }
636 
641  protected function getEditPermissionErrors( $rigor = 'secure' ) {
642  global $wgUser;
643 
644  $permErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $wgUser, $rigor );
645  # Can this title be created?
646  if ( !$this->mTitle->exists() ) {
647  $permErrors = array_merge(
648  $permErrors,
649  wfArrayDiff2(
650  $this->mTitle->getUserPermissionsErrors( 'create', $wgUser, $rigor ),
651  $permErrors
652  )
653  );
654  }
655  # Ignore some permissions errors when a user is just previewing/viewing diffs
656  $remove = [];
657  foreach ( $permErrors as $error ) {
658  if ( ( $this->preview || $this->diff )
659  && ( $error[0] == 'blockedtext' || $error[0] == 'autoblockedtext' )
660  ) {
661  $remove[] = $error;
662  }
663  }
664  $permErrors = wfArrayDiff2( $permErrors, $remove );
665 
666  return $permErrors;
667  }
668 
682  protected function displayPermissionsError( array $permErrors ) {
684 
685  if ( $wgRequest->getBool( 'redlink' ) ) {
686  // The edit page was reached via a red link.
687  // Redirect to the article page and let them click the edit tab if
688  // they really want a permission error.
689  $wgOut->redirect( $this->mTitle->getFullURL() );
690  return;
691  }
692 
693  $content = $this->getContentObject();
694 
695  # Use the normal message if there's nothing to display
696  if ( $this->firsttime && ( !$content || $content->isEmpty() ) ) {
697  $action = $this->mTitle->exists() ? 'edit' :
698  ( $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage' );
699  throw new PermissionsError( $action, $permErrors );
700  }
701 
702  $this->displayViewSourcePage(
703  $content,
704  $wgOut->formatPermissionsErrorMessage( $permErrors, 'edit' )
705  );
706  }
707 
713  protected function displayViewSourcePage( Content $content, $errorMessage = '' ) {
714  global $wgOut;
715 
716  Hooks::run( 'EditPage::showReadOnlyForm:initial', [ $this, &$wgOut ] );
717 
718  $wgOut->setRobotPolicy( 'noindex,nofollow' );
719  $wgOut->setPageTitle( $this->context->msg(
720  'viewsource-title',
721  $this->getContextTitle()->getPrefixedText()
722  ) );
723  $wgOut->addBacklinkSubtitle( $this->getContextTitle() );
724  $wgOut->addHTML( $this->editFormPageTop );
725  $wgOut->addHTML( $this->editFormTextTop );
726 
727  if ( $errorMessage !== '' ) {
728  $wgOut->addWikiText( $errorMessage );
729  $wgOut->addHTML( "<hr />\n" );
730  }
731 
732  # If the user made changes, preserve them when showing the markup
733  # (This happens when a user is blocked during edit, for instance)
734  if ( !$this->firsttime ) {
735  $text = $this->textbox1;
736  $wgOut->addWikiMsg( 'viewyourtext' );
737  } else {
738  try {
739  $text = $this->toEditText( $content );
740  } catch ( MWException $e ) {
741  # Serialize using the default format if the content model is not supported
742  # (e.g. for an old revision with a different model)
743  $text = $content->serialize();
744  }
745  $wgOut->addWikiMsg( 'viewsourcetext' );
746  }
747 
748  $wgOut->addHTML( $this->editFormTextBeforeContent );
749  $this->showTextbox( $text, 'wpTextbox1', [ 'readonly' ] );
750  $wgOut->addHTML( $this->editFormTextAfterContent );
751 
752  $wgOut->addHTML( $this->makeTemplatesOnThisPageList( $this->getTemplates() ) );
753 
754  $wgOut->addModules( 'mediawiki.action.edit.collapsibleFooter' );
755 
756  $wgOut->addHTML( $this->editFormTextBottom );
757  if ( $this->mTitle->exists() ) {
758  $wgOut->returnToMain( null, $this->mTitle );
759  }
760  }
761 
767  protected function previewOnOpen() {
768  global $wgRequest, $wgUser, $wgPreviewOnOpenNamespaces;
769  if ( $wgRequest->getVal( 'preview' ) == 'yes' ) {
770  // Explicit override from request
771  return true;
772  } elseif ( $wgRequest->getVal( 'preview' ) == 'no' ) {
773  // Explicit override from request
774  return false;
775  } elseif ( $this->section == 'new' ) {
776  // Nothing *to* preview for new sections
777  return false;
778  } elseif ( ( $wgRequest->getVal( 'preload' ) !== null || $this->mTitle->exists() )
779  && $wgUser->getOption( 'previewonfirst' )
780  ) {
781  // Standard preference behavior
782  return true;
783  } elseif ( !$this->mTitle->exists()
784  && isset( $wgPreviewOnOpenNamespaces[$this->mTitle->getNamespace()] )
785  && $wgPreviewOnOpenNamespaces[$this->mTitle->getNamespace()]
786  ) {
787  // Categories are special
788  return true;
789  } else {
790  return false;
791  }
792  }
793 
800  protected function isWrongCaseCssJsPage() {
801  if ( $this->mTitle->isCssJsSubpage() ) {
802  $name = $this->mTitle->getSkinFromCssJsSubpage();
803  $skins = array_merge(
804  array_keys( Skin::getSkinNames() ),
805  [ 'common' ]
806  );
807  return !in_array( $name, $skins )
808  && in_array( strtolower( $name ), $skins );
809  } else {
810  return false;
811  }
812  }
813 
821  protected function isSectionEditSupported() {
822  $contentHandler = ContentHandler::getForTitle( $this->mTitle );
823  return $contentHandler->supportsSections();
824  }
825 
831  function importFormData( &$request ) {
833 
834  # Section edit can come from either the form or a link
835  $this->section = $request->getVal( 'wpSection', $request->getVal( 'section' ) );
836 
837  if ( $this->section !== null && $this->section !== '' && !$this->isSectionEditSupported() ) {
838  throw new ErrorPageError( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
839  }
840 
841  $this->isNew = !$this->mTitle->exists() || $this->section == 'new';
842 
843  if ( $request->wasPosted() ) {
844  # These fields need to be checked for encoding.
845  # Also remove trailing whitespace, but don't remove _initial_
846  # whitespace from the text boxes. This may be significant formatting.
847  $this->textbox1 = $this->safeUnicodeInput( $request, 'wpTextbox1' );
848  if ( !$request->getCheck( 'wpTextbox2' ) ) {
849  // Skip this if wpTextbox2 has input, it indicates that we came
850  // from a conflict page with raw page text, not a custom form
851  // modified by subclasses
853  if ( $textbox1 !== null ) {
854  $this->textbox1 = $textbox1;
855  }
856  }
857 
858  # Truncate for whole multibyte characters
859  $this->summary = $wgContLang->truncate( $request->getText( 'wpSummary' ), 255 );
860 
861  # If the summary consists of a heading, e.g. '==Foobar==', extract the title from the
862  # header syntax, e.g. 'Foobar'. This is mainly an issue when we are using wpSummary for
863  # section titles.
864  $this->summary = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->summary );
865 
866  # Treat sectiontitle the same way as summary.
867  # Note that wpSectionTitle is not yet a part of the actual edit form, as wpSummary is
868  # currently doing double duty as both edit summary and section title. Right now this
869  # is just to allow API edits to work around this limitation, but this should be
870  # incorporated into the actual edit form when EditPage is rewritten (Bugs 18654, 26312).
871  $this->sectiontitle = $wgContLang->truncate( $request->getText( 'wpSectionTitle' ), 255 );
872  $this->sectiontitle = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->sectiontitle );
873 
874  $this->edittime = $request->getVal( 'wpEdittime' );
875  $this->editRevId = $request->getIntOrNull( 'editRevId' );
876  $this->starttime = $request->getVal( 'wpStarttime' );
877 
878  $undidRev = $request->getInt( 'wpUndidRevision' );
879  if ( $undidRev ) {
880  $this->undidRev = $undidRev;
881  }
882 
883  $this->scrolltop = $request->getIntOrNull( 'wpScrolltop' );
884 
885  if ( $this->textbox1 === '' && $request->getVal( 'wpTextbox1' ) === null ) {
886  // wpTextbox1 field is missing, possibly due to being "too big"
887  // according to some filter rules such as Suhosin's setting for
888  // suhosin.request.max_value_length (d'oh)
889  $this->incompleteForm = true;
890  } else {
891  // If we receive the last parameter of the request, we can fairly
892  // claim the POST request has not been truncated.
893 
894  // TODO: softened the check for cutover. Once we determine
895  // that it is safe, we should complete the transition by
896  // removing the "edittime" clause.
897  $this->incompleteForm = ( !$request->getVal( 'wpUltimateParam' )
898  && is_null( $this->edittime ) );
899  }
900  if ( $this->incompleteForm ) {
901  # If the form is incomplete, force to preview.
902  wfDebug( __METHOD__ . ": Form data appears to be incomplete\n" );
903  wfDebug( "POST DATA: " . var_export( $_POST, true ) . "\n" );
904  $this->preview = true;
905  } else {
906  $this->preview = $request->getCheck( 'wpPreview' );
907  $this->diff = $request->getCheck( 'wpDiff' );
908 
909  // Remember whether a save was requested, so we can indicate
910  // if we forced preview due to session failure.
911  $this->mTriedSave = !$this->preview;
912 
913  if ( $this->tokenOk( $request ) ) {
914  # Some browsers will not report any submit button
915  # if the user hits enter in the comment box.
916  # The unmarked state will be assumed to be a save,
917  # if the form seems otherwise complete.
918  wfDebug( __METHOD__ . ": Passed token check.\n" );
919  } elseif ( $this->diff ) {
920  # Failed token check, but only requested "Show Changes".
921  wfDebug( __METHOD__ . ": Failed token check; Show Changes requested.\n" );
922  } else {
923  # Page might be a hack attempt posted from
924  # an external site. Preview instead of saving.
925  wfDebug( __METHOD__ . ": Failed token check; forcing preview\n" );
926  $this->preview = true;
927  }
928  }
929  $this->save = !$this->preview && !$this->diff;
930  if ( !preg_match( '/^\d{14}$/', $this->edittime ) ) {
931  $this->edittime = null;
932  }
933 
934  if ( !preg_match( '/^\d{14}$/', $this->starttime ) ) {
935  $this->starttime = null;
936  }
937 
938  $this->recreate = $request->getCheck( 'wpRecreate' );
939 
940  $this->minoredit = $request->getCheck( 'wpMinoredit' );
941  $this->watchthis = $request->getCheck( 'wpWatchthis' );
942 
943  # Don't force edit summaries when a user is editing their own user or talk page
944  if ( ( $this->mTitle->mNamespace == NS_USER || $this->mTitle->mNamespace == NS_USER_TALK )
945  && $this->mTitle->getText() == $wgUser->getName()
946  ) {
947  $this->allowBlankSummary = true;
948  } else {
949  $this->allowBlankSummary = $request->getBool( 'wpIgnoreBlankSummary' )
950  || !$wgUser->getOption( 'forceeditsummary' );
951  }
952 
953  $this->autoSumm = $request->getText( 'wpAutoSummary' );
954 
955  $this->allowBlankArticle = $request->getBool( 'wpIgnoreBlankArticle' );
956  $this->allowSelfRedirect = $request->getBool( 'wpIgnoreSelfRedirect' );
957 
958  $changeTags = $request->getVal( 'wpChangeTags' );
959  if ( is_null( $changeTags ) || $changeTags === '' ) {
960  $this->changeTags = [];
961  } else {
962  $this->changeTags = array_filter( array_map( 'trim', explode( ',',
963  $changeTags ) ) );
964  }
965  } else {
966  # Not a posted form? Start with nothing.
967  wfDebug( __METHOD__ . ": Not a posted form.\n" );
968  $this->textbox1 = '';
969  $this->summary = '';
970  $this->sectiontitle = '';
971  $this->edittime = '';
972  $this->editRevId = null;
973  $this->starttime = wfTimestampNow();
974  $this->edit = false;
975  $this->preview = false;
976  $this->save = false;
977  $this->diff = false;
978  $this->minoredit = false;
979  // Watch may be overridden by request parameters
980  $this->watchthis = $request->getBool( 'watchthis', false );
981  $this->recreate = false;
982 
983  // When creating a new section, we can preload a section title by passing it as the
984  // preloadtitle parameter in the URL (Bug 13100)
985  if ( $this->section == 'new' && $request->getVal( 'preloadtitle' ) ) {
986  $this->sectiontitle = $request->getVal( 'preloadtitle' );
987  // Once wpSummary isn't being use for setting section titles, we should delete this.
988  $this->summary = $request->getVal( 'preloadtitle' );
989  } elseif ( $this->section != 'new' && $request->getVal( 'summary' ) ) {
990  $this->summary = $request->getText( 'summary' );
991  if ( $this->summary !== '' ) {
992  $this->hasPresetSummary = true;
993  }
994  }
995 
996  if ( $request->getVal( 'minor' ) ) {
997  $this->minoredit = true;
998  }
999  }
1000 
1001  $this->oldid = $request->getInt( 'oldid' );
1002  $this->parentRevId = $request->getInt( 'parentRevId' );
1003 
1004  $this->bot = $request->getBool( 'bot', true );
1005  $this->nosummary = $request->getBool( 'nosummary' );
1006 
1007  // May be overridden by revision.
1008  $this->contentModel = $request->getText( 'model', $this->contentModel );
1009  // May be overridden by revision.
1010  $this->contentFormat = $request->getText( 'format', $this->contentFormat );
1011 
1012  try {
1013  $handler = ContentHandler::getForModelID( $this->contentModel );
1014  } catch ( MWUnknownContentModelException $e ) {
1015  throw new ErrorPageError(
1016  'editpage-invalidcontentmodel-title',
1017  'editpage-invalidcontentmodel-text',
1018  [ wfEscapeWikiText( $this->contentModel ) ]
1019  );
1020  }
1021 
1022  if ( !$handler->isSupportedFormat( $this->contentFormat ) ) {
1023  throw new ErrorPageError(
1024  'editpage-notsupportedcontentformat-title',
1025  'editpage-notsupportedcontentformat-text',
1026  [
1027  wfEscapeWikiText( $this->contentFormat ),
1028  wfEscapeWikiText( ContentHandler::getLocalizedName( $this->contentModel ) )
1029  ]
1030  );
1031  }
1032 
1039  $this->editintro = $request->getText( 'editintro',
1040  // Custom edit intro for new sections
1041  $this->section === 'new' ? 'MediaWiki:addsection-editintro' : '' );
1042 
1043  // Allow extensions to modify form data
1044  Hooks::run( 'EditPage::importFormData', [ $this, $request ] );
1045 
1046  }
1047 
1057  protected function importContentFormData( &$request ) {
1058  return; // Don't do anything, EditPage already extracted wpTextbox1
1059  }
1060 
1066  function initialiseForm() {
1067  global $wgUser;
1068  $this->edittime = $this->page->getTimestamp();
1069  $this->editRevId = $this->page->getLatest();
1070 
1071  $content = $this->getContentObject( false ); # TODO: track content object?!
1072  if ( $content === false ) {
1073  return false;
1074  }
1075  $this->textbox1 = $this->toEditText( $content );
1076 
1077  // activate checkboxes if user wants them to be always active
1078  # Sort out the "watch" checkbox
1079  if ( $wgUser->getOption( 'watchdefault' ) ) {
1080  # Watch all edits
1081  $this->watchthis = true;
1082  } elseif ( $wgUser->getOption( 'watchcreations' ) && !$this->mTitle->exists() ) {
1083  # Watch creations
1084  $this->watchthis = true;
1085  } elseif ( $wgUser->isWatched( $this->mTitle ) ) {
1086  # Already watched
1087  $this->watchthis = true;
1088  }
1089  if ( $wgUser->getOption( 'minordefault' ) && !$this->isNew ) {
1090  $this->minoredit = true;
1091  }
1092  if ( $this->textbox1 === false ) {
1093  return false;
1094  }
1095  return true;
1096  }
1097 
1105  protected function getContentObject( $def_content = null ) {
1107 
1108  $content = false;
1109 
1110  // For message page not locally set, use the i18n message.
1111  // For other non-existent articles, use preload text if any.
1112  if ( !$this->mTitle->exists() || $this->section == 'new' ) {
1113  if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && $this->section != 'new' ) {
1114  # If this is a system message, get the default text.
1115  $msg = $this->mTitle->getDefaultMessageText();
1116 
1117  $content = $this->toEditContent( $msg );
1118  }
1119  if ( $content === false ) {
1120  # If requested, preload some text.
1121  $preload = $wgRequest->getVal( 'preload',
1122  // Custom preload text for new sections
1123  $this->section === 'new' ? 'MediaWiki:addsection-preload' : '' );
1124  $params = $wgRequest->getArray( 'preloadparams', [] );
1125 
1126  $content = $this->getPreloadedContent( $preload, $params );
1127  }
1128  // For existing pages, get text based on "undo" or section parameters.
1129  } else {
1130  if ( $this->section != '' ) {
1131  // Get section edit text (returns $def_text for invalid sections)
1132  $orig = $this->getOriginalContent( $wgUser );
1133  $content = $orig ? $orig->getSection( $this->section ) : null;
1134 
1135  if ( !$content ) {
1136  $content = $def_content;
1137  }
1138  } else {
1139  $undoafter = $wgRequest->getInt( 'undoafter' );
1140  $undo = $wgRequest->getInt( 'undo' );
1141 
1142  if ( $undo > 0 && $undoafter > 0 ) {
1143  $undorev = Revision::newFromId( $undo );
1144  $oldrev = Revision::newFromId( $undoafter );
1145 
1146  # Sanity check, make sure it's the right page,
1147  # the revisions exist and they were not deleted.
1148  # Otherwise, $content will be left as-is.
1149  if ( !is_null( $undorev ) && !is_null( $oldrev ) &&
1150  !$undorev->isDeleted( Revision::DELETED_TEXT ) &&
1151  !$oldrev->isDeleted( Revision::DELETED_TEXT )
1152  ) {
1153  $content = $this->page->getUndoContent( $undorev, $oldrev );
1154 
1155  if ( $content === false ) {
1156  # Warn the user that something went wrong
1157  $undoMsg = 'failure';
1158  } else {
1159  $oldContent = $this->page->getContent( Revision::RAW );
1160  $popts = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang );
1161  $newContent = $content->preSaveTransform( $this->mTitle, $wgUser, $popts );
1162  if ( $newContent->getModel() !== $oldContent->getModel() ) {
1163  // The undo may change content
1164  // model if its reverting the top
1165  // edit. This can result in
1166  // mismatched content model/format.
1167  $this->contentModel = $newContent->getModel();
1168  $this->contentFormat = $oldrev->getContentFormat();
1169  }
1170 
1171  if ( $newContent->equals( $oldContent ) ) {
1172  # Tell the user that the undo results in no change,
1173  # i.e. the revisions were already undone.
1174  $undoMsg = 'nochange';
1175  $content = false;
1176  } else {
1177  # Inform the user of our success and set an automatic edit summary
1178  $undoMsg = 'success';
1179 
1180  # If we just undid one rev, use an autosummary
1181  $firstrev = $oldrev->getNext();
1182  if ( $firstrev && $firstrev->getId() == $undo ) {
1183  $userText = $undorev->getUserText();
1184  if ( $userText === '' ) {
1185  $undoSummary = $this->context->msg(
1186  'undo-summary-username-hidden',
1187  $undo
1188  )->inContentLanguage()->text();
1189  } else {
1190  $undoSummary = $this->context->msg(
1191  'undo-summary',
1192  $undo,
1193  $userText
1194  )->inContentLanguage()->text();
1195  }
1196  if ( $this->summary === '' ) {
1197  $this->summary = $undoSummary;
1198  } else {
1199  $this->summary = $undoSummary . $this->context->msg( 'colon-separator' )
1200  ->inContentLanguage()->text() . $this->summary;
1201  }
1202  $this->undidRev = $undo;
1203  }
1204  $this->formtype = 'diff';
1205  }
1206  }
1207  } else {
1208  // Failed basic sanity checks.
1209  // Older revisions may have been removed since the link
1210  // was created, or we may simply have got bogus input.
1211  $undoMsg = 'norev';
1212  }
1213 
1214  // Messages: undo-success, undo-failure, undo-norev, undo-nochange
1215  $class = ( $undoMsg == 'success' ? '' : 'error ' ) . "mw-undo-{$undoMsg}";
1216  $this->editFormPageTop .= $wgOut->parse( "<div class=\"{$class}\">" .
1217  $this->context->msg( 'undo-' . $undoMsg )->plain() . '</div>', true, /* interface */true );
1218  }
1219 
1220  if ( $content === false ) {
1221  $content = $this->getOriginalContent( $wgUser );
1222  }
1223  }
1224  }
1225 
1226  return $content;
1227  }
1228 
1244  private function getOriginalContent( User $user ) {
1245  if ( $this->section == 'new' ) {
1246  return $this->getCurrentContent();
1247  }
1248  $revision = $this->mArticle->getRevisionFetched();
1249  if ( $revision === null ) {
1250  if ( !$this->contentModel ) {
1251  $this->contentModel = $this->getTitle()->getContentModel();
1252  }
1253  $handler = ContentHandler::getForModelID( $this->contentModel );
1254 
1255  return $handler->makeEmptyContent();
1256  }
1257  $content = $revision->getContent( Revision::FOR_THIS_USER, $user );
1258  return $content;
1259  }
1260 
1273  public function getParentRevId() {
1274  if ( $this->parentRevId ) {
1275  return $this->parentRevId;
1276  } else {
1277  return $this->mArticle->getRevIdFetched();
1278  }
1279  }
1280 
1289  protected function getCurrentContent() {
1290  $rev = $this->page->getRevision();
1291  $content = $rev ? $rev->getContent( Revision::RAW ) : null;
1292 
1293  if ( $content === false || $content === null ) {
1294  if ( !$this->contentModel ) {
1295  $this->contentModel = $this->getTitle()->getContentModel();
1296  }
1297  $handler = ContentHandler::getForModelID( $this->contentModel );
1298 
1299  return $handler->makeEmptyContent();
1300  } elseif ( !$this->undidRev ) {
1301  // Content models should always be the same since we error
1302  // out if they are different before this point (in ->edit()).
1303  // The exception being, during an undo, the current revision might
1304  // differ from the prior revision.
1305  $logger = LoggerFactory::getInstance( 'editpage' );
1306  if ( $this->contentModel !== $rev->getContentModel() ) {
1307  $logger->warning( "Overriding content model from current edit {prev} to {new}", [
1308  'prev' => $this->contentModel,
1309  'new' => $rev->getContentModel(),
1310  'title' => $this->getTitle()->getPrefixedDBkey(),
1311  'method' => __METHOD__
1312  ] );
1313  $this->contentModel = $rev->getContentModel();
1314  }
1315 
1316  // Given that the content models should match, the current selected
1317  // format should be supported.
1318  if ( !$content->isSupportedFormat( $this->contentFormat ) ) {
1319  $logger->warning( "Current revision content format unsupported. Overriding {prev} to {new}", [
1320 
1321  'prev' => $this->contentFormat,
1322  'new' => $rev->getContentFormat(),
1323  'title' => $this->getTitle()->getPrefixedDBkey(),
1324  'method' => __METHOD__
1325  ] );
1326  $this->contentFormat = $rev->getContentFormat();
1327  }
1328  }
1329  return $content;
1330  }
1331 
1339  public function setPreloadedContent( Content $content ) {
1340  $this->mPreloadContent = $content;
1341  }
1342 
1354  protected function getPreloadedContent( $preload, $params = [] ) {
1355  global $wgUser;
1356 
1357  if ( !empty( $this->mPreloadContent ) ) {
1358  return $this->mPreloadContent;
1359  }
1360 
1361  $handler = ContentHandler::getForModelID( $this->contentModel );
1362 
1363  if ( $preload === '' ) {
1364  return $handler->makeEmptyContent();
1365  }
1366 
1367  $title = Title::newFromText( $preload );
1368  # Check for existence to avoid getting MediaWiki:Noarticletext
1369  if ( $title === null || !$title->exists() || !$title->userCan( 'read', $wgUser ) ) {
1370  // TODO: somehow show a warning to the user!
1371  return $handler->makeEmptyContent();
1372  }
1373 
1375  if ( $page->isRedirect() ) {
1377  # Same as before
1378  if ( $title === null || !$title->exists() || !$title->userCan( 'read', $wgUser ) ) {
1379  // TODO: somehow show a warning to the user!
1380  return $handler->makeEmptyContent();
1381  }
1383  }
1384 
1385  $parserOptions = ParserOptions::newFromUser( $wgUser );
1387 
1388  if ( !$content ) {
1389  // TODO: somehow show a warning to the user!
1390  return $handler->makeEmptyContent();
1391  }
1392 
1393  if ( $content->getModel() !== $handler->getModelID() ) {
1394  $converted = $content->convert( $handler->getModelID() );
1395 
1396  if ( !$converted ) {
1397  // TODO: somehow show a warning to the user!
1398  wfDebug( "Attempt to preload incompatible content: " .
1399  "can't convert " . $content->getModel() .
1400  " to " . $handler->getModelID() );
1401 
1402  return $handler->makeEmptyContent();
1403  }
1404 
1405  $content = $converted;
1406  }
1407 
1408  return $content->preloadTransform( $title, $parserOptions, $params );
1409  }
1410 
1418  function tokenOk( &$request ) {
1419  global $wgUser;
1420  $token = $request->getVal( 'wpEditToken' );
1421  $this->mTokenOk = $wgUser->matchEditToken( $token );
1422  $this->mTokenOkExceptSuffix = $wgUser->matchEditTokenNoSuffix( $token );
1423  return $this->mTokenOk;
1424  }
1425 
1442  protected function setPostEditCookie( $statusValue ) {
1443  $revisionId = $this->page->getLatest();
1444  $postEditKey = self::POST_EDIT_COOKIE_KEY_PREFIX . $revisionId;
1445 
1446  $val = 'saved';
1447  if ( $statusValue == self::AS_SUCCESS_NEW_ARTICLE ) {
1448  $val = 'created';
1449  } elseif ( $this->oldid ) {
1450  $val = 'restored';
1451  }
1452 
1453  $response = RequestContext::getMain()->getRequest()->response();
1454  $response->setCookie( $postEditKey, $val, time() + self::POST_EDIT_COOKIE_DURATION, [
1455  'httpOnly' => false,
1456  ] );
1457  }
1458 
1465  public function attemptSave( &$resultDetails = false ) {
1466  global $wgUser;
1467 
1468  # Allow bots to exempt some edits from bot flagging
1469  $bot = $wgUser->isAllowed( 'bot' ) && $this->bot;
1470  $status = $this->internalAttemptSave( $resultDetails, $bot );
1471 
1472  Hooks::run( 'EditPage::attemptSave:after', [ $this, $status, $resultDetails ] );
1473 
1474  return $status;
1475  }
1476 
1486  private function handleStatus( Status $status, $resultDetails ) {
1488 
1493  if ( $status->value == self::AS_SUCCESS_UPDATE
1494  || $status->value == self::AS_SUCCESS_NEW_ARTICLE
1495  ) {
1496  $this->didSave = true;
1497  if ( !$resultDetails['nullEdit'] ) {
1498  $this->setPostEditCookie( $status->value );
1499  }
1500  }
1501 
1502  // "wpExtraQueryRedirect" is a hidden input to modify
1503  // after save URL and is not used by actual edit form
1504  $request = RequestContext::getMain()->getRequest();
1505  $extraQueryRedirect = $request->getVal( 'wpExtraQueryRedirect' );
1506 
1507  switch ( $status->value ) {
1508  case self::AS_HOOK_ERROR_EXPECTED:
1509  case self::AS_CONTENT_TOO_BIG:
1510  case self::AS_ARTICLE_WAS_DELETED:
1511  case self::AS_CONFLICT_DETECTED:
1512  case self::AS_SUMMARY_NEEDED:
1513  case self::AS_TEXTBOX_EMPTY:
1514  case self::AS_MAX_ARTICLE_SIZE_EXCEEDED:
1515  case self::AS_END:
1516  case self::AS_BLANK_ARTICLE:
1517  case self::AS_SELF_REDIRECT:
1518  return true;
1519 
1520  case self::AS_HOOK_ERROR:
1521  return false;
1522 
1523  case self::AS_CANNOT_USE_CUSTOM_MODEL:
1524  case self::AS_PARSE_ERROR:
1525  $wgOut->addWikiText( '<div class="error">' . "\n" . $status->getWikiText() . '</div>' );
1526  return true;
1527 
1528  case self::AS_SUCCESS_NEW_ARTICLE:
1529  $query = $resultDetails['redirect'] ? 'redirect=no' : '';
1530  if ( $extraQueryRedirect ) {
1531  if ( $query === '' ) {
1532  $query = $extraQueryRedirect;
1533  } else {
1534  $query = $query . '&' . $extraQueryRedirect;
1535  }
1536  }
1537  $anchor = isset( $resultDetails['sectionanchor'] ) ? $resultDetails['sectionanchor'] : '';
1538  $wgOut->redirect( $this->mTitle->getFullURL( $query ) . $anchor );
1539  return false;
1540 
1541  case self::AS_SUCCESS_UPDATE:
1542  $extraQuery = '';
1543  $sectionanchor = $resultDetails['sectionanchor'];
1544 
1545  // Give extensions a chance to modify URL query on update
1546  Hooks::run(
1547  'ArticleUpdateBeforeRedirect',
1548  [ $this->mArticle, &$sectionanchor, &$extraQuery ]
1549  );
1550 
1551  if ( $resultDetails['redirect'] ) {
1552  if ( $extraQuery == '' ) {
1553  $extraQuery = 'redirect=no';
1554  } else {
1555  $extraQuery = 'redirect=no&' . $extraQuery;
1556  }
1557  }
1558  if ( $extraQueryRedirect ) {
1559  if ( $extraQuery === '' ) {
1560  $extraQuery = $extraQueryRedirect;
1561  } else {
1562  $extraQuery = $extraQuery . '&' . $extraQueryRedirect;
1563  }
1564  }
1565 
1566  $wgOut->redirect( $this->mTitle->getFullURL( $extraQuery ) . $sectionanchor );
1567  return false;
1568 
1569  case self::AS_SPAM_ERROR:
1570  $this->spamPageWithContent( $resultDetails['spam'] );
1571  return false;
1572 
1573  case self::AS_BLOCKED_PAGE_FOR_USER:
1574  throw new UserBlockedError( $wgUser->getBlock() );
1575 
1576  case self::AS_IMAGE_REDIRECT_ANON:
1577  case self::AS_IMAGE_REDIRECT_LOGGED:
1578  throw new PermissionsError( 'upload' );
1579 
1580  case self::AS_READ_ONLY_PAGE_ANON:
1581  case self::AS_READ_ONLY_PAGE_LOGGED:
1582  throw new PermissionsError( 'edit' );
1583 
1584  case self::AS_READ_ONLY_PAGE:
1585  throw new ReadOnlyError;
1586 
1587  case self::AS_RATE_LIMITED:
1588  throw new ThrottledError();
1589 
1590  case self::AS_NO_CREATE_PERMISSION:
1591  $permission = $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage';
1592  throw new PermissionsError( $permission );
1593 
1594  case self::AS_NO_CHANGE_CONTENT_MODEL:
1595  throw new PermissionsError( 'editcontentmodel' );
1596 
1597  default:
1598  // We don't recognize $status->value. The only way that can happen
1599  // is if an extension hook aborted from inside ArticleSave.
1600  // Render the status object into $this->hookError
1601  // FIXME this sucks, we should just use the Status object throughout
1602  $this->hookError = '<div class="error">' ."\n" . $status->getWikiText() .
1603  '</div>';
1604  return true;
1605  }
1606  }
1607 
1618  // Run old style post-section-merge edit filter
1619  if ( !ContentHandler::runLegacyHooks( 'EditFilterMerged',
1620  [ $this, $content, &$this->hookError, $this->summary ],
1621  '1.21'
1622  ) ) {
1623  # Error messages etc. could be handled within the hook...
1624  $status->fatal( 'hookaborted' );
1625  $status->value = self::AS_HOOK_ERROR;
1626  return false;
1627  } elseif ( $this->hookError != '' ) {
1628  # ...or the hook could be expecting us to produce an error
1629  $status->fatal( 'hookaborted' );
1630  $status->value = self::AS_HOOK_ERROR_EXPECTED;
1631  return false;
1632  }
1633 
1634  // Run new style post-section-merge edit filter
1635  if ( !Hooks::run( 'EditFilterMergedContent',
1636  [ $this->mArticle->getContext(), $content, $status, $this->summary,
1637  $user, $this->minoredit ] )
1638  ) {
1639  # Error messages etc. could be handled within the hook...
1640  if ( $status->isGood() ) {
1641  $status->fatal( 'hookaborted' );
1642  // Not setting $this->hookError here is a hack to allow the hook
1643  // to cause a return to the edit page without $this->hookError
1644  // being set. This is used by ConfirmEdit to display a captcha
1645  // without any error message cruft.
1646  } else {
1647  $this->hookError = $status->getWikiText();
1648  }
1649  // Use the existing $status->value if the hook set it
1650  if ( !$status->value ) {
1651  $status->value = self::AS_HOOK_ERROR;
1652  }
1653  return false;
1654  } elseif ( !$status->isOK() ) {
1655  # ...or the hook could be expecting us to produce an error
1656  // FIXME this sucks, we should just use the Status object throughout
1657  $this->hookError = $status->getWikiText();
1658  $status->fatal( 'hookaborted' );
1659  $status->value = self::AS_HOOK_ERROR_EXPECTED;
1660  return false;
1661  }
1662 
1663  return true;
1664  }
1665 
1672  private function newSectionSummary( &$sectionanchor = null ) {
1673  global $wgParser;
1674 
1675  if ( $this->sectiontitle !== '' ) {
1676  $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle );
1677  // If no edit summary was specified, create one automatically from the section
1678  // title and have it link to the new section. Otherwise, respect the summary as
1679  // passed.
1680  if ( $this->summary === '' ) {
1681  $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
1682  return $this->context->msg( 'newsectionsummary' )
1683  ->rawParams( $cleanSectionTitle )->inContentLanguage()->text();
1684  }
1685  } elseif ( $this->summary !== '' ) {
1686  $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->summary );
1687  # This is a new section, so create a link to the new section
1688  # in the revision summary.
1689  $cleanSummary = $wgParser->stripSectionName( $this->summary );
1690  return $this->context->msg( 'newsectionsummary' )
1691  ->rawParams( $cleanSummary )->inContentLanguage()->text();
1692  }
1693  return $this->summary;
1694  }
1695 
1720  function internalAttemptSave( &$result, $bot = false ) {
1722  global $wgContentHandlerUseDB;
1723 
1725 
1726  if ( !Hooks::run( 'EditPage::attemptSave', [ $this ] ) ) {
1727  wfDebug( "Hook 'EditPage::attemptSave' aborted article saving\n" );
1728  $status->fatal( 'hookaborted' );
1729  $status->value = self::AS_HOOK_ERROR;
1730  return $status;
1731  }
1732 
1733  $spam = $wgRequest->getText( 'wpAntispam' );
1734  if ( $spam !== '' ) {
1735  wfDebugLog(
1736  'SimpleAntiSpam',
1737  $wgUser->getName() .
1738  ' editing "' .
1739  $this->mTitle->getPrefixedText() .
1740  '" submitted bogus field "' .
1741  $spam .
1742  '"'
1743  );
1744  $status->fatal( 'spamprotectionmatch', false );
1745  $status->value = self::AS_SPAM_ERROR;
1746  return $status;
1747  }
1748 
1749  try {
1750  # Construct Content object
1751  $textbox_content = $this->toEditContent( $this->textbox1 );
1752  } catch ( MWContentSerializationException $ex ) {
1753  $status->fatal(
1754  'content-failed-to-parse',
1755  $this->contentModel,
1756  $this->contentFormat,
1757  $ex->getMessage()
1758  );
1759  $status->value = self::AS_PARSE_ERROR;
1760  return $status;
1761  }
1762 
1763  # Check image redirect
1764  if ( $this->mTitle->getNamespace() == NS_FILE &&
1765  $textbox_content->isRedirect() &&
1766  !$wgUser->isAllowed( 'upload' )
1767  ) {
1768  $code = $wgUser->isAnon() ? self::AS_IMAGE_REDIRECT_ANON : self::AS_IMAGE_REDIRECT_LOGGED;
1769  $status->setResult( false, $code );
1770 
1771  return $status;
1772  }
1773 
1774  # Check for spam
1775  $match = self::matchSummarySpamRegex( $this->summary );
1776  if ( $match === false && $this->section == 'new' ) {
1777  # $wgSpamRegex is enforced on this new heading/summary because, unlike
1778  # regular summaries, it is added to the actual wikitext.
1779  if ( $this->sectiontitle !== '' ) {
1780  # This branch is taken when the API is used with the 'sectiontitle' parameter.
1781  $match = self::matchSpamRegex( $this->sectiontitle );
1782  } else {
1783  # This branch is taken when the "Add Topic" user interface is used, or the API
1784  # is used with the 'summary' parameter.
1785  $match = self::matchSpamRegex( $this->summary );
1786  }
1787  }
1788  if ( $match === false ) {
1789  $match = self::matchSpamRegex( $this->textbox1 );
1790  }
1791  if ( $match !== false ) {
1792  $result['spam'] = $match;
1793  $ip = $wgRequest->getIP();
1794  $pdbk = $this->mTitle->getPrefixedDBkey();
1795  $match = str_replace( "\n", '', $match );
1796  wfDebugLog( 'SpamRegex', "$ip spam regex hit [[$pdbk]]: \"$match\"" );
1797  $status->fatal( 'spamprotectionmatch', $match );
1798  $status->value = self::AS_SPAM_ERROR;
1799  return $status;
1800  }
1801  if ( !Hooks::run(
1802  'EditFilter',
1803  [ $this, $this->textbox1, $this->section, &$this->hookError, $this->summary ] )
1804  ) {
1805  # Error messages etc. could be handled within the hook...
1806  $status->fatal( 'hookaborted' );
1807  $status->value = self::AS_HOOK_ERROR;
1808  return $status;
1809  } elseif ( $this->hookError != '' ) {
1810  # ...or the hook could be expecting us to produce an error
1811  $status->fatal( 'hookaborted' );
1812  $status->value = self::AS_HOOK_ERROR_EXPECTED;
1813  return $status;
1814  }
1815 
1816  if ( $wgUser->isBlockedFrom( $this->mTitle, false ) ) {
1817  // Auto-block user's IP if the account was "hard" blocked
1818  if ( !wfReadOnly() ) {
1819  $wgUser->spreadAnyEditBlock();
1820  }
1821  # Check block state against master, thus 'false'.
1822  $status->setResult( false, self::AS_BLOCKED_PAGE_FOR_USER );
1823  return $status;
1824  }
1825 
1826  $this->contentLength = strlen( $this->textbox1 );
1827  if ( $this->contentLength > $wgMaxArticleSize * 1024 ) {
1828  // Error will be displayed by showEditForm()
1829  $this->tooBig = true;
1830  $status->setResult( false, self::AS_CONTENT_TOO_BIG );
1831  return $status;
1832  }
1833 
1834  if ( !$wgUser->isAllowed( 'edit' ) ) {
1835  if ( $wgUser->isAnon() ) {
1836  $status->setResult( false, self::AS_READ_ONLY_PAGE_ANON );
1837  return $status;
1838  } else {
1839  $status->fatal( 'readonlytext' );
1840  $status->value = self::AS_READ_ONLY_PAGE_LOGGED;
1841  return $status;
1842  }
1843  }
1844 
1845  $changingContentModel = false;
1846  if ( $this->contentModel !== $this->mTitle->getContentModel() ) {
1847  if ( !$wgContentHandlerUseDB ) {
1848  $status->fatal( 'editpage-cannot-use-custom-model' );
1849  $status->value = self::AS_CANNOT_USE_CUSTOM_MODEL;
1850  return $status;
1851  } elseif ( !$wgUser->isAllowed( 'editcontentmodel' ) ) {
1852  $status->setResult( false, self::AS_NO_CHANGE_CONTENT_MODEL );
1853  return $status;
1854  }
1855  // Make sure the user can edit the page under the new content model too
1856  $titleWithNewContentModel = clone $this->mTitle;
1857  $titleWithNewContentModel->setContentModel( $this->contentModel );
1858  if ( !$titleWithNewContentModel->userCan( 'editcontentmodel', $wgUser )
1859  || !$titleWithNewContentModel->userCan( 'edit', $wgUser )
1860  ) {
1861  $status->setResult( false, self::AS_NO_CHANGE_CONTENT_MODEL );
1862  return $status;
1863  }
1864 
1865  $changingContentModel = true;
1866  $oldContentModel = $this->mTitle->getContentModel();
1867  }
1868 
1869  if ( $this->changeTags ) {
1870  $changeTagsStatus = ChangeTags::canAddTagsAccompanyingChange(
1871  $this->changeTags, $wgUser );
1872  if ( !$changeTagsStatus->isOK() ) {
1873  $changeTagsStatus->value = self::AS_CHANGE_TAG_ERROR;
1874  return $changeTagsStatus;
1875  }
1876  }
1877 
1878  if ( wfReadOnly() ) {
1879  $status->fatal( 'readonlytext' );
1880  $status->value = self::AS_READ_ONLY_PAGE;
1881  return $status;
1882  }
1883  if ( $wgUser->pingLimiter() || $wgUser->pingLimiter( 'linkpurge', 0 )
1884  || ( $changingContentModel && $wgUser->pingLimiter( 'editcontentmodel' ) )
1885  ) {
1886  $status->fatal( 'actionthrottledtext' );
1887  $status->value = self::AS_RATE_LIMITED;
1888  return $status;
1889  }
1890 
1891  # If the article has been deleted while editing, don't save it without
1892  # confirmation
1893  if ( $this->wasDeletedSinceLastEdit() && !$this->recreate ) {
1894  $status->setResult( false, self::AS_ARTICLE_WAS_DELETED );
1895  return $status;
1896  }
1897 
1898  # Load the page data from the master. If anything changes in the meantime,
1899  # we detect it by using page_latest like a token in a 1 try compare-and-swap.
1900  $this->page->loadPageData( 'fromdbmaster' );
1901  $new = !$this->page->exists();
1902 
1903  if ( $new ) {
1904  // Late check for create permission, just in case *PARANOIA*
1905  if ( !$this->mTitle->userCan( 'create', $wgUser ) ) {
1906  $status->fatal( 'nocreatetext' );
1907  $status->value = self::AS_NO_CREATE_PERMISSION;
1908  wfDebug( __METHOD__ . ": no create permission\n" );
1909  return $status;
1910  }
1911 
1912  // Don't save a new page if it's blank or if it's a MediaWiki:
1913  // message with content equivalent to default (allow empty pages
1914  // in this case to disable messages, see bug 50124)
1915  $defaultMessageText = $this->mTitle->getDefaultMessageText();
1916  if ( $this->mTitle->getNamespace() === NS_MEDIAWIKI && $defaultMessageText !== false ) {
1917  $defaultText = $defaultMessageText;
1918  } else {
1919  $defaultText = '';
1920  }
1921 
1922  if ( !$this->allowBlankArticle && $this->textbox1 === $defaultText ) {
1923  $this->blankArticle = true;
1924  $status->fatal( 'blankarticle' );
1925  $status->setResult( false, self::AS_BLANK_ARTICLE );
1926  return $status;
1927  }
1928 
1929  if ( !$this->runPostMergeFilters( $textbox_content, $status, $wgUser ) ) {
1930  return $status;
1931  }
1932 
1933  $content = $textbox_content;
1934 
1935  $result['sectionanchor'] = '';
1936  if ( $this->section == 'new' ) {
1937  if ( $this->sectiontitle !== '' ) {
1938  // Insert the section title above the content.
1939  $content = $content->addSectionHeader( $this->sectiontitle );
1940  } elseif ( $this->summary !== '' ) {
1941  // Insert the section title above the content.
1942  $content = $content->addSectionHeader( $this->summary );
1943  }
1944  $this->summary = $this->newSectionSummary( $result['sectionanchor'] );
1945  }
1946 
1947  $status->value = self::AS_SUCCESS_NEW_ARTICLE;
1948 
1949  } else { # not $new
1950 
1951  # Article exists. Check for edit conflict.
1952 
1953  $this->page->clear(); # Force reload of dates, etc.
1954  $timestamp = $this->page->getTimestamp();
1955  $latest = $this->page->getLatest();
1956 
1957  wfDebug( "timestamp: {$timestamp}, edittime: {$this->edittime}\n" );
1958 
1959  // Check editRevId if set, which handles same-second timestamp collisions
1960  if ( $timestamp != $this->edittime
1961  || ( $this->editRevId !== null && $this->editRevId != $latest )
1962  ) {
1963  $this->isConflict = true;
1964  if ( $this->section == 'new' ) {
1965  if ( $this->page->getUserText() == $wgUser->getName() &&
1966  $this->page->getComment() == $this->newSectionSummary()
1967  ) {
1968  // Probably a duplicate submission of a new comment.
1969  // This can happen when CDN resends a request after
1970  // a timeout but the first one actually went through.
1971  wfDebug( __METHOD__
1972  . ": duplicate new section submission; trigger edit conflict!\n" );
1973  } else {
1974  // New comment; suppress conflict.
1975  $this->isConflict = false;
1976  wfDebug( __METHOD__ . ": conflict suppressed; new section\n" );
1977  }
1978  } elseif ( $this->section == ''
1980  DB_MASTER, $this->mTitle->getArticleID(),
1981  $wgUser->getId(), $this->edittime
1982  )
1983  ) {
1984  # Suppress edit conflict with self, except for section edits where merging is required.
1985  wfDebug( __METHOD__ . ": Suppressing edit conflict, same user.\n" );
1986  $this->isConflict = false;
1987  }
1988  }
1989 
1990  // If sectiontitle is set, use it, otherwise use the summary as the section title.
1991  if ( $this->sectiontitle !== '' ) {
1992  $sectionTitle = $this->sectiontitle;
1993  } else {
1994  $sectionTitle = $this->summary;
1995  }
1996 
1997  $content = null;
1998 
1999  if ( $this->isConflict ) {
2000  wfDebug( __METHOD__
2001  . ": conflict! getting section '{$this->section}' for time '{$this->edittime}'"
2002  . " (id '{$this->editRevId}') (article time '{$timestamp}')\n" );
2003  // @TODO: replaceSectionAtRev() with base ID (not prior current) for ?oldid=X case
2004  // ...or disable section editing for non-current revisions (not exposed anyway).
2005  if ( $this->editRevId !== null ) {
2006  $content = $this->page->replaceSectionAtRev(
2007  $this->section,
2008  $textbox_content,
2009  $sectionTitle,
2010  $this->editRevId
2011  );
2012  } else {
2013  $content = $this->page->replaceSectionContent(
2014  $this->section,
2015  $textbox_content,
2016  $sectionTitle,
2017  $this->edittime
2018  );
2019  }
2020  } else {
2021  wfDebug( __METHOD__ . ": getting section '{$this->section}'\n" );
2022  $content = $this->page->replaceSectionContent(
2023  $this->section,
2024  $textbox_content,
2025  $sectionTitle
2026  );
2027  }
2028 
2029  if ( is_null( $content ) ) {
2030  wfDebug( __METHOD__ . ": activating conflict; section replace failed.\n" );
2031  $this->isConflict = true;
2032  $content = $textbox_content; // do not try to merge here!
2033  } elseif ( $this->isConflict ) {
2034  # Attempt merge
2035  if ( $this->mergeChangesIntoContent( $content ) ) {
2036  // Successful merge! Maybe we should tell the user the good news?
2037  $this->isConflict = false;
2038  wfDebug( __METHOD__ . ": Suppressing edit conflict, successful merge.\n" );
2039  } else {
2040  $this->section = '';
2041  $this->textbox1 = ContentHandler::getContentText( $content );
2042  wfDebug( __METHOD__ . ": Keeping edit conflict, failed merge.\n" );
2043  }
2044  }
2045 
2046  if ( $this->isConflict ) {
2047  $status->setResult( false, self::AS_CONFLICT_DETECTED );
2048  return $status;
2049  }
2050 
2051  if ( !$this->runPostMergeFilters( $content, $status, $wgUser ) ) {
2052  return $status;
2053  }
2054 
2055  if ( $this->section == 'new' ) {
2056  // Handle the user preference to force summaries here
2057  if ( !$this->allowBlankSummary && trim( $this->summary ) == '' ) {
2058  $this->missingSummary = true;
2059  $status->fatal( 'missingsummary' ); // or 'missingcommentheader' if $section == 'new'. Blegh
2060  $status->value = self::AS_SUMMARY_NEEDED;
2061  return $status;
2062  }
2063 
2064  // Do not allow the user to post an empty comment
2065  if ( $this->textbox1 == '' ) {
2066  $this->missingComment = true;
2067  $status->fatal( 'missingcommenttext' );
2068  $status->value = self::AS_TEXTBOX_EMPTY;
2069  return $status;
2070  }
2071  } elseif ( !$this->allowBlankSummary
2072  && !$content->equals( $this->getOriginalContent( $wgUser ) )
2073  && !$content->isRedirect()
2074  && md5( $this->summary ) == $this->autoSumm
2075  ) {
2076  $this->missingSummary = true;
2077  $status->fatal( 'missingsummary' );
2078  $status->value = self::AS_SUMMARY_NEEDED;
2079  return $status;
2080  }
2081 
2082  # All's well
2083  $sectionanchor = '';
2084  if ( $this->section == 'new' ) {
2085  $this->summary = $this->newSectionSummary( $sectionanchor );
2086  } elseif ( $this->section != '' ) {
2087  # Try to get a section anchor from the section source, redirect
2088  # to edited section if header found.
2089  # XXX: Might be better to integrate this into Article::replaceSectionAtRev
2090  # for duplicate heading checking and maybe parsing.
2091  $hasmatch = preg_match( "/^ *([=]{1,6})(.*?)(\\1) *\\n/i", $this->textbox1, $matches );
2092  # We can't deal with anchors, includes, html etc in the header for now,
2093  # headline would need to be parsed to improve this.
2094  if ( $hasmatch && strlen( $matches[2] ) > 0 ) {
2095  $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $matches[2] );
2096  }
2097  }
2098  $result['sectionanchor'] = $sectionanchor;
2099 
2100  // Save errors may fall down to the edit form, but we've now
2101  // merged the section into full text. Clear the section field
2102  // so that later submission of conflict forms won't try to
2103  // replace that into a duplicated mess.
2104  $this->textbox1 = $this->toEditText( $content );
2105  $this->section = '';
2106 
2107  $status->value = self::AS_SUCCESS_UPDATE;
2108  }
2109 
2110  if ( !$this->allowSelfRedirect
2111  && $content->isRedirect()
2112  && $content->getRedirectTarget()->equals( $this->getTitle() )
2113  ) {
2114  // If the page already redirects to itself, don't warn.
2115  $currentTarget = $this->getCurrentContent()->getRedirectTarget();
2116  if ( !$currentTarget || !$currentTarget->equals( $this->getTitle() ) ) {
2117  $this->selfRedirect = true;
2118  $status->fatal( 'selfredirect' );
2119  $status->value = self::AS_SELF_REDIRECT;
2120  return $status;
2121  }
2122  }
2123 
2124  // Check for length errors again now that the section is merged in
2125  $this->contentLength = strlen( $this->toEditText( $content ) );
2126  if ( $this->contentLength > $wgMaxArticleSize * 1024 ) {
2127  $this->tooBig = true;
2128  $status->setResult( false, self::AS_MAX_ARTICLE_SIZE_EXCEEDED );
2129  return $status;
2130  }
2131 
2133  ( $new ? EDIT_NEW : EDIT_UPDATE ) |
2134  ( ( $this->minoredit && !$this->isNew ) ? EDIT_MINOR : 0 ) |
2135  ( $bot ? EDIT_FORCE_BOT : 0 );
2136 
2137  $doEditStatus = $this->page->doEditContent(
2138  $content,
2139  $this->summary,
2140  $flags,
2141  false,
2142  $wgUser,
2143  $content->getDefaultFormat(),
2145  );
2146 
2147  if ( !$doEditStatus->isOK() ) {
2148  // Failure from doEdit()
2149  // Show the edit conflict page for certain recognized errors from doEdit(),
2150  // but don't show it for errors from extension hooks
2151  $errors = $doEditStatus->getErrorsArray();
2152  if ( in_array( $errors[0][0],
2153  [ 'edit-gone-missing', 'edit-conflict', 'edit-already-exists' ] )
2154  ) {
2155  $this->isConflict = true;
2156  // Destroys data doEdit() put in $status->value but who cares
2157  $doEditStatus->value = self::AS_END;
2158  }
2159  return $doEditStatus;
2160  }
2161 
2162  $result['nullEdit'] = $doEditStatus->hasMessage( 'edit-no-change' );
2163  if ( $result['nullEdit'] ) {
2164  // We don't know if it was a null edit until now, so increment here
2165  $wgUser->pingLimiter( 'linkpurge' );
2166  }
2167  $result['redirect'] = $content->isRedirect();
2168 
2169  $this->updateWatchlist();
2170 
2171  // If the content model changed, add a log entry
2172  if ( $changingContentModel ) {
2174  $wgUser,
2175  $new ? false : $oldContentModel,
2176  $this->contentModel,
2177  $this->summary
2178  );
2179  }
2180 
2181  return $status;
2182  }
2183 
2190  protected function addContentModelChangeLogEntry( User $user, $oldModel, $newModel, $reason ) {
2191  $new = $oldModel === false;
2192  $log = new ManualLogEntry( 'contentmodel', $new ? 'new' : 'change' );
2193  $log->setPerformer( $user );
2194  $log->setTarget( $this->mTitle );
2195  $log->setComment( $reason );
2196  $log->setParameters( [
2197  '4::oldmodel' => $oldModel,
2198  '5::newmodel' => $newModel
2199  ] );
2200  $logid = $log->insert();
2201  $log->publish( $logid );
2202  }
2203 
2207  protected function updateWatchlist() {
2208  global $wgUser;
2209 
2210  if ( !$wgUser->isLoggedIn() ) {
2211  return;
2212  }
2213 
2214  $user = $wgUser;
2216  $watch = $this->watchthis;
2217  // Do this in its own transaction to reduce contention...
2218  DeferredUpdates::addCallableUpdate( function () use ( $user, $title, $watch ) {
2219  if ( $watch == $user->isWatched( $title, User::IGNORE_USER_RIGHTS ) ) {
2220  return; // nothing to change
2221  }
2223  } );
2224  }
2225 
2237  private function mergeChangesIntoContent( &$editContent ) {
2238 
2239  $db = wfGetDB( DB_MASTER );
2240 
2241  // This is the revision the editor started from
2242  $baseRevision = $this->getBaseRevision();
2243  $baseContent = $baseRevision ? $baseRevision->getContent() : null;
2244 
2245  if ( is_null( $baseContent ) ) {
2246  return false;
2247  }
2248 
2249  // The current state, we want to merge updates into it
2250  $currentRevision = Revision::loadFromTitle( $db, $this->mTitle );
2251  $currentContent = $currentRevision ? $currentRevision->getContent() : null;
2252 
2253  if ( is_null( $currentContent ) ) {
2254  return false;
2255  }
2256 
2257  $handler = ContentHandler::getForModelID( $baseContent->getModel() );
2258 
2259  $result = $handler->merge3( $baseContent, $editContent, $currentContent );
2260 
2261  if ( $result ) {
2262  $editContent = $result;
2263  // Update parentRevId to what we just merged.
2264  $this->parentRevId = $currentRevision->getId();
2265  return true;
2266  }
2267 
2268  return false;
2269  }
2270 
2276  function getBaseRevision() {
2277  if ( !$this->mBaseRevision ) {
2278  $db = wfGetDB( DB_MASTER );
2279  $this->mBaseRevision = $this->editRevId
2280  ? Revision::newFromId( $this->editRevId, Revision::READ_LATEST )
2281  : Revision::loadFromTimestamp( $db, $this->mTitle, $this->edittime );
2282  }
2283  return $this->mBaseRevision;
2284  }
2285 
2293  public static function matchSpamRegex( $text ) {
2294  global $wgSpamRegex;
2295  // For back compatibility, $wgSpamRegex may be a single string or an array of regexes.
2296  $regexes = (array)$wgSpamRegex;
2297  return self::matchSpamRegexInternal( $text, $regexes );
2298  }
2299 
2307  public static function matchSummarySpamRegex( $text ) {
2308  global $wgSummarySpamRegex;
2309  $regexes = (array)$wgSummarySpamRegex;
2310  return self::matchSpamRegexInternal( $text, $regexes );
2311  }
2312 
2318  protected static function matchSpamRegexInternal( $text, $regexes ) {
2319  foreach ( $regexes as $regex ) {
2320  $matches = [];
2321  if ( preg_match( $regex, $text, $matches ) ) {
2322  return $matches[0];
2323  }
2324  }
2325  return false;
2326  }
2327 
2328  function setHeaders() {
2329  global $wgOut, $wgUser, $wgAjaxEditStash;
2330 
2331  $wgOut->addModules( 'mediawiki.action.edit' );
2332  $wgOut->addModuleStyles( 'mediawiki.action.edit.styles' );
2333 
2334  if ( $wgUser->getOption( 'showtoolbar' ) ) {
2335  // The addition of default buttons is handled by getEditToolbar() which
2336  // has its own dependency on this module. The call here ensures the module
2337  // is loaded in time (it has position "top") for other modules to register
2338  // buttons (e.g. extensions, gadgets, user scripts).
2339  $wgOut->addModules( 'mediawiki.toolbar' );
2340  }
2341 
2342  if ( $wgUser->getOption( 'uselivepreview' ) ) {
2343  $wgOut->addModules( 'mediawiki.action.edit.preview' );
2344  }
2345 
2346  if ( $wgUser->getOption( 'useeditwarning' ) ) {
2347  $wgOut->addModules( 'mediawiki.action.edit.editWarning' );
2348  }
2349 
2350  # Enabled article-related sidebar, toplinks, etc.
2351  $wgOut->setArticleRelated( true );
2352 
2353  $contextTitle = $this->getContextTitle();
2354  if ( $this->isConflict ) {
2355  $msg = 'editconflict';
2356  } elseif ( $contextTitle->exists() && $this->section != '' ) {
2357  $msg = $this->section == 'new' ? 'editingcomment' : 'editingsection';
2358  } else {
2359  $msg = $contextTitle->exists()
2360  || ( $contextTitle->getNamespace() == NS_MEDIAWIKI
2361  && $contextTitle->getDefaultMessageText() !== false
2362  )
2363  ? 'editing'
2364  : 'creating';
2365  }
2366 
2367  # Use the title defined by DISPLAYTITLE magic word when present
2368  # NOTE: getDisplayTitle() returns HTML while getPrefixedText() returns plain text.
2369  # setPageTitle() treats the input as wikitext, which should be safe in either case.
2370  $displayTitle = isset( $this->mParserOutput ) ? $this->mParserOutput->getDisplayTitle() : false;
2371  if ( $displayTitle === false ) {
2372  $displayTitle = $contextTitle->getPrefixedText();
2373  }
2374  $wgOut->setPageTitle( $this->context->msg( $msg, $displayTitle ) );
2375  # Transmit the name of the message to JavaScript for live preview
2376  # Keep Resources.php/mediawiki.action.edit.preview in sync with the possible keys
2377  $wgOut->addJsConfigVars( [
2378  'wgEditMessage' => $msg,
2379  'wgAjaxEditStash' => $wgAjaxEditStash,
2380  ] );
2381  }
2382 
2386  protected function showIntro() {
2388  if ( $this->suppressIntro ) {
2389  return;
2390  }
2391 
2392  $namespace = $this->mTitle->getNamespace();
2393 
2394  if ( $namespace == NS_MEDIAWIKI ) {
2395  # Show a warning if editing an interface message
2396  $wgOut->wrapWikiMsg( "<div class='mw-editinginterface'>\n$1\n</div>", 'editinginterface' );
2397  # If this is a default message (but not css or js),
2398  # show a hint that it is translatable on translatewiki.net
2399  if ( !$this->mTitle->hasContentModel( CONTENT_MODEL_CSS )
2400  && !$this->mTitle->hasContentModel( CONTENT_MODEL_JAVASCRIPT )
2401  ) {
2402  $defaultMessageText = $this->mTitle->getDefaultMessageText();
2403  if ( $defaultMessageText !== false ) {
2404  $wgOut->wrapWikiMsg( "<div class='mw-translateinterface'>\n$1\n</div>",
2405  'translateinterface' );
2406  }
2407  }
2408  } elseif ( $namespace == NS_FILE ) {
2409  # Show a hint to shared repo
2410  $file = wfFindFile( $this->mTitle );
2411  if ( $file && !$file->isLocal() ) {
2412  $descUrl = $file->getDescriptionUrl();
2413  # there must be a description url to show a hint to shared repo
2414  if ( $descUrl ) {
2415  if ( !$this->mTitle->exists() ) {
2416  $wgOut->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-create\">\n$1\n</div>", [
2417  'sharedupload-desc-create', $file->getRepo()->getDisplayName(), $descUrl
2418  ] );
2419  } else {
2420  $wgOut->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-edit\">\n$1\n</div>", [
2421  'sharedupload-desc-edit', $file->getRepo()->getDisplayName(), $descUrl
2422  ] );
2423  }
2424  }
2425  }
2426  }
2427 
2428  # Show a warning message when someone creates/edits a user (talk) page but the user does not exist
2429  # Show log extract when the user is currently blocked
2430  if ( $namespace == NS_USER || $namespace == NS_USER_TALK ) {
2431  $username = explode( '/', $this->mTitle->getText(), 2 )[0];
2432  $user = User::newFromName( $username, false /* allow IP users*/ );
2433  $ip = User::isIP( $username );
2434  $block = Block::newFromTarget( $user, $user );
2435  if ( !( $user && $user->isLoggedIn() ) && !$ip ) { # User does not exist
2436  $wgOut->wrapWikiMsg( "<div class=\"mw-userpage-userdoesnotexist error\">\n$1\n</div>",
2437  [ 'userpage-userdoesnotexist', wfEscapeWikiText( $username ) ] );
2438  } elseif ( !is_null( $block ) && $block->getType() != Block::TYPE_AUTO ) {
2439  # Show log extract if the user is currently blocked
2441  $wgOut,
2442  'block',
2443  MWNamespace::getCanonicalName( NS_USER ) . ':' . $block->getTarget(),
2444  '',
2445  [
2446  'lim' => 1,
2447  'showIfEmpty' => false,
2448  'msgKey' => [
2449  'blocked-notice-logextract',
2450  $user->getName() # Support GENDER in notice
2451  ]
2452  ]
2453  );
2454  }
2455  }
2456  # Try to add a custom edit intro, or use the standard one if this is not possible.
2457  if ( !$this->showCustomIntro() && !$this->mTitle->exists() ) {
2459  $this->context->msg( 'helppage' )->inContentLanguage()->text()
2460  ) );
2461  if ( $wgUser->isLoggedIn() ) {
2462  $wgOut->wrapWikiMsg(
2463  // Suppress the external link icon, consider the help url an internal one
2464  "<div class=\"mw-newarticletext plainlinks\">\n$1\n</div>",
2465  [
2466  'newarticletext',
2467  $helpLink
2468  ]
2469  );
2470  } else {
2471  $wgOut->wrapWikiMsg(
2472  // Suppress the external link icon, consider the help url an internal one
2473  "<div class=\"mw-newarticletextanon plainlinks\">\n$1\n</div>",
2474  [
2475  'newarticletextanon',
2476  $helpLink
2477  ]
2478  );
2479  }
2480  }
2481  # Give a notice if the user is editing a deleted/moved page...
2482  if ( !$this->mTitle->exists() ) {
2483  LogEventsList::showLogExtract( $wgOut, [ 'delete', 'move' ], $this->mTitle,
2484  '',
2485  [
2486  'lim' => 10,
2487  'conds' => [ "log_action != 'revision'" ],
2488  'showIfEmpty' => false,
2489  'msgKey' => [ 'recreate-moveddeleted-warn' ]
2490  ]
2491  );
2492  }
2493  }
2494 
2500  protected function showCustomIntro() {
2501  if ( $this->editintro ) {
2502  $title = Title::newFromText( $this->editintro );
2503  if ( $title instanceof Title && $title->exists() && $title->userCan( 'read' ) ) {
2504  global $wgOut;
2505  // Added using template syntax, to take <noinclude>'s into account.
2506  $wgOut->addWikiTextTitleTidy(
2507  '<div class="mw-editintro">{{:' . $title->getFullText() . '}}</div>',
2509  );
2510  return true;
2511  }
2512  }
2513  return false;
2514  }
2515 
2534  protected function toEditText( $content ) {
2535  if ( $content === null || $content === false || is_string( $content ) ) {
2536  return $content;
2537  }
2538 
2539  if ( !$this->isSupportedContentModel( $content->getModel() ) ) {
2540  throw new MWException( 'This content model is not supported: ' . $content->getModel() );
2541  }
2542 
2543  return $content->serialize( $this->contentFormat );
2544  }
2545 
2562  protected function toEditContent( $text ) {
2563  if ( $text === false || $text === null ) {
2564  return $text;
2565  }
2566 
2567  $content = ContentHandler::makeContent( $text, $this->getTitle(),
2568  $this->contentModel, $this->contentFormat );
2569 
2570  if ( !$this->isSupportedContentModel( $content->getModel() ) ) {
2571  throw new MWException( 'This content model is not supported: ' . $content->getModel() );
2572  }
2573 
2574  return $content;
2575  }
2576 
2585  function showEditForm( $formCallback = null ) {
2587 
2588  # need to parse the preview early so that we know which templates are used,
2589  # otherwise users with "show preview after edit box" will get a blank list
2590  # we parse this near the beginning so that setHeaders can do the title
2591  # setting work instead of leaving it in getPreviewText
2592  $previewOutput = '';
2593  if ( $this->formtype == 'preview' ) {
2594  $previewOutput = $this->getPreviewText();
2595  }
2596 
2597  Hooks::run( 'EditPage::showEditForm:initial', [ &$this, &$wgOut ] );
2598 
2599  $this->setHeaders();
2600 
2601  if ( $this->showHeader() === false ) {
2602  return;
2603  }
2604 
2605  $wgOut->addHTML( $this->editFormPageTop );
2606 
2607  if ( $wgUser->getOption( 'previewontop' ) ) {
2608  $this->displayPreviewArea( $previewOutput, true );
2609  }
2610 
2611  $wgOut->addHTML( $this->editFormTextTop );
2612 
2613  $showToolbar = true;
2614  if ( $this->wasDeletedSinceLastEdit() ) {
2615  if ( $this->formtype == 'save' ) {
2616  // Hide the toolbar and edit area, user can click preview to get it back
2617  // Add an confirmation checkbox and explanation.
2618  $showToolbar = false;
2619  } else {
2620  $wgOut->wrapWikiMsg( "<div class='error mw-deleted-while-editing'>\n$1\n</div>",
2621  'deletedwhileediting' );
2622  }
2623  }
2624 
2625  // @todo add EditForm plugin interface and use it here!
2626  // search for textarea1 and textares2, and allow EditForm to override all uses.
2627  $wgOut->addHTML( Html::openElement(
2628  'form',
2629  [
2630  'id' => self::EDITFORM_ID,
2631  'name' => self::EDITFORM_ID,
2632  'method' => 'post',
2633  'action' => $this->getActionURL( $this->getContextTitle() ),
2634  'enctype' => 'multipart/form-data'
2635  ]
2636  ) );
2637 
2638  if ( is_callable( $formCallback ) ) {
2639  wfWarn( 'The $formCallback parameter to ' . __METHOD__ . 'is deprecated' );
2640  call_user_func_array( $formCallback, [ &$wgOut ] );
2641  }
2642 
2643  // Add an empty field to trip up spambots
2644  $wgOut->addHTML(
2645  Xml::openElement( 'div', [ 'id' => 'antispam-container', 'style' => 'display: none;' ] )
2646  . Html::rawElement(
2647  'label',
2648  [ 'for' => 'wpAntispam' ],
2649  $this->context->msg( 'simpleantispam-label' )->parse()
2650  )
2651  . Xml::element(
2652  'input',
2653  [
2654  'type' => 'text',
2655  'name' => 'wpAntispam',
2656  'id' => 'wpAntispam',
2657  'value' => ''
2658  ]
2659  )
2660  . Xml::closeElement( 'div' )
2661  );
2662 
2663  Hooks::run( 'EditPage::showEditForm:fields', [ &$this, &$wgOut ] );
2664 
2665  // Put these up at the top to ensure they aren't lost on early form submission
2666  $this->showFormBeforeText();
2667 
2668  if ( $this->wasDeletedSinceLastEdit() && 'save' == $this->formtype ) {
2669  $username = $this->lastDelete->user_name;
2670  $comment = $this->lastDelete->log_comment;
2671 
2672  // It is better to not parse the comment at all than to have templates expanded in the middle
2673  // TODO: can the checkLabel be moved outside of the div so that wrapWikiMsg could be used?
2674  $key = $comment === ''
2675  ? 'confirmrecreate-noreason'
2676  : 'confirmrecreate';
2677  $wgOut->addHTML(
2678  '<div class="mw-confirm-recreate">' .
2679  $this->context->msg( $key, $username, "<nowiki>$comment</nowiki>" )->parse() .
2680  Xml::checkLabel( $this->context->msg( 'recreate' )->text(), 'wpRecreate', 'wpRecreate', false,
2681  [ 'title' => Linker::titleAttrib( 'recreate' ), 'tabindex' => 1, 'id' => 'wpRecreate' ]
2682  ) .
2683  '</div>'
2684  );
2685  }
2686 
2687  # When the summary is hidden, also hide them on preview/show changes
2688  if ( $this->nosummary ) {
2689  $wgOut->addHTML( Html::hidden( 'nosummary', true ) );
2690  }
2691 
2692  # If a blank edit summary was previously provided, and the appropriate
2693  # user preference is active, pass a hidden tag as wpIgnoreBlankSummary. This will stop the
2694  # user being bounced back more than once in the event that a summary
2695  # is not required.
2696  # ####
2697  # For a bit more sophisticated detection of blank summaries, hash the
2698  # automatic one and pass that in the hidden field wpAutoSummary.
2699  if ( $this->missingSummary || ( $this->section == 'new' && $this->nosummary ) ) {
2700  $wgOut->addHTML( Html::hidden( 'wpIgnoreBlankSummary', true ) );
2701  }
2702 
2703  if ( $this->undidRev ) {
2704  $wgOut->addHTML( Html::hidden( 'wpUndidRevision', $this->undidRev ) );
2705  }
2706 
2707  if ( $this->selfRedirect ) {
2708  $wgOut->addHTML( Html::hidden( 'wpIgnoreSelfRedirect', true ) );
2709  }
2710 
2711  if ( $this->hasPresetSummary ) {
2712  // If a summary has been preset using &summary= we don't want to prompt for
2713  // a different summary. Only prompt for a summary if the summary is blanked.
2714  // (Bug 17416)
2715  $this->autoSumm = md5( '' );
2716  }
2717 
2718  $autosumm = $this->autoSumm ? $this->autoSumm : md5( $this->summary );
2719  $wgOut->addHTML( Html::hidden( 'wpAutoSummary', $autosumm ) );
2720 
2721  $wgOut->addHTML( Html::hidden( 'oldid', $this->oldid ) );
2722  $wgOut->addHTML( Html::hidden( 'parentRevId', $this->getParentRevId() ) );
2723 
2724  $wgOut->addHTML( Html::hidden( 'format', $this->contentFormat ) );
2725  $wgOut->addHTML( Html::hidden( 'model', $this->contentModel ) );
2726 
2727  if ( $this->section == 'new' ) {
2728  $this->showSummaryInput( true, $this->summary );
2729  $wgOut->addHTML( $this->getSummaryPreview( true, $this->summary ) );
2730  }
2731 
2732  $wgOut->addHTML( $this->editFormTextBeforeContent );
2733 
2734  if ( !$this->isCssJsSubpage && $showToolbar && $wgUser->getOption( 'showtoolbar' ) ) {
2735  $wgOut->addHTML( EditPage::getEditToolbar( $this->mTitle ) );
2736  }
2737 
2738  if ( $this->blankArticle ) {
2739  $wgOut->addHTML( Html::hidden( 'wpIgnoreBlankArticle', true ) );
2740  }
2741 
2742  if ( $this->isConflict ) {
2743  // In an edit conflict bypass the overridable content form method
2744  // and fallback to the raw wpTextbox1 since editconflicts can't be
2745  // resolved between page source edits and custom ui edits using the
2746  // custom edit ui.
2747  $this->textbox2 = $this->textbox1;
2748 
2749  $content = $this->getCurrentContent();
2750  $this->textbox1 = $this->toEditText( $content );
2751 
2752  $this->showTextbox1();
2753  } else {
2754  $this->showContentForm();
2755  }
2756 
2757  $wgOut->addHTML( $this->editFormTextAfterContent );
2758 
2759  $this->showStandardInputs();
2760 
2761  $this->showFormAfterText();
2762 
2763  $this->showTosSummary();
2764 
2765  $this->showEditTools();
2766 
2767  $wgOut->addHTML( $this->editFormTextAfterTools . "\n" );
2768 
2769  $wgOut->addHTML( $this->makeTemplatesOnThisPageList( $this->getTemplates() ) );
2770 
2771  $wgOut->addHTML( Html::rawElement( 'div', [ 'class' => 'hiddencats' ],
2772  Linker::formatHiddenCategories( $this->page->getHiddenCategories() ) ) );
2773 
2774  $wgOut->addHTML( Html::rawElement( 'div', [ 'class' => 'limitreport' ],
2775  self::getPreviewLimitReport( $this->mParserOutput ) ) );
2776 
2777  $wgOut->addModules( 'mediawiki.action.edit.collapsibleFooter' );
2778 
2779  if ( $this->isConflict ) {
2780  try {
2781  $this->showConflict();
2782  } catch ( MWContentSerializationException $ex ) {
2783  // this can't really happen, but be nice if it does.
2784  $msg = $this->context->msg(
2785  'content-failed-to-parse',
2786  $this->contentModel,
2787  $this->contentFormat,
2788  $ex->getMessage()
2789  );
2790  $wgOut->addWikiText( '<div class="error">' . $msg->text() . '</div>' );
2791  }
2792  }
2793 
2794  // Set a hidden field so JS knows what edit form mode we are in
2795  if ( $this->isConflict ) {
2796  $mode = 'conflict';
2797  } elseif ( $this->preview ) {
2798  $mode = 'preview';
2799  } elseif ( $this->diff ) {
2800  $mode = 'diff';
2801  } else {
2802  $mode = 'text';
2803  }
2804  $wgOut->addHTML( Html::hidden( 'mode', $mode, [ 'id' => 'mw-edit-mode' ] ) );
2805 
2806  // Marker for detecting truncated form data. This must be the last
2807  // parameter sent in order to be of use, so do not move me.
2808  $wgOut->addHTML( Html::hidden( 'wpUltimateParam', true ) );
2809  $wgOut->addHTML( $this->editFormTextBottom . "\n</form>\n" );
2810 
2811  if ( !$wgUser->getOption( 'previewontop' ) ) {
2812  $this->displayPreviewArea( $previewOutput, false );
2813  }
2814 
2815  }
2816 
2824  protected function makeTemplatesOnThisPageList( array $templates ) {
2825  $templateListFormatter = new TemplatesOnThisPageFormatter(
2826  $this->context, MediaWikiServices::getInstance()->getLinkRenderer()
2827  );
2828 
2829  // preview if preview, else section if section, else false
2830  $type = false;
2831  if ( $this->preview ) {
2832  $type = 'preview';
2833  } elseif ( $this->section != '' ) {
2834  $type = 'section';
2835  }
2836 
2837  return Html::rawElement( 'div', [ 'class' => 'templatesUsed' ],
2838  $templateListFormatter->format( $templates, $type )
2839  );
2840 
2841  }
2842 
2849  public static function extractSectionTitle( $text ) {
2850  preg_match( "/^(=+)(.+)\\1\\s*(\n|$)/i", $text, $matches );
2851  if ( !empty( $matches[2] ) ) {
2852  global $wgParser;
2853  return $wgParser->stripSectionName( trim( $matches[2] ) );
2854  } else {
2855  return false;
2856  }
2857  }
2858 
2862  protected function showHeader() {
2865 
2866  if ( $this->mTitle->isTalkPage() ) {
2867  $wgOut->addWikiMsg( 'talkpagetext' );
2868  }
2869 
2870  // Add edit notices
2871  $editNotices = $this->mTitle->getEditNotices( $this->oldid );
2872  if ( count( $editNotices ) ) {
2873  $wgOut->addHTML( implode( "\n", $editNotices ) );
2874  } else {
2875  $msg = $this->context->msg( 'editnotice-notext' );
2876  if ( !$msg->isDisabled() ) {
2877  $wgOut->addHTML(
2878  '<div class="mw-editnotice-notext">'
2879  . $msg->parseAsBlock()
2880  . '</div>'
2881  );
2882  }
2883  }
2884 
2885  if ( $this->isConflict ) {
2886  $wgOut->wrapWikiMsg( "<div class='mw-explainconflict'>\n$1\n</div>", 'explainconflict' );
2887  $this->editRevId = $this->page->getLatest();
2888  } else {
2889  if ( $this->section != '' && !$this->isSectionEditSupported() ) {
2890  // We use $this->section to much before this and getVal('wgSection') directly in other places
2891  // at this point we can't reset $this->section to '' to fallback to non-section editing.
2892  // Someone is welcome to try refactoring though
2893  $wgOut->showErrorPage( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
2894  return false;
2895  }
2896 
2897  if ( $this->section != '' && $this->section != 'new' ) {
2898  if ( !$this->summary && !$this->preview && !$this->diff ) {
2899  $sectionTitle = self::extractSectionTitle( $this->textbox1 ); // FIXME: use Content object
2900  if ( $sectionTitle !== false ) {
2901  $this->summary = "/* $sectionTitle */ ";
2902  }
2903  }
2904  }
2905 
2906  if ( $this->missingComment ) {
2907  $wgOut->wrapWikiMsg( "<div id='mw-missingcommenttext'>\n$1\n</div>", 'missingcommenttext' );
2908  }
2909 
2910  if ( $this->missingSummary && $this->section != 'new' ) {
2911  $wgOut->wrapWikiMsg( "<div id='mw-missingsummary'>\n$1\n</div>", 'missingsummary' );
2912  }
2913 
2914  if ( $this->missingSummary && $this->section == 'new' ) {
2915  $wgOut->wrapWikiMsg( "<div id='mw-missingcommentheader'>\n$1\n</div>", 'missingcommentheader' );
2916  }
2917 
2918  if ( $this->blankArticle ) {
2919  $wgOut->wrapWikiMsg( "<div id='mw-blankarticle'>\n$1\n</div>", 'blankarticle' );
2920  }
2921 
2922  if ( $this->selfRedirect ) {
2923  $wgOut->wrapWikiMsg( "<div id='mw-selfredirect'>\n$1\n</div>", 'selfredirect' );
2924  }
2925 
2926  if ( $this->hookError !== '' ) {
2927  $wgOut->addWikiText( $this->hookError );
2928  }
2929 
2930  if ( !$this->checkUnicodeCompliantBrowser() ) {
2931  $wgOut->addWikiMsg( 'nonunicodebrowser' );
2932  }
2933 
2934  if ( $this->section != 'new' ) {
2935  $revision = $this->mArticle->getRevisionFetched();
2936  if ( $revision ) {
2937  // Let sysop know that this will make private content public if saved
2938 
2939  if ( !$revision->userCan( Revision::DELETED_TEXT, $wgUser ) ) {
2940  $wgOut->wrapWikiMsg(
2941  "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
2942  'rev-deleted-text-permission'
2943  );
2944  } elseif ( $revision->isDeleted( Revision::DELETED_TEXT ) ) {
2945  $wgOut->wrapWikiMsg(
2946  "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
2947  'rev-deleted-text-view'
2948  );
2949  }
2950 
2951  if ( !$revision->isCurrent() ) {
2952  $this->mArticle->setOldSubtitle( $revision->getId() );
2953  $wgOut->addWikiMsg( 'editingold' );
2954  }
2955  } elseif ( $this->mTitle->exists() ) {
2956  // Something went wrong
2957 
2958  $wgOut->wrapWikiMsg( "<div class='errorbox'>\n$1\n</div>\n",
2959  [ 'missing-revision', $this->oldid ] );
2960  }
2961  }
2962  }
2963 
2964  if ( wfReadOnly() ) {
2965  $wgOut->wrapWikiMsg(
2966  "<div id=\"mw-read-only-warning\">\n$1\n</div>",
2967  [ 'readonlywarning', wfReadOnlyReason() ]
2968  );
2969  } elseif ( $wgUser->isAnon() ) {
2970  if ( $this->formtype != 'preview' ) {
2971  $wgOut->wrapWikiMsg(
2972  "<div id='mw-anon-edit-warning' class='warningbox'>\n$1\n</div>",
2973  [ 'anoneditwarning',
2974  // Log-in link
2975  SpecialPage::getTitleFor( 'Userlogin' )->getFullURL( [
2976  'returnto' => $this->getTitle()->getPrefixedDBkey()
2977  ] ),
2978  // Sign-up link
2979  SpecialPage::getTitleFor( 'CreateAccount' )->getFullURL( [
2980  'returnto' => $this->getTitle()->getPrefixedDBkey()
2981  ] )
2982  ]
2983  );
2984  } else {
2985  $wgOut->wrapWikiMsg( "<div id=\"mw-anon-preview-warning\" class=\"warningbox\">\n$1</div>",
2986  'anonpreviewwarning'
2987  );
2988  }
2989  } else {
2990  if ( $this->isCssJsSubpage ) {
2991  # Check the skin exists
2992  if ( $this->isWrongCaseCssJsPage ) {
2993  $wgOut->wrapWikiMsg(
2994  "<div class='error' id='mw-userinvalidcssjstitle'>\n$1\n</div>",
2995  [ 'userinvalidcssjstitle', $this->mTitle->getSkinFromCssJsSubpage() ]
2996  );
2997  }
2998  if ( $this->getTitle()->isSubpageOf( $wgUser->getUserPage() ) ) {
2999  $wgOut->wrapWikiMsg( '<div class="mw-usercssjspublic">$1</div>',
3000  $this->isCssSubpage ? 'usercssispublic' : 'userjsispublic'
3001  );
3002  if ( $this->formtype !== 'preview' ) {
3003  if ( $this->isCssSubpage && $wgAllowUserCss ) {
3004  $wgOut->wrapWikiMsg(
3005  "<div id='mw-usercssyoucanpreview'>\n$1\n</div>",
3006  [ 'usercssyoucanpreview' ]
3007  );
3008  }
3009 
3010  if ( $this->isJsSubpage && $wgAllowUserJs ) {
3011  $wgOut->wrapWikiMsg(
3012  "<div id='mw-userjsyoucanpreview'>\n$1\n</div>",
3013  [ 'userjsyoucanpreview' ]
3014  );
3015  }
3016  }
3017  }
3018  }
3019  }
3020 
3021  if ( $this->mTitle->isProtected( 'edit' ) &&
3022  MWNamespace::getRestrictionLevels( $this->mTitle->getNamespace() ) !== [ '' ]
3023  ) {
3024  # Is the title semi-protected?
3025  if ( $this->mTitle->isSemiProtected() ) {
3026  $noticeMsg = 'semiprotectedpagewarning';
3027  } else {
3028  # Then it must be protected based on static groups (regular)
3029  $noticeMsg = 'protectedpagewarning';
3030  }
3031  LogEventsList::showLogExtract( $wgOut, 'protect', $this->mTitle, '',
3032  [ 'lim' => 1, 'msgKey' => [ $noticeMsg ] ] );
3033  }
3034  if ( $this->mTitle->isCascadeProtected() ) {
3035  # Is this page under cascading protection from some source pages?
3036 
3037  list( $cascadeSources, /* $restrictions */ ) = $this->mTitle->getCascadeProtectionSources();
3038  $notice = "<div class='mw-cascadeprotectedwarning'>\n$1\n";
3039  $cascadeSourcesCount = count( $cascadeSources );
3040  if ( $cascadeSourcesCount > 0 ) {
3041  # Explain, and list the titles responsible
3042  foreach ( $cascadeSources as $page ) {
3043  $notice .= '* [[:' . $page->getPrefixedText() . "]]\n";
3044  }
3045  }
3046  $notice .= '</div>';
3047  $wgOut->wrapWikiMsg( $notice, [ 'cascadeprotectedwarning', $cascadeSourcesCount ] );
3048  }
3049  if ( !$this->mTitle->exists() && $this->mTitle->getRestrictions( 'create' ) ) {
3050  LogEventsList::showLogExtract( $wgOut, 'protect', $this->mTitle, '',
3051  [ 'lim' => 1,
3052  'showIfEmpty' => false,
3053  'msgKey' => [ 'titleprotectedwarning' ],
3054  'wrap' => "<div class=\"mw-titleprotectedwarning\">\n$1</div>" ] );
3055  }
3056 
3057  if ( $this->contentLength === false ) {
3058  $this->contentLength = strlen( $this->textbox1 );
3059  }
3060 
3061  if ( $this->tooBig || $this->contentLength > $wgMaxArticleSize * 1024 ) {
3062  $wgOut->wrapWikiMsg( "<div class='error' id='mw-edit-longpageerror'>\n$1\n</div>",
3063  [
3064  'longpageerror',
3065  $wgLang->formatNum( round( $this->contentLength / 1024, 3 ) ),
3066  $wgLang->formatNum( $wgMaxArticleSize )
3067  ]
3068  );
3069  } else {
3070  if ( !$this->context->msg( 'longpage-hint' )->isDisabled() ) {
3071  $wgOut->wrapWikiMsg( "<div id='mw-edit-longpage-hint'>\n$1\n</div>",
3072  [
3073  'longpage-hint',
3074  $wgLang->formatSize( strlen( $this->textbox1 ) ),
3075  strlen( $this->textbox1 )
3076  ]
3077  );
3078  }
3079  }
3080  # Add header copyright warning
3081  $this->showHeaderCopyrightWarning();
3082 
3083  return true;
3084  }
3085 
3100  function getSummaryInput( $summary = "", $labelText = null,
3101  $inputAttrs = null, $spanLabelAttrs = null
3102  ) {
3103  // Note: the maxlength is overridden in JS to 255 and to make it use UTF-8 bytes, not characters.
3104  $inputAttrs = ( is_array( $inputAttrs ) ? $inputAttrs : [] ) + [
3105  'id' => 'wpSummary',
3106  'maxlength' => '200',
3107  'tabindex' => '1',
3108  'size' => 60,
3109  'spellcheck' => 'true',
3110  ] + Linker::tooltipAndAccesskeyAttribs( 'summary' );
3111 
3112  $spanLabelAttrs = ( is_array( $spanLabelAttrs ) ? $spanLabelAttrs : [] ) + [
3113  'class' => $this->missingSummary ? 'mw-summarymissed' : 'mw-summary',
3114  'id' => "wpSummaryLabel"
3115  ];
3116 
3117  $label = null;
3118  if ( $labelText ) {
3119  $label = Xml::tags(
3120  'label',
3121  $inputAttrs['id'] ? [ 'for' => $inputAttrs['id'] ] : null,
3122  $labelText
3123  );
3124  $label = Xml::tags( 'span', $spanLabelAttrs, $label );
3125  }
3126 
3127  $input = Html::input( 'wpSummary', $summary, 'text', $inputAttrs );
3128 
3129  return [ $label, $input ];
3130  }
3131 
3138  protected function showSummaryInput( $isSubjectPreview, $summary = "" ) {
3139  global $wgOut;
3140  # Add a class if 'missingsummary' is triggered to allow styling of the summary line
3141  $summaryClass = $this->missingSummary ? 'mw-summarymissed' : 'mw-summary';
3142  if ( $isSubjectPreview ) {
3143  if ( $this->nosummary ) {
3144  return;
3145  }
3146  } else {
3147  if ( !$this->mShowSummaryField ) {
3148  return;
3149  }
3150  }
3151  $labelText = $this->context->msg( $isSubjectPreview ? 'subject' : 'summary' )->parse();
3152  list( $label, $input ) = $this->getSummaryInput(
3153  $summary,
3154  $labelText,
3155  [ 'class' => $summaryClass ],
3156  []
3157  );
3158  $wgOut->addHTML( "{$label} {$input}" );
3159  }
3160 
3168  protected function getSummaryPreview( $isSubjectPreview, $summary = "" ) {
3169  // avoid spaces in preview, gets always trimmed on save
3170  $summary = trim( $summary );
3171  if ( !$summary || ( !$this->preview && !$this->diff ) ) {
3172  return "";
3173  }
3174 
3175  global $wgParser;
3176 
3177  if ( $isSubjectPreview ) {
3178  $summary = $this->context->msg( 'newsectionsummary' )
3179  ->rawParams( $wgParser->stripSectionName( $summary ) )
3180  ->inContentLanguage()->text();
3181  }
3182 
3183  $message = $isSubjectPreview ? 'subject-preview' : 'summary-preview';
3184 
3185  $summary = $this->context->msg( $message )->parse()
3186  . Linker::commentBlock( $summary, $this->mTitle, $isSubjectPreview );
3187  return Xml::tags( 'div', [ 'class' => 'mw-summary-preview' ], $summary );
3188  }
3189 
3190  protected function showFormBeforeText() {
3191  global $wgOut;
3192  $section = htmlspecialchars( $this->section );
3193  $wgOut->addHTML( <<<HTML
3194 <input type='hidden' value="{$section}" name="wpSection"/>
3195 <input type='hidden' value="{$this->starttime}" name="wpStarttime" />
3196 <input type='hidden' value="{$this->edittime}" name="wpEdittime" />
3197 <input type='hidden' value="{$this->editRevId}" name="editRevId" />
3198 <input type='hidden' value="{$this->scrolltop}" name="wpScrolltop" id="wpScrolltop" />
3199 
3200 HTML
3201  );
3202  if ( !$this->checkUnicodeCompliantBrowser() ) {
3203  $wgOut->addHTML( Html::hidden( 'safemode', '1' ) );
3204  }
3205  }
3206 
3207  protected function showFormAfterText() {
3221  $wgOut->addHTML( "\n" . Html::hidden( "wpEditToken", $wgUser->getEditToken() ) . "\n" );
3222  }
3223 
3232  protected function showContentForm() {
3233  $this->showTextbox1();
3234  }
3235 
3244  protected function showTextbox1( $customAttribs = null, $textoverride = null ) {
3245  if ( $this->wasDeletedSinceLastEdit() && $this->formtype == 'save' ) {
3246  $attribs = [ 'style' => 'display:none;' ];
3247  } else {
3248  $classes = []; // Textarea CSS
3249  if ( $this->mTitle->isProtected( 'edit' ) &&
3250  MWNamespace::getRestrictionLevels( $this->mTitle->getNamespace() ) !== [ '' ]
3251  ) {
3252  # Is the title semi-protected?
3253  if ( $this->mTitle->isSemiProtected() ) {
3254  $classes[] = 'mw-textarea-sprotected';
3255  } else {
3256  # Then it must be protected based on static groups (regular)
3257  $classes[] = 'mw-textarea-protected';
3258  }
3259  # Is the title cascade-protected?
3260  if ( $this->mTitle->isCascadeProtected() ) {
3261  $classes[] = 'mw-textarea-cprotected';
3262  }
3263  }
3264 
3265  $attribs = [ 'tabindex' => 1 ];
3266 
3267  if ( is_array( $customAttribs ) ) {
3269  }
3270 
3271  if ( count( $classes ) ) {
3272  if ( isset( $attribs['class'] ) ) {
3273  $classes[] = $attribs['class'];
3274  }
3275  $attribs['class'] = implode( ' ', $classes );
3276  }
3277  }
3278 
3279  $this->showTextbox(
3280  $textoverride !== null ? $textoverride : $this->textbox1,
3281  'wpTextbox1',
3282  $attribs
3283  );
3284  }
3285 
3286  protected function showTextbox2() {
3287  $this->showTextbox( $this->textbox2, 'wpTextbox2', [ 'tabindex' => 6, 'readonly' ] );
3288  }
3289 
3290  protected function showTextbox( $text, $name, $customAttribs = [] ) {
3292 
3293  $wikitext = $this->safeUnicodeOutput( $text );
3294  if ( strval( $wikitext ) !== '' ) {
3295  // Ensure there's a newline at the end, otherwise adding lines
3296  // is awkward.
3297  // But don't add a newline if the ext is empty, or Firefox in XHTML
3298  // mode will show an extra newline. A bit annoying.
3299  $wikitext .= "\n";
3300  }
3301 
3302  $attribs = $customAttribs + [
3303  'accesskey' => ',',
3304  'id' => $name,
3305  'cols' => $wgUser->getIntOption( 'cols' ),
3306  'rows' => $wgUser->getIntOption( 'rows' ),
3307  // Avoid PHP notices when appending preferences
3308  // (appending allows customAttribs['style'] to still work).
3309  'style' => ''
3310  ];
3311 
3312  // The following classes can be used here:
3313  // * mw-editfont-default
3314  // * mw-editfont-monospace
3315  // * mw-editfont-sans-serif
3316  // * mw-editfont-serif
3317  $class = 'mw-editfont-' . $wgUser->getOption( 'editfont' );
3318 
3319  if ( isset( $attribs['class'] ) ) {
3320  if ( is_string( $attribs['class'] ) ) {
3321  $attribs['class'] .= ' ' . $class;
3322  } elseif ( is_array( $attribs['class'] ) ) {
3323  $attribs['class'][] = $class;
3324  }
3325  } else {
3326  $attribs['class'] = $class;
3327  }
3328 
3329  $pageLang = $this->mTitle->getPageLanguage();
3330  $attribs['lang'] = $pageLang->getHtmlCode();
3331  $attribs['dir'] = $pageLang->getDir();
3332 
3333  $wgOut->addHTML( Html::textarea( $name, $wikitext, $attribs ) );
3334  }
3335 
3336  protected function displayPreviewArea( $previewOutput, $isOnTop = false ) {
3337  global $wgOut;
3338  $classes = [];
3339  if ( $isOnTop ) {
3340  $classes[] = 'ontop';
3341  }
3342 
3343  $attribs = [ 'id' => 'wikiPreview', 'class' => implode( ' ', $classes ) ];
3344 
3345  if ( $this->formtype != 'preview' ) {
3346  $attribs['style'] = 'display: none;';
3347  }
3348 
3349  $wgOut->addHTML( Xml::openElement( 'div', $attribs ) );
3350 
3351  if ( $this->formtype == 'preview' ) {
3352  $this->showPreview( $previewOutput );
3353  } else {
3354  // Empty content container for LivePreview
3355  $pageViewLang = $this->mTitle->getPageViewLanguage();
3356  $attribs = [ 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(),
3357  'class' => 'mw-content-' . $pageViewLang->getDir() ];
3358  $wgOut->addHTML( Html::rawElement( 'div', $attribs ) );
3359  }
3360 
3361  $wgOut->addHTML( '</div>' );
3362 
3363  if ( $this->formtype == 'diff' ) {
3364  try {
3365  $this->showDiff();
3366  } catch ( MWContentSerializationException $ex ) {
3367  $msg = $this->context->msg(
3368  'content-failed-to-parse',
3369  $this->contentModel,
3370  $this->contentFormat,
3371  $ex->getMessage()
3372  );
3373  $wgOut->addWikiText( '<div class="error">' . $msg->text() . '</div>' );
3374  }
3375  }
3376  }
3377 
3384  protected function showPreview( $text ) {
3385  global $wgOut;
3386  if ( $this->mTitle->getNamespace() == NS_CATEGORY ) {
3387  $this->mArticle->openShowCategory();
3388  }
3389  # This hook seems slightly odd here, but makes things more
3390  # consistent for extensions.
3391  Hooks::run( 'OutputPageBeforeHTML', [ &$wgOut, &$text ] );
3392  $wgOut->addHTML( $text );
3393  if ( $this->mTitle->getNamespace() == NS_CATEGORY ) {
3394  $this->mArticle->closeShowCategory();
3395  }
3396  }
3397 
3405  function showDiff() {
3407 
3408  $oldtitlemsg = 'currentrev';
3409  # if message does not exist, show diff against the preloaded default
3410  if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && !$this->mTitle->exists() ) {
3411  $oldtext = $this->mTitle->getDefaultMessageText();
3412  if ( $oldtext !== false ) {
3413  $oldtitlemsg = 'defaultmessagetext';
3414  $oldContent = $this->toEditContent( $oldtext );
3415  } else {
3416  $oldContent = null;
3417  }
3418  } else {
3419  $oldContent = $this->getCurrentContent();
3420  }
3421 
3422  $textboxContent = $this->toEditContent( $this->textbox1 );
3423  if ( $this->editRevId !== null ) {
3424  $newContent = $this->page->replaceSectionAtRev(
3425  $this->section, $textboxContent, $this->summary, $this->editRevId
3426  );
3427  } else {
3428  $newContent = $this->page->replaceSectionContent(
3429  $this->section, $textboxContent, $this->summary, $this->edittime
3430  );
3431  }
3432 
3433  if ( $newContent ) {
3434  ContentHandler::runLegacyHooks( 'EditPageGetDiffText', [ $this, &$newContent ], '1.21' );
3435  Hooks::run( 'EditPageGetDiffContent', [ $this, &$newContent ] );
3436 
3437  $popts = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang );
3438  $newContent = $newContent->preSaveTransform( $this->mTitle, $wgUser, $popts );
3439  }
3440 
3441  if ( ( $oldContent && !$oldContent->isEmpty() ) || ( $newContent && !$newContent->isEmpty() ) ) {
3442  $oldtitle = $this->context->msg( $oldtitlemsg )->parse();
3443  $newtitle = $this->context->msg( 'yourtext' )->parse();
3444 
3445  if ( !$oldContent ) {
3446  $oldContent = $newContent->getContentHandler()->makeEmptyContent();
3447  }
3448 
3449  if ( !$newContent ) {
3450  $newContent = $oldContent->getContentHandler()->makeEmptyContent();
3451  }
3452 
3453  $de = $oldContent->getContentHandler()->createDifferenceEngine( $this->mArticle->getContext() );
3454  $de->setContent( $oldContent, $newContent );
3455 
3456  $difftext = $de->getDiff( $oldtitle, $newtitle );
3457  $de->showDiffStyle();
3458  } else {
3459  $difftext = '';
3460  }
3461 
3462  $wgOut->addHTML( '<div id="wikiDiff">' . $difftext . '</div>' );
3463  }
3464 
3468  protected function showHeaderCopyrightWarning() {
3469  $msg = 'editpage-head-copy-warn';
3470  if ( !$this->context->msg( $msg )->isDisabled() ) {
3471  global $wgOut;
3472  $wgOut->wrapWikiMsg( "<div class='editpage-head-copywarn'>\n$1\n</div>",
3473  'editpage-head-copy-warn' );
3474  }
3475  }
3476 
3485  protected function showTosSummary() {
3486  $msg = 'editpage-tos-summary';
3487  Hooks::run( 'EditPageTosSummary', [ $this->mTitle, &$msg ] );
3488  if ( !$this->context->msg( $msg )->isDisabled() ) {
3489  global $wgOut;
3490  $wgOut->addHTML( '<div class="mw-tos-summary">' );
3491  $wgOut->addWikiMsg( $msg );
3492  $wgOut->addHTML( '</div>' );
3493  }
3494  }
3495 
3496  protected function showEditTools() {
3497  global $wgOut;
3498  $wgOut->addHTML( '<div class="mw-editTools">' .
3499  $this->context->msg( 'edittools' )->inContentLanguage()->parse() .
3500  '</div>' );
3501  }
3502 
3509  protected function getCopywarn() {
3510  return self::getCopyrightWarning( $this->mTitle );
3511  }
3512 
3520  public static function getCopyrightWarning( $title, $format = 'plain', $langcode = null ) {
3521  global $wgRightsText;
3522  if ( $wgRightsText ) {
3523  $copywarnMsg = [ 'copyrightwarning',
3524  '[[' . wfMessage( 'copyrightpage' )->inContentLanguage()->text() . ']]',
3525  $wgRightsText ];
3526  } else {
3527  $copywarnMsg = [ 'copyrightwarning2',
3528  '[[' . wfMessage( 'copyrightpage' )->inContentLanguage()->text() . ']]' ];
3529  }
3530  // Allow for site and per-namespace customization of contribution/copyright notice.
3531  Hooks::run( 'EditPageCopyrightWarning', [ $title, &$copywarnMsg ] );
3532 
3533  $msg = call_user_func_array( 'wfMessage', $copywarnMsg )->title( $title );
3534  if ( $langcode ) {
3535  $msg->inLanguage( $langcode );
3536  }
3537  return "<div id=\"editpage-copywarn\">\n" .
3538  $msg->$format() . "\n</div>";
3539  }
3540 
3548  public static function getPreviewLimitReport( $output ) {
3549  if ( !$output || !$output->getLimitReportData() ) {
3550  return '';
3551  }
3552 
3553  $limitReport = Html::rawElement( 'div', [ 'class' => 'mw-limitReportExplanation' ],
3554  wfMessage( 'limitreport-title' )->parseAsBlock()
3555  );
3556 
3557  // Show/hide animation doesn't work correctly on a table, so wrap it in a div.
3558  $limitReport .= Html::openElement( 'div', [ 'class' => 'preview-limit-report-wrapper' ] );
3559 
3560  $limitReport .= Html::openElement( 'table', [
3561  'class' => 'preview-limit-report wikitable'
3562  ] ) .
3563  Html::openElement( 'tbody' );
3564 
3565  foreach ( $output->getLimitReportData() as $key => $value ) {
3566  if ( Hooks::run( 'ParserLimitReportFormat',
3567  [ $key, &$value, &$limitReport, true, true ]
3568  ) ) {
3569  $keyMsg = wfMessage( $key );
3570  $valueMsg = wfMessage( [ "$key-value-html", "$key-value" ] );
3571  if ( !$valueMsg->exists() ) {
3572  $valueMsg = new RawMessage( '$1' );
3573  }
3574  if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) {
3575  $limitReport .= Html::openElement( 'tr' ) .
3576  Html::rawElement( 'th', null, $keyMsg->parse() ) .
3577  Html::rawElement( 'td', null, $valueMsg->params( $value )->parse() ) .
3578  Html::closeElement( 'tr' );
3579  }
3580  }
3581  }
3582 
3583  $limitReport .= Html::closeElement( 'tbody' ) .
3584  Html::closeElement( 'table' ) .
3585  Html::closeElement( 'div' );
3586 
3587  return $limitReport;
3588  }
3589 
3590  protected function showStandardInputs( &$tabindex = 2 ) {
3591  global $wgOut;
3592  $wgOut->addHTML( "<div class='editOptions'>\n" );
3593 
3594  if ( $this->section != 'new' ) {
3595  $this->showSummaryInput( false, $this->summary );
3596  $wgOut->addHTML( $this->getSummaryPreview( false, $this->summary ) );
3597  }
3598 
3599  $checkboxes = $this->getCheckboxes( $tabindex,
3600  [ 'minor' => $this->minoredit, 'watch' => $this->watchthis ] );
3601  $wgOut->addHTML( "<div class='editCheckboxes'>" . implode( $checkboxes, "\n" ) . "</div>\n" );
3602 
3603  // Show copyright warning.
3604  $wgOut->addWikiText( $this->getCopywarn() );
3605  $wgOut->addHTML( $this->editFormTextAfterWarn );
3606 
3607  $wgOut->addHTML( "<div class='editButtons'>\n" );
3608  $wgOut->addHTML( implode( $this->getEditButtons( $tabindex ), "\n" ) . "\n" );
3609 
3610  $cancel = $this->getCancelLink();
3611  if ( $cancel !== '' ) {
3612  $cancel .= Html::element( 'span',
3613  [ 'class' => 'mw-editButtons-pipe-separator' ],
3614  $this->context->msg( 'pipe-separator' )->text() );
3615  }
3616 
3617  $message = $this->context->msg( 'edithelppage' )->inContentLanguage()->text();
3618  $edithelpurl = Skin::makeInternalOrExternalUrl( $message );
3619  $attrs = [
3620  'target' => 'helpwindow',
3621  'href' => $edithelpurl,
3622  ];
3623  $edithelp = Html::linkButton( $this->context->msg( 'edithelp' )->text(),
3624  $attrs, [ 'mw-ui-quiet' ] ) .
3625  $this->context->msg( 'word-separator' )->escaped() .
3626  $this->context->msg( 'newwindow' )->parse();
3627 
3628  $wgOut->addHTML( " <span class='cancelLink'>{$cancel}</span>\n" );
3629  $wgOut->addHTML( " <span class='editHelp'>{$edithelp}</span>\n" );
3630  $wgOut->addHTML( "</div><!-- editButtons -->\n" );
3631 
3632  Hooks::run( 'EditPage::showStandardInputs:options', [ $this, $wgOut, &$tabindex ] );
3633 
3634  $wgOut->addHTML( "</div><!-- editOptions -->\n" );
3635  }
3636 
3641  protected function showConflict() {
3642  global $wgOut;
3643 
3644  if ( Hooks::run( 'EditPageBeforeConflictDiff', [ &$this, &$wgOut ] ) ) {
3645  $stats = $wgOut->getContext()->getStats();
3646  $stats->increment( 'edit.failures.conflict' );
3647  // Only include 'standard' namespaces to avoid creating unknown numbers of statsd metrics
3648  if (
3649  $this->mTitle->getNamespace() >= NS_MAIN &&
3650  $this->mTitle->getNamespace() <= NS_CATEGORY_TALK
3651  ) {
3652  $stats->increment( 'edit.failures.conflict.byNamespaceId.' . $this->mTitle->getNamespace() );
3653  }
3654 
3655  $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
3656 
3657  $content1 = $this->toEditContent( $this->textbox1 );
3658  $content2 = $this->toEditContent( $this->textbox2 );
3659 
3660  $handler = ContentHandler::getForModelID( $this->contentModel );
3661  $de = $handler->createDifferenceEngine( $this->mArticle->getContext() );
3662  $de->setContent( $content2, $content1 );
3663  $de->showDiff(
3664  $this->context->msg( 'yourtext' )->parse(),
3665  $this->context->msg( 'storedversion' )->text()
3666  );
3667 
3668  $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
3669  $this->showTextbox2();
3670  }
3671  }
3672 
3676  public function getCancelLink() {
3677  $cancelParams = [];
3678  if ( !$this->isConflict && $this->oldid > 0 ) {
3679  $cancelParams['oldid'] = $this->oldid;
3680  } elseif ( $this->getContextTitle()->isRedirect() ) {
3681  $cancelParams['redirect'] = 'no';
3682  }
3683  $attrs = [ 'id' => 'mw-editform-cancel' ];
3684 
3685  return Linker::linkKnown(
3686  $this->getContextTitle(),
3687  $this->context->msg( 'cancel' )->parse(),
3688  Html::buttonAttributes( $attrs, [ 'mw-ui-quiet' ] ),
3689  $cancelParams
3690  );
3691  }
3692 
3702  protected function getActionURL( Title $title ) {
3703  return $title->getLocalURL( [ 'action' => $this->action ] );
3704  }
3705 
3713  protected function wasDeletedSinceLastEdit() {
3714  if ( $this->deletedSinceEdit !== null ) {
3715  return $this->deletedSinceEdit;
3716  }
3717 
3718  $this->deletedSinceEdit = false;
3719 
3720  if ( !$this->mTitle->exists() && $this->mTitle->isDeletedQuick() ) {
3721  $this->lastDelete = $this->getLastDelete();
3722  if ( $this->lastDelete ) {
3723  $deleteTime = wfTimestamp( TS_MW, $this->lastDelete->log_timestamp );
3724  if ( $deleteTime > $this->starttime ) {
3725  $this->deletedSinceEdit = true;
3726  }
3727  }
3728  }
3729 
3730  return $this->deletedSinceEdit;
3731  }
3732 
3736  protected function getLastDelete() {
3737  $dbr = wfGetDB( DB_REPLICA );
3738  $data = $dbr->selectRow(
3739  [ 'logging', 'user' ],
3740  [
3741  'log_type',
3742  'log_action',
3743  'log_timestamp',
3744  'log_user',
3745  'log_namespace',
3746  'log_title',
3747  'log_comment',
3748  'log_params',
3749  'log_deleted',
3750  'user_name'
3751  ], [
3752  'log_namespace' => $this->mTitle->getNamespace(),
3753  'log_title' => $this->mTitle->getDBkey(),
3754  'log_type' => 'delete',
3755  'log_action' => 'delete',
3756  'user_id=log_user'
3757  ],
3758  __METHOD__,
3759  [ 'LIMIT' => 1, 'ORDER BY' => 'log_timestamp DESC' ]
3760  );
3761  // Quick paranoid permission checks...
3762  if ( is_object( $data ) ) {
3763  if ( $data->log_deleted & LogPage::DELETED_USER ) {
3764  $data->user_name = $this->context->msg( 'rev-deleted-user' )->escaped();
3765  }
3766 
3767  if ( $data->log_deleted & LogPage::DELETED_COMMENT ) {
3768  $data->log_comment = $this->context->msg( 'rev-deleted-comment' )->escaped();
3769  }
3770  }
3771 
3772  return $data;
3773  }
3774 
3780  function getPreviewText() {
3781  global $wgOut, $wgRawHtml, $wgLang;
3783 
3784  $stats = $wgOut->getContext()->getStats();
3785 
3786  if ( $wgRawHtml && !$this->mTokenOk ) {
3787  // Could be an offsite preview attempt. This is very unsafe if
3788  // HTML is enabled, as it could be an attack.
3789  $parsedNote = '';
3790  if ( $this->textbox1 !== '' ) {
3791  // Do not put big scary notice, if previewing the empty
3792  // string, which happens when you initially edit
3793  // a category page, due to automatic preview-on-open.
3794  $parsedNote = $wgOut->parse( "<div class='previewnote'>" .
3795  $this->context->msg( 'session_fail_preview_html' )->text() . "</div>",
3796  true, /* interface */true );
3797  }
3798  $stats->increment( 'edit.failures.session_loss' );
3799  return $parsedNote;
3800  }
3801 
3802  $note = '';
3803 
3804  try {
3805  $content = $this->toEditContent( $this->textbox1 );
3806 
3807  $previewHTML = '';
3808  if ( !Hooks::run(
3809  'AlternateEditPreview',
3810  [ $this, &$content, &$previewHTML, &$this->mParserOutput ] )
3811  ) {
3812  return $previewHTML;
3813  }
3814 
3815  # provide a anchor link to the editform
3816  $continueEditing = '<span class="mw-continue-editing">' .
3817  '[[#' . self::EDITFORM_ID . '|' . $wgLang->getArrow() . ' ' .
3818  $this->context->msg( 'continue-editing' )->text() . ']]</span>';
3819  if ( $this->mTriedSave && !$this->mTokenOk ) {
3820  if ( $this->mTokenOkExceptSuffix ) {
3821  $note = $this->context->msg( 'token_suffix_mismatch' )->plain();
3822  $stats->increment( 'edit.failures.bad_token' );
3823  } else {
3824  $note = $this->context->msg( 'session_fail_preview' )->plain();
3825  $stats->increment( 'edit.failures.session_loss' );
3826  }
3827  } elseif ( $this->incompleteForm ) {
3828  $note = $this->context->msg( 'edit_form_incomplete' )->plain();
3829  if ( $this->mTriedSave ) {
3830  $stats->increment( 'edit.failures.incomplete_form' );
3831  }
3832  } else {
3833  $note = $this->context->msg( 'previewnote' )->plain() . ' ' . $continueEditing;
3834  }
3835 
3836  # don't parse non-wikitext pages, show message about preview
3837  if ( $this->mTitle->isCssJsSubpage() || $this->mTitle->isCssOrJsPage() ) {
3838  if ( $this->mTitle->isCssJsSubpage() ) {
3839  $level = 'user';
3840  } elseif ( $this->mTitle->isCssOrJsPage() ) {
3841  $level = 'site';
3842  } else {
3843  $level = false;
3844  }
3845 
3846  if ( $content->getModel() == CONTENT_MODEL_CSS ) {
3847  $format = 'css';
3848  if ( $level === 'user' && !$wgAllowUserCss ) {
3849  $format = false;
3850  }
3851  } elseif ( $content->getModel() == CONTENT_MODEL_JAVASCRIPT ) {
3852  $format = 'js';
3853  if ( $level === 'user' && !$wgAllowUserJs ) {
3854  $format = false;
3855  }
3856  } else {
3857  $format = false;
3858  }
3859 
3860  # Used messages to make sure grep find them:
3861  # Messages: usercsspreview, userjspreview, sitecsspreview, sitejspreview
3862  if ( $level && $format ) {
3863  $note = "<div id='mw-{$level}{$format}preview'>" .
3864  $this->context->msg( "{$level}{$format}preview" )->text() .
3865  ' ' . $continueEditing . "</div>";
3866  }
3867  }
3868 
3869  # If we're adding a comment, we need to show the
3870  # summary as the headline
3871  if ( $this->section === "new" && $this->summary !== "" ) {
3872  $content = $content->addSectionHeader( $this->summary );
3873  }
3874 
3875  $hook_args = [ $this, &$content ];
3876  ContentHandler::runLegacyHooks( 'EditPageGetPreviewText', $hook_args, '1.25' );
3877  Hooks::run( 'EditPageGetPreviewContent', $hook_args );
3878 
3879  $parserResult = $this->doPreviewParse( $content );
3880  $parserOutput = $parserResult['parserOutput'];
3881  $previewHTML = $parserResult['html'];
3882  $this->mParserOutput = $parserOutput;
3883  $wgOut->addParserOutputMetadata( $parserOutput );
3884 
3885  if ( count( $parserOutput->getWarnings() ) ) {
3886  $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
3887  }
3888 
3889  } catch ( MWContentSerializationException $ex ) {
3890  $m = $this->context->msg(
3891  'content-failed-to-parse',
3892  $this->contentModel,
3893  $this->contentFormat,
3894  $ex->getMessage()
3895  );
3896  $note .= "\n\n" . $m->parse();
3897  $previewHTML = '';
3898  }
3899 
3900  if ( $this->isConflict ) {
3901  $conflict = '<h2 id="mw-previewconflict">'
3902  . $this->context->msg( 'previewconflict' )->escaped() . "</h2>\n";
3903  } else {
3904  $conflict = '<hr />';
3905  }
3906 
3907  $previewhead = "<div class='previewnote'>\n" .
3908  '<h2 id="mw-previewheader">' . $this->context->msg( 'preview' )->escaped() . "</h2>" .
3909  $wgOut->parse( $note, true, /* interface */true ) . $conflict . "</div>\n";
3910 
3911  $pageViewLang = $this->mTitle->getPageViewLanguage();
3912  $attribs = [ 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(),
3913  'class' => 'mw-content-' . $pageViewLang->getDir() ];
3914  $previewHTML = Html::rawElement( 'div', $attribs, $previewHTML );
3915 
3916  return $previewhead . $previewHTML . $this->previewTextAfterContent;
3917  }
3918 
3923  protected function getPreviewParserOptions() {
3924  $parserOptions = $this->page->makeParserOptions( $this->mArticle->getContext() );
3925  $parserOptions->setIsPreview( true );
3926  $parserOptions->setIsSectionPreview( !is_null( $this->section ) && $this->section !== '' );
3927  $parserOptions->enableLimitReport();
3928  return $parserOptions;
3929  }
3930 
3940  protected function doPreviewParse( Content $content ) {
3941  global $wgUser;
3942  $parserOptions = $this->getPreviewParserOptions();
3943  $pstContent = $content->preSaveTransform( $this->mTitle, $wgUser, $parserOptions );
3944  $scopedCallback = $parserOptions->setupFakeRevision(
3945  $this->mTitle, $pstContent, $wgUser );
3946  $parserOutput = $pstContent->getParserOutput( $this->mTitle, null, $parserOptions );
3947  ScopedCallback::consume( $scopedCallback );
3948  $parserOutput->setEditSectionTokens( false ); // no section edit links
3949  return [
3950  'parserOutput' => $parserOutput,
3951  'html' => $parserOutput->getText() ];
3952  }
3953 
3957  function getTemplates() {
3958  if ( $this->preview || $this->section != '' ) {
3959  $templates = [];
3960  if ( !isset( $this->mParserOutput ) ) {
3961  return $templates;
3962  }
3963  foreach ( $this->mParserOutput->getTemplates() as $ns => $template ) {
3964  foreach ( array_keys( $template ) as $dbk ) {
3965  $templates[] = Title::makeTitle( $ns, $dbk );
3966  }
3967  }
3968  return $templates;
3969  } else {
3970  return $this->mTitle->getTemplateLinksFrom();
3971  }
3972  }
3973 
3981  static function getEditToolbar( $title = null ) {
3984 
3985  $imagesAvailable = $wgEnableUploads || count( $wgForeignFileRepos );
3986  $showSignature = true;
3987  if ( $title ) {
3988  $showSignature = MWNamespace::wantSignatures( $title->getNamespace() );
3989  }
3990 
4000  $toolarray = [
4001  [
4002  'id' => 'mw-editbutton-bold',
4003  'open' => '\'\'\'',
4004  'close' => '\'\'\'',
4005  'sample' => wfMessage( 'bold_sample' )->text(),
4006  'tip' => wfMessage( 'bold_tip' )->text(),
4007  ],
4008  [
4009  'id' => 'mw-editbutton-italic',
4010  'open' => '\'\'',
4011  'close' => '\'\'',
4012  'sample' => wfMessage( 'italic_sample' )->text(),
4013  'tip' => wfMessage( 'italic_tip' )->text(),
4014  ],
4015  [
4016  'id' => 'mw-editbutton-link',
4017  'open' => '[[',
4018  'close' => ']]',
4019  'sample' => wfMessage( 'link_sample' )->text(),
4020  'tip' => wfMessage( 'link_tip' )->text(),
4021  ],
4022  [
4023  'id' => 'mw-editbutton-extlink',
4024  'open' => '[',
4025  'close' => ']',
4026  'sample' => wfMessage( 'extlink_sample' )->text(),
4027  'tip' => wfMessage( 'extlink_tip' )->text(),
4028  ],
4029  [
4030  'id' => 'mw-editbutton-headline',
4031  'open' => "\n== ",
4032  'close' => " ==\n",
4033  'sample' => wfMessage( 'headline_sample' )->text(),
4034  'tip' => wfMessage( 'headline_tip' )->text(),
4035  ],
4036  $imagesAvailable ? [
4037  'id' => 'mw-editbutton-image',
4038  'open' => '[[' . $wgContLang->getNsText( NS_FILE ) . ':',
4039  'close' => ']]',
4040  'sample' => wfMessage( 'image_sample' )->text(),
4041  'tip' => wfMessage( 'image_tip' )->text(),
4042  ] : false,
4043  $imagesAvailable ? [
4044  'id' => 'mw-editbutton-media',
4045  'open' => '[[' . $wgContLang->getNsText( NS_MEDIA ) . ':',
4046  'close' => ']]',
4047  'sample' => wfMessage( 'media_sample' )->text(),
4048  'tip' => wfMessage( 'media_tip' )->text(),
4049  ] : false,
4050  [
4051  'id' => 'mw-editbutton-nowiki',
4052  'open' => "<nowiki>",
4053  'close' => "</nowiki>",
4054  'sample' => wfMessage( 'nowiki_sample' )->text(),
4055  'tip' => wfMessage( 'nowiki_tip' )->text(),
4056  ],
4057  $showSignature ? [
4058  'id' => 'mw-editbutton-signature',
4059  'open' => wfMessage( 'sig-text', '~~~~' )->inContentLanguage()->text(),
4060  'close' => '',
4061  'sample' => '',
4062  'tip' => wfMessage( 'sig_tip' )->text(),
4063  ] : false,
4064  [
4065  'id' => 'mw-editbutton-hr',
4066  'open' => "\n----\n",
4067  'close' => '',
4068  'sample' => '',
4069  'tip' => wfMessage( 'hr_tip' )->text(),
4070  ]
4071  ];
4072 
4073  $script = 'mw.loader.using("mediawiki.toolbar", function () {';
4074  foreach ( $toolarray as $tool ) {
4075  if ( !$tool ) {
4076  continue;
4077  }
4078 
4079  $params = [
4080  // Images are defined in ResourceLoaderEditToolbarModule
4081  false,
4082  // Note that we use the tip both for the ALT tag and the TITLE tag of the image.
4083  // Older browsers show a "speedtip" type message only for ALT.
4084  // Ideally these should be different, realistically they
4085  // probably don't need to be.
4086  $tool['tip'],
4087  $tool['open'],
4088  $tool['close'],
4089  $tool['sample'],
4090  $tool['id'],
4091  ];
4092 
4093  $script .= Xml::encodeJsCall(
4094  'mw.toolbar.addButton',
4095  $params,
4097  );
4098  }
4099 
4100  $script .= '});';
4101  $wgOut->addScript( ResourceLoader::makeInlineScript( $script ) );
4102 
4103  $toolbar = '<div id="toolbar"></div>';
4104 
4105  Hooks::run( 'EditPageBeforeEditToolbar', [ &$toolbar ] );
4106 
4107  return $toolbar;
4108  }
4109 
4120  public function getCheckboxes( &$tabindex, $checked ) {
4122 
4123  $checkboxes = [];
4124 
4125  // don't show the minor edit checkbox if it's a new page or section
4126  if ( !$this->isNew ) {
4127  $checkboxes['minor'] = '';
4128  $minorLabel = $this->context->msg( 'minoredit' )->parse();
4129  if ( $wgUser->isAllowed( 'minoredit' ) ) {
4130  $attribs = [
4131  'tabindex' => ++$tabindex,
4132  'accesskey' => $this->context->msg( 'accesskey-minoredit' )->text(),
4133  'id' => 'wpMinoredit',
4134  ];
4135  $minorEditHtml =
4136  Xml::check( 'wpMinoredit', $checked['minor'], $attribs ) .
4137  "&#160;<label for='wpMinoredit' id='mw-editpage-minoredit'" .
4138  Xml::expandAttributes( [ 'title' => Linker::titleAttrib( 'minoredit', 'withaccess' ) ] ) .
4139  ">{$minorLabel}</label>";
4140 
4141  if ( $wgUseMediaWikiUIEverywhere ) {
4142  $checkboxes['minor'] = Html::openElement( 'div', [ 'class' => 'mw-ui-checkbox' ] ) .
4143  $minorEditHtml .
4144  Html::closeElement( 'div' );
4145  } else {
4146  $checkboxes['minor'] = $minorEditHtml;
4147  }
4148  }
4149  }
4150 
4151  $watchLabel = $this->context->msg( 'watchthis' )->parse();
4152  $checkboxes['watch'] = '';
4153  if ( $wgUser->isLoggedIn() ) {
4154  $attribs = [
4155  'tabindex' => ++$tabindex,
4156  'accesskey' => $this->context->msg( 'accesskey-watch' )->text(),
4157  'id' => 'wpWatchthis',
4158  ];
4159  $watchThisHtml =
4160  Xml::check( 'wpWatchthis', $checked['watch'], $attribs ) .
4161  "&#160;<label for='wpWatchthis' id='mw-editpage-watch'" .
4162  Xml::expandAttributes( [ 'title' => Linker::titleAttrib( 'watch', 'withaccess' ) ] ) .
4163  ">{$watchLabel}</label>";
4164  if ( $wgUseMediaWikiUIEverywhere ) {
4165  $checkboxes['watch'] = Html::openElement( 'div', [ 'class' => 'mw-ui-checkbox' ] ) .
4166  $watchThisHtml .
4167  Html::closeElement( 'div' );
4168  } else {
4169  $checkboxes['watch'] = $watchThisHtml;
4170  }
4171  }
4172  Hooks::run( 'EditPageBeforeEditChecks', [ &$this, &$checkboxes, &$tabindex ] );
4173  return $checkboxes;
4174  }
4175 
4184  public function getEditButtons( &$tabindex ) {
4185  $buttons = [];
4186 
4187  $labelAsPublish =
4188  $this->mArticle->getContext()->getConfig()->get( 'EditSubmitButtonLabelPublish' );
4189 
4190  // Can't use $this->isNew as that's also true if we're adding a new section to an extant page
4191  if ( $labelAsPublish ) {
4192  $buttonLabelKey = !$this->mTitle->exists() ? 'publishpage' : 'publishchanges';
4193  } else {
4194  $buttonLabelKey = !$this->mTitle->exists() ? 'savearticle' : 'savechanges';
4195  }
4196  $buttonLabel = $this->context->msg( $buttonLabelKey )->text();
4197  $attribs = [
4198  'id' => 'wpSave',
4199  'name' => 'wpSave',
4200  'tabindex' => ++$tabindex,
4201  ] + Linker::tooltipAndAccesskeyAttribs( 'save' );
4202  $buttons['save'] = Html::submitButton( $buttonLabel, $attribs, [ 'mw-ui-progressive' ] );
4203 
4204  ++$tabindex; // use the same for preview and live preview
4205  $attribs = [
4206  'id' => 'wpPreview',
4207  'name' => 'wpPreview',
4208  'tabindex' => $tabindex,
4209  ] + Linker::tooltipAndAccesskeyAttribs( 'preview' );
4210  $buttons['preview'] = Html::submitButton( $this->context->msg( 'showpreview' )->text(),
4211  $attribs );
4212  $buttons['live'] = '';
4213 
4214  $attribs = [
4215  'id' => 'wpDiff',
4216  'name' => 'wpDiff',
4217  'tabindex' => ++$tabindex,
4218  ] + Linker::tooltipAndAccesskeyAttribs( 'diff' );
4219  $buttons['diff'] = Html::submitButton( $this->context->msg( 'showdiff' )->text(),
4220  $attribs );
4221 
4222  Hooks::run( 'EditPageBeforeEditButtons', [ &$this, &$buttons, &$tabindex ] );
4223  return $buttons;
4224  }
4225 
4230  function noSuchSectionPage() {
4231  global $wgOut;
4232 
4233  $wgOut->prepareErrorPage( $this->context->msg( 'nosuchsectiontitle' ) );
4234 
4235  $res = $this->context->msg( 'nosuchsectiontext', $this->section )->parseAsBlock();
4236  Hooks::run( 'EditPageNoSuchSection', [ &$this, &$res ] );
4237  $wgOut->addHTML( $res );
4238 
4239  $wgOut->returnToMain( false, $this->mTitle );
4240  }
4241 
4247  public function spamPageWithContent( $match = false ) {
4249  $this->textbox2 = $this->textbox1;
4250 
4251  if ( is_array( $match ) ) {
4252  $match = $wgLang->listToText( $match );
4253  }
4254  $wgOut->prepareErrorPage( $this->context->msg( 'spamprotectiontitle' ) );
4255 
4256  $wgOut->addHTML( '<div id="spamprotected">' );
4257  $wgOut->addWikiMsg( 'spamprotectiontext' );
4258  if ( $match ) {
4259  $wgOut->addWikiMsg( 'spamprotectionmatch', wfEscapeWikiText( $match ) );
4260  }
4261  $wgOut->addHTML( '</div>' );
4262 
4263  $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
4264  $this->showDiff();
4265 
4266  $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
4267  $this->showTextbox2();
4268 
4269  $wgOut->addReturnTo( $this->getContextTitle(), [ 'action' => 'edit' ] );
4270  }
4271 
4278  private function checkUnicodeCompliantBrowser() {
4280 
4281  $currentbrowser = $wgRequest->getHeader( 'User-Agent' );
4282  if ( $currentbrowser === false ) {
4283  // No User-Agent header sent? Trust it by default...
4284  return true;
4285  }
4286 
4287  foreach ( $wgBrowserBlackList as $browser ) {
4288  if ( preg_match( $browser, $currentbrowser ) ) {
4289  return false;
4290  }
4291  }
4292  return true;
4293  }
4294 
4303  protected function safeUnicodeInput( $request, $field ) {
4304  $text = rtrim( $request->getText( $field ) );
4305  return $request->getBool( 'safemode' )
4306  ? $this->unmakeSafe( $text )
4307  : $text;
4308  }
4309 
4317  protected function safeUnicodeOutput( $text ) {
4318  return $this->checkUnicodeCompliantBrowser()
4319  ? $text
4320  : $this->makeSafe( $text );
4321  }
4322 
4335  private function makeSafe( $invalue ) {
4336  // Armor existing references for reversibility.
4337  $invalue = strtr( $invalue, [ "&#x" => "&#x0" ] );
4338 
4339  $bytesleft = 0;
4340  $result = "";
4341  $working = 0;
4342  $valueLength = strlen( $invalue );
4343  for ( $i = 0; $i < $valueLength; $i++ ) {
4344  $bytevalue = ord( $invalue[$i] );
4345  if ( $bytevalue <= 0x7F ) { // 0xxx xxxx
4346  $result .= chr( $bytevalue );
4347  $bytesleft = 0;
4348  } elseif ( $bytevalue <= 0xBF ) { // 10xx xxxx
4349  $working = $working << 6;
4350  $working += ( $bytevalue & 0x3F );
4351  $bytesleft--;
4352  if ( $bytesleft <= 0 ) {
4353  $result .= "&#x" . strtoupper( dechex( $working ) ) . ";";
4354  }
4355  } elseif ( $bytevalue <= 0xDF ) { // 110x xxxx
4356  $working = $bytevalue & 0x1F;
4357  $bytesleft = 1;
4358  } elseif ( $bytevalue <= 0xEF ) { // 1110 xxxx
4359  $working = $bytevalue & 0x0F;
4360  $bytesleft = 2;
4361  } else { // 1111 0xxx
4362  $working = $bytevalue & 0x07;
4363  $bytesleft = 3;
4364  }
4365  }
4366  return $result;
4367  }
4368 
4377  private function unmakeSafe( $invalue ) {
4378  $result = "";
4379  $valueLength = strlen( $invalue );
4380  for ( $i = 0; $i < $valueLength; $i++ ) {
4381  if ( ( substr( $invalue, $i, 3 ) == "&#x" ) && ( $invalue[$i + 3] != '0' ) ) {
4382  $i += 3;
4383  $hexstring = "";
4384  do {
4385  $hexstring .= $invalue[$i];
4386  $i++;
4387  } while ( ctype_xdigit( $invalue[$i] ) && ( $i < strlen( $invalue ) ) );
4388 
4389  // Do some sanity checks. These aren't needed for reversibility,
4390  // but should help keep the breakage down if the editor
4391  // breaks one of the entities whilst editing.
4392  if ( ( substr( $invalue, $i, 1 ) == ";" ) && ( strlen( $hexstring ) <= 6 ) ) {
4393  $codepoint = hexdec( $hexstring );
4394  $result .= UtfNormal\Utils::codepointToUtf8( $codepoint );
4395  } else {
4396  $result .= "&#x" . $hexstring . substr( $invalue, $i, 1 );
4397  }
4398  } else {
4399  $result .= substr( $invalue, $i, 1 );
4400  }
4401  }
4402  // reverse the transform that we made for reversibility reasons.
4403  return strtr( $result, [ "&#x0" => "&#x" ] );
4404  }
4405 }
string $autoSumm
Definition: EditPage.php:287
static newFromName($name, $validate= 'valid')
Static factory method for creation from username.
Definition: User.php:525
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:682
$wgForeignFileRepos
makeSafe($invalue)
A number of web browsers are known to corrupt non-ASCII characters in a UTF-8 text editing environmen...
Definition: EditPage.php:4335
const FOR_THIS_USER
Definition: Revision.php:93
bool $nosummary
Definition: EditPage.php:334
static closeElement($element)
Returns "".
Definition: Html.php:305
$editFormTextBottom
Definition: EditPage.php:384
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:76
wfGetDB($db, $groups=[], $wiki=false)
Get a Database object.
$wgMaxArticleSize
Maximum article size in kilobytes.
bool $missingSummary
Definition: EditPage.php:269
the array() calling protocol came about after MediaWiki 1.4rc1.
bool $bot
Definition: EditPage.php:364
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:1555
string $textbox2
Definition: EditPage.php:328
either a plain
Definition: hooks.txt:1987
bool $mTokenOk
Definition: EditPage.php:251
$editFormTextAfterContent
Definition: EditPage.php:385
showContentForm()
Subpage overridable method for printing the form for page content editing By default this simply outp...
Definition: EditPage.php:3232
bool $allowBlankSummary
Definition: EditPage.php:272
getPreviewText()
Get the rendered text for previewing.
Definition: EditPage.php:3780
serialize($format=null)
Convenience method for serializing this Content object.
bool $isConflict
Definition: EditPage.php:221
int $oldid
Definition: EditPage.php:352
const NS_MAIN
Definition: Defines.php:56
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:1486
string $summary
Definition: EditPage.php:331
processing should stop and the error should be shown to the user * false
Definition: hooks.txt:189
setHeaders()
Definition: EditPage.php:2328
WikiPage $page
Definition: EditPage.php:209
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:97
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
Handles formatting for the "templates used on this page" lists.
if(!$wgDBerrorLogTZ) $wgRequest
Definition: Setup.php:664
const AS_HOOK_ERROR
Status: Article update aborted by a hook function.
Definition: EditPage.php:56
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:2102
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:4303
showTextbox2()
Definition: EditPage.php:3286
bool $tooBig
Definition: EditPage.php:263
$wgParser
Definition: Setup.php:821
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:3468
getPage()
Get the WikiPage object of this instance.
Definition: Article.php:183
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
static expandAttributes($attribs)
Given an array of ('attributename' => 'value'), it generates the code to set the XML attributes : att...
Definition: Xml.php:67
showTosSummary()
Give a chance for site and per-namespace customizations of terms of service summary link that might e...
Definition: EditPage.php:3485
The edit page/HTML interface (split from Article) The actual database and text munging is still in Ar...
Definition: EditPage.php:42
Title $mTitle
Definition: EditPage.php:212
static hidden($name, $value, array $attribs=[])
Convenience function to produce an input element with type=hidden.
Definition: Html.php:758
setContextTitle($title)
Set the context Title object.
Definition: EditPage.php:454
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:119
const AS_HOOK_ERROR_EXPECTED
Status: A hook function returned an error.
Definition: EditPage.php:61
getEditButtons(&$tabindex)
Returns an array of html code of the following buttons: save, diff, preview and live.
Definition: EditPage.php:4184
$comment
$wgAllowUserCss
Allow user Cascading Style Sheets (CSS)? This enables a lot of neat customizations, but may increase security risk to users and server load.
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:1211
string $editintro
Definition: EditPage.php:358
Class for viewing MediaWiki article and history.
Definition: Article.php:34
null for the local wiki Added in
Definition: hooks.txt:1555
static getSkinNames()
Fetch the set of available skins.
Definition: Skin.php:49
bool $allowBlankArticle
Definition: EditPage.php:278
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:78
IContextSource $context
Definition: EditPage.php:409
$value
Article $mArticle
Definition: EditPage.php:207
null string $contentFormat
Definition: EditPage.php:370
const AS_BLOCKED_PAGE_FOR_USER
Status: User is blocked from editing this page.
Definition: EditPage.php:66
bool $blankArticle
Definition: EditPage.php:275
setPostEditCookie($statusValue)
Sets post-edit cookie indicating the user just saved a particular revision.
Definition: EditPage.php:1442
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:103
The First
Definition: primes.txt:1
static getPreviewLimitReport($output)
Get the Limit report for page previews.
Definition: EditPage.php:3548
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:4247
it s the revision text itself In either if gzip is the revision text is gzipped $flags
Definition: hooks.txt:2703
bool $missingComment
Definition: EditPage.php:266
const EDIT_MINOR
Definition: Defines.php:148
static check($name, $checked=false, $attribs=[])
Convenience function to build an HTML checkbox.
Definition: Xml.php:324
const POST_EDIT_COOKIE_DURATION
Duration of PostEdit cookie, in seconds.
Definition: EditPage.php:204
const EDIT_UPDATE
Definition: Defines.php:147
static newFromText($text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition: Title.php:262
this hook is for auditing only $response
Definition: hooks.txt:802
showFormBeforeText()
Definition: EditPage.php:3190
null means default & $customAttribs
Definition: hooks.txt:1936
internalAttemptSave(&$result, $bot=false)
Attempt submission (no UI)
Definition: EditPage.php:1720
bool stdClass $lastDelete
Definition: EditPage.php:248
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.
static newFromUser($user)
Get a ParserOptions object from a given user.
edit()
This is the function that gets called for "action=edit".
Definition: EditPage.php:511
getContextTitle()
Get the context title object.
Definition: EditPage.php:465
bool $mBaseRevision
Definition: EditPage.php:299
getContentObject($def_content=null)
Definition: EditPage.php:1105
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:2237
const DB_MASTER
Definition: defines.php:23
static getRestrictionLevels($index, User $user=null)
Determine which restriction levels it makes sense to use in a namespace, optionally filtered by a use...
static tooltipAndAccesskeyAttribs($name, array $msgParams=[])
Returns the attributes for the tooltip and access key.
Definition: Linker.php:2182
null string $contentModel
Definition: EditPage.php:367
getEditPermissionErrors($rigor= 'secure')
Definition: EditPage.php:641
isRedirect()
Tests if the article content represents a redirect.
Definition: WikiPage.php:481
null Title $mContextTitle
Definition: EditPage.php:215
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.
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:2307
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 '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:Associative array mapping language codes to prefixed links of the form"language:title".&$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:1934
const AS_CONTENT_TOO_BIG
Status: Content too big (> $wgMaxArticleSize)
Definition: EditPage.php:71
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:802
safeUnicodeOutput($text)
Filter an output field through a Unicode armoring process if it is going to an old browser with known...
Definition: EditPage.php:4317
$wgEnableUploads
Uploads have to be specially set up to be secure.
getContext()
Gets the context this Article is executed in.
Definition: Article.php:2034
bool $isWrongCaseCssJsPage
Definition: EditPage.php:233
attemptSave(&$resultDetails=false)
Attempt submission.
Definition: EditPage.php:1465
showIntro()
Show all applicable editing introductions.
Definition: EditPage.php:2386
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:1936
static getLocalizedName($name, Language $lang=null)
Returns the localized name for a given content model.
getArticle()
Definition: EditPage.php:429
bool $isCssSubpage
Definition: EditPage.php:227
bool $watchthis
Definition: EditPage.php:319
$previewTextAfterContent
Definition: EditPage.php:386
static closeElement($element)
Shortcut to close an XML element.
Definition: Xml.php:118
const DELETED_COMMENT
Definition: LogPage.php:34
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:680
static openElement($element, $attribs=[])
Identical to rawElement(), but has no third parameter and omits the end tag (and the self-closing '/'...
Definition: Html.php:247
getParentRevId()
Get the edit's parent revision ID.
Definition: EditPage.php:1273
isWrongCaseCssJsPage()
Checks whether the user entered a skin name in uppercase, e.g.
Definition: EditPage.php:800
getTemplates()
Definition: EditPage.php:3957
wfEscapeWikiText($text)
Escapes the given text so that it may be output using addWikiText() without any linking, formatting, etc.
bool $save
Definition: EditPage.php:307
wfReadOnly()
Check whether the wiki is in read-only mode.
static getMain()
Static methods.
integer $editRevId
Definition: EditPage.php:340
static getCopyrightWarning($title, $format= 'plain', $langcode=null)
Get the copyright warning, by default returns wikitext.
Definition: EditPage.php:3520
static textarea($name, $value= '', array $attribs=[])
Convenience function to produce a