MediaWiki  1.27.1
EditPage.php
Go to the documentation of this file.
1 <?php
38 class EditPage {
42  const AS_SUCCESS_UPDATE = 200;
43 
48 
52  const AS_HOOK_ERROR = 210;
53 
58 
63 
67  const AS_CONTENT_TOO_BIG = 216;
68 
73 
78 
82  const AS_READ_ONLY_PAGE = 220;
83 
87  const AS_RATE_LIMITED = 221;
88 
94 
100 
104  const AS_BLANK_ARTICLE = 224;
105 
109  const AS_CONFLICT_DETECTED = 225;
110 
115  const AS_SUMMARY_NEEDED = 226;
116 
120  const AS_TEXTBOX_EMPTY = 228;
121 
126 
130  const AS_END = 231;
131 
135  const AS_SPAM_ERROR = 232;
136 
141 
146 
152 
157  const AS_SELF_REDIRECT = 236;
158 
163  const AS_CHANGE_TAG_ERROR = 237;
164 
168  const AS_PARSE_ERROR = 240;
169 
175 
179  const EDITFORM_ID = 'editform';
180 
185  const POST_EDIT_COOKIE_KEY_PREFIX = 'PostEditRevision';
186 
201 
203  public $mArticle;
205  private $page;
206 
208  public $mTitle;
209 
211  private $mContextTitle = null;
212 
214  public $action = 'submit';
215 
217  public $isConflict = false;
218 
220  public $isCssJsSubpage = false;
221 
223  public $isCssSubpage = false;
224 
226  public $isJsSubpage = false;
227 
229  public $isWrongCaseCssJsPage = false;
230 
232  public $isNew = false;
233 
236 
238  public $formtype;
239 
241  public $firsttime;
242 
244  public $lastDelete;
245 
247  public $mTokenOk = false;
248 
250  public $mTokenOkExceptSuffix = false;
251 
253  public $mTriedSave = false;
254 
256  public $incompleteForm = false;
257 
259  public $tooBig = false;
260 
262  public $kblength = false;
263 
265  public $missingComment = false;
266 
268  public $missingSummary = false;
269 
271  public $allowBlankSummary = false;
272 
274  protected $blankArticle = false;
275 
277  protected $allowBlankArticle = false;
278 
280  protected $selfRedirect = false;
281 
283  protected $allowSelfRedirect = false;
284 
286  public $autoSumm = '';
287 
289  public $hookError = '';
290 
293 
295  public $hasPresetSummary = false;
296 
298  public $mBaseRevision = false;
299 
301  public $mShowSummaryField = true;
302 
303  # Form values
304 
306  public $save = false;
307 
309  public $preview = false;
310 
312  public $diff = false;
313 
315  public $minoredit = false;
316 
318  public $watchthis = false;
319 
321  public $recreate = false;
322 
324  public $textbox1 = '';
325 
327  public $textbox2 = '';
328 
330  public $summary = '';
331 
333  public $nosummary = false;
334 
336  public $edittime = '';
337 
339  public $section = '';
340 
342  public $sectiontitle = '';
343 
345  public $starttime = '';
346 
348  public $oldid = 0;
349 
351  public $parentRevId = 0;
352 
354  public $editintro = '';
355 
357  public $scrolltop = null;
358 
360  public $bot = true;
361 
363  public $contentModel = null;
364 
366  public $contentFormat = null;
367 
369  private $changeTags = null;
370 
371  # Placeholders for text injection by hooks (must be HTML)
372  # extensions should take care to _append_ to the present value
373 
375  public $editFormPageTop = '';
376  public $editFormTextTop = '';
380  public $editFormTextBottom = '';
383  public $mPreloadContent = null;
384 
385  /* $didSave should be set to true whenever an article was successfully altered. */
386  public $didSave = false;
387  public $undidRev = 0;
388 
389  public $suppressIntro = false;
390 
392  protected $edit;
393 
397  private $enableApiEditOverride = false;
398 
402  public function __construct( Article $article ) {
403  $this->mArticle = $article;
404  $this->page = $article->getPage(); // model object
405  $this->mTitle = $article->getTitle();
406 
407  $this->contentModel = $this->mTitle->getContentModel();
408 
409  $handler = ContentHandler::getForModelID( $this->contentModel );
410  $this->contentFormat = $handler->getDefaultFormat();
411  }
412 
416  public function getArticle() {
417  return $this->mArticle;
418  }
419 
424  public function getTitle() {
425  return $this->mTitle;
426  }
427 
433  public function setContextTitle( $title ) {
434  $this->mContextTitle = $title;
435  }
436 
444  public function getContextTitle() {
445  if ( is_null( $this->mContextTitle ) ) {
447  return $wgTitle;
448  } else {
449  return $this->mContextTitle;
450  }
451  }
452 
460  public function isSupportedContentModel( $modelId ) {
461  return $this->enableApiEditOverride === true ||
462  ContentHandler::getForModelID( $modelId )->supportsDirectEditing();
463  }
464 
471  public function setApiEditOverride( $enableOverride ) {
472  $this->enableApiEditOverride = $enableOverride;
473  }
474 
475  function submit() {
476  $this->edit();
477  }
478 
490  function edit() {
492  // Allow extensions to modify/prevent this form or submission
493  if ( !Hooks::run( 'AlternateEdit', [ $this ] ) ) {
494  return;
495  }
496 
497  wfDebug( __METHOD__ . ": enter\n" );
498 
499  // If they used redlink=1 and the page exists, redirect to the main article
500  if ( $wgRequest->getBool( 'redlink' ) && $this->mTitle->exists() ) {
501  $wgOut->redirect( $this->mTitle->getFullURL() );
502  return;
503  }
504 
505  $this->importFormData( $wgRequest );
506  $this->firsttime = false;
507 
508  if ( wfReadOnly() && $this->save ) {
509  // Force preview
510  $this->save = false;
511  $this->preview = true;
512  }
513 
514  if ( $this->save ) {
515  $this->formtype = 'save';
516  } elseif ( $this->preview ) {
517  $this->formtype = 'preview';
518  } elseif ( $this->diff ) {
519  $this->formtype = 'diff';
520  } else { # First time through
521  $this->firsttime = true;
522  if ( $this->previewOnOpen() ) {
523  $this->formtype = 'preview';
524  } else {
525  $this->formtype = 'initial';
526  }
527  }
528 
529  $permErrors = $this->getEditPermissionErrors( $this->save ? 'secure' : 'full' );
530  if ( $permErrors ) {
531  wfDebug( __METHOD__ . ": User can't edit\n" );
532  // Auto-block user's IP if the account was "hard" blocked
533  if ( !wfReadOnly() ) {
534  $user = $wgUser;
535  DeferredUpdates::addCallableUpdate( function () use ( $user ) {
536  $user->spreadAnyEditBlock();
537  } );
538  }
539  $this->displayPermissionsError( $permErrors );
540 
541  return;
542  }
543 
544  $revision = $this->mArticle->getRevisionFetched();
545  // Disallow editing revisions with content models different from the current one
546  if ( $revision && $revision->getContentModel() !== $this->contentModel ) {
547  $this->displayViewSourcePage(
548  $this->getContentObject(),
549  wfMessage(
550  'contentmodelediterror',
551  $revision->getContentModel(),
553  )->plain()
554  );
555  return;
556  }
557 
558  $this->isConflict = false;
559  // css / js subpages of user pages get a special treatment
560  $this->isCssJsSubpage = $this->mTitle->isCssJsSubpage();
561  $this->isCssSubpage = $this->mTitle->isCssSubpage();
562  $this->isJsSubpage = $this->mTitle->isJsSubpage();
563  // @todo FIXME: Silly assignment.
564  $this->isWrongCaseCssJsPage = $this->isWrongCaseCssJsPage();
565 
566  # Show applicable editing introductions
567  if ( $this->formtype == 'initial' || $this->firsttime ) {
568  $this->showIntro();
569  }
570 
571  # Attempt submission here. This will check for edit conflicts,
572  # and redundantly check for locked database, blocked IPs, etc.
573  # that edit() already checked just in case someone tries to sneak
574  # in the back door with a hand-edited submission URL.
575 
576  if ( 'save' == $this->formtype ) {
577  $resultDetails = null;
578  $status = $this->attemptSave( $resultDetails );
579  if ( !$this->handleStatus( $status, $resultDetails ) ) {
580  return;
581  }
582  }
583 
584  # First time through: get contents, set time for conflict
585  # checking, etc.
586  if ( 'initial' == $this->formtype || $this->firsttime ) {
587  if ( $this->initialiseForm() === false ) {
588  $this->noSuchSectionPage();
589  return;
590  }
591 
592  if ( !$this->mTitle->getArticleID() ) {
593  Hooks::run( 'EditFormPreloadText', [ &$this->textbox1, &$this->mTitle ] );
594  } else {
595  Hooks::run( 'EditFormInitialText', [ $this ] );
596  }
597 
598  }
599 
600  $this->showEditForm();
601  }
602 
607  protected function getEditPermissionErrors( $rigor = 'secure' ) {
608  global $wgUser;
609 
610  $permErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $wgUser, $rigor );
611  # Can this title be created?
612  if ( !$this->mTitle->exists() ) {
613  $permErrors = array_merge(
614  $permErrors,
615  wfArrayDiff2(
616  $this->mTitle->getUserPermissionsErrors( 'create', $wgUser, $rigor ),
617  $permErrors
618  )
619  );
620  }
621  # Ignore some permissions errors when a user is just previewing/viewing diffs
622  $remove = [];
623  foreach ( $permErrors as $error ) {
624  if ( ( $this->preview || $this->diff )
625  && ( $error[0] == 'blockedtext' || $error[0] == 'autoblockedtext' )
626  ) {
627  $remove[] = $error;
628  }
629  }
630  $permErrors = wfArrayDiff2( $permErrors, $remove );
631 
632  return $permErrors;
633  }
634 
648  protected function displayPermissionsError( array $permErrors ) {
650 
651  if ( $wgRequest->getBool( 'redlink' ) ) {
652  // The edit page was reached via a red link.
653  // Redirect to the article page and let them click the edit tab if
654  // they really want a permission error.
655  $wgOut->redirect( $this->mTitle->getFullURL() );
656  return;
657  }
658 
659  $content = $this->getContentObject();
660 
661  # Use the normal message if there's nothing to display
662  if ( $this->firsttime && ( !$content || $content->isEmpty() ) ) {
663  $action = $this->mTitle->exists() ? 'edit' :
664  ( $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage' );
665  throw new PermissionsError( $action, $permErrors );
666  }
667 
668  $this->displayViewSourcePage(
669  $content,
670  $wgOut->formatPermissionsErrorMessage( $permErrors, 'edit' )
671  );
672  }
673 
679  protected function displayViewSourcePage( Content $content, $errorMessage = '' ) {
680  global $wgOut;
681 
682  Hooks::run( 'EditPage::showReadOnlyForm:initial', [ $this, &$wgOut ] );
683 
684  $wgOut->setRobotPolicy( 'noindex,nofollow' );
685  $wgOut->setPageTitle( wfMessage(
686  'viewsource-title',
687  $this->getContextTitle()->getPrefixedText()
688  ) );
689  $wgOut->addBacklinkSubtitle( $this->getContextTitle() );
690  $wgOut->addHTML( $this->editFormPageTop );
691  $wgOut->addHTML( $this->editFormTextTop );
692 
693  if ( $errorMessage !== '' ) {
694  $wgOut->addWikiText( $errorMessage );
695  $wgOut->addHTML( "<hr />\n" );
696  }
697 
698  # If the user made changes, preserve them when showing the markup
699  # (This happens when a user is blocked during edit, for instance)
700  if ( !$this->firsttime ) {
701  $text = $this->textbox1;
702  $wgOut->addWikiMsg( 'viewyourtext' );
703  } else {
704  try {
705  $text = $this->toEditText( $content );
706  } catch ( MWException $e ) {
707  # Serialize using the default format if the content model is not supported
708  # (e.g. for an old revision with a different model)
709  $text = $content->serialize();
710  }
711  $wgOut->addWikiMsg( 'viewsourcetext' );
712  }
713 
714  $wgOut->addHTML( $this->editFormTextBeforeContent );
715  $this->showTextbox( $text, 'wpTextbox1', [ 'readonly' ] );
716  $wgOut->addHTML( $this->editFormTextAfterContent );
717 
718  $wgOut->addHTML( Html::rawElement( 'div', [ 'class' => 'templatesUsed' ],
719  Linker::formatTemplates( $this->getTemplates() ) ) );
720 
721  $wgOut->addModules( 'mediawiki.action.edit.collapsibleFooter' );
722 
723  $wgOut->addHTML( $this->editFormTextBottom );
724  if ( $this->mTitle->exists() ) {
725  $wgOut->returnToMain( null, $this->mTitle );
726  }
727  }
728 
734  protected function previewOnOpen() {
735  global $wgRequest, $wgUser, $wgPreviewOnOpenNamespaces;
736  if ( $wgRequest->getVal( 'preview' ) == 'yes' ) {
737  // Explicit override from request
738  return true;
739  } elseif ( $wgRequest->getVal( 'preview' ) == 'no' ) {
740  // Explicit override from request
741  return false;
742  } elseif ( $this->section == 'new' ) {
743  // Nothing *to* preview for new sections
744  return false;
745  } elseif ( ( $wgRequest->getVal( 'preload' ) !== null || $this->mTitle->exists() )
746  && $wgUser->getOption( 'previewonfirst' )
747  ) {
748  // Standard preference behavior
749  return true;
750  } elseif ( !$this->mTitle->exists()
751  && isset( $wgPreviewOnOpenNamespaces[$this->mTitle->getNamespace()] )
752  && $wgPreviewOnOpenNamespaces[$this->mTitle->getNamespace()]
753  ) {
754  // Categories are special
755  return true;
756  } else {
757  return false;
758  }
759  }
760 
767  protected function isWrongCaseCssJsPage() {
768  if ( $this->mTitle->isCssJsSubpage() ) {
769  $name = $this->mTitle->getSkinFromCssJsSubpage();
770  $skins = array_merge(
771  array_keys( Skin::getSkinNames() ),
772  [ 'common' ]
773  );
774  return !in_array( $name, $skins )
775  && in_array( strtolower( $name ), $skins );
776  } else {
777  return false;
778  }
779  }
780 
788  protected function isSectionEditSupported() {
789  $contentHandler = ContentHandler::getForTitle( $this->mTitle );
790  return $contentHandler->supportsSections();
791  }
792 
798  function importFormData( &$request ) {
800 
801  # Section edit can come from either the form or a link
802  $this->section = $request->getVal( 'wpSection', $request->getVal( 'section' ) );
803 
804  if ( $this->section !== null && $this->section !== '' && !$this->isSectionEditSupported() ) {
805  throw new ErrorPageError( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
806  }
807 
808  $this->isNew = !$this->mTitle->exists() || $this->section == 'new';
809 
810  if ( $request->wasPosted() ) {
811  # These fields need to be checked for encoding.
812  # Also remove trailing whitespace, but don't remove _initial_
813  # whitespace from the text boxes. This may be significant formatting.
814  $this->textbox1 = $this->safeUnicodeInput( $request, 'wpTextbox1' );
815  if ( !$request->getCheck( 'wpTextbox2' ) ) {
816  // Skip this if wpTextbox2 has input, it indicates that we came
817  // from a conflict page with raw page text, not a custom form
818  // modified by subclasses
820  if ( $textbox1 !== null ) {
821  $this->textbox1 = $textbox1;
822  }
823  }
824 
825  # Truncate for whole multibyte characters
826  $this->summary = $wgContLang->truncate( $request->getText( 'wpSummary' ), 255 );
827 
828  # If the summary consists of a heading, e.g. '==Foobar==', extract the title from the
829  # header syntax, e.g. 'Foobar'. This is mainly an issue when we are using wpSummary for
830  # section titles.
831  $this->summary = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->summary );
832 
833  # Treat sectiontitle the same way as summary.
834  # Note that wpSectionTitle is not yet a part of the actual edit form, as wpSummary is
835  # currently doing double duty as both edit summary and section title. Right now this
836  # is just to allow API edits to work around this limitation, but this should be
837  # incorporated into the actual edit form when EditPage is rewritten (Bugs 18654, 26312).
838  $this->sectiontitle = $wgContLang->truncate( $request->getText( 'wpSectionTitle' ), 255 );
839  $this->sectiontitle = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->sectiontitle );
840 
841  $this->edittime = $request->getVal( 'wpEdittime' );
842  $this->starttime = $request->getVal( 'wpStarttime' );
843 
844  $undidRev = $request->getInt( 'wpUndidRevision' );
845  if ( $undidRev ) {
846  $this->undidRev = $undidRev;
847  }
848 
849  $this->scrolltop = $request->getIntOrNull( 'wpScrolltop' );
850 
851  if ( $this->textbox1 === '' && $request->getVal( 'wpTextbox1' ) === null ) {
852  // wpTextbox1 field is missing, possibly due to being "too big"
853  // according to some filter rules such as Suhosin's setting for
854  // suhosin.request.max_value_length (d'oh)
855  $this->incompleteForm = true;
856  } else {
857  // If we receive the last parameter of the request, we can fairly
858  // claim the POST request has not been truncated.
859 
860  // TODO: softened the check for cutover. Once we determine
861  // that it is safe, we should complete the transition by
862  // removing the "edittime" clause.
863  $this->incompleteForm = ( !$request->getVal( 'wpUltimateParam' )
864  && is_null( $this->edittime ) );
865  }
866  if ( $this->incompleteForm ) {
867  # If the form is incomplete, force to preview.
868  wfDebug( __METHOD__ . ": Form data appears to be incomplete\n" );
869  wfDebug( "POST DATA: " . var_export( $_POST, true ) . "\n" );
870  $this->preview = true;
871  } else {
872  $this->preview = $request->getCheck( 'wpPreview' );
873  $this->diff = $request->getCheck( 'wpDiff' );
874 
875  // Remember whether a save was requested, so we can indicate
876  // if we forced preview due to session failure.
877  $this->mTriedSave = !$this->preview;
878 
879  if ( $this->tokenOk( $request ) ) {
880  # Some browsers will not report any submit button
881  # if the user hits enter in the comment box.
882  # The unmarked state will be assumed to be a save,
883  # if the form seems otherwise complete.
884  wfDebug( __METHOD__ . ": Passed token check.\n" );
885  } elseif ( $this->diff ) {
886  # Failed token check, but only requested "Show Changes".
887  wfDebug( __METHOD__ . ": Failed token check; Show Changes requested.\n" );
888  } else {
889  # Page might be a hack attempt posted from
890  # an external site. Preview instead of saving.
891  wfDebug( __METHOD__ . ": Failed token check; forcing preview\n" );
892  $this->preview = true;
893  }
894  }
895  $this->save = !$this->preview && !$this->diff;
896  if ( !preg_match( '/^\d{14}$/', $this->edittime ) ) {
897  $this->edittime = null;
898  }
899 
900  if ( !preg_match( '/^\d{14}$/', $this->starttime ) ) {
901  $this->starttime = null;
902  }
903 
904  $this->recreate = $request->getCheck( 'wpRecreate' );
905 
906  $this->minoredit = $request->getCheck( 'wpMinoredit' );
907  $this->watchthis = $request->getCheck( 'wpWatchthis' );
908 
909  # Don't force edit summaries when a user is editing their own user or talk page
910  if ( ( $this->mTitle->mNamespace == NS_USER || $this->mTitle->mNamespace == NS_USER_TALK )
911  && $this->mTitle->getText() == $wgUser->getName()
912  ) {
913  $this->allowBlankSummary = true;
914  } else {
915  $this->allowBlankSummary = $request->getBool( 'wpIgnoreBlankSummary' )
916  || !$wgUser->getOption( 'forceeditsummary' );
917  }
918 
919  $this->autoSumm = $request->getText( 'wpAutoSummary' );
920 
921  $this->allowBlankArticle = $request->getBool( 'wpIgnoreBlankArticle' );
922  $this->allowSelfRedirect = $request->getBool( 'wpIgnoreSelfRedirect' );
923 
924  $changeTags = $request->getVal( 'wpChangeTags' );
925  if ( is_null( $changeTags ) || $changeTags === '' ) {
926  $this->changeTags = [];
927  } else {
928  $this->changeTags = array_filter( array_map( 'trim', explode( ',',
929  $changeTags ) ) );
930  }
931  } else {
932  # Not a posted form? Start with nothing.
933  wfDebug( __METHOD__ . ": Not a posted form.\n" );
934  $this->textbox1 = '';
935  $this->summary = '';
936  $this->sectiontitle = '';
937  $this->edittime = '';
938  $this->starttime = wfTimestampNow();
939  $this->edit = false;
940  $this->preview = false;
941  $this->save = false;
942  $this->diff = false;
943  $this->minoredit = false;
944  // Watch may be overridden by request parameters
945  $this->watchthis = $request->getBool( 'watchthis', false );
946  $this->recreate = false;
947 
948  // When creating a new section, we can preload a section title by passing it as the
949  // preloadtitle parameter in the URL (Bug 13100)
950  if ( $this->section == 'new' && $request->getVal( 'preloadtitle' ) ) {
951  $this->sectiontitle = $request->getVal( 'preloadtitle' );
952  // Once wpSummary isn't being use for setting section titles, we should delete this.
953  $this->summary = $request->getVal( 'preloadtitle' );
954  } elseif ( $this->section != 'new' && $request->getVal( 'summary' ) ) {
955  $this->summary = $request->getText( 'summary' );
956  if ( $this->summary !== '' ) {
957  $this->hasPresetSummary = true;
958  }
959  }
960 
961  if ( $request->getVal( 'minor' ) ) {
962  $this->minoredit = true;
963  }
964  }
965 
966  $this->oldid = $request->getInt( 'oldid' );
967  $this->parentRevId = $request->getInt( 'parentRevId' );
968 
969  $this->bot = $request->getBool( 'bot', true );
970  $this->nosummary = $request->getBool( 'nosummary' );
971 
972  // May be overridden by revision.
973  $this->contentModel = $request->getText( 'model', $this->contentModel );
974  // May be overridden by revision.
975  $this->contentFormat = $request->getText( 'format', $this->contentFormat );
976 
977  if ( !ContentHandler::getForModelID( $this->contentModel )
978  ->isSupportedFormat( $this->contentFormat )
979  ) {
980  throw new ErrorPageError(
981  'editpage-notsupportedcontentformat-title',
982  'editpage-notsupportedcontentformat-text',
983  [ $this->contentFormat, ContentHandler::getLocalizedName( $this->contentModel ) ]
984  );
985  }
986 
993  $this->editintro = $request->getText( 'editintro',
994  // Custom edit intro for new sections
995  $this->section === 'new' ? 'MediaWiki:addsection-editintro' : '' );
996 
997  // Allow extensions to modify form data
998  Hooks::run( 'EditPage::importFormData', [ $this, $request ] );
999 
1000  }
1001 
1011  protected function importContentFormData( &$request ) {
1012  return; // Don't do anything, EditPage already extracted wpTextbox1
1013  }
1014 
1020  function initialiseForm() {
1021  global $wgUser;
1022  $this->edittime = $this->page->getTimestamp();
1023 
1024  $content = $this->getContentObject( false ); # TODO: track content object?!
1025  if ( $content === false ) {
1026  return false;
1027  }
1028  $this->textbox1 = $this->toEditText( $content );
1029 
1030  // activate checkboxes if user wants them to be always active
1031  # Sort out the "watch" checkbox
1032  if ( $wgUser->getOption( 'watchdefault' ) ) {
1033  # Watch all edits
1034  $this->watchthis = true;
1035  } elseif ( $wgUser->getOption( 'watchcreations' ) && !$this->mTitle->exists() ) {
1036  # Watch creations
1037  $this->watchthis = true;
1038  } elseif ( $wgUser->isWatched( $this->mTitle ) ) {
1039  # Already watched
1040  $this->watchthis = true;
1041  }
1042  if ( $wgUser->getOption( 'minordefault' ) && !$this->isNew ) {
1043  $this->minoredit = true;
1044  }
1045  if ( $this->textbox1 === false ) {
1046  return false;
1047  }
1048  return true;
1049  }
1050 
1058  protected function getContentObject( $def_content = null ) {
1060 
1061  $content = false;
1062 
1063  // For message page not locally set, use the i18n message.
1064  // For other non-existent articles, use preload text if any.
1065  if ( !$this->mTitle->exists() || $this->section == 'new' ) {
1066  if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && $this->section != 'new' ) {
1067  # If this is a system message, get the default text.
1068  $msg = $this->mTitle->getDefaultMessageText();
1069 
1070  $content = $this->toEditContent( $msg );
1071  }
1072  if ( $content === false ) {
1073  # If requested, preload some text.
1074  $preload = $wgRequest->getVal( 'preload',
1075  // Custom preload text for new sections
1076  $this->section === 'new' ? 'MediaWiki:addsection-preload' : '' );
1077  $params = $wgRequest->getArray( 'preloadparams', [] );
1078 
1079  $content = $this->getPreloadedContent( $preload, $params );
1080  }
1081  // For existing pages, get text based on "undo" or section parameters.
1082  } else {
1083  if ( $this->section != '' ) {
1084  // Get section edit text (returns $def_text for invalid sections)
1085  $orig = $this->getOriginalContent( $wgUser );
1086  $content = $orig ? $orig->getSection( $this->section ) : null;
1087 
1088  if ( !$content ) {
1089  $content = $def_content;
1090  }
1091  } else {
1092  $undoafter = $wgRequest->getInt( 'undoafter' );
1093  $undo = $wgRequest->getInt( 'undo' );
1094 
1095  if ( $undo > 0 && $undoafter > 0 ) {
1096  $undorev = Revision::newFromId( $undo );
1097  $oldrev = Revision::newFromId( $undoafter );
1098 
1099  # Sanity check, make sure it's the right page,
1100  # the revisions exist and they were not deleted.
1101  # Otherwise, $content will be left as-is.
1102  if ( !is_null( $undorev ) && !is_null( $oldrev ) &&
1103  !$undorev->isDeleted( Revision::DELETED_TEXT ) &&
1104  !$oldrev->isDeleted( Revision::DELETED_TEXT )
1105  ) {
1106  $content = $this->page->getUndoContent( $undorev, $oldrev );
1107 
1108  if ( $content === false ) {
1109  # Warn the user that something went wrong
1110  $undoMsg = 'failure';
1111  } else {
1112  $oldContent = $this->page->getContent( Revision::RAW );
1113  $popts = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang );
1114  $newContent = $content->preSaveTransform( $this->mTitle, $wgUser, $popts );
1115 
1116  if ( $newContent->equals( $oldContent ) ) {
1117  # Tell the user that the undo results in no change,
1118  # i.e. the revisions were already undone.
1119  $undoMsg = 'nochange';
1120  $content = false;
1121  } else {
1122  # Inform the user of our success and set an automatic edit summary
1123  $undoMsg = 'success';
1124 
1125  # If we just undid one rev, use an autosummary
1126  $firstrev = $oldrev->getNext();
1127  if ( $firstrev && $firstrev->getId() == $undo ) {
1128  $userText = $undorev->getUserText();
1129  if ( $userText === '' ) {
1130  $undoSummary = wfMessage(
1131  'undo-summary-username-hidden',
1132  $undo
1133  )->inContentLanguage()->text();
1134  } else {
1135  $undoSummary = wfMessage(
1136  'undo-summary',
1137  $undo,
1138  $userText
1139  )->inContentLanguage()->text();
1140  }
1141  if ( $this->summary === '' ) {
1142  $this->summary = $undoSummary;
1143  } else {
1144  $this->summary = $undoSummary . wfMessage( 'colon-separator' )
1145  ->inContentLanguage()->text() . $this->summary;
1146  }
1147  $this->undidRev = $undo;
1148  }
1149  $this->formtype = 'diff';
1150  }
1151  }
1152  } else {
1153  // Failed basic sanity checks.
1154  // Older revisions may have been removed since the link
1155  // was created, or we may simply have got bogus input.
1156  $undoMsg = 'norev';
1157  }
1158 
1159  // Messages: undo-success, undo-failure, undo-norev, undo-nochange
1160  $class = ( $undoMsg == 'success' ? '' : 'error ' ) . "mw-undo-{$undoMsg}";
1161  $this->editFormPageTop .= $wgOut->parse( "<div class=\"{$class}\">" .
1162  wfMessage( 'undo-' . $undoMsg )->plain() . '</div>', true, /* interface */true );
1163  }
1164 
1165  if ( $content === false ) {
1166  $content = $this->getOriginalContent( $wgUser );
1167  }
1168  }
1169  }
1170 
1171  return $content;
1172  }
1173 
1189  private function getOriginalContent( User $user ) {
1190  if ( $this->section == 'new' ) {
1191  return $this->getCurrentContent();
1192  }
1193  $revision = $this->mArticle->getRevisionFetched();
1194  if ( $revision === null ) {
1195  if ( !$this->contentModel ) {
1196  $this->contentModel = $this->getTitle()->getContentModel();
1197  }
1198  $handler = ContentHandler::getForModelID( $this->contentModel );
1199 
1200  return $handler->makeEmptyContent();
1201  }
1202  $content = $revision->getContent( Revision::FOR_THIS_USER, $user );
1203  return $content;
1204  }
1205 
1218  public function getParentRevId() {
1219  if ( $this->parentRevId ) {
1220  return $this->parentRevId;
1221  } else {
1222  return $this->mArticle->getRevIdFetched();
1223  }
1224  }
1225 
1234  protected function getCurrentContent() {
1235  $rev = $this->page->getRevision();
1236  $content = $rev ? $rev->getContent( Revision::RAW ) : null;
1237 
1238  if ( $content === false || $content === null ) {
1239  if ( !$this->contentModel ) {
1240  $this->contentModel = $this->getTitle()->getContentModel();
1241  }
1242  $handler = ContentHandler::getForModelID( $this->contentModel );
1243 
1244  return $handler->makeEmptyContent();
1245  } else {
1246  # nasty side-effect, but needed for consistency
1247  $this->contentModel = $rev->getContentModel();
1248  $this->contentFormat = $rev->getContentFormat();
1249 
1250  return $content;
1251  }
1252  }
1253 
1261  public function setPreloadedContent( Content $content ) {
1262  $this->mPreloadContent = $content;
1263  }
1264 
1276  protected function getPreloadedContent( $preload, $params = [] ) {
1277  global $wgUser;
1278 
1279  if ( !empty( $this->mPreloadContent ) ) {
1280  return $this->mPreloadContent;
1281  }
1282 
1284 
1285  if ( $preload === '' ) {
1286  return $handler->makeEmptyContent();
1287  }
1288 
1289  $title = Title::newFromText( $preload );
1290  # Check for existence to avoid getting MediaWiki:Noarticletext
1291  if ( $title === null || !$title->exists() || !$title->userCan( 'read', $wgUser ) ) {
1292  // TODO: somehow show a warning to the user!
1293  return $handler->makeEmptyContent();
1294  }
1295 
1297  if ( $page->isRedirect() ) {
1299  # Same as before
1300  if ( $title === null || !$title->exists() || !$title->userCan( 'read', $wgUser ) ) {
1301  // TODO: somehow show a warning to the user!
1302  return $handler->makeEmptyContent();
1303  }
1305  }
1306 
1307  $parserOptions = ParserOptions::newFromUser( $wgUser );
1309 
1310  if ( !$content ) {
1311  // TODO: somehow show a warning to the user!
1312  return $handler->makeEmptyContent();
1313  }
1314 
1315  if ( $content->getModel() !== $handler->getModelID() ) {
1316  $converted = $content->convert( $handler->getModelID() );
1317 
1318  if ( !$converted ) {
1319  // TODO: somehow show a warning to the user!
1320  wfDebug( "Attempt to preload incompatible content: " .
1321  "can't convert " . $content->getModel() .
1322  " to " . $handler->getModelID() );
1323 
1324  return $handler->makeEmptyContent();
1325  }
1326 
1327  $content = $converted;
1328  }
1329 
1330  return $content->preloadTransform( $title, $parserOptions, $params );
1331  }
1332 
1340  function tokenOk( &$request ) {
1341  global $wgUser;
1342  $token = $request->getVal( 'wpEditToken' );
1343  $this->mTokenOk = $wgUser->matchEditToken( $token );
1344  $this->mTokenOkExceptSuffix = $wgUser->matchEditTokenNoSuffix( $token );
1345  return $this->mTokenOk;
1346  }
1347 
1364  protected function setPostEditCookie( $statusValue ) {
1365  $revisionId = $this->page->getLatest();
1366  $postEditKey = self::POST_EDIT_COOKIE_KEY_PREFIX . $revisionId;
1367 
1368  $val = 'saved';
1369  if ( $statusValue == self::AS_SUCCESS_NEW_ARTICLE ) {
1370  $val = 'created';
1371  } elseif ( $this->oldid ) {
1372  $val = 'restored';
1373  }
1374 
1375  $response = RequestContext::getMain()->getRequest()->response();
1376  $response->setCookie( $postEditKey, $val, time() + self::POST_EDIT_COOKIE_DURATION, [
1377  'httpOnly' => false,
1378  ] );
1379  }
1380 
1387  public function attemptSave( &$resultDetails = false ) {
1388  global $wgUser;
1389 
1390  # Allow bots to exempt some edits from bot flagging
1391  $bot = $wgUser->isAllowed( 'bot' ) && $this->bot;
1392  $status = $this->internalAttemptSave( $resultDetails, $bot );
1393 
1394  Hooks::run( 'EditPage::attemptSave:after', [ $this, $status, $resultDetails ] );
1395 
1396  return $status;
1397  }
1398 
1408  private function handleStatus( Status $status, $resultDetails ) {
1410 
1415  if ( $status->value == self::AS_SUCCESS_UPDATE
1416  || $status->value == self::AS_SUCCESS_NEW_ARTICLE
1417  ) {
1418  $this->didSave = true;
1419  if ( !$resultDetails['nullEdit'] ) {
1420  $this->setPostEditCookie( $status->value );
1421  }
1422  }
1423 
1424  // "wpExtraQueryRedirect" is a hidden input to modify
1425  // after save URL and is not used by actual edit form
1426  $request = RequestContext::getMain()->getRequest();
1427  $extraQueryRedirect = $request->getVal( 'wpExtraQueryRedirect' );
1428 
1429  switch ( $status->value ) {
1430  case self::AS_HOOK_ERROR_EXPECTED:
1431  case self::AS_CONTENT_TOO_BIG:
1432  case self::AS_ARTICLE_WAS_DELETED:
1433  case self::AS_CONFLICT_DETECTED:
1434  case self::AS_SUMMARY_NEEDED:
1435  case self::AS_TEXTBOX_EMPTY:
1436  case self::AS_MAX_ARTICLE_SIZE_EXCEEDED:
1437  case self::AS_END:
1438  case self::AS_BLANK_ARTICLE:
1439  case self::AS_SELF_REDIRECT:
1440  return true;
1441 
1442  case self::AS_HOOK_ERROR:
1443  return false;
1444 
1445  case self::AS_CANNOT_USE_CUSTOM_MODEL:
1446  case self::AS_PARSE_ERROR:
1447  $wgOut->addWikiText( '<div class="error">' . $status->getWikiText() . '</div>' );
1448  return true;
1449 
1450  case self::AS_SUCCESS_NEW_ARTICLE:
1451  $query = $resultDetails['redirect'] ? 'redirect=no' : '';
1452  if ( $extraQueryRedirect ) {
1453  if ( $query === '' ) {
1454  $query = $extraQueryRedirect;
1455  } else {
1456  $query = $query . '&' . $extraQueryRedirect;
1457  }
1458  }
1459  $anchor = isset( $resultDetails['sectionanchor'] ) ? $resultDetails['sectionanchor'] : '';
1460  $wgOut->redirect( $this->mTitle->getFullURL( $query ) . $anchor );
1461  return false;
1462 
1463  case self::AS_SUCCESS_UPDATE:
1464  $extraQuery = '';
1465  $sectionanchor = $resultDetails['sectionanchor'];
1466 
1467  // Give extensions a chance to modify URL query on update
1468  Hooks::run(
1469  'ArticleUpdateBeforeRedirect',
1470  [ $this->mArticle, &$sectionanchor, &$extraQuery ]
1471  );
1472 
1473  if ( $resultDetails['redirect'] ) {
1474  if ( $extraQuery == '' ) {
1475  $extraQuery = 'redirect=no';
1476  } else {
1477  $extraQuery = 'redirect=no&' . $extraQuery;
1478  }
1479  }
1480  if ( $extraQueryRedirect ) {
1481  if ( $extraQuery === '' ) {
1482  $extraQuery = $extraQueryRedirect;
1483  } else {
1484  $extraQuery = $extraQuery . '&' . $extraQueryRedirect;
1485  }
1486  }
1487 
1488  $wgOut->redirect( $this->mTitle->getFullURL( $extraQuery ) . $sectionanchor );
1489  return false;
1490 
1491  case self::AS_SPAM_ERROR:
1492  $this->spamPageWithContent( $resultDetails['spam'] );
1493  return false;
1494 
1495  case self::AS_BLOCKED_PAGE_FOR_USER:
1496  throw new UserBlockedError( $wgUser->getBlock() );
1497 
1498  case self::AS_IMAGE_REDIRECT_ANON:
1499  case self::AS_IMAGE_REDIRECT_LOGGED:
1500  throw new PermissionsError( 'upload' );
1501 
1502  case self::AS_READ_ONLY_PAGE_ANON:
1503  case self::AS_READ_ONLY_PAGE_LOGGED:
1504  throw new PermissionsError( 'edit' );
1505 
1506  case self::AS_READ_ONLY_PAGE:
1507  throw new ReadOnlyError;
1508 
1509  case self::AS_RATE_LIMITED:
1510  throw new ThrottledError();
1511 
1512  case self::AS_NO_CREATE_PERMISSION:
1513  $permission = $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage';
1514  throw new PermissionsError( $permission );
1515 
1516  case self::AS_NO_CHANGE_CONTENT_MODEL:
1517  throw new PermissionsError( 'editcontentmodel' );
1518 
1519  default:
1520  // We don't recognize $status->value. The only way that can happen
1521  // is if an extension hook aborted from inside ArticleSave.
1522  // Render the status object into $this->hookError
1523  // FIXME this sucks, we should just use the Status object throughout
1524  $this->hookError = '<div class="error">' . $status->getWikiText() .
1525  '</div>';
1526  return true;
1527  }
1528  }
1529 
1540  // Run old style post-section-merge edit filter
1541  if ( !ContentHandler::runLegacyHooks( 'EditFilterMerged',
1542  [ $this, $content, &$this->hookError, $this->summary ] )
1543  ) {
1544  # Error messages etc. could be handled within the hook...
1545  $status->fatal( 'hookaborted' );
1546  $status->value = self::AS_HOOK_ERROR;
1547  return false;
1548  } elseif ( $this->hookError != '' ) {
1549  # ...or the hook could be expecting us to produce an error
1550  $status->fatal( 'hookaborted' );
1551  $status->value = self::AS_HOOK_ERROR_EXPECTED;
1552  return false;
1553  }
1554 
1555  // Run new style post-section-merge edit filter
1556  if ( !Hooks::run( 'EditFilterMergedContent',
1557  [ $this->mArticle->getContext(), $content, $status, $this->summary,
1558  $user, $this->minoredit ] )
1559  ) {
1560  # Error messages etc. could be handled within the hook...
1561  if ( $status->isGood() ) {
1562  $status->fatal( 'hookaborted' );
1563  // Not setting $this->hookError here is a hack to allow the hook
1564  // to cause a return to the edit page without $this->hookError
1565  // being set. This is used by ConfirmEdit to display a captcha
1566  // without any error message cruft.
1567  } else {
1568  $this->hookError = $status->getWikiText();
1569  }
1570  // Use the existing $status->value if the hook set it
1571  if ( !$status->value ) {
1572  $status->value = self::AS_HOOK_ERROR;
1573  }
1574  return false;
1575  } elseif ( !$status->isOK() ) {
1576  # ...or the hook could be expecting us to produce an error
1577  // FIXME this sucks, we should just use the Status object throughout
1578  $this->hookError = $status->getWikiText();
1579  $status->fatal( 'hookaborted' );
1580  $status->value = self::AS_HOOK_ERROR_EXPECTED;
1581  return false;
1582  }
1583 
1584  return true;
1585  }
1586 
1593  private function newSectionSummary( &$sectionanchor = null ) {
1594  global $wgParser;
1595 
1596  if ( $this->sectiontitle !== '' ) {
1597  $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle );
1598  // If no edit summary was specified, create one automatically from the section
1599  // title and have it link to the new section. Otherwise, respect the summary as
1600  // passed.
1601  if ( $this->summary === '' ) {
1602  $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
1603  return wfMessage( 'newsectionsummary' )
1604  ->rawParams( $cleanSectionTitle )->inContentLanguage()->text();
1605  }
1606  } elseif ( $this->summary !== '' ) {
1607  $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->summary );
1608  # This is a new section, so create a link to the new section
1609  # in the revision summary.
1610  $cleanSummary = $wgParser->stripSectionName( $this->summary );
1611  return wfMessage( 'newsectionsummary' )
1612  ->rawParams( $cleanSummary )->inContentLanguage()->text();
1613  }
1614  return $this->summary;
1615  }
1616 
1641  function internalAttemptSave( &$result, $bot = false ) {
1643  global $wgContentHandlerUseDB;
1644 
1646 
1647  if ( !Hooks::run( 'EditPage::attemptSave', [ $this ] ) ) {
1648  wfDebug( "Hook 'EditPage::attemptSave' aborted article saving\n" );
1649  $status->fatal( 'hookaborted' );
1650  $status->value = self::AS_HOOK_ERROR;
1651  return $status;
1652  }
1653 
1654  $spam = $wgRequest->getText( 'wpAntispam' );
1655  if ( $spam !== '' ) {
1656  wfDebugLog(
1657  'SimpleAntiSpam',
1658  $wgUser->getName() .
1659  ' editing "' .
1660  $this->mTitle->getPrefixedText() .
1661  '" submitted bogus field "' .
1662  $spam .
1663  '"'
1664  );
1665  $status->fatal( 'spamprotectionmatch', false );
1666  $status->value = self::AS_SPAM_ERROR;
1667  return $status;
1668  }
1669 
1670  try {
1671  # Construct Content object
1672  $textbox_content = $this->toEditContent( $this->textbox1 );
1673  } catch ( MWContentSerializationException $ex ) {
1674  $status->fatal(
1675  'content-failed-to-parse',
1676  $this->contentModel,
1677  $this->contentFormat,
1678  $ex->getMessage()
1679  );
1680  $status->value = self::AS_PARSE_ERROR;
1681  return $status;
1682  }
1683 
1684  # Check image redirect
1685  if ( $this->mTitle->getNamespace() == NS_FILE &&
1686  $textbox_content->isRedirect() &&
1687  !$wgUser->isAllowed( 'upload' )
1688  ) {
1689  $code = $wgUser->isAnon() ? self::AS_IMAGE_REDIRECT_ANON : self::AS_IMAGE_REDIRECT_LOGGED;
1690  $status->setResult( false, $code );
1691 
1692  return $status;
1693  }
1694 
1695  # Check for spam
1696  $match = self::matchSummarySpamRegex( $this->summary );
1697  if ( $match === false && $this->section == 'new' ) {
1698  # $wgSpamRegex is enforced on this new heading/summary because, unlike
1699  # regular summaries, it is added to the actual wikitext.
1700  if ( $this->sectiontitle !== '' ) {
1701  # This branch is taken when the API is used with the 'sectiontitle' parameter.
1702  $match = self::matchSpamRegex( $this->sectiontitle );
1703  } else {
1704  # This branch is taken when the "Add Topic" user interface is used, or the API
1705  # is used with the 'summary' parameter.
1706  $match = self::matchSpamRegex( $this->summary );
1707  }
1708  }
1709  if ( $match === false ) {
1710  $match = self::matchSpamRegex( $this->textbox1 );
1711  }
1712  if ( $match !== false ) {
1713  $result['spam'] = $match;
1714  $ip = $wgRequest->getIP();
1715  $pdbk = $this->mTitle->getPrefixedDBkey();
1716  $match = str_replace( "\n", '', $match );
1717  wfDebugLog( 'SpamRegex', "$ip spam regex hit [[$pdbk]]: \"$match\"" );
1718  $status->fatal( 'spamprotectionmatch', $match );
1719  $status->value = self::AS_SPAM_ERROR;
1720  return $status;
1721  }
1722  if ( !Hooks::run(
1723  'EditFilter',
1724  [ $this, $this->textbox1, $this->section, &$this->hookError, $this->summary ] )
1725  ) {
1726  # Error messages etc. could be handled within the hook...
1727  $status->fatal( 'hookaborted' );
1728  $status->value = self::AS_HOOK_ERROR;
1729  return $status;
1730  } elseif ( $this->hookError != '' ) {
1731  # ...or the hook could be expecting us to produce an error
1732  $status->fatal( 'hookaborted' );
1733  $status->value = self::AS_HOOK_ERROR_EXPECTED;
1734  return $status;
1735  }
1736 
1737  if ( $wgUser->isBlockedFrom( $this->mTitle, false ) ) {
1738  // Auto-block user's IP if the account was "hard" blocked
1739  if ( !wfReadOnly() ) {
1740  $wgUser->spreadAnyEditBlock();
1741  }
1742  # Check block state against master, thus 'false'.
1743  $status->setResult( false, self::AS_BLOCKED_PAGE_FOR_USER );
1744  return $status;
1745  }
1746 
1747  $this->kblength = (int)( strlen( $this->textbox1 ) / 1024 );
1748  if ( $this->kblength > $wgMaxArticleSize ) {
1749  // Error will be displayed by showEditForm()
1750  $this->tooBig = true;
1751  $status->setResult( false, self::AS_CONTENT_TOO_BIG );
1752  return $status;
1753  }
1754 
1755  if ( !$wgUser->isAllowed( 'edit' ) ) {
1756  if ( $wgUser->isAnon() ) {
1757  $status->setResult( false, self::AS_READ_ONLY_PAGE_ANON );
1758  return $status;
1759  } else {
1760  $status->fatal( 'readonlytext' );
1761  $status->value = self::AS_READ_ONLY_PAGE_LOGGED;
1762  return $status;
1763  }
1764  }
1765 
1766  $changingContentModel = false;
1767  if ( $this->contentModel !== $this->mTitle->getContentModel() ) {
1768  if ( !$wgContentHandlerUseDB ) {
1769  $status->fatal( 'editpage-cannot-use-custom-model' );
1770  $status->value = self::AS_CANNOT_USE_CUSTOM_MODEL;
1771  return $status;
1772  } elseif ( !$wgUser->isAllowed( 'editcontentmodel' ) ) {
1773  $status->setResult( false, self::AS_NO_CHANGE_CONTENT_MODEL );
1774  return $status;
1775 
1776  }
1777  $changingContentModel = true;
1778  $oldContentModel = $this->mTitle->getContentModel();
1779  }
1780 
1781  if ( $this->changeTags ) {
1782  $changeTagsStatus = ChangeTags::canAddTagsAccompanyingChange(
1783  $this->changeTags, $wgUser );
1784  if ( !$changeTagsStatus->isOK() ) {
1785  $changeTagsStatus->value = self::AS_CHANGE_TAG_ERROR;
1786  return $changeTagsStatus;
1787  }
1788  }
1789 
1790  if ( wfReadOnly() ) {
1791  $status->fatal( 'readonlytext' );
1792  $status->value = self::AS_READ_ONLY_PAGE;
1793  return $status;
1794  }
1795  if ( $wgUser->pingLimiter() || $wgUser->pingLimiter( 'linkpurge', 0 ) ) {
1796  $status->fatal( 'actionthrottledtext' );
1797  $status->value = self::AS_RATE_LIMITED;
1798  return $status;
1799  }
1800 
1801  # If the article has been deleted while editing, don't save it without
1802  # confirmation
1803  if ( $this->wasDeletedSinceLastEdit() && !$this->recreate ) {
1804  $status->setResult( false, self::AS_ARTICLE_WAS_DELETED );
1805  return $status;
1806  }
1807 
1808  # Load the page data from the master. If anything changes in the meantime,
1809  # we detect it by using page_latest like a token in a 1 try compare-and-swap.
1810  $this->page->loadPageData( 'fromdbmaster' );
1811  $new = !$this->page->exists();
1812 
1813  if ( $new ) {
1814  // Late check for create permission, just in case *PARANOIA*
1815  if ( !$this->mTitle->userCan( 'create', $wgUser ) ) {
1816  $status->fatal( 'nocreatetext' );
1817  $status->value = self::AS_NO_CREATE_PERMISSION;
1818  wfDebug( __METHOD__ . ": no create permission\n" );
1819  return $status;
1820  }
1821 
1822  // Don't save a new page if it's blank or if it's a MediaWiki:
1823  // message with content equivalent to default (allow empty pages
1824  // in this case to disable messages, see bug 50124)
1825  $defaultMessageText = $this->mTitle->getDefaultMessageText();
1826  if ( $this->mTitle->getNamespace() === NS_MEDIAWIKI && $defaultMessageText !== false ) {
1827  $defaultText = $defaultMessageText;
1828  } else {
1829  $defaultText = '';
1830  }
1831 
1832  if ( !$this->allowBlankArticle && $this->textbox1 === $defaultText ) {
1833  $this->blankArticle = true;
1834  $status->fatal( 'blankarticle' );
1835  $status->setResult( false, self::AS_BLANK_ARTICLE );
1836  return $status;
1837  }
1838 
1839  if ( !$this->runPostMergeFilters( $textbox_content, $status, $wgUser ) ) {
1840  return $status;
1841  }
1842 
1843  $content = $textbox_content;
1844 
1845  $result['sectionanchor'] = '';
1846  if ( $this->section == 'new' ) {
1847  if ( $this->sectiontitle !== '' ) {
1848  // Insert the section title above the content.
1849  $content = $content->addSectionHeader( $this->sectiontitle );
1850  } elseif ( $this->summary !== '' ) {
1851  // Insert the section title above the content.
1852  $content = $content->addSectionHeader( $this->summary );
1853  }
1854  $this->summary = $this->newSectionSummary( $result['sectionanchor'] );
1855  }
1856 
1857  $status->value = self::AS_SUCCESS_NEW_ARTICLE;
1858 
1859  } else { # not $new
1860 
1861  # Article exists. Check for edit conflict.
1862 
1863  $this->page->clear(); # Force reload of dates, etc.
1864  $timestamp = $this->page->getTimestamp();
1865 
1866  wfDebug( "timestamp: {$timestamp}, edittime: {$this->edittime}\n" );
1867 
1868  if ( $timestamp != $this->edittime ) {
1869  $this->isConflict = true;
1870  if ( $this->section == 'new' ) {
1871  if ( $this->page->getUserText() == $wgUser->getName() &&
1872  $this->page->getComment() == $this->newSectionSummary()
1873  ) {
1874  // Probably a duplicate submission of a new comment.
1875  // This can happen when CDN resends a request after
1876  // a timeout but the first one actually went through.
1877  wfDebug( __METHOD__
1878  . ": duplicate new section submission; trigger edit conflict!\n" );
1879  } else {
1880  // New comment; suppress conflict.
1881  $this->isConflict = false;
1882  wfDebug( __METHOD__ . ": conflict suppressed; new section\n" );
1883  }
1884  } elseif ( $this->section == ''
1886  DB_MASTER, $this->mTitle->getArticleID(),
1887  $wgUser->getId(), $this->edittime
1888  )
1889  ) {
1890  # Suppress edit conflict with self, except for section edits where merging is required.
1891  wfDebug( __METHOD__ . ": Suppressing edit conflict, same user.\n" );
1892  $this->isConflict = false;
1893  }
1894  }
1895 
1896  // If sectiontitle is set, use it, otherwise use the summary as the section title.
1897  if ( $this->sectiontitle !== '' ) {
1898  $sectionTitle = $this->sectiontitle;
1899  } else {
1900  $sectionTitle = $this->summary;
1901  }
1902 
1903  $content = null;
1904 
1905  if ( $this->isConflict ) {
1906  wfDebug( __METHOD__
1907  . ": conflict! getting section '{$this->section}' for time '{$this->edittime}'"
1908  . " (article time '{$timestamp}')\n" );
1909 
1910  $content = $this->page->replaceSectionContent(
1911  $this->section,
1912  $textbox_content,
1913  $sectionTitle,
1914  $this->edittime
1915  );
1916  } else {
1917  wfDebug( __METHOD__ . ": getting section '{$this->section}'\n" );
1918  $content = $this->page->replaceSectionContent(
1919  $this->section,
1920  $textbox_content,
1921  $sectionTitle
1922  );
1923  }
1924 
1925  if ( is_null( $content ) ) {
1926  wfDebug( __METHOD__ . ": activating conflict; section replace failed.\n" );
1927  $this->isConflict = true;
1928  $content = $textbox_content; // do not try to merge here!
1929  } elseif ( $this->isConflict ) {
1930  # Attempt merge
1931  if ( $this->mergeChangesIntoContent( $content ) ) {
1932  // Successful merge! Maybe we should tell the user the good news?
1933  $this->isConflict = false;
1934  wfDebug( __METHOD__ . ": Suppressing edit conflict, successful merge.\n" );
1935  } else {
1936  $this->section = '';
1937  $this->textbox1 = ContentHandler::getContentText( $content );
1938  wfDebug( __METHOD__ . ": Keeping edit conflict, failed merge.\n" );
1939  }
1940  }
1941 
1942  if ( $this->isConflict ) {
1943  $status->setResult( false, self::AS_CONFLICT_DETECTED );
1944  return $status;
1945  }
1946 
1947  if ( !$this->runPostMergeFilters( $content, $status, $wgUser ) ) {
1948  return $status;
1949  }
1950 
1951  if ( $this->section == 'new' ) {
1952  // Handle the user preference to force summaries here
1953  if ( !$this->allowBlankSummary && trim( $this->summary ) == '' ) {
1954  $this->missingSummary = true;
1955  $status->fatal( 'missingsummary' ); // or 'missingcommentheader' if $section == 'new'. Blegh
1956  $status->value = self::AS_SUMMARY_NEEDED;
1957  return $status;
1958  }
1959 
1960  // Do not allow the user to post an empty comment
1961  if ( $this->textbox1 == '' ) {
1962  $this->missingComment = true;
1963  $status->fatal( 'missingcommenttext' );
1964  $status->value = self::AS_TEXTBOX_EMPTY;
1965  return $status;
1966  }
1967  } elseif ( !$this->allowBlankSummary
1968  && !$content->equals( $this->getOriginalContent( $wgUser ) )
1969  && !$content->isRedirect()
1970  && md5( $this->summary ) == $this->autoSumm
1971  ) {
1972  $this->missingSummary = true;
1973  $status->fatal( 'missingsummary' );
1974  $status->value = self::AS_SUMMARY_NEEDED;
1975  return $status;
1976  }
1977 
1978  # All's well
1979  $sectionanchor = '';
1980  if ( $this->section == 'new' ) {
1981  $this->summary = $this->newSectionSummary( $sectionanchor );
1982  } elseif ( $this->section != '' ) {
1983  # Try to get a section anchor from the section source, redirect
1984  # to edited section if header found.
1985  # XXX: Might be better to integrate this into Article::replaceSectionAtRev
1986  # for duplicate heading checking and maybe parsing.
1987  $hasmatch = preg_match( "/^ *([=]{1,6})(.*?)(\\1) *\\n/i", $this->textbox1, $matches );
1988  # We can't deal with anchors, includes, html etc in the header for now,
1989  # headline would need to be parsed to improve this.
1990  if ( $hasmatch && strlen( $matches[2] ) > 0 ) {
1991  $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $matches[2] );
1992  }
1993  }
1994  $result['sectionanchor'] = $sectionanchor;
1995 
1996  // Save errors may fall down to the edit form, but we've now
1997  // merged the section into full text. Clear the section field
1998  // so that later submission of conflict forms won't try to
1999  // replace that into a duplicated mess.
2000  $this->textbox1 = $this->toEditText( $content );
2001  $this->section = '';
2002 
2003  $status->value = self::AS_SUCCESS_UPDATE;
2004  }
2005 
2006  if ( !$this->allowSelfRedirect
2007  && $content->isRedirect()
2008  && $content->getRedirectTarget()->equals( $this->getTitle() )
2009  ) {
2010  // If the page already redirects to itself, don't warn.
2011  $currentTarget = $this->getCurrentContent()->getRedirectTarget();
2012  if ( !$currentTarget || !$currentTarget->equals( $this->getTitle() ) ) {
2013  $this->selfRedirect = true;
2014  $status->fatal( 'selfredirect' );
2015  $status->value = self::AS_SELF_REDIRECT;
2016  return $status;
2017  }
2018  }
2019 
2020  // Check for length errors again now that the section is merged in
2021  $this->kblength = (int)( strlen( $this->toEditText( $content ) ) / 1024 );
2022  if ( $this->kblength > $wgMaxArticleSize ) {
2023  $this->tooBig = true;
2024  $status->setResult( false, self::AS_MAX_ARTICLE_SIZE_EXCEEDED );
2025  return $status;
2026  }
2027 
2029  ( $new ? EDIT_NEW : EDIT_UPDATE ) |
2030  ( ( $this->minoredit && !$this->isNew ) ? EDIT_MINOR : 0 ) |
2031  ( $bot ? EDIT_FORCE_BOT : 0 );
2032 
2033  $doEditStatus = $this->page->doEditContent(
2034  $content,
2035  $this->summary,
2036  $flags,
2037  false,
2038  $wgUser,
2039  $content->getDefaultFormat(),
2041  );
2042 
2043  if ( !$doEditStatus->isOK() ) {
2044  // Failure from doEdit()
2045  // Show the edit conflict page for certain recognized errors from doEdit(),
2046  // but don't show it for errors from extension hooks
2047  $errors = $doEditStatus->getErrorsArray();
2048  if ( in_array( $errors[0][0],
2049  [ 'edit-gone-missing', 'edit-conflict', 'edit-already-exists' ] )
2050  ) {
2051  $this->isConflict = true;
2052  // Destroys data doEdit() put in $status->value but who cares
2053  $doEditStatus->value = self::AS_END;
2054  }
2055  return $doEditStatus;
2056  }
2057 
2058  $result['nullEdit'] = $doEditStatus->hasMessage( 'edit-no-change' );
2059  if ( $result['nullEdit'] ) {
2060  // We don't know if it was a null edit until now, so increment here
2061  $wgUser->pingLimiter( 'linkpurge' );
2062  }
2063  $result['redirect'] = $content->isRedirect();
2064 
2065  $this->updateWatchlist();
2066 
2067  // If the content model changed, add a log entry
2068  if ( $changingContentModel ) {
2070  $wgUser,
2071  $new ? false : $oldContentModel,
2072  $this->contentModel,
2073  $this->summary
2074  );
2075  }
2076 
2077  return $status;
2078  }
2079 
2086  protected function addContentModelChangeLogEntry( User $user, $oldModel, $newModel, $reason ) {
2087  $new = $oldModel === false;
2088  $log = new ManualLogEntry( 'contentmodel', $new ? 'new' : 'change' );
2089  $log->setPerformer( $user );
2090  $log->setTarget( $this->mTitle );
2091  $log->setComment( $reason );
2092  $log->setParameters( [
2093  '4::oldmodel' => $oldModel,
2094  '5::newmodel' => $newModel
2095  ] );
2096  $logid = $log->insert();
2097  $log->publish( $logid );
2098  }
2099 
2103  protected function updateWatchlist() {
2104  global $wgUser;
2105 
2106  if ( !$wgUser->isLoggedIn() ) {
2107  return;
2108  }
2109 
2110  $user = $wgUser;
2112  $watch = $this->watchthis;
2113  // Do this in its own transaction to reduce contention...
2114  DeferredUpdates::addCallableUpdate( function () use ( $user, $title, $watch ) {
2115  if ( $watch == $user->isWatched( $title, User::IGNORE_USER_RIGHTS ) ) {
2116  return; // nothing to change
2117  }
2119  } );
2120  }
2121 
2133  private function mergeChangesIntoContent( &$editContent ) {
2134 
2135  $db = wfGetDB( DB_MASTER );
2136 
2137  // This is the revision the editor started from
2138  $baseRevision = $this->getBaseRevision();
2139  $baseContent = $baseRevision ? $baseRevision->getContent() : null;
2140 
2141  if ( is_null( $baseContent ) ) {
2142  return false;
2143  }
2144 
2145  // The current state, we want to merge updates into it
2146  $currentRevision = Revision::loadFromTitle( $db, $this->mTitle );
2147  $currentContent = $currentRevision ? $currentRevision->getContent() : null;
2148 
2149  if ( is_null( $currentContent ) ) {
2150  return false;
2151  }
2152 
2153  $handler = ContentHandler::getForModelID( $baseContent->getModel() );
2154 
2155  $result = $handler->merge3( $baseContent, $editContent, $currentContent );
2156 
2157  if ( $result ) {
2158  $editContent = $result;
2159  // Update parentRevId to what we just merged.
2160  $this->parentRevId = $currentRevision->getId();
2161  return true;
2162  }
2163 
2164  return false;
2165  }
2166 
2172  function getBaseRevision() {
2173  if ( !$this->mBaseRevision ) {
2174  $db = wfGetDB( DB_MASTER );
2175  $this->mBaseRevision = Revision::loadFromTimestamp(
2176  $db, $this->mTitle, $this->edittime );
2177  }
2178  return $this->mBaseRevision;
2179  }
2180 
2188  public static function matchSpamRegex( $text ) {
2189  global $wgSpamRegex;
2190  // For back compatibility, $wgSpamRegex may be a single string or an array of regexes.
2191  $regexes = (array)$wgSpamRegex;
2192  return self::matchSpamRegexInternal( $text, $regexes );
2193  }
2194 
2202  public static function matchSummarySpamRegex( $text ) {
2203  global $wgSummarySpamRegex;
2204  $regexes = (array)$wgSummarySpamRegex;
2205  return self::matchSpamRegexInternal( $text, $regexes );
2206  }
2207 
2213  protected static function matchSpamRegexInternal( $text, $regexes ) {
2214  foreach ( $regexes as $regex ) {
2215  $matches = [];
2216  if ( preg_match( $regex, $text, $matches ) ) {
2217  return $matches[0];
2218  }
2219  }
2220  return false;
2221  }
2222 
2223  function setHeaders() {
2224  global $wgOut, $wgUser, $wgAjaxEditStash;
2225 
2226  $wgOut->addModules( 'mediawiki.action.edit' );
2227  $wgOut->addModuleStyles( 'mediawiki.action.edit.styles' );
2228 
2229  if ( $wgUser->getOption( 'showtoolbar' ) ) {
2230  // The addition of default buttons is handled by getEditToolbar() which
2231  // has its own dependency on this module. The call here ensures the module
2232  // is loaded in time (it has position "top") for other modules to register
2233  // buttons (e.g. extensions, gadgets, user scripts).
2234  $wgOut->addModules( 'mediawiki.toolbar' );
2235  }
2236 
2237  if ( $wgUser->getOption( 'uselivepreview' ) ) {
2238  $wgOut->addModules( 'mediawiki.action.edit.preview' );
2239  }
2240 
2241  if ( $wgUser->getOption( 'useeditwarning' ) ) {
2242  $wgOut->addModules( 'mediawiki.action.edit.editWarning' );
2243  }
2244 
2245  if ( $wgAjaxEditStash ) {
2246  $wgOut->addModules( 'mediawiki.action.edit.stash' );
2247  }
2248 
2249  # Enabled article-related sidebar, toplinks, etc.
2250  $wgOut->setArticleRelated( true );
2251 
2252  $contextTitle = $this->getContextTitle();
2253  if ( $this->isConflict ) {
2254  $msg = 'editconflict';
2255  } elseif ( $contextTitle->exists() && $this->section != '' ) {
2256  $msg = $this->section == 'new' ? 'editingcomment' : 'editingsection';
2257  } else {
2258  $msg = $contextTitle->exists()
2259  || ( $contextTitle->getNamespace() == NS_MEDIAWIKI
2260  && $contextTitle->getDefaultMessageText() !== false
2261  )
2262  ? 'editing'
2263  : 'creating';
2264  }
2265 
2266  # Use the title defined by DISPLAYTITLE magic word when present
2267  # NOTE: getDisplayTitle() returns HTML while getPrefixedText() returns plain text.
2268  # setPageTitle() treats the input as wikitext, which should be safe in either case.
2269  $displayTitle = isset( $this->mParserOutput ) ? $this->mParserOutput->getDisplayTitle() : false;
2270  if ( $displayTitle === false ) {
2271  $displayTitle = $contextTitle->getPrefixedText();
2272  }
2273  $wgOut->setPageTitle( wfMessage( $msg, $displayTitle ) );
2274  # Transmit the name of the message to JavaScript for live preview
2275  # Keep Resources.php/mediawiki.action.edit.preview in sync with the possible keys
2276  $wgOut->addJsConfigVars( 'wgEditMessage', $msg );
2277  }
2278 
2282  protected function showIntro() {
2284  if ( $this->suppressIntro ) {
2285  return;
2286  }
2287 
2288  $namespace = $this->mTitle->getNamespace();
2289 
2290  if ( $namespace == NS_MEDIAWIKI ) {
2291  # Show a warning if editing an interface message
2292  $wgOut->wrapWikiMsg( "<div class='mw-editinginterface'>\n$1\n</div>", 'editinginterface' );
2293  # If this is a default message (but not css or js),
2294  # show a hint that it is translatable on translatewiki.net
2295  if ( !$this->mTitle->hasContentModel( CONTENT_MODEL_CSS )
2296  && !$this->mTitle->hasContentModel( CONTENT_MODEL_JAVASCRIPT )
2297  ) {
2298  $defaultMessageText = $this->mTitle->getDefaultMessageText();
2299  if ( $defaultMessageText !== false ) {
2300  $wgOut->wrapWikiMsg( "<div class='mw-translateinterface'>\n$1\n</div>",
2301  'translateinterface' );
2302  }
2303  }
2304  } elseif ( $namespace == NS_FILE ) {
2305  # Show a hint to shared repo
2306  $file = wfFindFile( $this->mTitle );
2307  if ( $file && !$file->isLocal() ) {
2308  $descUrl = $file->getDescriptionUrl();
2309  # there must be a description url to show a hint to shared repo
2310  if ( $descUrl ) {
2311  if ( !$this->mTitle->exists() ) {
2312  $wgOut->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-create\">\n$1\n</div>", [
2313  'sharedupload-desc-create', $file->getRepo()->getDisplayName(), $descUrl
2314  ] );
2315  } else {
2316  $wgOut->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-edit\">\n$1\n</div>", [
2317  'sharedupload-desc-edit', $file->getRepo()->getDisplayName(), $descUrl
2318  ] );
2319  }
2320  }
2321  }
2322  }
2323 
2324  # Show a warning message when someone creates/edits a user (talk) page but the user does not exist
2325  # Show log extract when the user is currently blocked
2326  if ( $namespace == NS_USER || $namespace == NS_USER_TALK ) {
2327  $username = explode( '/', $this->mTitle->getText(), 2 )[0];
2328  $user = User::newFromName( $username, false /* allow IP users*/ );
2329  $ip = User::isIP( $username );
2330  $block = Block::newFromTarget( $user, $user );
2331  if ( !( $user && $user->isLoggedIn() ) && !$ip ) { # User does not exist
2332  $wgOut->wrapWikiMsg( "<div class=\"mw-userpage-userdoesnotexist error\">\n$1\n</div>",
2333  [ 'userpage-userdoesnotexist', wfEscapeWikiText( $username ) ] );
2334  } elseif ( !is_null( $block ) && $block->getType() != Block::TYPE_AUTO ) {
2335  # Show log extract if the user is currently blocked
2337  $wgOut,
2338  'block',
2339  MWNamespace::getCanonicalName( NS_USER ) . ':' . $block->getTarget(),
2340  '',
2341  [
2342  'lim' => 1,
2343  'showIfEmpty' => false,
2344  'msgKey' => [
2345  'blocked-notice-logextract',
2346  $user->getName() # Support GENDER in notice
2347  ]
2348  ]
2349  );
2350  }
2351  }
2352  # Try to add a custom edit intro, or use the standard one if this is not possible.
2353  if ( !$this->showCustomIntro() && !$this->mTitle->exists() ) {
2355  wfMessage( 'helppage' )->inContentLanguage()->text()
2356  ) );
2357  if ( $wgUser->isLoggedIn() ) {
2358  $wgOut->wrapWikiMsg(
2359  // Suppress the external link icon, consider the help url an internal one
2360  "<div class=\"mw-newarticletext plainlinks\">\n$1\n</div>",
2361  [
2362  'newarticletext',
2363  $helpLink
2364  ]
2365  );
2366  } else {
2367  $wgOut->wrapWikiMsg(
2368  // Suppress the external link icon, consider the help url an internal one
2369  "<div class=\"mw-newarticletextanon plainlinks\">\n$1\n</div>",
2370  [
2371  'newarticletextanon',
2372  $helpLink
2373  ]
2374  );
2375  }
2376  }
2377  # Give a notice if the user is editing a deleted/moved page...
2378  if ( !$this->mTitle->exists() ) {
2379  LogEventsList::showLogExtract( $wgOut, [ 'delete', 'move' ], $this->mTitle,
2380  '',
2381  [
2382  'lim' => 10,
2383  'conds' => [ "log_action != 'revision'" ],
2384  'showIfEmpty' => false,
2385  'msgKey' => [ 'recreate-moveddeleted-warn' ]
2386  ]
2387  );
2388  }
2389  }
2390 
2396  protected function showCustomIntro() {
2397  if ( $this->editintro ) {
2398  $title = Title::newFromText( $this->editintro );
2399  if ( $title instanceof Title && $title->exists() && $title->userCan( 'read' ) ) {
2400  global $wgOut;
2401  // Added using template syntax, to take <noinclude>'s into account.
2402  $wgOut->addWikiTextTitleTidy(
2403  '<div class="mw-editintro">{{:' . $title->getFullText() . '}}</div>',
2405  );
2406  return true;
2407  }
2408  }
2409  return false;
2410  }
2411 
2430  protected function toEditText( $content ) {
2431  if ( $content === null || $content === false || is_string( $content ) ) {
2432  return $content;
2433  }
2434 
2435  if ( !$this->isSupportedContentModel( $content->getModel() ) ) {
2436  throw new MWException( 'This content model is not supported: '
2437  . ContentHandler::getLocalizedName( $content->getModel() ) );
2438  }
2439 
2440  return $content->serialize( $this->contentFormat );
2441  }
2442 
2459  protected function toEditContent( $text ) {
2460  if ( $text === false || $text === null ) {
2461  return $text;
2462  }
2463 
2464  $content = ContentHandler::makeContent( $text, $this->getTitle(),
2465  $this->contentModel, $this->contentFormat );
2466 
2467  if ( !$this->isSupportedContentModel( $content->getModel() ) ) {
2468  throw new MWException( 'This content model is not supported: '
2469  . ContentHandler::getLocalizedName( $content->getModel() ) );
2470  }
2471 
2472  return $content;
2473  }
2474 
2483  function showEditForm( $formCallback = null ) {
2485 
2486  # need to parse the preview early so that we know which templates are used,
2487  # otherwise users with "show preview after edit box" will get a blank list
2488  # we parse this near the beginning so that setHeaders can do the title
2489  # setting work instead of leaving it in getPreviewText
2490  $previewOutput = '';
2491  if ( $this->formtype == 'preview' ) {
2492  $previewOutput = $this->getPreviewText();
2493  }
2494 
2495  Hooks::run( 'EditPage::showEditForm:initial', [ &$this, &$wgOut ] );
2496 
2497  $this->setHeaders();
2498 
2499  if ( $this->showHeader() === false ) {
2500  return;
2501  }
2502 
2503  $wgOut->addHTML( $this->editFormPageTop );
2504 
2505  if ( $wgUser->getOption( 'previewontop' ) ) {
2506  $this->displayPreviewArea( $previewOutput, true );
2507  }
2508 
2509  $wgOut->addHTML( $this->editFormTextTop );
2510 
2511  $showToolbar = true;
2512  if ( $this->wasDeletedSinceLastEdit() ) {
2513  if ( $this->formtype == 'save' ) {
2514  // Hide the toolbar and edit area, user can click preview to get it back
2515  // Add an confirmation checkbox and explanation.
2516  $showToolbar = false;
2517  } else {
2518  $wgOut->wrapWikiMsg( "<div class='error mw-deleted-while-editing'>\n$1\n</div>",
2519  'deletedwhileediting' );
2520  }
2521  }
2522 
2523  // @todo add EditForm plugin interface and use it here!
2524  // search for textarea1 and textares2, and allow EditForm to override all uses.
2525  $wgOut->addHTML( Html::openElement(
2526  'form',
2527  [
2528  'id' => self::EDITFORM_ID,
2529  'name' => self::EDITFORM_ID,
2530  'method' => 'post',
2531  'action' => $this->getActionURL( $this->getContextTitle() ),
2532  'enctype' => 'multipart/form-data'
2533  ]
2534  ) );
2535 
2536  if ( is_callable( $formCallback ) ) {
2537  wfWarn( 'The $formCallback parameter to ' . __METHOD__ . 'is deprecated' );
2538  call_user_func_array( $formCallback, [ &$wgOut ] );
2539  }
2540 
2541  // Add an empty field to trip up spambots
2542  $wgOut->addHTML(
2543  Xml::openElement( 'div', [ 'id' => 'antispam-container', 'style' => 'display: none;' ] )
2544  . Html::rawElement(
2545  'label',
2546  [ 'for' => 'wpAntispam' ],
2547  wfMessage( 'simpleantispam-label' )->parse()
2548  )
2549  . Xml::element(
2550  'input',
2551  [
2552  'type' => 'text',
2553  'name' => 'wpAntispam',
2554  'id' => 'wpAntispam',
2555  'value' => ''
2556  ]
2557  )
2558  . Xml::closeElement( 'div' )
2559  );
2560 
2561  Hooks::run( 'EditPage::showEditForm:fields', [ &$this, &$wgOut ] );
2562 
2563  // Put these up at the top to ensure they aren't lost on early form submission
2564  $this->showFormBeforeText();
2565 
2566  if ( $this->wasDeletedSinceLastEdit() && 'save' == $this->formtype ) {
2567  $username = $this->lastDelete->user_name;
2568  $comment = $this->lastDelete->log_comment;
2569 
2570  // It is better to not parse the comment at all than to have templates expanded in the middle
2571  // TODO: can the checkLabel be moved outside of the div so that wrapWikiMsg could be used?
2572  $key = $comment === ''
2573  ? 'confirmrecreate-noreason'
2574  : 'confirmrecreate';
2575  $wgOut->addHTML(
2576  '<div class="mw-confirm-recreate">' .
2577  wfMessage( $key, $username, "<nowiki>$comment</nowiki>" )->parse() .
2578  Xml::checkLabel( wfMessage( 'recreate' )->text(), 'wpRecreate', 'wpRecreate', false,
2579  [ 'title' => Linker::titleAttrib( 'recreate' ), 'tabindex' => 1, 'id' => 'wpRecreate' ]
2580  ) .
2581  '</div>'
2582  );
2583  }
2584 
2585  # When the summary is hidden, also hide them on preview/show changes
2586  if ( $this->nosummary ) {
2587  $wgOut->addHTML( Html::hidden( 'nosummary', true ) );
2588  }
2589 
2590  # If a blank edit summary was previously provided, and the appropriate
2591  # user preference is active, pass a hidden tag as wpIgnoreBlankSummary. This will stop the
2592  # user being bounced back more than once in the event that a summary
2593  # is not required.
2594  # ####
2595  # For a bit more sophisticated detection of blank summaries, hash the
2596  # automatic one and pass that in the hidden field wpAutoSummary.
2597  if ( $this->missingSummary || ( $this->section == 'new' && $this->nosummary ) ) {
2598  $wgOut->addHTML( Html::hidden( 'wpIgnoreBlankSummary', true ) );
2599  }
2600 
2601  if ( $this->undidRev ) {
2602  $wgOut->addHTML( Html::hidden( 'wpUndidRevision', $this->undidRev ) );
2603  }
2604 
2605  if ( $this->selfRedirect ) {
2606  $wgOut->addHTML( Html::hidden( 'wpIgnoreSelfRedirect', true ) );
2607  }
2608 
2609  if ( $this->hasPresetSummary ) {
2610  // If a summary has been preset using &summary= we don't want to prompt for
2611  // a different summary. Only prompt for a summary if the summary is blanked.
2612  // (Bug 17416)
2613  $this->autoSumm = md5( '' );
2614  }
2615 
2616  $autosumm = $this->autoSumm ? $this->autoSumm : md5( $this->summary );
2617  $wgOut->addHTML( Html::hidden( 'wpAutoSummary', $autosumm ) );
2618 
2619  $wgOut->addHTML( Html::hidden( 'oldid', $this->oldid ) );
2620  $wgOut->addHTML( Html::hidden( 'parentRevId', $this->getParentRevId() ) );
2621 
2622  $wgOut->addHTML( Html::hidden( 'format', $this->contentFormat ) );
2623  $wgOut->addHTML( Html::hidden( 'model', $this->contentModel ) );
2624 
2625  if ( $this->section == 'new' ) {
2626  $this->showSummaryInput( true, $this->summary );
2627  $wgOut->addHTML( $this->getSummaryPreview( true, $this->summary ) );
2628  }
2629 
2630  $wgOut->addHTML( $this->editFormTextBeforeContent );
2631 
2632  if ( !$this->isCssJsSubpage && $showToolbar && $wgUser->getOption( 'showtoolbar' ) ) {
2633  $wgOut->addHTML( EditPage::getEditToolbar( $this->mTitle ) );
2634  }
2635 
2636  if ( $this->blankArticle ) {
2637  $wgOut->addHTML( Html::hidden( 'wpIgnoreBlankArticle', true ) );
2638  }
2639 
2640  if ( $this->isConflict ) {
2641  // In an edit conflict bypass the overridable content form method
2642  // and fallback to the raw wpTextbox1 since editconflicts can't be
2643  // resolved between page source edits and custom ui edits using the
2644  // custom edit ui.
2645  $this->textbox2 = $this->textbox1;
2646 
2647  $content = $this->getCurrentContent();
2648  $this->textbox1 = $this->toEditText( $content );
2649 
2650  $this->showTextbox1();
2651  } else {
2652  $this->showContentForm();
2653  }
2654 
2655  $wgOut->addHTML( $this->editFormTextAfterContent );
2656 
2657  $this->showStandardInputs();
2658 
2659  $this->showFormAfterText();
2660 
2661  $this->showTosSummary();
2662 
2663  $this->showEditTools();
2664 
2665  $wgOut->addHTML( $this->editFormTextAfterTools . "\n" );
2666 
2667  $wgOut->addHTML( Html::rawElement( 'div', [ 'class' => 'templatesUsed' ],
2668  Linker::formatTemplates( $this->getTemplates(), $this->preview, $this->section != '' ) ) );
2669 
2670  $wgOut->addHTML( Html::rawElement( 'div', [ 'class' => 'hiddencats' ],
2671  Linker::formatHiddenCategories( $this->page->getHiddenCategories() ) ) );
2672 
2673  $wgOut->addHTML( Html::rawElement( 'div', [ 'class' => 'limitreport' ],
2674  self::getPreviewLimitReport( $this->mParserOutput ) ) );
2675 
2676  $wgOut->addModules( 'mediawiki.action.edit.collapsibleFooter' );
2677 
2678  if ( $this->isConflict ) {
2679  try {
2680  $this->showConflict();
2681  } catch ( MWContentSerializationException $ex ) {
2682  // this can't really happen, but be nice if it does.
2683  $msg = wfMessage(
2684  'content-failed-to-parse',
2685  $this->contentModel,
2686  $this->contentFormat,
2687  $ex->getMessage()
2688  );
2689  $wgOut->addWikiText( '<div class="error">' . $msg->text() . '</div>' );
2690  }
2691  }
2692 
2693  // Marker for detecting truncated form data. This must be the last
2694  // parameter sent in order to be of use, so do not move me.
2695  $wgOut->addHTML( Html::hidden( 'wpUltimateParam', true ) );
2696  $wgOut->addHTML( $this->editFormTextBottom . "\n</form>\n" );
2697 
2698  if ( !$wgUser->getOption( 'previewontop' ) ) {
2699  $this->displayPreviewArea( $previewOutput, false );
2700  }
2701 
2702  }
2703 
2710  public static function extractSectionTitle( $text ) {
2711  preg_match( "/^(=+)(.+)\\1\\s*(\n|$)/i", $text, $matches );
2712  if ( !empty( $matches[2] ) ) {
2713  global $wgParser;
2714  return $wgParser->stripSectionName( trim( $matches[2] ) );
2715  } else {
2716  return false;
2717  }
2718  }
2719 
2723  protected function showHeader() {
2726 
2727  if ( $this->mTitle->isTalkPage() ) {
2728  $wgOut->addWikiMsg( 'talkpagetext' );
2729  }
2730 
2731  // Add edit notices
2732  $editNotices = $this->mTitle->getEditNotices( $this->oldid );
2733  if ( count( $editNotices ) ) {
2734  $wgOut->addHTML( implode( "\n", $editNotices ) );
2735  } else {
2736  $msg = wfMessage( 'editnotice-notext' );
2737  if ( !$msg->isDisabled() ) {
2738  $wgOut->addHTML(
2739  '<div class="mw-editnotice-notext">'
2740  . $msg->parseAsBlock()
2741  . '</div>'
2742  );
2743  }
2744  }
2745 
2746  if ( $this->isConflict ) {
2747  $wgOut->wrapWikiMsg( "<div class='mw-explainconflict'>\n$1\n</div>", 'explainconflict' );
2748  $this->edittime = $this->page->getTimestamp();
2749  } else {
2750  if ( $this->section != '' && !$this->isSectionEditSupported() ) {
2751  // We use $this->section to much before this and getVal('wgSection') directly in other places
2752  // at this point we can't reset $this->section to '' to fallback to non-section editing.
2753  // Someone is welcome to try refactoring though
2754  $wgOut->showErrorPage( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
2755  return false;
2756  }
2757 
2758  if ( $this->section != '' && $this->section != 'new' ) {
2759  if ( !$this->summary && !$this->preview && !$this->diff ) {
2760  $sectionTitle = self::extractSectionTitle( $this->textbox1 ); // FIXME: use Content object
2761  if ( $sectionTitle !== false ) {
2762  $this->summary = "/* $sectionTitle */ ";
2763  }
2764  }
2765  }
2766 
2767  if ( $this->missingComment ) {
2768  $wgOut->wrapWikiMsg( "<div id='mw-missingcommenttext'>\n$1\n</div>", 'missingcommenttext' );
2769  }
2770 
2771  if ( $this->missingSummary && $this->section != 'new' ) {
2772  $wgOut->wrapWikiMsg( "<div id='mw-missingsummary'>\n$1\n</div>", 'missingsummary' );
2773  }
2774 
2775  if ( $this->missingSummary && $this->section == 'new' ) {
2776  $wgOut->wrapWikiMsg( "<div id='mw-missingcommentheader'>\n$1\n</div>", 'missingcommentheader' );
2777  }
2778 
2779  if ( $this->blankArticle ) {
2780  $wgOut->wrapWikiMsg( "<div id='mw-blankarticle'>\n$1\n</div>", 'blankarticle' );
2781  }
2782 
2783  if ( $this->selfRedirect ) {
2784  $wgOut->wrapWikiMsg( "<div id='mw-selfredirect'>\n$1\n</div>", 'selfredirect' );
2785  }
2786 
2787  if ( $this->hookError !== '' ) {
2788  $wgOut->addWikiText( $this->hookError );
2789  }
2790 
2791  if ( !$this->checkUnicodeCompliantBrowser() ) {
2792  $wgOut->addWikiMsg( 'nonunicodebrowser' );
2793  }
2794 
2795  if ( $this->section != 'new' ) {
2796  $revision = $this->mArticle->getRevisionFetched();
2797  if ( $revision ) {
2798  // Let sysop know that this will make private content public if saved
2799 
2800  if ( !$revision->userCan( Revision::DELETED_TEXT, $wgUser ) ) {
2801  $wgOut->wrapWikiMsg(
2802  "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
2803  'rev-deleted-text-permission'
2804  );
2805  } elseif ( $revision->isDeleted( Revision::DELETED_TEXT ) ) {
2806  $wgOut->wrapWikiMsg(
2807  "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
2808  'rev-deleted-text-view'
2809  );
2810  }
2811 
2812  if ( !$revision->isCurrent() ) {
2813  $this->mArticle->setOldSubtitle( $revision->getId() );
2814  $wgOut->addWikiMsg( 'editingold' );
2815  }
2816  } elseif ( $this->mTitle->exists() ) {
2817  // Something went wrong
2818 
2819  $wgOut->wrapWikiMsg( "<div class='errorbox'>\n$1\n</div>\n",
2820  [ 'missing-revision', $this->oldid ] );
2821  }
2822  }
2823  }
2824 
2825  if ( wfReadOnly() ) {
2826  $wgOut->wrapWikiMsg(
2827  "<div id=\"mw-read-only-warning\">\n$1\n</div>",
2828  [ 'readonlywarning', wfReadOnlyReason() ]
2829  );
2830  } elseif ( $wgUser->isAnon() ) {
2831  if ( $this->formtype != 'preview' ) {
2832  $wgOut->wrapWikiMsg(
2833  "<div id='mw-anon-edit-warning' class='warningbox'>\n$1\n</div>",
2834  [ 'anoneditwarning',
2835  // Log-in link
2836  '{{fullurl:Special:UserLogin|returnto={{FULLPAGENAMEE}}}}',
2837  // Sign-up link
2838  '{{fullurl:Special:CreateAccount|returnto={{FULLPAGENAMEE}}}}' ]
2839  );
2840  } else {
2841  $wgOut->wrapWikiMsg( "<div id=\"mw-anon-preview-warning\" class=\"warningbox\">\n$1</div>",
2842  'anonpreviewwarning'
2843  );
2844  }
2845  } else {
2846  if ( $this->isCssJsSubpage ) {
2847  # Check the skin exists
2848  if ( $this->isWrongCaseCssJsPage ) {
2849  $wgOut->wrapWikiMsg(
2850  "<div class='error' id='mw-userinvalidcssjstitle'>\n$1\n</div>",
2851  [ 'userinvalidcssjstitle', $this->mTitle->getSkinFromCssJsSubpage() ]
2852  );
2853  }
2854  if ( $this->getTitle()->isSubpageOf( $wgUser->getUserPage() ) ) {
2855  if ( $this->formtype !== 'preview' ) {
2856  if ( $this->isCssSubpage && $wgAllowUserCss ) {
2857  $wgOut->wrapWikiMsg(
2858  "<div id='mw-usercssyoucanpreview'>\n$1\n</div>",
2859  [ 'usercssyoucanpreview' ]
2860  );
2861  }
2862 
2863  if ( $this->isJsSubpage && $wgAllowUserJs ) {
2864  $wgOut->wrapWikiMsg(
2865  "<div id='mw-userjsyoucanpreview'>\n$1\n</div>",
2866  [ 'userjsyoucanpreview' ]
2867  );
2868  }
2869  }
2870  }
2871  }
2872  }
2873 
2874  if ( $this->mTitle->isProtected( 'edit' ) &&
2875  MWNamespace::getRestrictionLevels( $this->mTitle->getNamespace() ) !== [ '' ]
2876  ) {
2877  # Is the title semi-protected?
2878  if ( $this->mTitle->isSemiProtected() ) {
2879  $noticeMsg = 'semiprotectedpagewarning';
2880  } else {
2881  # Then it must be protected based on static groups (regular)
2882  $noticeMsg = 'protectedpagewarning';
2883  }
2884  LogEventsList::showLogExtract( $wgOut, 'protect', $this->mTitle, '',
2885  [ 'lim' => 1, 'msgKey' => [ $noticeMsg ] ] );
2886  }
2887  if ( $this->mTitle->isCascadeProtected() ) {
2888  # Is this page under cascading protection from some source pages?
2889 
2890  list( $cascadeSources, /* $restrictions */ ) = $this->mTitle->getCascadeProtectionSources();
2891  $notice = "<div class='mw-cascadeprotectedwarning'>\n$1\n";
2892  $cascadeSourcesCount = count( $cascadeSources );
2893  if ( $cascadeSourcesCount > 0 ) {
2894  # Explain, and list the titles responsible
2895  foreach ( $cascadeSources as $page ) {
2896  $notice .= '* [[:' . $page->getPrefixedText() . "]]\n";
2897  }
2898  }
2899  $notice .= '</div>';
2900  $wgOut->wrapWikiMsg( $notice, [ 'cascadeprotectedwarning', $cascadeSourcesCount ] );
2901  }
2902  if ( !$this->mTitle->exists() && $this->mTitle->getRestrictions( 'create' ) ) {
2903  LogEventsList::showLogExtract( $wgOut, 'protect', $this->mTitle, '',
2904  [ 'lim' => 1,
2905  'showIfEmpty' => false,
2906  'msgKey' => [ 'titleprotectedwarning' ],
2907  'wrap' => "<div class=\"mw-titleprotectedwarning\">\n$1</div>" ] );
2908  }
2909 
2910  if ( $this->kblength === false ) {
2911  $this->kblength = (int)( strlen( $this->textbox1 ) / 1024 );
2912  }
2913 
2914  if ( $this->tooBig || $this->kblength > $wgMaxArticleSize ) {
2915  $wgOut->wrapWikiMsg( "<div class='error' id='mw-edit-longpageerror'>\n$1\n</div>",
2916  [
2917  'longpageerror',
2918  $wgLang->formatNum( $this->kblength ),
2919  $wgLang->formatNum( $wgMaxArticleSize )
2920  ]
2921  );
2922  } else {
2923  if ( !wfMessage( 'longpage-hint' )->isDisabled() ) {
2924  $wgOut->wrapWikiMsg( "<div id='mw-edit-longpage-hint'>\n$1\n</div>",
2925  [
2926  'longpage-hint',
2927  $wgLang->formatSize( strlen( $this->textbox1 ) ),
2928  strlen( $this->textbox1 )
2929  ]
2930  );
2931  }
2932  }
2933  # Add header copyright warning
2934  $this->showHeaderCopyrightWarning();
2935 
2936  return true;
2937  }
2938 
2953  function getSummaryInput( $summary = "", $labelText = null,
2954  $inputAttrs = null, $spanLabelAttrs = null
2955  ) {
2956  // Note: the maxlength is overridden in JS to 255 and to make it use UTF-8 bytes, not characters.
2957  $inputAttrs = ( is_array( $inputAttrs ) ? $inputAttrs : [] ) + [
2958  'id' => 'wpSummary',
2959  'maxlength' => '200',
2960  'tabindex' => '1',
2961  'size' => 60,
2962  'spellcheck' => 'true',
2963  ] + Linker::tooltipAndAccesskeyAttribs( 'summary' );
2964 
2965  $spanLabelAttrs = ( is_array( $spanLabelAttrs ) ? $spanLabelAttrs : [] ) + [
2966  'class' => $this->missingSummary ? 'mw-summarymissed' : 'mw-summary',
2967  'id' => "wpSummaryLabel"
2968  ];
2969 
2970  $label = null;
2971  if ( $labelText ) {
2972  $label = Xml::tags(
2973  'label',
2974  $inputAttrs['id'] ? [ 'for' => $inputAttrs['id'] ] : null,
2975  $labelText
2976  );
2977  $label = Xml::tags( 'span', $spanLabelAttrs, $label );
2978  }
2979 
2980  $input = Html::input( 'wpSummary', $summary, 'text', $inputAttrs );
2981 
2982  return [ $label, $input ];
2983  }
2984 
2991  protected function showSummaryInput( $isSubjectPreview, $summary = "" ) {
2993  # Add a class if 'missingsummary' is triggered to allow styling of the summary line
2994  $summaryClass = $this->missingSummary ? 'mw-summarymissed' : 'mw-summary';
2995  if ( $isSubjectPreview ) {
2996  if ( $this->nosummary ) {
2997  return;
2998  }
2999  } else {
3000  if ( !$this->mShowSummaryField ) {
3001  return;
3002  }
3003  }
3004  $summary = $wgContLang->recodeForEdit( $summary );
3005  $labelText = wfMessage( $isSubjectPreview ? 'subject' : 'summary' )->parse();
3006  list( $label, $input ) = $this->getSummaryInput(
3007  $summary,
3008  $labelText,
3009  [ 'class' => $summaryClass ],
3010  []
3011  );
3012  $wgOut->addHTML( "{$label} {$input}" );
3013  }
3014 
3022  protected function getSummaryPreview( $isSubjectPreview, $summary = "" ) {
3023  // avoid spaces in preview, gets always trimmed on save
3024  $summary = trim( $summary );
3025  if ( !$summary || ( !$this->preview && !$this->diff ) ) {
3026  return "";
3027  }
3028 
3029  global $wgParser;
3030 
3031  if ( $isSubjectPreview ) {
3032  $summary = wfMessage( 'newsectionsummary' )->rawParams( $wgParser->stripSectionName( $summary ) )
3033  ->inContentLanguage()->text();
3034  }
3035 
3036  $message = $isSubjectPreview ? 'subject-preview' : 'summary-preview';
3037 
3038  $summary = wfMessage( $message )->parse()
3039  . Linker::commentBlock( $summary, $this->mTitle, $isSubjectPreview );
3040  return Xml::tags( 'div', [ 'class' => 'mw-summary-preview' ], $summary );
3041  }
3042 
3043  protected function showFormBeforeText() {
3044  global $wgOut;
3045  $section = htmlspecialchars( $this->section );
3046  $wgOut->addHTML( <<<HTML
3047 <input type='hidden' value="{$section}" name="wpSection"/>
3048 <input type='hidden' value="{$this->starttime}" name="wpStarttime" />
3049 <input type='hidden' value="{$this->edittime}" name="wpEdittime" />
3050 <input type='hidden' value="{$this->scrolltop}" name="wpScrolltop" id="wpScrolltop" />
3051 
3052 HTML
3053  );
3054  if ( !$this->checkUnicodeCompliantBrowser() ) {
3055  $wgOut->addHTML( Html::hidden( 'safemode', '1' ) );
3056  }
3057  }
3058 
3059  protected function showFormAfterText() {
3073  $wgOut->addHTML( "\n" . Html::hidden( "wpEditToken", $wgUser->getEditToken() ) . "\n" );
3074  }
3075 
3084  protected function showContentForm() {
3085  $this->showTextbox1();
3086  }
3087 
3096  protected function showTextbox1( $customAttribs = null, $textoverride = null ) {
3097  if ( $this->wasDeletedSinceLastEdit() && $this->formtype == 'save' ) {
3098  $attribs = [ 'style' => 'display:none;' ];
3099  } else {
3100  $classes = []; // Textarea CSS
3101  if ( $this->mTitle->isProtected( 'edit' ) &&
3102  MWNamespace::getRestrictionLevels( $this->mTitle->getNamespace() ) !== [ '' ]
3103  ) {
3104  # Is the title semi-protected?
3105  if ( $this->mTitle->isSemiProtected() ) {
3106  $classes[] = 'mw-textarea-sprotected';
3107  } else {
3108  # Then it must be protected based on static groups (regular)
3109  $classes[] = 'mw-textarea-protected';
3110  }
3111  # Is the title cascade-protected?
3112  if ( $this->mTitle->isCascadeProtected() ) {
3113  $classes[] = 'mw-textarea-cprotected';
3114  }
3115  }
3116 
3117  $attribs = [ 'tabindex' => 1 ];
3118 
3119  if ( is_array( $customAttribs ) ) {
3121  }
3122 
3123  if ( count( $classes ) ) {
3124  if ( isset( $attribs['class'] ) ) {
3125  $classes[] = $attribs['class'];
3126  }
3127  $attribs['class'] = implode( ' ', $classes );
3128  }
3129  }
3130 
3131  $this->showTextbox(
3132  $textoverride !== null ? $textoverride : $this->textbox1,
3133  'wpTextbox1',
3134  $attribs
3135  );
3136  }
3137 
3138  protected function showTextbox2() {
3139  $this->showTextbox( $this->textbox2, 'wpTextbox2', [ 'tabindex' => 6, 'readonly' ] );
3140  }
3141 
3142  protected function showTextbox( $text, $name, $customAttribs = [] ) {
3144 
3145  $wikitext = $this->safeUnicodeOutput( $text );
3146  if ( strval( $wikitext ) !== '' ) {
3147  // Ensure there's a newline at the end, otherwise adding lines
3148  // is awkward.
3149  // But don't add a newline if the ext is empty, or Firefox in XHTML
3150  // mode will show an extra newline. A bit annoying.
3151  $wikitext .= "\n";
3152  }
3153 
3154  $attribs = $customAttribs + [
3155  'accesskey' => ',',
3156  'id' => $name,
3157  'cols' => $wgUser->getIntOption( 'cols' ),
3158  'rows' => $wgUser->getIntOption( 'rows' ),
3159  // Avoid PHP notices when appending preferences
3160  // (appending allows customAttribs['style'] to still work).
3161  'style' => ''
3162  ];
3163 
3164  $pageLang = $this->mTitle->getPageLanguage();
3165  $attribs['lang'] = $pageLang->getHtmlCode();
3166  $attribs['dir'] = $pageLang->getDir();
3167 
3168  $wgOut->addHTML( Html::textarea( $name, $wikitext, $attribs ) );
3169  }
3170 
3171  protected function displayPreviewArea( $previewOutput, $isOnTop = false ) {
3172  global $wgOut;
3173  $classes = [];
3174  if ( $isOnTop ) {
3175  $classes[] = 'ontop';
3176  }
3177 
3178  $attribs = [ 'id' => 'wikiPreview', 'class' => implode( ' ', $classes ) ];
3179 
3180  if ( $this->formtype != 'preview' ) {
3181  $attribs['style'] = 'display: none;';
3182  }
3183 
3184  $wgOut->addHTML( Xml::openElement( 'div', $attribs ) );
3185 
3186  if ( $this->formtype == 'preview' ) {
3187  $this->showPreview( $previewOutput );
3188  } else {
3189  // Empty content container for LivePreview
3190  $pageViewLang = $this->mTitle->getPageViewLanguage();
3191  $attribs = [ 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(),
3192  'class' => 'mw-content-' . $pageViewLang->getDir() ];
3193  $wgOut->addHTML( Html::rawElement( 'div', $attribs ) );
3194  }
3195 
3196  $wgOut->addHTML( '</div>' );
3197 
3198  if ( $this->formtype == 'diff' ) {
3199  try {
3200  $this->showDiff();
3201  } catch ( MWContentSerializationException $ex ) {
3202  $msg = wfMessage(
3203  'content-failed-to-parse',
3204  $this->contentModel,
3205  $this->contentFormat,
3206  $ex->getMessage()
3207  );
3208  $wgOut->addWikiText( '<div class="error">' . $msg->text() . '</div>' );
3209  }
3210  }
3211  }
3212 
3219  protected function showPreview( $text ) {
3220  global $wgOut;
3221  if ( $this->mTitle->getNamespace() == NS_CATEGORY ) {
3222  $this->mArticle->openShowCategory();
3223  }
3224  # This hook seems slightly odd here, but makes things more
3225  # consistent for extensions.
3226  Hooks::run( 'OutputPageBeforeHTML', [ &$wgOut, &$text ] );
3227  $wgOut->addHTML( $text );
3228  if ( $this->mTitle->getNamespace() == NS_CATEGORY ) {
3229  $this->mArticle->closeShowCategory();
3230  }
3231  }
3232 
3240  function showDiff() {
3242 
3243  $oldtitlemsg = 'currentrev';
3244  # if message does not exist, show diff against the preloaded default
3245  if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && !$this->mTitle->exists() ) {
3246  $oldtext = $this->mTitle->getDefaultMessageText();
3247  if ( $oldtext !== false ) {
3248  $oldtitlemsg = 'defaultmessagetext';
3249  $oldContent = $this->toEditContent( $oldtext );
3250  } else {
3251  $oldContent = null;
3252  }
3253  } else {
3254  $oldContent = $this->getCurrentContent();
3255  }
3256 
3257  $textboxContent = $this->toEditContent( $this->textbox1 );
3258 
3259  $newContent = $this->page->replaceSectionContent(
3260  $this->section, $textboxContent,
3261  $this->summary, $this->edittime );
3262 
3263  if ( $newContent ) {
3264  ContentHandler::runLegacyHooks( 'EditPageGetDiffText', [ $this, &$newContent ] );
3265  Hooks::run( 'EditPageGetDiffContent', [ $this, &$newContent ] );
3266 
3267  $popts = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang );
3268  $newContent = $newContent->preSaveTransform( $this->mTitle, $wgUser, $popts );
3269  }
3270 
3271  if ( ( $oldContent && !$oldContent->isEmpty() ) || ( $newContent && !$newContent->isEmpty() ) ) {
3272  $oldtitle = wfMessage( $oldtitlemsg )->parse();
3273  $newtitle = wfMessage( 'yourtext' )->parse();
3274 
3275  if ( !$oldContent ) {
3276  $oldContent = $newContent->getContentHandler()->makeEmptyContent();
3277  }
3278 
3279  if ( !$newContent ) {
3280  $newContent = $oldContent->getContentHandler()->makeEmptyContent();
3281  }
3282 
3283  $de = $oldContent->getContentHandler()->createDifferenceEngine( $this->mArticle->getContext() );
3284  $de->setContent( $oldContent, $newContent );
3285 
3286  $difftext = $de->getDiff( $oldtitle, $newtitle );
3287  $de->showDiffStyle();
3288  } else {
3289  $difftext = '';
3290  }
3291 
3292  $wgOut->addHTML( '<div id="wikiDiff">' . $difftext . '</div>' );
3293  }
3294 
3298  protected function showHeaderCopyrightWarning() {
3299  $msg = 'editpage-head-copy-warn';
3300  if ( !wfMessage( $msg )->isDisabled() ) {
3301  global $wgOut;
3302  $wgOut->wrapWikiMsg( "<div class='editpage-head-copywarn'>\n$1\n</div>",
3303  'editpage-head-copy-warn' );
3304  }
3305  }
3306 
3315  protected function showTosSummary() {
3316  $msg = 'editpage-tos-summary';
3317  Hooks::run( 'EditPageTosSummary', [ $this->mTitle, &$msg ] );
3318  if ( !wfMessage( $msg )->isDisabled() ) {
3319  global $wgOut;
3320  $wgOut->addHTML( '<div class="mw-tos-summary">' );
3321  $wgOut->addWikiMsg( $msg );
3322  $wgOut->addHTML( '</div>' );
3323  }
3324  }
3325 
3326  protected function showEditTools() {
3327  global $wgOut;
3328  $wgOut->addHTML( '<div class="mw-editTools">' .
3329  wfMessage( 'edittools' )->inContentLanguage()->parse() .
3330  '</div>' );
3331  }
3332 
3339  protected function getCopywarn() {
3340  return self::getCopyrightWarning( $this->mTitle );
3341  }
3342 
3350  public static function getCopyrightWarning( $title, $format = 'plain' ) {
3351  global $wgRightsText;
3352  if ( $wgRightsText ) {
3353  $copywarnMsg = [ 'copyrightwarning',
3354  '[[' . wfMessage( 'copyrightpage' )->inContentLanguage()->text() . ']]',
3355  $wgRightsText ];
3356  } else {
3357  $copywarnMsg = [ 'copyrightwarning2',
3358  '[[' . wfMessage( 'copyrightpage' )->inContentLanguage()->text() . ']]' ];
3359  }
3360  // Allow for site and per-namespace customization of contribution/copyright notice.
3361  Hooks::run( 'EditPageCopyrightWarning', [ $title, &$copywarnMsg ] );
3362 
3363  return "<div id=\"editpage-copywarn\">\n" .
3364  call_user_func_array( 'wfMessage', $copywarnMsg )->$format() . "\n</div>";
3365  }
3366 
3374  public static function getPreviewLimitReport( $output ) {
3375  if ( !$output || !$output->getLimitReportData() ) {
3376  return '';
3377  }
3378 
3379  $limitReport = Html::rawElement( 'div', [ 'class' => 'mw-limitReportExplanation' ],
3380  wfMessage( 'limitreport-title' )->parseAsBlock()
3381  );
3382 
3383  // Show/hide animation doesn't work correctly on a table, so wrap it in a div.
3384  $limitReport .= Html::openElement( 'div', [ 'class' => 'preview-limit-report-wrapper' ] );
3385 
3386  $limitReport .= Html::openElement( 'table', [
3387  'class' => 'preview-limit-report wikitable'
3388  ] ) .
3389  Html::openElement( 'tbody' );
3390 
3391  foreach ( $output->getLimitReportData() as $key => $value ) {
3392  if ( Hooks::run( 'ParserLimitReportFormat',
3393  [ $key, &$value, &$limitReport, true, true ]
3394  ) ) {
3395  $keyMsg = wfMessage( $key );
3396  $valueMsg = wfMessage( [ "$key-value-html", "$key-value" ] );
3397  if ( !$valueMsg->exists() ) {
3398  $valueMsg = new RawMessage( '$1' );
3399  }
3400  if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) {
3401  $limitReport .= Html::openElement( 'tr' ) .
3402  Html::rawElement( 'th', null, $keyMsg->parse() ) .
3403  Html::rawElement( 'td', null, $valueMsg->params( $value )->parse() ) .
3404  Html::closeElement( 'tr' );
3405  }
3406  }
3407  }
3408 
3409  $limitReport .= Html::closeElement( 'tbody' ) .
3410  Html::closeElement( 'table' ) .
3411  Html::closeElement( 'div' );
3412 
3413  return $limitReport;
3414  }
3415 
3416  protected function showStandardInputs( &$tabindex = 2 ) {
3417  global $wgOut;
3418  $wgOut->addHTML( "<div class='editOptions'>\n" );
3419 
3420  if ( $this->section != 'new' ) {
3421  $this->showSummaryInput( false, $this->summary );
3422  $wgOut->addHTML( $this->getSummaryPreview( false, $this->summary ) );
3423  }
3424 
3425  $checkboxes = $this->getCheckboxes( $tabindex,
3426  [ 'minor' => $this->minoredit, 'watch' => $this->watchthis ] );
3427  $wgOut->addHTML( "<div class='editCheckboxes'>" . implode( $checkboxes, "\n" ) . "</div>\n" );
3428 
3429  // Show copyright warning.
3430  $wgOut->addWikiText( $this->getCopywarn() );
3431  $wgOut->addHTML( $this->editFormTextAfterWarn );
3432 
3433  $wgOut->addHTML( "<div class='editButtons'>\n" );
3434  $wgOut->addHTML( implode( $this->getEditButtons( $tabindex ), "\n" ) . "\n" );
3435 
3436  $cancel = $this->getCancelLink();
3437  if ( $cancel !== '' ) {
3438  $cancel .= Html::element( 'span',
3439  [ 'class' => 'mw-editButtons-pipe-separator' ],
3440  wfMessage( 'pipe-separator' )->text() );
3441  }
3442 
3443  $message = wfMessage( 'edithelppage' )->inContentLanguage()->text();
3444  $edithelpurl = Skin::makeInternalOrExternalUrl( $message );
3445  $attrs = [
3446  'target' => 'helpwindow',
3447  'href' => $edithelpurl,
3448  ];
3449  $edithelp = Html::linkButton( wfMessage( 'edithelp' )->text(),
3450  $attrs, [ 'mw-ui-quiet' ] ) .
3451  wfMessage( 'word-separator' )->escaped() .
3452  wfMessage( 'newwindow' )->parse();
3453 
3454  $wgOut->addHTML( " <span class='cancelLink'>{$cancel}</span>\n" );
3455  $wgOut->addHTML( " <span class='editHelp'>{$edithelp}</span>\n" );
3456  $wgOut->addHTML( "</div><!-- editButtons -->\n" );
3457 
3458  Hooks::run( 'EditPage::showStandardInputs:options', [ $this, $wgOut, &$tabindex ] );
3459 
3460  $wgOut->addHTML( "</div><!-- editOptions -->\n" );
3461  }
3462 
3467  protected function showConflict() {
3468  global $wgOut;
3469 
3470  if ( Hooks::run( 'EditPageBeforeConflictDiff', [ &$this, &$wgOut ] ) ) {
3471  $stats = $wgOut->getContext()->getStats();
3472  $stats->increment( 'edit.failures.conflict' );
3473 
3474  $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
3475 
3476  $content1 = $this->toEditContent( $this->textbox1 );
3477  $content2 = $this->toEditContent( $this->textbox2 );
3478 
3479  $handler = ContentHandler::getForModelID( $this->contentModel );
3480  $de = $handler->createDifferenceEngine( $this->mArticle->getContext() );
3481  $de->setContent( $content2, $content1 );
3482  $de->showDiff(
3483  wfMessage( 'yourtext' )->parse(),
3484  wfMessage( 'storedversion' )->text()
3485  );
3486 
3487  $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
3488  $this->showTextbox2();
3489  }
3490  }
3491 
3495  public function getCancelLink() {
3496  $cancelParams = [];
3497  if ( !$this->isConflict && $this->oldid > 0 ) {
3498  $cancelParams['oldid'] = $this->oldid;
3499  }
3500  $attrs = [ 'id' => 'mw-editform-cancel' ];
3501 
3502  return Linker::linkKnown(
3503  $this->getContextTitle(),
3504  wfMessage( 'cancel' )->parse(),
3505  Html::buttonAttributes( $attrs, [ 'mw-ui-quiet' ] ),
3506  $cancelParams
3507  );
3508  }
3509 
3519  protected function getActionURL( Title $title ) {
3520  return $title->getLocalURL( [ 'action' => $this->action ] );
3521  }
3522 
3530  protected function wasDeletedSinceLastEdit() {
3531  if ( $this->deletedSinceEdit !== null ) {
3532  return $this->deletedSinceEdit;
3533  }
3534 
3535  $this->deletedSinceEdit = false;
3536 
3537  if ( !$this->mTitle->exists() && $this->mTitle->isDeletedQuick() ) {
3538  $this->lastDelete = $this->getLastDelete();
3539  if ( $this->lastDelete ) {
3540  $deleteTime = wfTimestamp( TS_MW, $this->lastDelete->log_timestamp );
3541  if ( $deleteTime > $this->starttime ) {
3542  $this->deletedSinceEdit = true;
3543  }
3544  }
3545  }
3546 
3547  return $this->deletedSinceEdit;
3548  }
3549 
3553  protected function getLastDelete() {
3554  $dbr = wfGetDB( DB_SLAVE );
3555  $data = $dbr->selectRow(
3556  [ 'logging', 'user' ],
3557  [
3558  'log_type',
3559  'log_action',
3560  'log_timestamp',
3561  'log_user',
3562  'log_namespace',
3563  'log_title',
3564  'log_comment',
3565  'log_params',
3566  'log_deleted',
3567  'user_name'
3568  ], [
3569  'log_namespace' => $this->mTitle->getNamespace(),
3570  'log_title' => $this->mTitle->getDBkey(),
3571  'log_type' => 'delete',
3572  'log_action' => 'delete',
3573  'user_id=log_user'
3574  ],
3575  __METHOD__,
3576  [ 'LIMIT' => 1, 'ORDER BY' => 'log_timestamp DESC' ]
3577  );
3578  // Quick paranoid permission checks...
3579  if ( is_object( $data ) ) {
3580  if ( $data->log_deleted & LogPage::DELETED_USER ) {
3581  $data->user_name = wfMessage( 'rev-deleted-user' )->escaped();
3582  }
3583 
3584  if ( $data->log_deleted & LogPage::DELETED_COMMENT ) {
3585  $data->log_comment = wfMessage( 'rev-deleted-comment' )->escaped();
3586  }
3587  }
3588 
3589  return $data;
3590  }
3591 
3597  function getPreviewText() {
3598  global $wgOut, $wgUser, $wgRawHtml, $wgLang;
3600 
3601  $stats = $wgOut->getContext()->getStats();
3602 
3603  if ( $wgRawHtml && !$this->mTokenOk ) {
3604  // Could be an offsite preview attempt. This is very unsafe if
3605  // HTML is enabled, as it could be an attack.
3606  $parsedNote = '';
3607  if ( $this->textbox1 !== '' ) {
3608  // Do not put big scary notice, if previewing the empty
3609  // string, which happens when you initially edit
3610  // a category page, due to automatic preview-on-open.
3611  $parsedNote = $wgOut->parse( "<div class='previewnote'>" .
3612  wfMessage( 'session_fail_preview_html' )->text() . "</div>", true, /* interface */true );
3613  }
3614  $stats->increment( 'edit.failures.session_loss' );
3615  return $parsedNote;
3616  }
3617 
3618  $note = '';
3619 
3620  try {
3621  $content = $this->toEditContent( $this->textbox1 );
3622 
3623  $previewHTML = '';
3624  if ( !Hooks::run(
3625  'AlternateEditPreview',
3626  [ $this, &$content, &$previewHTML, &$this->mParserOutput ] )
3627  ) {
3628  return $previewHTML;
3629  }
3630 
3631  # provide a anchor link to the editform
3632  $continueEditing = '<span class="mw-continue-editing">' .
3633  '[[#' . self::EDITFORM_ID . '|' . $wgLang->getArrow() . ' ' .
3634  wfMessage( 'continue-editing' )->text() . ']]</span>';
3635  if ( $this->mTriedSave && !$this->mTokenOk ) {
3636  if ( $this->mTokenOkExceptSuffix ) {
3637  $note = wfMessage( 'token_suffix_mismatch' )->plain();
3638  $stats->increment( 'edit.failures.bad_token' );
3639  } else {
3640  $note = wfMessage( 'session_fail_preview' )->plain();
3641  $stats->increment( 'edit.failures.session_loss' );
3642  }
3643  } elseif ( $this->incompleteForm ) {
3644  $note = wfMessage( 'edit_form_incomplete' )->plain();
3645  if ( $this->mTriedSave ) {
3646  $stats->increment( 'edit.failures.incomplete_form' );
3647  }
3648  } else {
3649  $note = wfMessage( 'previewnote' )->plain() . ' ' . $continueEditing;
3650  }
3651 
3652  $parserOptions = $this->page->makeParserOptions( $this->mArticle->getContext() );
3653  $parserOptions->setIsPreview( true );
3654  $parserOptions->setIsSectionPreview( !is_null( $this->section ) && $this->section !== '' );
3655 
3656  # don't parse non-wikitext pages, show message about preview
3657  if ( $this->mTitle->isCssJsSubpage() || $this->mTitle->isCssOrJsPage() ) {
3658  if ( $this->mTitle->isCssJsSubpage() ) {
3659  $level = 'user';
3660  } elseif ( $this->mTitle->isCssOrJsPage() ) {
3661  $level = 'site';
3662  } else {
3663  $level = false;
3664  }
3665 
3666  if ( $content->getModel() == CONTENT_MODEL_CSS ) {
3667  $format = 'css';
3668  if ( $level === 'user' && !$wgAllowUserCss ) {
3669  $format = false;
3670  }
3671  } elseif ( $content->getModel() == CONTENT_MODEL_JAVASCRIPT ) {
3672  $format = 'js';
3673  if ( $level === 'user' && !$wgAllowUserJs ) {
3674  $format = false;
3675  }
3676  } else {
3677  $format = false;
3678  }
3679 
3680  # Used messages to make sure grep find them:
3681  # Messages: usercsspreview, userjspreview, sitecsspreview, sitejspreview
3682  if ( $level && $format ) {
3683  $note = "<div id='mw-{$level}{$format}preview'>" .
3684  wfMessage( "{$level}{$format}preview" )->text() .
3685  ' ' . $continueEditing . "</div>";
3686  }
3687  }
3688 
3689  # If we're adding a comment, we need to show the
3690  # summary as the headline
3691  if ( $this->section === "new" && $this->summary !== "" ) {
3692  $content = $content->addSectionHeader( $this->summary );
3693  }
3694 
3695  $hook_args = [ $this, &$content ];
3696  ContentHandler::runLegacyHooks( 'EditPageGetPreviewText', $hook_args );
3697  Hooks::run( 'EditPageGetPreviewContent', $hook_args );
3698 
3699  $parserOptions->enableLimitReport();
3700 
3701  # For CSS/JS pages, we should have called the ShowRawCssJs hook here.
3702  # But it's now deprecated, so never mind
3703 
3704  $pstContent = $content->preSaveTransform( $this->mTitle, $wgUser, $parserOptions );
3705  $scopedCallback = $parserOptions->setupFakeRevision(
3706  $this->mTitle, $pstContent, $wgUser );
3707  $parserOutput = $pstContent->getParserOutput( $this->mTitle, null, $parserOptions );
3708 
3709  # Try to stash the edit for the final submission step
3710  # @todo: different date format preferences cause cache misses
3712  $this->getArticle(), $content, $pstContent,
3713  $parserOutput, $parserOptions, $parserOptions, wfTimestampNow()
3714  );
3715 
3716  $parserOutput->setEditSectionTokens( false ); // no section edit links
3717  $previewHTML = $parserOutput->getText();
3718  $this->mParserOutput = $parserOutput;
3719  $wgOut->addParserOutputMetadata( $parserOutput );
3720 
3721  if ( count( $parserOutput->getWarnings() ) ) {
3722  $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
3723  }
3724 
3725  ScopedCallback::consume( $scopedCallback );
3726  } catch ( MWContentSerializationException $ex ) {
3727  $m = wfMessage(
3728  'content-failed-to-parse',
3729  $this->contentModel,
3730  $this->contentFormat,
3731  $ex->getMessage()
3732  );
3733  $note .= "\n\n" . $m->parse();
3734  $previewHTML = '';
3735  }
3736 
3737  if ( $this->isConflict ) {
3738  $conflict = '<h2 id="mw-previewconflict">'
3739  . wfMessage( 'previewconflict' )->escaped() . "</h2>\n";
3740  } else {
3741  $conflict = '<hr />';
3742  }
3743 
3744  $previewhead = "<div class='previewnote'>\n" .
3745  '<h2 id="mw-previewheader">' . wfMessage( 'preview' )->escaped() . "</h2>" .
3746  $wgOut->parse( $note, true, /* interface */true ) . $conflict . "</div>\n";
3747 
3748  $pageViewLang = $this->mTitle->getPageViewLanguage();
3749  $attribs = [ 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(),
3750  'class' => 'mw-content-' . $pageViewLang->getDir() ];
3751  $previewHTML = Html::rawElement( 'div', $attribs, $previewHTML );
3752 
3753  return $previewhead . $previewHTML . $this->previewTextAfterContent;
3754  }
3755 
3759  function getTemplates() {
3760  if ( $this->preview || $this->section != '' ) {
3761  $templates = [];
3762  if ( !isset( $this->mParserOutput ) ) {
3763  return $templates;
3764  }
3765  foreach ( $this->mParserOutput->getTemplates() as $ns => $template ) {
3766  foreach ( array_keys( $template ) as $dbk ) {
3767  $templates[] = Title::makeTitle( $ns, $dbk );
3768  }
3769  }
3770  return $templates;
3771  } else {
3772  return $this->mTitle->getTemplateLinksFrom();
3773  }
3774  }
3775 
3783  static function getEditToolbar( $title = null ) {
3786 
3787  $imagesAvailable = $wgEnableUploads || count( $wgForeignFileRepos );
3788  $showSignature = true;
3789  if ( $title ) {
3790  $showSignature = MWNamespace::wantSignatures( $title->getNamespace() );
3791  }
3792 
3802  $toolarray = [
3803  [
3804  'id' => 'mw-editbutton-bold',
3805  'open' => '\'\'\'',
3806  'close' => '\'\'\'',
3807  'sample' => wfMessage( 'bold_sample' )->text(),
3808  'tip' => wfMessage( 'bold_tip' )->text(),
3809  ],
3810  [
3811  'id' => 'mw-editbutton-italic',
3812  'open' => '\'\'',
3813  'close' => '\'\'',
3814  'sample' => wfMessage( 'italic_sample' )->text(),
3815  'tip' => wfMessage( 'italic_tip' )->text(),
3816  ],
3817  [
3818  'id' => 'mw-editbutton-link',
3819  'open' => '[[',
3820  'close' => ']]',
3821  'sample' => wfMessage( 'link_sample' )->text(),
3822  'tip' => wfMessage( 'link_tip' )->text(),
3823  ],
3824  [
3825  'id' => 'mw-editbutton-extlink',
3826  'open' => '[',
3827  'close' => ']',
3828  'sample' => wfMessage( 'extlink_sample' )->text(),
3829  'tip' => wfMessage( 'extlink_tip' )->text(),
3830  ],
3831  [
3832  'id' => 'mw-editbutton-headline',
3833  'open' => "\n== ",
3834  'close' => " ==\n",
3835  'sample' => wfMessage( 'headline_sample' )->text(),
3836  'tip' => wfMessage( 'headline_tip' )->text(),
3837  ],
3838  $imagesAvailable ? [
3839  'id' => 'mw-editbutton-image',
3840  'open' => '[[' . $wgContLang->getNsText( NS_FILE ) . ':',
3841  'close' => ']]',
3842  'sample' => wfMessage( 'image_sample' )->text(),
3843  'tip' => wfMessage( 'image_tip' )->text(),
3844  ] : false,
3845  $imagesAvailable ? [
3846  'id' => 'mw-editbutton-media',
3847  'open' => '[[' . $wgContLang->getNsText( NS_MEDIA ) . ':',
3848  'close' => ']]',
3849  'sample' => wfMessage( 'media_sample' )->text(),
3850  'tip' => wfMessage( 'media_tip' )->text(),
3851  ] : false,
3852  [
3853  'id' => 'mw-editbutton-nowiki',
3854  'open' => "<nowiki>",
3855  'close' => "</nowiki>",
3856  'sample' => wfMessage( 'nowiki_sample' )->text(),
3857  'tip' => wfMessage( 'nowiki_tip' )->text(),
3858  ],
3859  $showSignature ? [
3860  'id' => 'mw-editbutton-signature',
3861  'open' => '--~~~~',
3862  'close' => '',
3863  'sample' => '',
3864  'tip' => wfMessage( 'sig_tip' )->text(),
3865  ] : false,
3866  [
3867  'id' => 'mw-editbutton-hr',
3868  'open' => "\n----\n",
3869  'close' => '',
3870  'sample' => '',
3871  'tip' => wfMessage( 'hr_tip' )->text(),
3872  ]
3873  ];
3874 
3875  $script = 'mw.loader.using("mediawiki.toolbar", function () {';
3876  foreach ( $toolarray as $tool ) {
3877  if ( !$tool ) {
3878  continue;
3879  }
3880 
3881  $params = [
3882  // Images are defined in ResourceLoaderEditToolbarModule
3883  false,
3884  // Note that we use the tip both for the ALT tag and the TITLE tag of the image.
3885  // Older browsers show a "speedtip" type message only for ALT.
3886  // Ideally these should be different, realistically they
3887  // probably don't need to be.
3888  $tool['tip'],
3889  $tool['open'],
3890  $tool['close'],
3891  $tool['sample'],
3892  $tool['id'],
3893  ];
3894 
3895  $script .= Xml::encodeJsCall(
3896  'mw.toolbar.addButton',
3897  $params,
3899  );
3900  }
3901 
3902  $script .= '});';
3903  $wgOut->addScript( ResourceLoader::makeInlineScript( $script ) );
3904 
3905  $toolbar = '<div id="toolbar"></div>';
3906 
3907  Hooks::run( 'EditPageBeforeEditToolbar', [ &$toolbar ] );
3908 
3909  return $toolbar;
3910  }
3911 
3922  public function getCheckboxes( &$tabindex, $checked ) {
3924 
3925  $checkboxes = [];
3926 
3927  // don't show the minor edit checkbox if it's a new page or section
3928  if ( !$this->isNew ) {
3929  $checkboxes['minor'] = '';
3930  $minorLabel = wfMessage( 'minoredit' )->parse();
3931  if ( $wgUser->isAllowed( 'minoredit' ) ) {
3932  $attribs = [
3933  'tabindex' => ++$tabindex,
3934  'accesskey' => wfMessage( 'accesskey-minoredit' )->text(),
3935  'id' => 'wpMinoredit',
3936  ];
3937  $minorEditHtml =
3938  Xml::check( 'wpMinoredit', $checked['minor'], $attribs ) .
3939  "&#160;<label for='wpMinoredit' id='mw-editpage-minoredit'" .
3940  Xml::expandAttributes( [ 'title' => Linker::titleAttrib( 'minoredit', 'withaccess' ) ] ) .
3941  ">{$minorLabel}</label>";
3942 
3943  if ( $wgUseMediaWikiUIEverywhere ) {
3944  $checkboxes['minor'] = Html::openElement( 'div', [ 'class' => 'mw-ui-checkbox' ] ) .
3945  $minorEditHtml .
3946  Html::closeElement( 'div' );
3947  } else {
3948  $checkboxes['minor'] = $minorEditHtml;
3949  }
3950  }
3951  }
3952 
3953  $watchLabel = wfMessage( 'watchthis' )->parse();
3954  $checkboxes['watch'] = '';
3955  if ( $wgUser->isLoggedIn() ) {
3956  $attribs = [
3957  'tabindex' => ++$tabindex,
3958  'accesskey' => wfMessage( 'accesskey-watch' )->text(),
3959  'id' => 'wpWatchthis',
3960  ];
3961  $watchThisHtml =
3962  Xml::check( 'wpWatchthis', $checked['watch'], $attribs ) .
3963  "&#160;<label for='wpWatchthis' id='mw-editpage-watch'" .
3964  Xml::expandAttributes( [ 'title' => Linker::titleAttrib( 'watch', 'withaccess' ) ] ) .
3965  ">{$watchLabel}</label>";
3966  if ( $wgUseMediaWikiUIEverywhere ) {
3967  $checkboxes['watch'] = Html::openElement( 'div', [ 'class' => 'mw-ui-checkbox' ] ) .
3968  $watchThisHtml .
3969  Html::closeElement( 'div' );
3970  } else {
3971  $checkboxes['watch'] = $watchThisHtml;
3972  }
3973  }
3974  Hooks::run( 'EditPageBeforeEditChecks', [ &$this, &$checkboxes, &$tabindex ] );
3975  return $checkboxes;
3976  }
3977 
3986  public function getEditButtons( &$tabindex ) {
3987  $buttons = [];
3988 
3989  $attribs = [
3990  'id' => 'wpSave',
3991  'name' => 'wpSave',
3992  'tabindex' => ++$tabindex,
3993  ] + Linker::tooltipAndAccesskeyAttribs( 'save' );
3994  $buttons['save'] = Html::submitButton( wfMessage( 'savearticle' )->text(),
3995  $attribs, [ 'mw-ui-constructive' ] );
3996 
3997  ++$tabindex; // use the same for preview and live preview
3998  $attribs = [
3999  'id' => 'wpPreview',
4000  'name' => 'wpPreview',
4001  'tabindex' => $tabindex,
4002  ] + Linker::tooltipAndAccesskeyAttribs( 'preview' );
4003  $buttons['preview'] = Html::submitButton( wfMessage( 'showpreview' )->text(),
4004  $attribs );
4005  $buttons['live'] = '';
4006 
4007  $attribs = [
4008  'id' => 'wpDiff',
4009  'name' => 'wpDiff',
4010  'tabindex' => ++$tabindex,
4011  ] + Linker::tooltipAndAccesskeyAttribs( 'diff' );
4012  $buttons['diff'] = Html::submitButton( wfMessage( 'showdiff' )->text(),
4013  $attribs );
4014 
4015  Hooks::run( 'EditPageBeforeEditButtons', [ &$this, &$buttons, &$tabindex ] );
4016  return $buttons;
4017  }
4018 
4023  function noSuchSectionPage() {
4024  global $wgOut;
4025 
4026  $wgOut->prepareErrorPage( wfMessage( 'nosuchsectiontitle' ) );
4027 
4028  $res = wfMessage( 'nosuchsectiontext', $this->section )->parseAsBlock();
4029  Hooks::run( 'EditPageNoSuchSection', [ &$this, &$res ] );
4030  $wgOut->addHTML( $res );
4031 
4032  $wgOut->returnToMain( false, $this->mTitle );
4033  }
4034 
4040  public function spamPageWithContent( $match = false ) {
4042  $this->textbox2 = $this->textbox1;
4043 
4044  if ( is_array( $match ) ) {
4045  $match = $wgLang->listToText( $match );
4046  }
4047  $wgOut->prepareErrorPage( wfMessage( 'spamprotectiontitle' ) );
4048 
4049  $wgOut->addHTML( '<div id="spamprotected">' );
4050  $wgOut->addWikiMsg( 'spamprotectiontext' );
4051  if ( $match ) {
4052  $wgOut->addWikiMsg( 'spamprotectionmatch', wfEscapeWikiText( $match ) );
4053  }
4054  $wgOut->addHTML( '</div>' );
4055 
4056  $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
4057  $this->showDiff();
4058 
4059  $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
4060  $this->showTextbox2();
4061 
4062  $wgOut->addReturnTo( $this->getContextTitle(), [ 'action' => 'edit' ] );
4063  }
4064 
4071  private function checkUnicodeCompliantBrowser() {
4073 
4074  $currentbrowser = $wgRequest->getHeader( 'User-Agent' );
4075  if ( $currentbrowser === false ) {
4076  // No User-Agent header sent? Trust it by default...
4077  return true;
4078  }
4079 
4080  foreach ( $wgBrowserBlackList as $browser ) {
4081  if ( preg_match( $browser, $currentbrowser ) ) {
4082  return false;
4083  }
4084  }
4085  return true;
4086  }
4087 
4096  protected function safeUnicodeInput( $request, $field ) {
4097  $text = rtrim( $request->getText( $field ) );
4098  return $request->getBool( 'safemode' )
4099  ? $this->unmakeSafe( $text )
4100  : $text;
4101  }
4102 
4110  protected function safeUnicodeOutput( $text ) {
4112  $codedText = $wgContLang->recodeForEdit( $text );
4113  return $this->checkUnicodeCompliantBrowser()
4114  ? $codedText
4115  : $this->makeSafe( $codedText );
4116  }
4117 
4130  private function makeSafe( $invalue ) {
4131  // Armor existing references for reversibility.
4132  $invalue = strtr( $invalue, [ "&#x" => "&#x0" ] );
4133 
4134  $bytesleft = 0;
4135  $result = "";
4136  $working = 0;
4137  $valueLength = strlen( $invalue );
4138  for ( $i = 0; $i < $valueLength; $i++ ) {
4139  $bytevalue = ord( $invalue[$i] );
4140  if ( $bytevalue <= 0x7F ) { // 0xxx xxxx
4141  $result .= chr( $bytevalue );
4142  $bytesleft = 0;
4143  } elseif ( $bytevalue <= 0xBF ) { // 10xx xxxx
4144  $working = $working << 6;
4145  $working += ( $bytevalue & 0x3F );
4146  $bytesleft--;
4147  if ( $bytesleft <= 0 ) {
4148  $result .= "&#x" . strtoupper( dechex( $working ) ) . ";";
4149  }
4150  } elseif ( $bytevalue <= 0xDF ) { // 110x xxxx
4151  $working = $bytevalue & 0x1F;
4152  $bytesleft = 1;
4153  } elseif ( $bytevalue <= 0xEF ) { // 1110 xxxx
4154  $working = $bytevalue & 0x0F;
4155  $bytesleft = 2;
4156  } else { // 1111 0xxx
4157  $working = $bytevalue & 0x07;
4158  $bytesleft = 3;
4159  }
4160  }
4161  return $result;
4162  }
4163 
4172  private function unmakeSafe( $invalue ) {
4173  $result = "";
4174  $valueLength = strlen( $invalue );
4175  for ( $i = 0; $i < $valueLength; $i++ ) {
4176  if ( ( substr( $invalue, $i, 3 ) == "&#x" ) && ( $invalue[$i + 3] != '0' ) ) {
4177  $i += 3;
4178  $hexstring = "";
4179  do {
4180  $hexstring .= $invalue[$i];
4181  $i++;
4182  } while ( ctype_xdigit( $invalue[$i] ) && ( $i < strlen( $invalue ) ) );
4183 
4184  // Do some sanity checks. These aren't needed for reversibility,
4185  // but should help keep the breakage down if the editor
4186  // breaks one of the entities whilst editing.
4187  if ( ( substr( $invalue, $i, 1 ) == ";" ) && ( strlen( $hexstring ) <= 6 ) ) {
4188  $codepoint = hexdec( $hexstring );
4189  $result .= UtfNormal\Utils::codepointToUtf8( $codepoint );
4190  } else {
4191  $result .= "&#x" . $hexstring . substr( $invalue, $i, 1 );
4192  }
4193  } else {
4194  $result .= substr( $invalue, $i, 1 );
4195  }
4196  }
4197  // reverse the transform that we made for reversibility reasons.
4198  return strtr( $result, [ "&#x0" => "&#x" ] );
4199  }
4200 }
string $autoSumm
Definition: EditPage.php:286
static newFromName($name, $validate= 'valid')
Static factory method for creation from username.
Definition: User.php:568
static factory(Title $title)
Create a WikiPage object of the appropriate class for the given title.
Definition: WikiPage.php:99
displayPermissionsError(array $permErrors)
Display a permissions error page, like OutputPage::showPermissionsErrorPage(), but with the following...
Definition: EditPage.php:648
$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:4130
const FOR_THIS_USER
Definition: Revision.php:84
bool $nosummary
Definition: EditPage.php:333
static closeElement($element)
Returns "".
Definition: Html.php:306
$editFormTextBottom
Definition: EditPage.php:380
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:72
wfGetDB($db, $groups=[], $wiki=false)
Get a Database object.
$wgMaxArticleSize
Maximum article size in kilobytes.
bool $missingSummary
Definition: EditPage.php:268
the array() calling protocol came about after MediaWiki 1.4rc1.
bool $bot
Definition: EditPage.php:360
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:1418
string $textbox2
Definition: EditPage.php:327
bool $mTokenOk
Definition: EditPage.php:247
static linkKnown($target, $html=null, $customAttribs=[], $query=[], $options=[ 'known', 'noclasses'])
Identical to link(), except $options defaults to 'known'.
Definition: Linker.php:264
magic word the default is to use $key to get the and $key value or $key value text $key value html to format the value $key
Definition: hooks.txt:2321
$editFormTextAfterContent
Definition: EditPage.php:381
showContentForm()
Subpage overridable method for printing the form for page content editing By default this simply outp...
Definition: EditPage.php:3084
bool $allowBlankSummary
Definition: EditPage.php:271
getPreviewText()
Get the rendered text for previewing.
Definition: EditPage.php:3597
serialize($format=null)
Convenience method for serializing this Content object.
bool $isConflict
Definition: EditPage.php:217
int $oldid
Definition: EditPage.php:348
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:1408
string $summary
Definition: EditPage.php:330
setHeaders()
Definition: EditPage.php:2223
WikiPage $page
Definition: EditPage.php:205
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:93
const AS_HOOK_ERROR
Status: Article update aborted by a hook function.
Definition: EditPage.php:52
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:1932
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:4096
showTextbox2()
Definition: EditPage.php:3138
bool $tooBig
Definition: EditPage.php:259
$wgParser
Definition: Setup.php:809
static rawElement($element, $attribs=[], $contents= '')
Returns an HTML element in a string.
Definition: Html.php:210
showHeaderCopyrightWarning()
Show the header copyright warning.
Definition: EditPage.php:3298
getPage()
Get the WikiPage object of this instance.
Definition: Article.php:176
getWikiText($shortContext=false, $longContext=false, $lang=null)
Get the error list as a wikitext formatted list.
Definition: Status.php:216
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
processing should stop and the error should be shown to the user * false
Definition: hooks.txt:189
showTosSummary()
Give a chance for site and per-namespace customizations of terms of service summary link that might e...
Definition: EditPage.php:3315
The edit page/HTML interface (split from Article) The actual database and text munging is still in Ar...
Definition: EditPage.php:38
Title $mTitle
Definition: EditPage.php:208
static hidden($name, $value, array $attribs=[])
Convenience function to produce an input element with type=hidden.
Definition: Html.php:759
setContextTitle($title)
Set the context Title object.
Definition: EditPage.php:433
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:115
const AS_HOOK_ERROR_EXPECTED
Status: A hook function returned an error.
Definition: EditPage.php:57
getEditButtons(&$tabindex)
Returns an array of html code of the following buttons: save, diff, preview and live.
Definition: EditPage.php:3986
$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.
string $editintro
Definition: EditPage.php:354
Class for viewing MediaWiki article and history.
Definition: Article.php:34
null for the local wiki Added in
Definition: hooks.txt:1418
static getSkinNames()
Fetch the set of available skins.
Definition: Skin.php:49
bool $allowBlankArticle
Definition: EditPage.php:277
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
$value
Article $mArticle
Definition: EditPage.php:203
null string $contentFormat
Definition: EditPage.php:366
const AS_BLOCKED_PAGE_FOR_USER
Status: User is blocked from editing this page.
Definition: EditPage.php:62
bool $blankArticle
Definition: EditPage.php:274
setPostEditCookie($statusValue)
Sets post-edit cookie indicating the user just saved a particular revision.
Definition: EditPage.php:1364
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:99
The First
Definition: primes.txt:1
static getPreviewLimitReport($output)
Get the Limit report for page previews.
Definition: EditPage.php:3374
spamPageWithContent($match=false)
Show "your edit contains spam" page with your diff and text.
Definition: EditPage.php:4040
it s the revision text itself In either if gzip is the revision text is gzipped $flags
Definition: hooks.txt:2548
static getCopyrightWarning($title, $format= 'plain')
Get the copyright warning, by default returns wikitext.
Definition: EditPage.php:3350
bool $missingComment
Definition: EditPage.php:265
const EDIT_MINOR
Definition: Defines.php:181
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:200
const EDIT_UPDATE
Definition: Defines.php:180
static newFromText($text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition: Title.php:277
this hook is for auditing only $response
Definition: hooks.txt:762
showFormBeforeText()
Definition: EditPage.php:3043
internalAttemptSave(&$result, $bot=false)
Attempt submission (no UI)
Definition: EditPage.php:1641
bool stdClass $lastDelete
Definition: EditPage.php:244
Represents a title within MediaWiki.
Definition: Title.php:34
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:490
getContextTitle()
Get the context title object.
Definition: EditPage.php:444
bool $mBaseRevision
Definition: EditPage.php:298
getContentObject($def_content=null)
Definition: EditPage.php:1058
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:2133
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:2335
null string $contentModel
Definition: EditPage.php:363
getEditPermissionErrors($rigor= 'secure')
Definition: EditPage.php:607
isRedirect()
Tests if the article content represents a redirect.
Definition: WikiPage.php:466
null Title $mContextTitle
Definition: EditPage.php:211
wfArrayDiff2($a, $b)
Like array_diff( $a, $b ) except that it works with two-dimensional arrays.
static formatTemplates($templates, $preview=false, $section=false, $more=null)
Returns HTML for the "templates used on this page" list.
Definition: Linker.php:2039
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:2202
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':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:1796
const AS_CONTENT_TOO_BIG
Status: Content too big (> $wgMaxArticleSize)
Definition: EditPage.php:67
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:762
safeUnicodeOutput($text)
Filter an output field through a Unicode armoring process if it is going to an old browser with known...
Definition: EditPage.php:4110
static addCallableUpdate($callable, $type=self::POSTSEND)
Add a callable update.
$wgEnableUploads
Uploads have to be specially set up to be secure.
bool $isWrongCaseCssJsPage
Definition: EditPage.php:229
attemptSave(&$resultDetails=false)
Attempt submission.
Definition: EditPage.php:1387
showIntro()
Show all applicable editing introductions.
Definition: EditPage.php:2282
wfTimestamp($outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
null means default & $customAttribs
Definition: hooks.txt:1798
static getLocalizedName($name, Language $lang=null)
Returns the localized name for a given content model.
getArticle()
Definition: EditPage.php:416
bool $isCssSubpage
Definition: EditPage.php:223
bool $watchthis
Definition: EditPage.php:318
$previewTextAfterContent
Definition: EditPage.php:382
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...
getContent($audience=Revision::FOR_PUBLIC, User $user=null)
Get the content of the current revision.
Definition: WikiPage.php:654
static openElement($element, $attribs=[])
Identical to rawElement(), but has no third parameter and omits the end tag (and the self-closing '/'...
Definition: Html.php:248
getParentRevId()
Get the edit's parent revision ID.
Definition: EditPage.php:1218
isWrongCaseCssJsPage()
Checks whether the user entered a skin name in uppercase, e.g.
Definition: EditPage.php:767
getTemplates()
Definition: EditPage.php:3759
wfEscapeWikiText($text)
Escapes the given text so that it may be output using addWikiText() without any linking, formatting, etc.
bool $save
Definition: EditPage.php:306
wfReadOnly()
Check whether the wiki is in read-only mode.
static getMain()
Static methods.
static textarea($name, $value= '', array $attribs=[])
Convenience function to produce a