MediaWiki  1.27.2
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  [
984  wfEscapeWikiText( $this->contentFormat ),
985  wfEscapeWikiText( ContentHandler::getLocalizedName( $this->contentModel ) )
986  ]
987  );
988  }
989 
996  $this->editintro = $request->getText( 'editintro',
997  // Custom edit intro for new sections
998  $this->section === 'new' ? 'MediaWiki:addsection-editintro' : '' );
999 
1000  // Allow extensions to modify form data
1001  Hooks::run( 'EditPage::importFormData', [ $this, $request ] );
1002 
1003  }
1004 
1014  protected function importContentFormData( &$request ) {
1015  return; // Don't do anything, EditPage already extracted wpTextbox1
1016  }
1017 
1023  function initialiseForm() {
1024  global $wgUser;
1025  $this->edittime = $this->page->getTimestamp();
1026 
1027  $content = $this->getContentObject( false ); # TODO: track content object?!
1028  if ( $content === false ) {
1029  return false;
1030  }
1031  $this->textbox1 = $this->toEditText( $content );
1032 
1033  // activate checkboxes if user wants them to be always active
1034  # Sort out the "watch" checkbox
1035  if ( $wgUser->getOption( 'watchdefault' ) ) {
1036  # Watch all edits
1037  $this->watchthis = true;
1038  } elseif ( $wgUser->getOption( 'watchcreations' ) && !$this->mTitle->exists() ) {
1039  # Watch creations
1040  $this->watchthis = true;
1041  } elseif ( $wgUser->isWatched( $this->mTitle ) ) {
1042  # Already watched
1043  $this->watchthis = true;
1044  }
1045  if ( $wgUser->getOption( 'minordefault' ) && !$this->isNew ) {
1046  $this->minoredit = true;
1047  }
1048  if ( $this->textbox1 === false ) {
1049  return false;
1050  }
1051  return true;
1052  }
1053 
1061  protected function getContentObject( $def_content = null ) {
1063 
1064  $content = false;
1065 
1066  // For message page not locally set, use the i18n message.
1067  // For other non-existent articles, use preload text if any.
1068  if ( !$this->mTitle->exists() || $this->section == 'new' ) {
1069  if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && $this->section != 'new' ) {
1070  # If this is a system message, get the default text.
1071  $msg = $this->mTitle->getDefaultMessageText();
1072 
1073  $content = $this->toEditContent( $msg );
1074  }
1075  if ( $content === false ) {
1076  # If requested, preload some text.
1077  $preload = $wgRequest->getVal( 'preload',
1078  // Custom preload text for new sections
1079  $this->section === 'new' ? 'MediaWiki:addsection-preload' : '' );
1080  $params = $wgRequest->getArray( 'preloadparams', [] );
1081 
1082  $content = $this->getPreloadedContent( $preload, $params );
1083  }
1084  // For existing pages, get text based on "undo" or section parameters.
1085  } else {
1086  if ( $this->section != '' ) {
1087  // Get section edit text (returns $def_text for invalid sections)
1088  $orig = $this->getOriginalContent( $wgUser );
1089  $content = $orig ? $orig->getSection( $this->section ) : null;
1090 
1091  if ( !$content ) {
1092  $content = $def_content;
1093  }
1094  } else {
1095  $undoafter = $wgRequest->getInt( 'undoafter' );
1096  $undo = $wgRequest->getInt( 'undo' );
1097 
1098  if ( $undo > 0 && $undoafter > 0 ) {
1099  $undorev = Revision::newFromId( $undo );
1100  $oldrev = Revision::newFromId( $undoafter );
1101 
1102  # Sanity check, make sure it's the right page,
1103  # the revisions exist and they were not deleted.
1104  # Otherwise, $content will be left as-is.
1105  if ( !is_null( $undorev ) && !is_null( $oldrev ) &&
1106  !$undorev->isDeleted( Revision::DELETED_TEXT ) &&
1107  !$oldrev->isDeleted( Revision::DELETED_TEXT )
1108  ) {
1109  $content = $this->page->getUndoContent( $undorev, $oldrev );
1110 
1111  if ( $content === false ) {
1112  # Warn the user that something went wrong
1113  $undoMsg = 'failure';
1114  } else {
1115  $oldContent = $this->page->getContent( Revision::RAW );
1116  $popts = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang );
1117  $newContent = $content->preSaveTransform( $this->mTitle, $wgUser, $popts );
1118 
1119  if ( $newContent->equals( $oldContent ) ) {
1120  # Tell the user that the undo results in no change,
1121  # i.e. the revisions were already undone.
1122  $undoMsg = 'nochange';
1123  $content = false;
1124  } else {
1125  # Inform the user of our success and set an automatic edit summary
1126  $undoMsg = 'success';
1127 
1128  # If we just undid one rev, use an autosummary
1129  $firstrev = $oldrev->getNext();
1130  if ( $firstrev && $firstrev->getId() == $undo ) {
1131  $userText = $undorev->getUserText();
1132  if ( $userText === '' ) {
1133  $undoSummary = wfMessage(
1134  'undo-summary-username-hidden',
1135  $undo
1136  )->inContentLanguage()->text();
1137  } else {
1138  $undoSummary = wfMessage(
1139  'undo-summary',
1140  $undo,
1141  $userText
1142  )->inContentLanguage()->text();
1143  }
1144  if ( $this->summary === '' ) {
1145  $this->summary = $undoSummary;
1146  } else {
1147  $this->summary = $undoSummary . wfMessage( 'colon-separator' )
1148  ->inContentLanguage()->text() . $this->summary;
1149  }
1150  $this->undidRev = $undo;
1151  }
1152  $this->formtype = 'diff';
1153  }
1154  }
1155  } else {
1156  // Failed basic sanity checks.
1157  // Older revisions may have been removed since the link
1158  // was created, or we may simply have got bogus input.
1159  $undoMsg = 'norev';
1160  }
1161 
1162  // Messages: undo-success, undo-failure, undo-norev, undo-nochange
1163  $class = ( $undoMsg == 'success' ? '' : 'error ' ) . "mw-undo-{$undoMsg}";
1164  $this->editFormPageTop .= $wgOut->parse( "<div class=\"{$class}\">" .
1165  wfMessage( 'undo-' . $undoMsg )->plain() . '</div>', true, /* interface */true );
1166  }
1167 
1168  if ( $content === false ) {
1169  $content = $this->getOriginalContent( $wgUser );
1170  }
1171  }
1172  }
1173 
1174  return $content;
1175  }
1176 
1192  private function getOriginalContent( User $user ) {
1193  if ( $this->section == 'new' ) {
1194  return $this->getCurrentContent();
1195  }
1196  $revision = $this->mArticle->getRevisionFetched();
1197  if ( $revision === null ) {
1198  if ( !$this->contentModel ) {
1199  $this->contentModel = $this->getTitle()->getContentModel();
1200  }
1201  $handler = ContentHandler::getForModelID( $this->contentModel );
1202 
1203  return $handler->makeEmptyContent();
1204  }
1205  $content = $revision->getContent( Revision::FOR_THIS_USER, $user );
1206  return $content;
1207  }
1208 
1221  public function getParentRevId() {
1222  if ( $this->parentRevId ) {
1223  return $this->parentRevId;
1224  } else {
1225  return $this->mArticle->getRevIdFetched();
1226  }
1227  }
1228 
1237  protected function getCurrentContent() {
1238  $rev = $this->page->getRevision();
1239  $content = $rev ? $rev->getContent( Revision::RAW ) : null;
1240 
1241  if ( $content === false || $content === null ) {
1242  if ( !$this->contentModel ) {
1243  $this->contentModel = $this->getTitle()->getContentModel();
1244  }
1245  $handler = ContentHandler::getForModelID( $this->contentModel );
1246 
1247  return $handler->makeEmptyContent();
1248  } else {
1249  # nasty side-effect, but needed for consistency
1250  $this->contentModel = $rev->getContentModel();
1251  $this->contentFormat = $rev->getContentFormat();
1252 
1253  return $content;
1254  }
1255  }
1256 
1264  public function setPreloadedContent( Content $content ) {
1265  $this->mPreloadContent = $content;
1266  }
1267 
1279  protected function getPreloadedContent( $preload, $params = [] ) {
1280  global $wgUser;
1281 
1282  if ( !empty( $this->mPreloadContent ) ) {
1283  return $this->mPreloadContent;
1284  }
1285 
1287 
1288  if ( $preload === '' ) {
1289  return $handler->makeEmptyContent();
1290  }
1291 
1292  $title = Title::newFromText( $preload );
1293  # Check for existence to avoid getting MediaWiki:Noarticletext
1294  if ( $title === null || !$title->exists() || !$title->userCan( 'read', $wgUser ) ) {
1295  // TODO: somehow show a warning to the user!
1296  return $handler->makeEmptyContent();
1297  }
1298 
1300  if ( $page->isRedirect() ) {
1302  # Same as before
1303  if ( $title === null || !$title->exists() || !$title->userCan( 'read', $wgUser ) ) {
1304  // TODO: somehow show a warning to the user!
1305  return $handler->makeEmptyContent();
1306  }
1308  }
1309 
1310  $parserOptions = ParserOptions::newFromUser( $wgUser );
1312 
1313  if ( !$content ) {
1314  // TODO: somehow show a warning to the user!
1315  return $handler->makeEmptyContent();
1316  }
1317 
1318  if ( $content->getModel() !== $handler->getModelID() ) {
1319  $converted = $content->convert( $handler->getModelID() );
1320 
1321  if ( !$converted ) {
1322  // TODO: somehow show a warning to the user!
1323  wfDebug( "Attempt to preload incompatible content: " .
1324  "can't convert " . $content->getModel() .
1325  " to " . $handler->getModelID() );
1326 
1327  return $handler->makeEmptyContent();
1328  }
1329 
1330  $content = $converted;
1331  }
1332 
1333  return $content->preloadTransform( $title, $parserOptions, $params );
1334  }
1335 
1343  function tokenOk( &$request ) {
1344  global $wgUser;
1345  $token = $request->getVal( 'wpEditToken' );
1346  $this->mTokenOk = $wgUser->matchEditToken( $token );
1347  $this->mTokenOkExceptSuffix = $wgUser->matchEditTokenNoSuffix( $token );
1348  return $this->mTokenOk;
1349  }
1350 
1367  protected function setPostEditCookie( $statusValue ) {
1368  $revisionId = $this->page->getLatest();
1369  $postEditKey = self::POST_EDIT_COOKIE_KEY_PREFIX . $revisionId;
1370 
1371  $val = 'saved';
1372  if ( $statusValue == self::AS_SUCCESS_NEW_ARTICLE ) {
1373  $val = 'created';
1374  } elseif ( $this->oldid ) {
1375  $val = 'restored';
1376  }
1377 
1378  $response = RequestContext::getMain()->getRequest()->response();
1379  $response->setCookie( $postEditKey, $val, time() + self::POST_EDIT_COOKIE_DURATION, [
1380  'httpOnly' => false,
1381  ] );
1382  }
1383 
1390  public function attemptSave( &$resultDetails = false ) {
1391  global $wgUser;
1392 
1393  # Allow bots to exempt some edits from bot flagging
1394  $bot = $wgUser->isAllowed( 'bot' ) && $this->bot;
1395  $status = $this->internalAttemptSave( $resultDetails, $bot );
1396 
1397  Hooks::run( 'EditPage::attemptSave:after', [ $this, $status, $resultDetails ] );
1398 
1399  return $status;
1400  }
1401 
1411  private function handleStatus( Status $status, $resultDetails ) {
1413 
1418  if ( $status->value == self::AS_SUCCESS_UPDATE
1419  || $status->value == self::AS_SUCCESS_NEW_ARTICLE
1420  ) {
1421  $this->didSave = true;
1422  if ( !$resultDetails['nullEdit'] ) {
1423  $this->setPostEditCookie( $status->value );
1424  }
1425  }
1426 
1427  // "wpExtraQueryRedirect" is a hidden input to modify
1428  // after save URL and is not used by actual edit form
1429  $request = RequestContext::getMain()->getRequest();
1430  $extraQueryRedirect = $request->getVal( 'wpExtraQueryRedirect' );
1431 
1432  switch ( $status->value ) {
1433  case self::AS_HOOK_ERROR_EXPECTED:
1434  case self::AS_CONTENT_TOO_BIG:
1435  case self::AS_ARTICLE_WAS_DELETED:
1436  case self::AS_CONFLICT_DETECTED:
1437  case self::AS_SUMMARY_NEEDED:
1438  case self::AS_TEXTBOX_EMPTY:
1439  case self::AS_MAX_ARTICLE_SIZE_EXCEEDED:
1440  case self::AS_END:
1441  case self::AS_BLANK_ARTICLE:
1442  case self::AS_SELF_REDIRECT:
1443  return true;
1444 
1445  case self::AS_HOOK_ERROR:
1446  return false;
1447 
1448  case self::AS_CANNOT_USE_CUSTOM_MODEL:
1449  case self::AS_PARSE_ERROR:
1450  $wgOut->addWikiText( '<div class="error">' . $status->getWikiText() . '</div>' );
1451  return true;
1452 
1453  case self::AS_SUCCESS_NEW_ARTICLE:
1454  $query = $resultDetails['redirect'] ? 'redirect=no' : '';
1455  if ( $extraQueryRedirect ) {
1456  if ( $query === '' ) {
1457  $query = $extraQueryRedirect;
1458  } else {
1459  $query = $query . '&' . $extraQueryRedirect;
1460  }
1461  }
1462  $anchor = isset( $resultDetails['sectionanchor'] ) ? $resultDetails['sectionanchor'] : '';
1463  $wgOut->redirect( $this->mTitle->getFullURL( $query ) . $anchor );
1464  return false;
1465 
1466  case self::AS_SUCCESS_UPDATE:
1467  $extraQuery = '';
1468  $sectionanchor = $resultDetails['sectionanchor'];
1469 
1470  // Give extensions a chance to modify URL query on update
1471  Hooks::run(
1472  'ArticleUpdateBeforeRedirect',
1473  [ $this->mArticle, &$sectionanchor, &$extraQuery ]
1474  );
1475 
1476  if ( $resultDetails['redirect'] ) {
1477  if ( $extraQuery == '' ) {
1478  $extraQuery = 'redirect=no';
1479  } else {
1480  $extraQuery = 'redirect=no&' . $extraQuery;
1481  }
1482  }
1483  if ( $extraQueryRedirect ) {
1484  if ( $extraQuery === '' ) {
1485  $extraQuery = $extraQueryRedirect;
1486  } else {
1487  $extraQuery = $extraQuery . '&' . $extraQueryRedirect;
1488  }
1489  }
1490 
1491  $wgOut->redirect( $this->mTitle->getFullURL( $extraQuery ) . $sectionanchor );
1492  return false;
1493 
1494  case self::AS_SPAM_ERROR:
1495  $this->spamPageWithContent( $resultDetails['spam'] );
1496  return false;
1497 
1498  case self::AS_BLOCKED_PAGE_FOR_USER:
1499  throw new UserBlockedError( $wgUser->getBlock() );
1500 
1501  case self::AS_IMAGE_REDIRECT_ANON:
1502  case self::AS_IMAGE_REDIRECT_LOGGED:
1503  throw new PermissionsError( 'upload' );
1504 
1505  case self::AS_READ_ONLY_PAGE_ANON:
1506  case self::AS_READ_ONLY_PAGE_LOGGED:
1507  throw new PermissionsError( 'edit' );
1508 
1509  case self::AS_READ_ONLY_PAGE:
1510  throw new ReadOnlyError;
1511 
1512  case self::AS_RATE_LIMITED:
1513  throw new ThrottledError();
1514 
1515  case self::AS_NO_CREATE_PERMISSION:
1516  $permission = $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage';
1517  throw new PermissionsError( $permission );
1518 
1519  case self::AS_NO_CHANGE_CONTENT_MODEL:
1520  throw new PermissionsError( 'editcontentmodel' );
1521 
1522  default:
1523  // We don't recognize $status->value. The only way that can happen
1524  // is if an extension hook aborted from inside ArticleSave.
1525  // Render the status object into $this->hookError
1526  // FIXME this sucks, we should just use the Status object throughout
1527  $this->hookError = '<div class="error">' . $status->getWikiText() .
1528  '</div>';
1529  return true;
1530  }
1531  }
1532 
1543  // Run old style post-section-merge edit filter
1544  if ( !ContentHandler::runLegacyHooks( 'EditFilterMerged',
1545  [ $this, $content, &$this->hookError, $this->summary ] )
1546  ) {
1547  # Error messages etc. could be handled within the hook...
1548  $status->fatal( 'hookaborted' );
1549  $status->value = self::AS_HOOK_ERROR;
1550  return false;
1551  } elseif ( $this->hookError != '' ) {
1552  # ...or the hook could be expecting us to produce an error
1553  $status->fatal( 'hookaborted' );
1554  $status->value = self::AS_HOOK_ERROR_EXPECTED;
1555  return false;
1556  }
1557 
1558  // Run new style post-section-merge edit filter
1559  if ( !Hooks::run( 'EditFilterMergedContent',
1560  [ $this->mArticle->getContext(), $content, $status, $this->summary,
1561  $user, $this->minoredit ] )
1562  ) {
1563  # Error messages etc. could be handled within the hook...
1564  if ( $status->isGood() ) {
1565  $status->fatal( 'hookaborted' );
1566  // Not setting $this->hookError here is a hack to allow the hook
1567  // to cause a return to the edit page without $this->hookError
1568  // being set. This is used by ConfirmEdit to display a captcha
1569  // without any error message cruft.
1570  } else {
1571  $this->hookError = $status->getWikiText();
1572  }
1573  // Use the existing $status->value if the hook set it
1574  if ( !$status->value ) {
1575  $status->value = self::AS_HOOK_ERROR;
1576  }
1577  return false;
1578  } elseif ( !$status->isOK() ) {
1579  # ...or the hook could be expecting us to produce an error
1580  // FIXME this sucks, we should just use the Status object throughout
1581  $this->hookError = $status->getWikiText();
1582  $status->fatal( 'hookaborted' );
1583  $status->value = self::AS_HOOK_ERROR_EXPECTED;
1584  return false;
1585  }
1586 
1587  return true;
1588  }
1589 
1596  private function newSectionSummary( &$sectionanchor = null ) {
1597  global $wgParser;
1598 
1599  if ( $this->sectiontitle !== '' ) {
1600  $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle );
1601  // If no edit summary was specified, create one automatically from the section
1602  // title and have it link to the new section. Otherwise, respect the summary as
1603  // passed.
1604  if ( $this->summary === '' ) {
1605  $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
1606  return wfMessage( 'newsectionsummary' )
1607  ->rawParams( $cleanSectionTitle )->inContentLanguage()->text();
1608  }
1609  } elseif ( $this->summary !== '' ) {
1610  $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->summary );
1611  # This is a new section, so create a link to the new section
1612  # in the revision summary.
1613  $cleanSummary = $wgParser->stripSectionName( $this->summary );
1614  return wfMessage( 'newsectionsummary' )
1615  ->rawParams( $cleanSummary )->inContentLanguage()->text();
1616  }
1617  return $this->summary;
1618  }
1619 
1644  function internalAttemptSave( &$result, $bot = false ) {
1646  global $wgContentHandlerUseDB;
1647 
1649 
1650  if ( !Hooks::run( 'EditPage::attemptSave', [ $this ] ) ) {
1651  wfDebug( "Hook 'EditPage::attemptSave' aborted article saving\n" );
1652  $status->fatal( 'hookaborted' );
1653  $status->value = self::AS_HOOK_ERROR;
1654  return $status;
1655  }
1656 
1657  $spam = $wgRequest->getText( 'wpAntispam' );
1658  if ( $spam !== '' ) {
1659  wfDebugLog(
1660  'SimpleAntiSpam',
1661  $wgUser->getName() .
1662  ' editing "' .
1663  $this->mTitle->getPrefixedText() .
1664  '" submitted bogus field "' .
1665  $spam .
1666  '"'
1667  );
1668  $status->fatal( 'spamprotectionmatch', false );
1669  $status->value = self::AS_SPAM_ERROR;
1670  return $status;
1671  }
1672 
1673  try {
1674  # Construct Content object
1675  $textbox_content = $this->toEditContent( $this->textbox1 );
1676  } catch ( MWContentSerializationException $ex ) {
1677  $status->fatal(
1678  'content-failed-to-parse',
1679  $this->contentModel,
1680  $this->contentFormat,
1681  $ex->getMessage()
1682  );
1683  $status->value = self::AS_PARSE_ERROR;
1684  return $status;
1685  }
1686 
1687  # Check image redirect
1688  if ( $this->mTitle->getNamespace() == NS_FILE &&
1689  $textbox_content->isRedirect() &&
1690  !$wgUser->isAllowed( 'upload' )
1691  ) {
1692  $code = $wgUser->isAnon() ? self::AS_IMAGE_REDIRECT_ANON : self::AS_IMAGE_REDIRECT_LOGGED;
1693  $status->setResult( false, $code );
1694 
1695  return $status;
1696  }
1697 
1698  # Check for spam
1699  $match = self::matchSummarySpamRegex( $this->summary );
1700  if ( $match === false && $this->section == 'new' ) {
1701  # $wgSpamRegex is enforced on this new heading/summary because, unlike
1702  # regular summaries, it is added to the actual wikitext.
1703  if ( $this->sectiontitle !== '' ) {
1704  # This branch is taken when the API is used with the 'sectiontitle' parameter.
1705  $match = self::matchSpamRegex( $this->sectiontitle );
1706  } else {
1707  # This branch is taken when the "Add Topic" user interface is used, or the API
1708  # is used with the 'summary' parameter.
1709  $match = self::matchSpamRegex( $this->summary );
1710  }
1711  }
1712  if ( $match === false ) {
1713  $match = self::matchSpamRegex( $this->textbox1 );
1714  }
1715  if ( $match !== false ) {
1716  $result['spam'] = $match;
1717  $ip = $wgRequest->getIP();
1718  $pdbk = $this->mTitle->getPrefixedDBkey();
1719  $match = str_replace( "\n", '', $match );
1720  wfDebugLog( 'SpamRegex', "$ip spam regex hit [[$pdbk]]: \"$match\"" );
1721  $status->fatal( 'spamprotectionmatch', $match );
1722  $status->value = self::AS_SPAM_ERROR;
1723  return $status;
1724  }
1725  if ( !Hooks::run(
1726  'EditFilter',
1727  [ $this, $this->textbox1, $this->section, &$this->hookError, $this->summary ] )
1728  ) {
1729  # Error messages etc. could be handled within the hook...
1730  $status->fatal( 'hookaborted' );
1731  $status->value = self::AS_HOOK_ERROR;
1732  return $status;
1733  } elseif ( $this->hookError != '' ) {
1734  # ...or the hook could be expecting us to produce an error
1735  $status->fatal( 'hookaborted' );
1736  $status->value = self::AS_HOOK_ERROR_EXPECTED;
1737  return $status;
1738  }
1739 
1740  if ( $wgUser->isBlockedFrom( $this->mTitle, false ) ) {
1741  // Auto-block user's IP if the account was "hard" blocked
1742  if ( !wfReadOnly() ) {
1743  $wgUser->spreadAnyEditBlock();
1744  }
1745  # Check block state against master, thus 'false'.
1746  $status->setResult( false, self::AS_BLOCKED_PAGE_FOR_USER );
1747  return $status;
1748  }
1749 
1750  $this->kblength = (int)( strlen( $this->textbox1 ) / 1024 );
1751  if ( $this->kblength > $wgMaxArticleSize ) {
1752  // Error will be displayed by showEditForm()
1753  $this->tooBig = true;
1754  $status->setResult( false, self::AS_CONTENT_TOO_BIG );
1755  return $status;
1756  }
1757 
1758  if ( !$wgUser->isAllowed( 'edit' ) ) {
1759  if ( $wgUser->isAnon() ) {
1760  $status->setResult( false, self::AS_READ_ONLY_PAGE_ANON );
1761  return $status;
1762  } else {
1763  $status->fatal( 'readonlytext' );
1764  $status->value = self::AS_READ_ONLY_PAGE_LOGGED;
1765  return $status;
1766  }
1767  }
1768 
1769  $changingContentModel = false;
1770  if ( $this->contentModel !== $this->mTitle->getContentModel() ) {
1771  if ( !$wgContentHandlerUseDB ) {
1772  $status->fatal( 'editpage-cannot-use-custom-model' );
1773  $status->value = self::AS_CANNOT_USE_CUSTOM_MODEL;
1774  return $status;
1775  } elseif ( !$wgUser->isAllowed( 'editcontentmodel' ) ) {
1776  $status->setResult( false, self::AS_NO_CHANGE_CONTENT_MODEL );
1777  return $status;
1778 
1779  }
1780  $changingContentModel = true;
1781  $oldContentModel = $this->mTitle->getContentModel();
1782  }
1783 
1784  if ( $this->changeTags ) {
1785  $changeTagsStatus = ChangeTags::canAddTagsAccompanyingChange(
1786  $this->changeTags, $wgUser );
1787  if ( !$changeTagsStatus->isOK() ) {
1788  $changeTagsStatus->value = self::AS_CHANGE_TAG_ERROR;
1789  return $changeTagsStatus;
1790  }
1791  }
1792 
1793  if ( wfReadOnly() ) {
1794  $status->fatal( 'readonlytext' );
1795  $status->value = self::AS_READ_ONLY_PAGE;
1796  return $status;
1797  }
1798  if ( $wgUser->pingLimiter() || $wgUser->pingLimiter( 'linkpurge', 0 ) ) {
1799  $status->fatal( 'actionthrottledtext' );
1800  $status->value = self::AS_RATE_LIMITED;
1801  return $status;
1802  }
1803 
1804  # If the article has been deleted while editing, don't save it without
1805  # confirmation
1806  if ( $this->wasDeletedSinceLastEdit() && !$this->recreate ) {
1807  $status->setResult( false, self::AS_ARTICLE_WAS_DELETED );
1808  return $status;
1809  }
1810 
1811  # Load the page data from the master. If anything changes in the meantime,
1812  # we detect it by using page_latest like a token in a 1 try compare-and-swap.
1813  $this->page->loadPageData( 'fromdbmaster' );
1814  $new = !$this->page->exists();
1815 
1816  if ( $new ) {
1817  // Late check for create permission, just in case *PARANOIA*
1818  if ( !$this->mTitle->userCan( 'create', $wgUser ) ) {
1819  $status->fatal( 'nocreatetext' );
1820  $status->value = self::AS_NO_CREATE_PERMISSION;
1821  wfDebug( __METHOD__ . ": no create permission\n" );
1822  return $status;
1823  }
1824 
1825  // Don't save a new page if it's blank or if it's a MediaWiki:
1826  // message with content equivalent to default (allow empty pages
1827  // in this case to disable messages, see bug 50124)
1828  $defaultMessageText = $this->mTitle->getDefaultMessageText();
1829  if ( $this->mTitle->getNamespace() === NS_MEDIAWIKI && $defaultMessageText !== false ) {
1830  $defaultText = $defaultMessageText;
1831  } else {
1832  $defaultText = '';
1833  }
1834 
1835  if ( !$this->allowBlankArticle && $this->textbox1 === $defaultText ) {
1836  $this->blankArticle = true;
1837  $status->fatal( 'blankarticle' );
1838  $status->setResult( false, self::AS_BLANK_ARTICLE );
1839  return $status;
1840  }
1841 
1842  if ( !$this->runPostMergeFilters( $textbox_content, $status, $wgUser ) ) {
1843  return $status;
1844  }
1845 
1846  $content = $textbox_content;
1847 
1848  $result['sectionanchor'] = '';
1849  if ( $this->section == 'new' ) {
1850  if ( $this->sectiontitle !== '' ) {
1851  // Insert the section title above the content.
1852  $content = $content->addSectionHeader( $this->sectiontitle );
1853  } elseif ( $this->summary !== '' ) {
1854  // Insert the section title above the content.
1855  $content = $content->addSectionHeader( $this->summary );
1856  }
1857  $this->summary = $this->newSectionSummary( $result['sectionanchor'] );
1858  }
1859 
1860  $status->value = self::AS_SUCCESS_NEW_ARTICLE;
1861 
1862  } else { # not $new
1863 
1864  # Article exists. Check for edit conflict.
1865 
1866  $this->page->clear(); # Force reload of dates, etc.
1867  $timestamp = $this->page->getTimestamp();
1868 
1869  wfDebug( "timestamp: {$timestamp}, edittime: {$this->edittime}\n" );
1870 
1871  if ( $timestamp != $this->edittime ) {
1872  $this->isConflict = true;
1873  if ( $this->section == 'new' ) {
1874  if ( $this->page->getUserText() == $wgUser->getName() &&
1875  $this->page->getComment() == $this->newSectionSummary()
1876  ) {
1877  // Probably a duplicate submission of a new comment.
1878  // This can happen when CDN resends a request after
1879  // a timeout but the first one actually went through.
1880  wfDebug( __METHOD__
1881  . ": duplicate new section submission; trigger edit conflict!\n" );
1882  } else {
1883  // New comment; suppress conflict.
1884  $this->isConflict = false;
1885  wfDebug( __METHOD__ . ": conflict suppressed; new section\n" );
1886  }
1887  } elseif ( $this->section == ''
1889  DB_MASTER, $this->mTitle->getArticleID(),
1890  $wgUser->getId(), $this->edittime
1891  )
1892  ) {
1893  # Suppress edit conflict with self, except for section edits where merging is required.
1894  wfDebug( __METHOD__ . ": Suppressing edit conflict, same user.\n" );
1895  $this->isConflict = false;
1896  }
1897  }
1898 
1899  // If sectiontitle is set, use it, otherwise use the summary as the section title.
1900  if ( $this->sectiontitle !== '' ) {
1901  $sectionTitle = $this->sectiontitle;
1902  } else {
1903  $sectionTitle = $this->summary;
1904  }
1905 
1906  $content = null;
1907 
1908  if ( $this->isConflict ) {
1909  wfDebug( __METHOD__
1910  . ": conflict! getting section '{$this->section}' for time '{$this->edittime}'"
1911  . " (article time '{$timestamp}')\n" );
1912 
1913  $content = $this->page->replaceSectionContent(
1914  $this->section,
1915  $textbox_content,
1916  $sectionTitle,
1917  $this->edittime
1918  );
1919  } else {
1920  wfDebug( __METHOD__ . ": getting section '{$this->section}'\n" );
1921  $content = $this->page->replaceSectionContent(
1922  $this->section,
1923  $textbox_content,
1924  $sectionTitle
1925  );
1926  }
1927 
1928  if ( is_null( $content ) ) {
1929  wfDebug( __METHOD__ . ": activating conflict; section replace failed.\n" );
1930  $this->isConflict = true;
1931  $content = $textbox_content; // do not try to merge here!
1932  } elseif ( $this->isConflict ) {
1933  # Attempt merge
1934  if ( $this->mergeChangesIntoContent( $content ) ) {
1935  // Successful merge! Maybe we should tell the user the good news?
1936  $this->isConflict = false;
1937  wfDebug( __METHOD__ . ": Suppressing edit conflict, successful merge.\n" );
1938  } else {
1939  $this->section = '';
1940  $this->textbox1 = ContentHandler::getContentText( $content );
1941  wfDebug( __METHOD__ . ": Keeping edit conflict, failed merge.\n" );
1942  }
1943  }
1944 
1945  if ( $this->isConflict ) {
1946  $status->setResult( false, self::AS_CONFLICT_DETECTED );
1947  return $status;
1948  }
1949 
1950  if ( !$this->runPostMergeFilters( $content, $status, $wgUser ) ) {
1951  return $status;
1952  }
1953 
1954  if ( $this->section == 'new' ) {
1955  // Handle the user preference to force summaries here
1956  if ( !$this->allowBlankSummary && trim( $this->summary ) == '' ) {
1957  $this->missingSummary = true;
1958  $status->fatal( 'missingsummary' ); // or 'missingcommentheader' if $section == 'new'. Blegh
1959  $status->value = self::AS_SUMMARY_NEEDED;
1960  return $status;
1961  }
1962 
1963  // Do not allow the user to post an empty comment
1964  if ( $this->textbox1 == '' ) {
1965  $this->missingComment = true;
1966  $status->fatal( 'missingcommenttext' );
1967  $status->value = self::AS_TEXTBOX_EMPTY;
1968  return $status;
1969  }
1970  } elseif ( !$this->allowBlankSummary
1971  && !$content->equals( $this->getOriginalContent( $wgUser ) )
1972  && !$content->isRedirect()
1973  && md5( $this->summary ) == $this->autoSumm
1974  ) {
1975  $this->missingSummary = true;
1976  $status->fatal( 'missingsummary' );
1977  $status->value = self::AS_SUMMARY_NEEDED;
1978  return $status;
1979  }
1980 
1981  # All's well
1982  $sectionanchor = '';
1983  if ( $this->section == 'new' ) {
1984  $this->summary = $this->newSectionSummary( $sectionanchor );
1985  } elseif ( $this->section != '' ) {
1986  # Try to get a section anchor from the section source, redirect
1987  # to edited section if header found.
1988  # XXX: Might be better to integrate this into Article::replaceSectionAtRev
1989  # for duplicate heading checking and maybe parsing.
1990  $hasmatch = preg_match( "/^ *([=]{1,6})(.*?)(\\1) *\\n/i", $this->textbox1, $matches );
1991  # We can't deal with anchors, includes, html etc in the header for now,
1992  # headline would need to be parsed to improve this.
1993  if ( $hasmatch && strlen( $matches[2] ) > 0 ) {
1994  $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $matches[2] );
1995  }
1996  }
1997  $result['sectionanchor'] = $sectionanchor;
1998 
1999  // Save errors may fall down to the edit form, but we've now
2000  // merged the section into full text. Clear the section field
2001  // so that later submission of conflict forms won't try to
2002  // replace that into a duplicated mess.
2003  $this->textbox1 = $this->toEditText( $content );
2004  $this->section = '';
2005 
2006  $status->value = self::AS_SUCCESS_UPDATE;
2007  }
2008 
2009  if ( !$this->allowSelfRedirect
2010  && $content->isRedirect()
2011  && $content->getRedirectTarget()->equals( $this->getTitle() )
2012  ) {
2013  // If the page already redirects to itself, don't warn.
2014  $currentTarget = $this->getCurrentContent()->getRedirectTarget();
2015  if ( !$currentTarget || !$currentTarget->equals( $this->getTitle() ) ) {
2016  $this->selfRedirect = true;
2017  $status->fatal( 'selfredirect' );
2018  $status->value = self::AS_SELF_REDIRECT;
2019  return $status;
2020  }
2021  }
2022 
2023  // Check for length errors again now that the section is merged in
2024  $this->kblength = (int)( strlen( $this->toEditText( $content ) ) / 1024 );
2025  if ( $this->kblength > $wgMaxArticleSize ) {
2026  $this->tooBig = true;
2027  $status->setResult( false, self::AS_MAX_ARTICLE_SIZE_EXCEEDED );
2028  return $status;
2029  }
2030 
2032  ( $new ? EDIT_NEW : EDIT_UPDATE ) |
2033  ( ( $this->minoredit && !$this->isNew ) ? EDIT_MINOR : 0 ) |
2034  ( $bot ? EDIT_FORCE_BOT : 0 );
2035 
2036  $doEditStatus = $this->page->doEditContent(
2037  $content,
2038  $this->summary,
2039  $flags,
2040  false,
2041  $wgUser,
2042  $content->getDefaultFormat(),
2044  );
2045 
2046  if ( !$doEditStatus->isOK() ) {
2047  // Failure from doEdit()
2048  // Show the edit conflict page for certain recognized errors from doEdit(),
2049  // but don't show it for errors from extension hooks
2050  $errors = $doEditStatus->getErrorsArray();
2051  if ( in_array( $errors[0][0],
2052  [ 'edit-gone-missing', 'edit-conflict', 'edit-already-exists' ] )
2053  ) {
2054  $this->isConflict = true;
2055  // Destroys data doEdit() put in $status->value but who cares
2056  $doEditStatus->value = self::AS_END;
2057  }
2058  return $doEditStatus;
2059  }
2060 
2061  $result['nullEdit'] = $doEditStatus->hasMessage( 'edit-no-change' );
2062  if ( $result['nullEdit'] ) {
2063  // We don't know if it was a null edit until now, so increment here
2064  $wgUser->pingLimiter( 'linkpurge' );
2065  }
2066  $result['redirect'] = $content->isRedirect();
2067 
2068  $this->updateWatchlist();
2069 
2070  // If the content model changed, add a log entry
2071  if ( $changingContentModel ) {
2073  $wgUser,
2074  $new ? false : $oldContentModel,
2075  $this->contentModel,
2076  $this->summary
2077  );
2078  }
2079 
2080  return $status;
2081  }
2082 
2089  protected function addContentModelChangeLogEntry( User $user, $oldModel, $newModel, $reason ) {
2090  $new = $oldModel === false;
2091  $log = new ManualLogEntry( 'contentmodel', $new ? 'new' : 'change' );
2092  $log->setPerformer( $user );
2093  $log->setTarget( $this->mTitle );
2094  $log->setComment( $reason );
2095  $log->setParameters( [
2096  '4::oldmodel' => $oldModel,
2097  '5::newmodel' => $newModel
2098  ] );
2099  $logid = $log->insert();
2100  $log->publish( $logid );
2101  }
2102 
2106  protected function updateWatchlist() {
2107  global $wgUser;
2108 
2109  if ( !$wgUser->isLoggedIn() ) {
2110  return;
2111  }
2112 
2113  $user = $wgUser;
2115  $watch = $this->watchthis;
2116  // Do this in its own transaction to reduce contention...
2117  DeferredUpdates::addCallableUpdate( function () use ( $user, $title, $watch ) {
2118  if ( $watch == $user->isWatched( $title, User::IGNORE_USER_RIGHTS ) ) {
2119  return; // nothing to change
2120  }
2122  } );
2123  }
2124 
2136  private function mergeChangesIntoContent( &$editContent ) {
2137 
2138  $db = wfGetDB( DB_MASTER );
2139 
2140  // This is the revision the editor started from
2141  $baseRevision = $this->getBaseRevision();
2142  $baseContent = $baseRevision ? $baseRevision->getContent() : null;
2143 
2144  if ( is_null( $baseContent ) ) {
2145  return false;
2146  }
2147 
2148  // The current state, we want to merge updates into it
2149  $currentRevision = Revision::loadFromTitle( $db, $this->mTitle );
2150  $currentContent = $currentRevision ? $currentRevision->getContent() : null;
2151 
2152  if ( is_null( $currentContent ) ) {
2153  return false;
2154  }
2155 
2156  $handler = ContentHandler::getForModelID( $baseContent->getModel() );
2157 
2158  $result = $handler->merge3( $baseContent, $editContent, $currentContent );
2159 
2160  if ( $result ) {
2161  $editContent = $result;
2162  // Update parentRevId to what we just merged.
2163  $this->parentRevId = $currentRevision->getId();
2164  return true;
2165  }
2166 
2167  return false;
2168  }
2169 
2175  function getBaseRevision() {
2176  if ( !$this->mBaseRevision ) {
2177  $db = wfGetDB( DB_MASTER );
2178  $this->mBaseRevision = Revision::loadFromTimestamp(
2179  $db, $this->mTitle, $this->edittime );
2180  }
2181  return $this->mBaseRevision;
2182  }
2183 
2191  public static function matchSpamRegex( $text ) {
2192  global $wgSpamRegex;
2193  // For back compatibility, $wgSpamRegex may be a single string or an array of regexes.
2194  $regexes = (array)$wgSpamRegex;
2195  return self::matchSpamRegexInternal( $text, $regexes );
2196  }
2197 
2205  public static function matchSummarySpamRegex( $text ) {
2206  global $wgSummarySpamRegex;
2207  $regexes = (array)$wgSummarySpamRegex;
2208  return self::matchSpamRegexInternal( $text, $regexes );
2209  }
2210 
2216  protected static function matchSpamRegexInternal( $text, $regexes ) {
2217  foreach ( $regexes as $regex ) {
2218  $matches = [];
2219  if ( preg_match( $regex, $text, $matches ) ) {
2220  return $matches[0];
2221  }
2222  }
2223  return false;
2224  }
2225 
2226  function setHeaders() {
2227  global $wgOut, $wgUser, $wgAjaxEditStash;
2228 
2229  $wgOut->addModules( 'mediawiki.action.edit' );
2230  $wgOut->addModuleStyles( 'mediawiki.action.edit.styles' );
2231 
2232  if ( $wgUser->getOption( 'showtoolbar' ) ) {
2233  // The addition of default buttons is handled by getEditToolbar() which
2234  // has its own dependency on this module. The call here ensures the module
2235  // is loaded in time (it has position "top") for other modules to register
2236  // buttons (e.g. extensions, gadgets, user scripts).
2237  $wgOut->addModules( 'mediawiki.toolbar' );
2238  }
2239 
2240  if ( $wgUser->getOption( 'uselivepreview' ) ) {
2241  $wgOut->addModules( 'mediawiki.action.edit.preview' );
2242  }
2243 
2244  if ( $wgUser->getOption( 'useeditwarning' ) ) {
2245  $wgOut->addModules( 'mediawiki.action.edit.editWarning' );
2246  }
2247 
2248  if ( $wgAjaxEditStash ) {
2249  $wgOut->addModules( 'mediawiki.action.edit.stash' );
2250  }
2251 
2252  # Enabled article-related sidebar, toplinks, etc.
2253  $wgOut->setArticleRelated( true );
2254 
2255  $contextTitle = $this->getContextTitle();
2256  if ( $this->isConflict ) {
2257  $msg = 'editconflict';
2258  } elseif ( $contextTitle->exists() && $this->section != '' ) {
2259  $msg = $this->section == 'new' ? 'editingcomment' : 'editingsection';
2260  } else {
2261  $msg = $contextTitle->exists()
2262  || ( $contextTitle->getNamespace() == NS_MEDIAWIKI
2263  && $contextTitle->getDefaultMessageText() !== false
2264  )
2265  ? 'editing'
2266  : 'creating';
2267  }
2268 
2269  # Use the title defined by DISPLAYTITLE magic word when present
2270  # NOTE: getDisplayTitle() returns HTML while getPrefixedText() returns plain text.
2271  # setPageTitle() treats the input as wikitext, which should be safe in either case.
2272  $displayTitle = isset( $this->mParserOutput ) ? $this->mParserOutput->getDisplayTitle() : false;
2273  if ( $displayTitle === false ) {
2274  $displayTitle = $contextTitle->getPrefixedText();
2275  }
2276  $wgOut->setPageTitle( wfMessage( $msg, $displayTitle ) );
2277  # Transmit the name of the message to JavaScript for live preview
2278  # Keep Resources.php/mediawiki.action.edit.preview in sync with the possible keys
2279  $wgOut->addJsConfigVars( 'wgEditMessage', $msg );
2280  }
2281 
2285  protected function showIntro() {
2287  if ( $this->suppressIntro ) {
2288  return;
2289  }
2290 
2291  $namespace = $this->mTitle->getNamespace();
2292 
2293  if ( $namespace == NS_MEDIAWIKI ) {
2294  # Show a warning if editing an interface message
2295  $wgOut->wrapWikiMsg( "<div class='mw-editinginterface'>\n$1\n</div>", 'editinginterface' );
2296  # If this is a default message (but not css or js),
2297  # show a hint that it is translatable on translatewiki.net
2298  if ( !$this->mTitle->hasContentModel( CONTENT_MODEL_CSS )
2299  && !$this->mTitle->hasContentModel( CONTENT_MODEL_JAVASCRIPT )
2300  ) {
2301  $defaultMessageText = $this->mTitle->getDefaultMessageText();
2302  if ( $defaultMessageText !== false ) {
2303  $wgOut->wrapWikiMsg( "<div class='mw-translateinterface'>\n$1\n</div>",
2304  'translateinterface' );
2305  }
2306  }
2307  } elseif ( $namespace == NS_FILE ) {
2308  # Show a hint to shared repo
2309  $file = wfFindFile( $this->mTitle );
2310  if ( $file && !$file->isLocal() ) {
2311  $descUrl = $file->getDescriptionUrl();
2312  # there must be a description url to show a hint to shared repo
2313  if ( $descUrl ) {
2314  if ( !$this->mTitle->exists() ) {
2315  $wgOut->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-create\">\n$1\n</div>", [
2316  'sharedupload-desc-create', $file->getRepo()->getDisplayName(), $descUrl
2317  ] );
2318  } else {
2319  $wgOut->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-edit\">\n$1\n</div>", [
2320  'sharedupload-desc-edit', $file->getRepo()->getDisplayName(), $descUrl
2321  ] );
2322  }
2323  }
2324  }
2325  }
2326 
2327  # Show a warning message when someone creates/edits a user (talk) page but the user does not exist
2328  # Show log extract when the user is currently blocked
2329  if ( $namespace == NS_USER || $namespace == NS_USER_TALK ) {
2330  $username = explode( '/', $this->mTitle->getText(), 2 )[0];
2331  $user = User::newFromName( $username, false /* allow IP users*/ );
2332  $ip = User::isIP( $username );
2333  $block = Block::newFromTarget( $user, $user );
2334  if ( !( $user && $user->isLoggedIn() ) && !$ip ) { # User does not exist
2335  $wgOut->wrapWikiMsg( "<div class=\"mw-userpage-userdoesnotexist error\">\n$1\n</div>",
2336  [ 'userpage-userdoesnotexist', wfEscapeWikiText( $username ) ] );
2337  } elseif ( !is_null( $block ) && $block->getType() != Block::TYPE_AUTO ) {
2338  # Show log extract if the user is currently blocked
2340  $wgOut,
2341  'block',
2342  MWNamespace::getCanonicalName( NS_USER ) . ':' . $block->getTarget(),
2343  '',
2344  [
2345  'lim' => 1,
2346  'showIfEmpty' => false,
2347  'msgKey' => [
2348  'blocked-notice-logextract',
2349  $user->getName() # Support GENDER in notice
2350  ]
2351  ]
2352  );
2353  }
2354  }
2355  # Try to add a custom edit intro, or use the standard one if this is not possible.
2356  if ( !$this->showCustomIntro() && !$this->mTitle->exists() ) {
2358  wfMessage( 'helppage' )->inContentLanguage()->text()
2359  ) );
2360  if ( $wgUser->isLoggedIn() ) {
2361  $wgOut->wrapWikiMsg(
2362  // Suppress the external link icon, consider the help url an internal one
2363  "<div class=\"mw-newarticletext plainlinks\">\n$1\n</div>",
2364  [
2365  'newarticletext',
2366  $helpLink
2367  ]
2368  );
2369  } else {
2370  $wgOut->wrapWikiMsg(
2371  // Suppress the external link icon, consider the help url an internal one
2372  "<div class=\"mw-newarticletextanon plainlinks\">\n$1\n</div>",
2373  [
2374  'newarticletextanon',
2375  $helpLink
2376  ]
2377  );
2378  }
2379  }
2380  # Give a notice if the user is editing a deleted/moved page...
2381  if ( !$this->mTitle->exists() ) {
2382  LogEventsList::showLogExtract( $wgOut, [ 'delete', 'move' ], $this->mTitle,
2383  '',
2384  [
2385  'lim' => 10,
2386  'conds' => [ "log_action != 'revision'" ],
2387  'showIfEmpty' => false,
2388  'msgKey' => [ 'recreate-moveddeleted-warn' ]
2389  ]
2390  );
2391  }
2392  }
2393 
2399  protected function showCustomIntro() {
2400  if ( $this->editintro ) {
2401  $title = Title::newFromText( $this->editintro );
2402  if ( $title instanceof Title && $title->exists() && $title->userCan( 'read' ) ) {
2403  global $wgOut;
2404  // Added using template syntax, to take <noinclude>'s into account.
2405  $wgOut->addWikiTextTitleTidy(
2406  '<div class="mw-editintro">{{:' . $title->getFullText() . '}}</div>',
2408  );
2409  return true;
2410  }
2411  }
2412  return false;
2413  }
2414 
2433  protected function toEditText( $content ) {
2434  if ( $content === null || $content === false || is_string( $content ) ) {
2435  return $content;
2436  }
2437 
2438  if ( !$this->isSupportedContentModel( $content->getModel() ) ) {
2439  throw new MWException( 'This content model is not supported: '
2440  . ContentHandler::getLocalizedName( $content->getModel() ) );
2441  }
2442 
2443  return $content->serialize( $this->contentFormat );
2444  }
2445 
2462  protected function toEditContent( $text ) {
2463  if ( $text === false || $text === null ) {
2464  return $text;
2465  }
2466 
2467  $content = ContentHandler::makeContent( $text, $this->getTitle(),
2468  $this->contentModel, $this->contentFormat );
2469 
2470  if ( !$this->isSupportedContentModel( $content->getModel() ) ) {
2471  throw new MWException( 'This content model is not supported: '
2472  . ContentHandler::getLocalizedName( $content->getModel() ) );
2473  }
2474 
2475  return $content;
2476  }
2477 
2486  function showEditForm( $formCallback = null ) {
2488 
2489  # need to parse the preview early so that we know which templates are used,
2490  # otherwise users with "show preview after edit box" will get a blank list
2491  # we parse this near the beginning so that setHeaders can do the title
2492  # setting work instead of leaving it in getPreviewText
2493  $previewOutput = '';
2494  if ( $this->formtype == 'preview' ) {
2495  $previewOutput = $this->getPreviewText();
2496  }
2497 
2498  Hooks::run( 'EditPage::showEditForm:initial', [ &$this, &$wgOut ] );
2499 
2500  $this->setHeaders();
2501 
2502  if ( $this->showHeader() === false ) {
2503  return;
2504  }
2505 
2506  $wgOut->addHTML( $this->editFormPageTop );
2507 
2508  if ( $wgUser->getOption( 'previewontop' ) ) {
2509  $this->displayPreviewArea( $previewOutput, true );
2510  }
2511 
2512  $wgOut->addHTML( $this->editFormTextTop );
2513 
2514  $showToolbar = true;
2515  if ( $this->wasDeletedSinceLastEdit() ) {
2516  if ( $this->formtype == 'save' ) {
2517  // Hide the toolbar and edit area, user can click preview to get it back
2518  // Add an confirmation checkbox and explanation.
2519  $showToolbar = false;
2520  } else {
2521  $wgOut->wrapWikiMsg( "<div class='error mw-deleted-while-editing'>\n$1\n</div>",
2522  'deletedwhileediting' );
2523  }
2524  }
2525 
2526  // @todo add EditForm plugin interface and use it here!
2527  // search for textarea1 and textares2, and allow EditForm to override all uses.
2528  $wgOut->addHTML( Html::openElement(
2529  'form',
2530  [
2531  'id' => self::EDITFORM_ID,
2532  'name' => self::EDITFORM_ID,
2533  'method' => 'post',
2534  'action' => $this->getActionURL( $this->getContextTitle() ),
2535  'enctype' => 'multipart/form-data'
2536  ]
2537  ) );
2538 
2539  if ( is_callable( $formCallback ) ) {
2540  wfWarn( 'The $formCallback parameter to ' . __METHOD__ . 'is deprecated' );
2541  call_user_func_array( $formCallback, [ &$wgOut ] );
2542  }
2543 
2544  // Add an empty field to trip up spambots
2545  $wgOut->addHTML(
2546  Xml::openElement( 'div', [ 'id' => 'antispam-container', 'style' => 'display: none;' ] )
2547  . Html::rawElement(
2548  'label',
2549  [ 'for' => 'wpAntispam' ],
2550  wfMessage( 'simpleantispam-label' )->parse()
2551  )
2552  . Xml::element(
2553  'input',
2554  [
2555  'type' => 'text',
2556  'name' => 'wpAntispam',
2557  'id' => 'wpAntispam',
2558  'value' => ''
2559  ]
2560  )
2561  . Xml::closeElement( 'div' )
2562  );
2563 
2564  Hooks::run( 'EditPage::showEditForm:fields', [ &$this, &$wgOut ] );
2565 
2566  // Put these up at the top to ensure they aren't lost on early form submission
2567  $this->showFormBeforeText();
2568 
2569  if ( $this->wasDeletedSinceLastEdit() && 'save' == $this->formtype ) {
2570  $username = $this->lastDelete->user_name;
2571  $comment = $this->lastDelete->log_comment;
2572 
2573  // It is better to not parse the comment at all than to have templates expanded in the middle
2574  // TODO: can the checkLabel be moved outside of the div so that wrapWikiMsg could be used?
2575  $key = $comment === ''
2576  ? 'confirmrecreate-noreason'
2577  : 'confirmrecreate';
2578  $wgOut->addHTML(
2579  '<div class="mw-confirm-recreate">' .
2580  wfMessage( $key, $username, "<nowiki>$comment</nowiki>" )->parse() .
2581  Xml::checkLabel( wfMessage( 'recreate' )->text(), 'wpRecreate', 'wpRecreate', false,
2582  [ 'title' => Linker::titleAttrib( 'recreate' ), 'tabindex' => 1, 'id' => 'wpRecreate' ]
2583  ) .
2584  '</div>'
2585  );
2586  }
2587 
2588  # When the summary is hidden, also hide them on preview/show changes
2589  if ( $this->nosummary ) {
2590  $wgOut->addHTML( Html::hidden( 'nosummary', true ) );
2591  }
2592 
2593  # If a blank edit summary was previously provided, and the appropriate
2594  # user preference is active, pass a hidden tag as wpIgnoreBlankSummary. This will stop the
2595  # user being bounced back more than once in the event that a summary
2596  # is not required.
2597  # ####
2598  # For a bit more sophisticated detection of blank summaries, hash the
2599  # automatic one and pass that in the hidden field wpAutoSummary.
2600  if ( $this->missingSummary || ( $this->section == 'new' && $this->nosummary ) ) {
2601  $wgOut->addHTML( Html::hidden( 'wpIgnoreBlankSummary', true ) );
2602  }
2603 
2604  if ( $this->undidRev ) {
2605  $wgOut->addHTML( Html::hidden( 'wpUndidRevision', $this->undidRev ) );
2606  }
2607 
2608  if ( $this->selfRedirect ) {
2609  $wgOut->addHTML( Html::hidden( 'wpIgnoreSelfRedirect', true ) );
2610  }
2611 
2612  if ( $this->hasPresetSummary ) {
2613  // If a summary has been preset using &summary= we don't want to prompt for
2614  // a different summary. Only prompt for a summary if the summary is blanked.
2615  // (Bug 17416)
2616  $this->autoSumm = md5( '' );
2617  }
2618 
2619  $autosumm = $this->autoSumm ? $this->autoSumm : md5( $this->summary );
2620  $wgOut->addHTML( Html::hidden( 'wpAutoSummary', $autosumm ) );
2621 
2622  $wgOut->addHTML( Html::hidden( 'oldid', $this->oldid ) );
2623  $wgOut->addHTML( Html::hidden( 'parentRevId', $this->getParentRevId() ) );
2624 
2625  $wgOut->addHTML( Html::hidden( 'format', $this->contentFormat ) );
2626  $wgOut->addHTML( Html::hidden( 'model', $this->contentModel ) );
2627 
2628  if ( $this->section == 'new' ) {
2629  $this->showSummaryInput( true, $this->summary );
2630  $wgOut->addHTML( $this->getSummaryPreview( true, $this->summary ) );
2631  }
2632 
2633  $wgOut->addHTML( $this->editFormTextBeforeContent );
2634 
2635  if ( !$this->isCssJsSubpage && $showToolbar && $wgUser->getOption( 'showtoolbar' ) ) {
2636  $wgOut->addHTML( EditPage::getEditToolbar( $this->mTitle ) );
2637  }
2638 
2639  if ( $this->blankArticle ) {
2640  $wgOut->addHTML( Html::hidden( 'wpIgnoreBlankArticle', true ) );
2641  }
2642 
2643  if ( $this->isConflict ) {
2644  // In an edit conflict bypass the overridable content form method
2645  // and fallback to the raw wpTextbox1 since editconflicts can't be
2646  // resolved between page source edits and custom ui edits using the
2647  // custom edit ui.
2648  $this->textbox2 = $this->textbox1;
2649 
2650  $content = $this->getCurrentContent();
2651  $this->textbox1 = $this->toEditText( $content );
2652 
2653  $this->showTextbox1();
2654  } else {
2655  $this->showContentForm();
2656  }
2657 
2658  $wgOut->addHTML( $this->editFormTextAfterContent );
2659 
2660  $this->showStandardInputs();
2661 
2662  $this->showFormAfterText();
2663 
2664  $this->showTosSummary();
2665 
2666  $this->showEditTools();
2667 
2668  $wgOut->addHTML( $this->editFormTextAfterTools . "\n" );
2669 
2670  $wgOut->addHTML( Html::rawElement( 'div', [ 'class' => 'templatesUsed' ],
2671  Linker::formatTemplates( $this->getTemplates(), $this->preview, $this->section != '' ) ) );
2672 
2673  $wgOut->addHTML( Html::rawElement( 'div', [ 'class' => 'hiddencats' ],
2674  Linker::formatHiddenCategories( $this->page->getHiddenCategories() ) ) );
2675 
2676  $wgOut->addHTML( Html::rawElement( 'div', [ 'class' => 'limitreport' ],
2677  self::getPreviewLimitReport( $this->mParserOutput ) ) );
2678 
2679  $wgOut->addModules( 'mediawiki.action.edit.collapsibleFooter' );
2680 
2681  if ( $this->isConflict ) {
2682  try {
2683  $this->showConflict();
2684  } catch ( MWContentSerializationException $ex ) {
2685  // this can't really happen, but be nice if it does.
2686  $msg = wfMessage(
2687  'content-failed-to-parse',
2688  $this->contentModel,
2689  $this->contentFormat,
2690  $ex->getMessage()
2691  );
2692  $wgOut->addWikiText( '<div class="error">' . $msg->text() . '</div>' );
2693  }
2694  }
2695 
2696  // Marker for detecting truncated form data. This must be the last
2697  // parameter sent in order to be of use, so do not move me.
2698  $wgOut->addHTML( Html::hidden( 'wpUltimateParam', true ) );
2699  $wgOut->addHTML( $this->editFormTextBottom . "\n</form>\n" );
2700 
2701  if ( !$wgUser->getOption( 'previewontop' ) ) {
2702  $this->displayPreviewArea( $previewOutput, false );
2703  }
2704 
2705  }
2706 
2713  public static function extractSectionTitle( $text ) {
2714  preg_match( "/^(=+)(.+)\\1\\s*(\n|$)/i", $text, $matches );
2715  if ( !empty( $matches[2] ) ) {
2716  global $wgParser;
2717  return $wgParser->stripSectionName( trim( $matches[2] ) );
2718  } else {
2719  return false;
2720  }
2721  }
2722 
2726  protected function showHeader() {
2729 
2730  if ( $this->mTitle->isTalkPage() ) {
2731  $wgOut->addWikiMsg( 'talkpagetext' );
2732  }
2733 
2734  // Add edit notices
2735  $editNotices = $this->mTitle->getEditNotices( $this->oldid );
2736  if ( count( $editNotices ) ) {
2737  $wgOut->addHTML( implode( "\n", $editNotices ) );
2738  } else {
2739  $msg = wfMessage( 'editnotice-notext' );
2740  if ( !$msg->isDisabled() ) {
2741  $wgOut->addHTML(
2742  '<div class="mw-editnotice-notext">'
2743  . $msg->parseAsBlock()
2744  . '</div>'
2745  );
2746  }
2747  }
2748 
2749  if ( $this->isConflict ) {
2750  $wgOut->wrapWikiMsg( "<div class='mw-explainconflict'>\n$1\n</div>", 'explainconflict' );
2751  $this->edittime = $this->page->getTimestamp();
2752  } else {
2753  if ( $this->section != '' && !$this->isSectionEditSupported() ) {
2754  // We use $this->section to much before this and getVal('wgSection') directly in other places
2755  // at this point we can't reset $this->section to '' to fallback to non-section editing.
2756  // Someone is welcome to try refactoring though
2757  $wgOut->showErrorPage( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
2758  return false;
2759  }
2760 
2761  if ( $this->section != '' && $this->section != 'new' ) {
2762  if ( !$this->summary && !$this->preview && !$this->diff ) {
2763  $sectionTitle = self::extractSectionTitle( $this->textbox1 ); // FIXME: use Content object
2764  if ( $sectionTitle !== false ) {
2765  $this->summary = "/* $sectionTitle */ ";
2766  }
2767  }
2768  }
2769 
2770  if ( $this->missingComment ) {
2771  $wgOut->wrapWikiMsg( "<div id='mw-missingcommenttext'>\n$1\n</div>", 'missingcommenttext' );
2772  }
2773 
2774  if ( $this->missingSummary && $this->section != 'new' ) {
2775  $wgOut->wrapWikiMsg( "<div id='mw-missingsummary'>\n$1\n</div>", 'missingsummary' );
2776  }
2777 
2778  if ( $this->missingSummary && $this->section == 'new' ) {
2779  $wgOut->wrapWikiMsg( "<div id='mw-missingcommentheader'>\n$1\n</div>", 'missingcommentheader' );
2780  }
2781 
2782  if ( $this->blankArticle ) {
2783  $wgOut->wrapWikiMsg( "<div id='mw-blankarticle'>\n$1\n</div>", 'blankarticle' );
2784  }
2785 
2786  if ( $this->selfRedirect ) {
2787  $wgOut->wrapWikiMsg( "<div id='mw-selfredirect'>\n$1\n</div>", 'selfredirect' );
2788  }
2789 
2790  if ( $this->hookError !== '' ) {
2791  $wgOut->addWikiText( $this->hookError );
2792  }
2793 
2794  if ( !$this->checkUnicodeCompliantBrowser() ) {
2795  $wgOut->addWikiMsg( 'nonunicodebrowser' );
2796  }
2797 
2798  if ( $this->section != 'new' ) {
2799  $revision = $this->mArticle->getRevisionFetched();
2800  if ( $revision ) {
2801  // Let sysop know that this will make private content public if saved
2802 
2803  if ( !$revision->userCan( Revision::DELETED_TEXT, $wgUser ) ) {
2804  $wgOut->wrapWikiMsg(
2805  "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
2806  'rev-deleted-text-permission'
2807  );
2808  } elseif ( $revision->isDeleted( Revision::DELETED_TEXT ) ) {
2809  $wgOut->wrapWikiMsg(
2810  "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
2811  'rev-deleted-text-view'
2812  );
2813  }
2814 
2815  if ( !$revision->isCurrent() ) {
2816  $this->mArticle->setOldSubtitle( $revision->getId() );
2817  $wgOut->addWikiMsg( 'editingold' );
2818  }
2819  } elseif ( $this->mTitle->exists() ) {
2820  // Something went wrong
2821 
2822  $wgOut->wrapWikiMsg( "<div class='errorbox'>\n$1\n</div>\n",
2823  [ 'missing-revision', $this->oldid ] );
2824  }
2825  }
2826  }
2827 
2828  if ( wfReadOnly() ) {
2829  $wgOut->wrapWikiMsg(
2830  "<div id=\"mw-read-only-warning\">\n$1\n</div>",
2831  [ 'readonlywarning', wfReadOnlyReason() ]
2832  );
2833  } elseif ( $wgUser->isAnon() ) {
2834  if ( $this->formtype != 'preview' ) {
2835  $wgOut->wrapWikiMsg(
2836  "<div id='mw-anon-edit-warning' class='warningbox'>\n$1\n</div>",
2837  [ 'anoneditwarning',
2838  // Log-in link
2839  '{{fullurl:Special:UserLogin|returnto={{FULLPAGENAMEE}}}}',
2840  // Sign-up link
2841  '{{fullurl:Special:CreateAccount|returnto={{FULLPAGENAMEE}}}}' ]
2842  );
2843  } else {
2844  $wgOut->wrapWikiMsg( "<div id=\"mw-anon-preview-warning\" class=\"warningbox\">\n$1</div>",
2845  'anonpreviewwarning'
2846  );
2847  }
2848  } else {
2849  if ( $this->isCssJsSubpage ) {
2850  # Check the skin exists
2851  if ( $this->isWrongCaseCssJsPage ) {
2852  $wgOut->wrapWikiMsg(
2853  "<div class='error' id='mw-userinvalidcssjstitle'>\n$1\n</div>",
2854  [ 'userinvalidcssjstitle', $this->mTitle->getSkinFromCssJsSubpage() ]
2855  );
2856  }
2857  if ( $this->getTitle()->isSubpageOf( $wgUser->getUserPage() ) ) {
2858  if ( $this->formtype !== 'preview' ) {
2859  if ( $this->isCssSubpage && $wgAllowUserCss ) {
2860  $wgOut->wrapWikiMsg(
2861  "<div id='mw-usercssyoucanpreview'>\n$1\n</div>",
2862  [ 'usercssyoucanpreview' ]
2863  );
2864  }
2865 
2866  if ( $this->isJsSubpage && $wgAllowUserJs ) {
2867  $wgOut->wrapWikiMsg(
2868  "<div id='mw-userjsyoucanpreview'>\n$1\n</div>",
2869  [ 'userjsyoucanpreview' ]
2870  );
2871  }
2872  }
2873  }
2874  }
2875  }
2876 
2877  if ( $this->mTitle->isProtected( 'edit' ) &&
2878  MWNamespace::getRestrictionLevels( $this->mTitle->getNamespace() ) !== [ '' ]
2879  ) {
2880  # Is the title semi-protected?
2881  if ( $this->mTitle->isSemiProtected() ) {
2882  $noticeMsg = 'semiprotectedpagewarning';
2883  } else {
2884  # Then it must be protected based on static groups (regular)
2885  $noticeMsg = 'protectedpagewarning';
2886  }
2887  LogEventsList::showLogExtract( $wgOut, 'protect', $this->mTitle, '',
2888  [ 'lim' => 1, 'msgKey' => [ $noticeMsg ] ] );
2889  }
2890  if ( $this->mTitle->isCascadeProtected() ) {
2891  # Is this page under cascading protection from some source pages?
2892 
2893  list( $cascadeSources, /* $restrictions */ ) = $this->mTitle->getCascadeProtectionSources();
2894  $notice = "<div class='mw-cascadeprotectedwarning'>\n$1\n";
2895  $cascadeSourcesCount = count( $cascadeSources );
2896  if ( $cascadeSourcesCount > 0 ) {
2897  # Explain, and list the titles responsible
2898  foreach ( $cascadeSources as $page ) {
2899  $notice .= '* [[:' . $page->getPrefixedText() . "]]\n";
2900  }
2901  }
2902  $notice .= '</div>';
2903  $wgOut->wrapWikiMsg( $notice, [ 'cascadeprotectedwarning', $cascadeSourcesCount ] );
2904  }
2905  if ( !$this->mTitle->exists() && $this->mTitle->getRestrictions( 'create' ) ) {
2906  LogEventsList::showLogExtract( $wgOut, 'protect', $this->mTitle, '',
2907  [ 'lim' => 1,
2908  'showIfEmpty' => false,
2909  'msgKey' => [ 'titleprotectedwarning' ],
2910  'wrap' => "<div class=\"mw-titleprotectedwarning\">\n$1</div>" ] );
2911  }
2912 
2913  if ( $this->kblength === false ) {
2914  $this->kblength = (int)( strlen( $this->textbox1 ) / 1024 );
2915  }
2916 
2917  if ( $this->tooBig || $this->kblength > $wgMaxArticleSize ) {
2918  $wgOut->wrapWikiMsg( "<div class='error' id='mw-edit-longpageerror'>\n$1\n</div>",
2919  [
2920  'longpageerror',
2921  $wgLang->formatNum( $this->kblength ),
2922  $wgLang->formatNum( $wgMaxArticleSize )
2923  ]
2924  );
2925  } else {
2926  if ( !wfMessage( 'longpage-hint' )->isDisabled() ) {
2927  $wgOut->wrapWikiMsg( "<div id='mw-edit-longpage-hint'>\n$1\n</div>",
2928  [
2929  'longpage-hint',
2930  $wgLang->formatSize( strlen( $this->textbox1 ) ),
2931  strlen( $this->textbox1 )
2932  ]
2933  );
2934  }
2935  }
2936  # Add header copyright warning
2937  $this->showHeaderCopyrightWarning();
2938 
2939  return true;
2940  }
2941 
2956  function getSummaryInput( $summary = "", $labelText = null,
2957  $inputAttrs = null, $spanLabelAttrs = null
2958  ) {
2959  // Note: the maxlength is overridden in JS to 255 and to make it use UTF-8 bytes, not characters.
2960  $inputAttrs = ( is_array( $inputAttrs ) ? $inputAttrs : [] ) + [
2961  'id' => 'wpSummary',
2962  'maxlength' => '200',
2963  'tabindex' => '1',
2964  'size' => 60,
2965  'spellcheck' => 'true',
2966  ] + Linker::tooltipAndAccesskeyAttribs( 'summary' );
2967 
2968  $spanLabelAttrs = ( is_array( $spanLabelAttrs ) ? $spanLabelAttrs : [] ) + [
2969  'class' => $this->missingSummary ? 'mw-summarymissed' : 'mw-summary',
2970  'id' => "wpSummaryLabel"
2971  ];
2972 
2973  $label = null;
2974  if ( $labelText ) {
2975  $label = Xml::tags(
2976  'label',
2977  $inputAttrs['id'] ? [ 'for' => $inputAttrs['id'] ] : null,
2978  $labelText
2979  );
2980  $label = Xml::tags( 'span', $spanLabelAttrs, $label );
2981  }
2982 
2983  $input = Html::input( 'wpSummary', $summary, 'text', $inputAttrs );
2984 
2985  return [ $label, $input ];
2986  }
2987 
2994  protected function showSummaryInput( $isSubjectPreview, $summary = "" ) {
2996  # Add a class if 'missingsummary' is triggered to allow styling of the summary line
2997  $summaryClass = $this->missingSummary ? 'mw-summarymissed' : 'mw-summary';
2998  if ( $isSubjectPreview ) {
2999  if ( $this->nosummary ) {
3000  return;
3001  }
3002  } else {
3003  if ( !$this->mShowSummaryField ) {
3004  return;
3005  }
3006  }
3007  $summary = $wgContLang->recodeForEdit( $summary );
3008  $labelText = wfMessage( $isSubjectPreview ? 'subject' : 'summary' )->parse();
3009  list( $label, $input ) = $this->getSummaryInput(
3010  $summary,
3011  $labelText,
3012  [ 'class' => $summaryClass ],
3013  []
3014  );
3015  $wgOut->addHTML( "{$label} {$input}" );
3016  }
3017 
3025  protected function getSummaryPreview( $isSubjectPreview, $summary = "" ) {
3026  // avoid spaces in preview, gets always trimmed on save
3027  $summary = trim( $summary );
3028  if ( !$summary || ( !$this->preview && !$this->diff ) ) {
3029  return "";
3030  }
3031 
3032  global $wgParser;
3033 
3034  if ( $isSubjectPreview ) {
3035  $summary = wfMessage( 'newsectionsummary' )->rawParams( $wgParser->stripSectionName( $summary ) )
3036  ->inContentLanguage()->text();
3037  }
3038 
3039  $message = $isSubjectPreview ? 'subject-preview' : 'summary-preview';
3040 
3041  $summary = wfMessage( $message )->parse()
3042  . Linker::commentBlock( $summary, $this->mTitle, $isSubjectPreview );
3043  return Xml::tags( 'div', [ 'class' => 'mw-summary-preview' ], $summary );
3044  }
3045 
3046  protected function showFormBeforeText() {
3047  global $wgOut;
3048  $section = htmlspecialchars( $this->section );
3049  $wgOut->addHTML( <<<HTML
3050 <input type='hidden' value="{$section}" name="wpSection"/>
3051 <input type='hidden' value="{$this->starttime}" name="wpStarttime" />
3052 <input type='hidden' value="{$this->edittime}" name="wpEdittime" />
3053 <input type='hidden' value="{$this->scrolltop}" name="wpScrolltop" id="wpScrolltop" />
3054 
3055 HTML
3056  );
3057  if ( !$this->checkUnicodeCompliantBrowser() ) {
3058  $wgOut->addHTML( Html::hidden( 'safemode', '1' ) );
3059  }
3060  }
3061 
3062  protected function showFormAfterText() {
3076  $wgOut->addHTML( "\n" . Html::hidden( "wpEditToken", $wgUser->getEditToken() ) . "\n" );
3077  }
3078 
3087  protected function showContentForm() {
3088  $this->showTextbox1();
3089  }
3090 
3099  protected function showTextbox1( $customAttribs = null, $textoverride = null ) {
3100  if ( $this->wasDeletedSinceLastEdit() && $this->formtype == 'save' ) {
3101  $attribs = [ 'style' => 'display:none;' ];
3102  } else {
3103  $classes = []; // Textarea CSS
3104  if ( $this->mTitle->isProtected( 'edit' ) &&
3105  MWNamespace::getRestrictionLevels( $this->mTitle->getNamespace() ) !== [ '' ]
3106  ) {
3107  # Is the title semi-protected?
3108  if ( $this->mTitle->isSemiProtected() ) {
3109  $classes[] = 'mw-textarea-sprotected';
3110  } else {
3111  # Then it must be protected based on static groups (regular)
3112  $classes[] = 'mw-textarea-protected';
3113  }
3114  # Is the title cascade-protected?
3115  if ( $this->mTitle->isCascadeProtected() ) {
3116  $classes[] = 'mw-textarea-cprotected';
3117  }
3118  }
3119 
3120  $attribs = [ 'tabindex' => 1 ];
3121 
3122  if ( is_array( $customAttribs ) ) {
3124  }
3125 
3126  if ( count( $classes ) ) {
3127  if ( isset( $attribs['class'] ) ) {
3128  $classes[] = $attribs['class'];
3129  }
3130  $attribs['class'] = implode( ' ', $classes );
3131  }
3132  }
3133 
3134  $this->showTextbox(
3135  $textoverride !== null ? $textoverride : $this->textbox1,
3136  'wpTextbox1',
3137  $attribs
3138  );
3139  }
3140 
3141  protected function showTextbox2() {
3142  $this->showTextbox( $this->textbox2, 'wpTextbox2', [ 'tabindex' => 6, 'readonly' ] );
3143  }
3144 
3145  protected function showTextbox( $text, $name, $customAttribs = [] ) {
3147 
3148  $wikitext = $this->safeUnicodeOutput( $text );
3149  if ( strval( $wikitext ) !== '' ) {
3150  // Ensure there's a newline at the end, otherwise adding lines
3151  // is awkward.
3152  // But don't add a newline if the ext is empty, or Firefox in XHTML
3153  // mode will show an extra newline. A bit annoying.
3154  $wikitext .= "\n";
3155  }
3156 
3157  $attribs = $customAttribs + [
3158  'accesskey' => ',',
3159  'id' => $name,
3160  'cols' => $wgUser->getIntOption( 'cols' ),
3161  'rows' => $wgUser->getIntOption( 'rows' ),
3162  // Avoid PHP notices when appending preferences
3163  // (appending allows customAttribs['style'] to still work).
3164  'style' => ''
3165  ];
3166 
3167  $pageLang = $this->mTitle->getPageLanguage();
3168  $attribs['lang'] = $pageLang->getHtmlCode();
3169  $attribs['dir'] = $pageLang->getDir();
3170 
3171  $wgOut->addHTML( Html::textarea( $name, $wikitext, $attribs ) );
3172  }
3173 
3174  protected function displayPreviewArea( $previewOutput, $isOnTop = false ) {
3175  global $wgOut;
3176  $classes = [];
3177  if ( $isOnTop ) {
3178  $classes[] = 'ontop';
3179  }
3180 
3181  $attribs = [ 'id' => 'wikiPreview', 'class' => implode( ' ', $classes ) ];
3182 
3183  if ( $this->formtype != 'preview' ) {
3184  $attribs['style'] = 'display: none;';
3185  }
3186 
3187  $wgOut->addHTML( Xml::openElement( 'div', $attribs ) );
3188 
3189  if ( $this->formtype == 'preview' ) {
3190  $this->showPreview( $previewOutput );
3191  } else {
3192  // Empty content container for LivePreview
3193  $pageViewLang = $this->mTitle->getPageViewLanguage();
3194  $attribs = [ 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(),
3195  'class' => 'mw-content-' . $pageViewLang->getDir() ];
3196  $wgOut->addHTML( Html::rawElement( 'div', $attribs ) );
3197  }
3198 
3199  $wgOut->addHTML( '</div>' );
3200 
3201  if ( $this->formtype == 'diff' ) {
3202  try {
3203  $this->showDiff();
3204  } catch ( MWContentSerializationException $ex ) {
3205  $msg = wfMessage(
3206  'content-failed-to-parse',
3207  $this->contentModel,
3208  $this->contentFormat,
3209  $ex->getMessage()
3210  );
3211  $wgOut->addWikiText( '<div class="error">' . $msg->text() . '</div>' );
3212  }
3213  }
3214  }
3215 
3222  protected function showPreview( $text ) {
3223  global $wgOut;
3224  if ( $this->mTitle->getNamespace() == NS_CATEGORY ) {
3225  $this->mArticle->openShowCategory();
3226  }
3227  # This hook seems slightly odd here, but makes things more
3228  # consistent for extensions.
3229  Hooks::run( 'OutputPageBeforeHTML', [ &$wgOut, &$text ] );
3230  $wgOut->addHTML( $text );
3231  if ( $this->mTitle->getNamespace() == NS_CATEGORY ) {
3232  $this->mArticle->closeShowCategory();
3233  }
3234  }
3235 
3243  function showDiff() {
3245 
3246  $oldtitlemsg = 'currentrev';
3247  # if message does not exist, show diff against the preloaded default
3248  if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && !$this->mTitle->exists() ) {
3249  $oldtext = $this->mTitle->getDefaultMessageText();
3250  if ( $oldtext !== false ) {
3251  $oldtitlemsg = 'defaultmessagetext';
3252  $oldContent = $this->toEditContent( $oldtext );
3253  } else {
3254  $oldContent = null;
3255  }
3256  } else {
3257  $oldContent = $this->getCurrentContent();
3258  }
3259 
3260  $textboxContent = $this->toEditContent( $this->textbox1 );
3261 
3262  $newContent = $this->page->replaceSectionContent(
3263  $this->section, $textboxContent,
3264  $this->summary, $this->edittime );
3265 
3266  if ( $newContent ) {
3267  ContentHandler::runLegacyHooks( 'EditPageGetDiffText', [ $this, &$newContent ] );
3268  Hooks::run( 'EditPageGetDiffContent', [ $this, &$newContent ] );
3269 
3270  $popts = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang );
3271  $newContent = $newContent->preSaveTransform( $this->mTitle, $wgUser, $popts );
3272  }
3273 
3274  if ( ( $oldContent && !$oldContent->isEmpty() ) || ( $newContent && !$newContent->isEmpty() ) ) {
3275  $oldtitle = wfMessage( $oldtitlemsg )->parse();
3276  $newtitle = wfMessage( 'yourtext' )->parse();
3277 
3278  if ( !$oldContent ) {
3279  $oldContent = $newContent->getContentHandler()->makeEmptyContent();
3280  }
3281 
3282  if ( !$newContent ) {
3283  $newContent = $oldContent->getContentHandler()->makeEmptyContent();
3284  }
3285 
3286  $de = $oldContent->getContentHandler()->createDifferenceEngine( $this->mArticle->getContext() );
3287  $de->setContent( $oldContent, $newContent );
3288 
3289  $difftext = $de->getDiff( $oldtitle, $newtitle );
3290  $de->showDiffStyle();
3291  } else {
3292  $difftext = '';
3293  }
3294 
3295  $wgOut->addHTML( '<div id="wikiDiff">' . $difftext . '</div>' );
3296  }
3297 
3301  protected function showHeaderCopyrightWarning() {
3302  $msg = 'editpage-head-copy-warn';
3303  if ( !wfMessage( $msg )->isDisabled() ) {
3304  global $wgOut;
3305  $wgOut->wrapWikiMsg( "<div class='editpage-head-copywarn'>\n$1\n</div>",
3306  'editpage-head-copy-warn' );
3307  }
3308  }
3309 
3318  protected function showTosSummary() {
3319  $msg = 'editpage-tos-summary';
3320  Hooks::run( 'EditPageTosSummary', [ $this->mTitle, &$msg ] );
3321  if ( !wfMessage( $msg )->isDisabled() ) {
3322  global $wgOut;
3323  $wgOut->addHTML( '<div class="mw-tos-summary">' );
3324  $wgOut->addWikiMsg( $msg );
3325  $wgOut->addHTML( '</div>' );
3326  }
3327  }
3328 
3329  protected function showEditTools() {
3330  global $wgOut;
3331  $wgOut->addHTML( '<div class="mw-editTools">' .
3332  wfMessage( 'edittools' )->inContentLanguage()->parse() .
3333  '</div>' );
3334  }
3335 
3342  protected function getCopywarn() {
3343  return self::getCopyrightWarning( $this->mTitle );
3344  }
3345 
3353  public static function getCopyrightWarning( $title, $format = 'plain' ) {
3354  global $wgRightsText;
3355  if ( $wgRightsText ) {
3356  $copywarnMsg = [ 'copyrightwarning',
3357  '[[' . wfMessage( 'copyrightpage' )->inContentLanguage()->text() . ']]',
3358  $wgRightsText ];
3359  } else {
3360  $copywarnMsg = [ 'copyrightwarning2',
3361  '[[' . wfMessage( 'copyrightpage' )->inContentLanguage()->text() . ']]' ];
3362  }
3363  // Allow for site and per-namespace customization of contribution/copyright notice.
3364  Hooks::run( 'EditPageCopyrightWarning', [ $title, &$copywarnMsg ] );
3365 
3366  return "<div id=\"editpage-copywarn\">\n" .
3367  call_user_func_array( 'wfMessage', $copywarnMsg )->$format() . "\n</div>";
3368  }
3369 
3377  public static function getPreviewLimitReport( $output ) {
3378  if ( !$output || !$output->getLimitReportData() ) {
3379  return '';
3380  }
3381 
3382  $limitReport = Html::rawElement( 'div', [ 'class' => 'mw-limitReportExplanation' ],
3383  wfMessage( 'limitreport-title' )->parseAsBlock()
3384  );
3385 
3386  // Show/hide animation doesn't work correctly on a table, so wrap it in a div.
3387  $limitReport .= Html::openElement( 'div', [ 'class' => 'preview-limit-report-wrapper' ] );
3388 
3389  $limitReport .= Html::openElement( 'table', [
3390  'class' => 'preview-limit-report wikitable'
3391  ] ) .
3392  Html::openElement( 'tbody' );
3393 
3394  foreach ( $output->getLimitReportData() as $key => $value ) {
3395  if ( Hooks::run( 'ParserLimitReportFormat',
3396  [ $key, &$value, &$limitReport, true, true ]
3397  ) ) {
3398  $keyMsg = wfMessage( $key );
3399  $valueMsg = wfMessage( [ "$key-value-html", "$key-value" ] );
3400  if ( !$valueMsg->exists() ) {
3401  $valueMsg = new RawMessage( '$1' );
3402  }
3403  if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) {
3404  $limitReport .= Html::openElement( 'tr' ) .
3405  Html::rawElement( 'th', null, $keyMsg->parse() ) .
3406  Html::rawElement( 'td', null, $valueMsg->params( $value )->parse() ) .
3407  Html::closeElement( 'tr' );
3408  }
3409  }
3410  }
3411 
3412  $limitReport .= Html::closeElement( 'tbody' ) .
3413  Html::closeElement( 'table' ) .
3414  Html::closeElement( 'div' );
3415 
3416  return $limitReport;
3417  }
3418 
3419  protected function showStandardInputs( &$tabindex = 2 ) {
3420  global $wgOut;
3421  $wgOut->addHTML( "<div class='editOptions'>\n" );
3422 
3423  if ( $this->section != 'new' ) {
3424  $this->showSummaryInput( false, $this->summary );
3425  $wgOut->addHTML( $this->getSummaryPreview( false, $this->summary ) );
3426  }
3427 
3428  $checkboxes = $this->getCheckboxes( $tabindex,
3429  [ 'minor' => $this->minoredit, 'watch' => $this->watchthis ] );
3430  $wgOut->addHTML( "<div class='editCheckboxes'>" . implode( $checkboxes, "\n" ) . "</div>\n" );
3431 
3432  // Show copyright warning.
3433  $wgOut->addWikiText( $this->getCopywarn() );
3434  $wgOut->addHTML( $this->editFormTextAfterWarn );
3435 
3436  $wgOut->addHTML( "<div class='editButtons'>\n" );
3437  $wgOut->addHTML( implode( $this->getEditButtons( $tabindex ), "\n" ) . "\n" );
3438 
3439  $cancel = $this->getCancelLink();
3440  if ( $cancel !== '' ) {
3441  $cancel .= Html::element( 'span',
3442  [ 'class' => 'mw-editButtons-pipe-separator' ],
3443  wfMessage( 'pipe-separator' )->text() );
3444  }
3445 
3446  $message = wfMessage( 'edithelppage' )->inContentLanguage()->text();
3447  $edithelpurl = Skin::makeInternalOrExternalUrl( $message );
3448  $attrs = [
3449  'target' => 'helpwindow',
3450  'href' => $edithelpurl,
3451  ];
3452  $edithelp = Html::linkButton( wfMessage( 'edithelp' )->text(),
3453  $attrs, [ 'mw-ui-quiet' ] ) .
3454  wfMessage( 'word-separator' )->escaped() .
3455  wfMessage( 'newwindow' )->parse();
3456 
3457  $wgOut->addHTML( " <span class='cancelLink'>{$cancel}</span>\n" );
3458  $wgOut->addHTML( " <span class='editHelp'>{$edithelp}</span>\n" );
3459  $wgOut->addHTML( "</div><!-- editButtons -->\n" );
3460 
3461  Hooks::run( 'EditPage::showStandardInputs:options', [ $this, $wgOut, &$tabindex ] );
3462 
3463  $wgOut->addHTML( "</div><!-- editOptions -->\n" );
3464  }
3465 
3470  protected function showConflict() {
3471  global $wgOut;
3472 
3473  if ( Hooks::run( 'EditPageBeforeConflictDiff', [ &$this, &$wgOut ] ) ) {
3474  $stats = $wgOut->getContext()->getStats();
3475  $stats->increment( 'edit.failures.conflict' );
3476 
3477  $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
3478 
3479  $content1 = $this->toEditContent( $this->textbox1 );
3480  $content2 = $this->toEditContent( $this->textbox2 );
3481 
3482  $handler = ContentHandler::getForModelID( $this->contentModel );
3483  $de = $handler->createDifferenceEngine( $this->mArticle->getContext() );
3484  $de->setContent( $content2, $content1 );
3485  $de->showDiff(
3486  wfMessage( 'yourtext' )->parse(),
3487  wfMessage( 'storedversion' )->text()
3488  );
3489 
3490  $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
3491  $this->showTextbox2();
3492  }
3493  }
3494 
3498  public function getCancelLink() {
3499  $cancelParams = [];
3500  if ( !$this->isConflict && $this->oldid > 0 ) {
3501  $cancelParams['oldid'] = $this->oldid;
3502  }
3503  $attrs = [ 'id' => 'mw-editform-cancel' ];
3504 
3505  return Linker::linkKnown(
3506  $this->getContextTitle(),
3507  wfMessage( 'cancel' )->parse(),
3508  Html::buttonAttributes( $attrs, [ 'mw-ui-quiet' ] ),
3509  $cancelParams
3510  );
3511  }
3512 
3522  protected function getActionURL( Title $title ) {
3523  return $title->getLocalURL( [ 'action' => $this->action ] );
3524  }
3525 
3533  protected function wasDeletedSinceLastEdit() {
3534  if ( $this->deletedSinceEdit !== null ) {
3535  return $this->deletedSinceEdit;
3536  }
3537 
3538  $this->deletedSinceEdit = false;
3539 
3540  if ( !$this->mTitle->exists() && $this->mTitle->isDeletedQuick() ) {
3541  $this->lastDelete = $this->getLastDelete();
3542  if ( $this->lastDelete ) {
3543  $deleteTime = wfTimestamp( TS_MW, $this->lastDelete->log_timestamp );
3544  if ( $deleteTime > $this->starttime ) {
3545  $this->deletedSinceEdit = true;
3546  }
3547  }
3548  }
3549 
3550  return $this->deletedSinceEdit;
3551  }
3552 
3556  protected function getLastDelete() {
3557  $dbr = wfGetDB( DB_SLAVE );
3558  $data = $dbr->selectRow(
3559  [ 'logging', 'user' ],
3560  [
3561  'log_type',
3562  'log_action',
3563  'log_timestamp',
3564  'log_user',
3565  'log_namespace',
3566  'log_title',
3567  'log_comment',
3568  'log_params',
3569  'log_deleted',
3570  'user_name'
3571  ], [
3572  'log_namespace' => $this->mTitle->getNamespace(),
3573  'log_title' => $this->mTitle->getDBkey(),
3574  'log_type' => 'delete',
3575  'log_action' => 'delete',
3576  'user_id=log_user'
3577  ],
3578  __METHOD__,
3579  [ 'LIMIT' => 1, 'ORDER BY' => 'log_timestamp DESC' ]
3580  );
3581  // Quick paranoid permission checks...
3582  if ( is_object( $data ) ) {
3583  if ( $data->log_deleted & LogPage::DELETED_USER ) {
3584  $data->user_name = wfMessage( 'rev-deleted-user' )->escaped();
3585  }
3586 
3587  if ( $data->log_deleted & LogPage::DELETED_COMMENT ) {
3588  $data->log_comment = wfMessage( 'rev-deleted-comment' )->escaped();
3589  }
3590  }
3591 
3592  return $data;
3593  }
3594 
3600  function getPreviewText() {
3601  global $wgOut, $wgUser, $wgRawHtml, $wgLang;
3603 
3604  $stats = $wgOut->getContext()->getStats();
3605 
3606  if ( $wgRawHtml && !$this->mTokenOk ) {
3607  // Could be an offsite preview attempt. This is very unsafe if
3608  // HTML is enabled, as it could be an attack.
3609  $parsedNote = '';
3610  if ( $this->textbox1 !== '' ) {
3611  // Do not put big scary notice, if previewing the empty
3612  // string, which happens when you initially edit
3613  // a category page, due to automatic preview-on-open.
3614  $parsedNote = $wgOut->parse( "<div class='previewnote'>" .
3615  wfMessage( 'session_fail_preview_html' )->text() . "</div>", true, /* interface */true );
3616  }
3617  $stats->increment( 'edit.failures.session_loss' );
3618  return $parsedNote;
3619  }
3620 
3621  $note = '';
3622 
3623  try {
3624  $content = $this->toEditContent( $this->textbox1 );
3625 
3626  $previewHTML = '';
3627  if ( !Hooks::run(
3628  'AlternateEditPreview',
3629  [ $this, &$content, &$previewHTML, &$this->mParserOutput ] )
3630  ) {
3631  return $previewHTML;
3632  }
3633 
3634  # provide a anchor link to the editform
3635  $continueEditing = '<span class="mw-continue-editing">' .
3636  '[[#' . self::EDITFORM_ID . '|' . $wgLang->getArrow() . ' ' .
3637  wfMessage( 'continue-editing' )->text() . ']]</span>';
3638  if ( $this->mTriedSave && !$this->mTokenOk ) {
3639  if ( $this->mTokenOkExceptSuffix ) {
3640  $note = wfMessage( 'token_suffix_mismatch' )->plain();
3641  $stats->increment( 'edit.failures.bad_token' );
3642  } else {
3643  $note = wfMessage( 'session_fail_preview' )->plain();
3644  $stats->increment( 'edit.failures.session_loss' );
3645  }
3646  } elseif ( $this->incompleteForm ) {
3647  $note = wfMessage( 'edit_form_incomplete' )->plain();
3648  if ( $this->mTriedSave ) {
3649  $stats->increment( 'edit.failures.incomplete_form' );
3650  }
3651  } else {
3652  $note = wfMessage( 'previewnote' )->plain() . ' ' . $continueEditing;
3653  }
3654 
3655  $parserOptions = $this->page->makeParserOptions( $this->mArticle->getContext() );
3656  $parserOptions->setIsPreview( true );
3657  $parserOptions->setIsSectionPreview( !is_null( $this->section ) && $this->section !== '' );
3658 
3659  # don't parse non-wikitext pages, show message about preview
3660  if ( $this->mTitle->isCssJsSubpage() || $this->mTitle->isCssOrJsPage() ) {
3661  if ( $this->mTitle->isCssJsSubpage() ) {
3662  $level = 'user';
3663  } elseif ( $this->mTitle->isCssOrJsPage() ) {
3664  $level = 'site';
3665  } else {
3666  $level = false;
3667  }
3668 
3669  if ( $content->getModel() == CONTENT_MODEL_CSS ) {
3670  $format = 'css';
3671  if ( $level === 'user' && !$wgAllowUserCss ) {
3672  $format = false;
3673  }
3674  } elseif ( $content->getModel() == CONTENT_MODEL_JAVASCRIPT ) {
3675  $format = 'js';
3676  if ( $level === 'user' && !$wgAllowUserJs ) {
3677  $format = false;
3678  }
3679  } else {
3680  $format = false;
3681  }
3682 
3683  # Used messages to make sure grep find them:
3684  # Messages: usercsspreview, userjspreview, sitecsspreview, sitejspreview
3685  if ( $level && $format ) {
3686  $note = "<div id='mw-{$level}{$format}preview'>" .
3687  wfMessage( "{$level}{$format}preview" )->text() .
3688  ' ' . $continueEditing . "</div>";
3689  }
3690  }
3691 
3692  # If we're adding a comment, we need to show the
3693  # summary as the headline
3694  if ( $this->section === "new" && $this->summary !== "" ) {
3695  $content = $content->addSectionHeader( $this->summary );
3696  }
3697 
3698  $hook_args = [ $this, &$content ];
3699  ContentHandler::runLegacyHooks( 'EditPageGetPreviewText', $hook_args );
3700  Hooks::run( 'EditPageGetPreviewContent', $hook_args );
3701 
3702  $parserOptions->enableLimitReport();
3703 
3704  # For CSS/JS pages, we should have called the ShowRawCssJs hook here.
3705  # But it's now deprecated, so never mind
3706 
3707  $pstContent = $content->preSaveTransform( $this->mTitle, $wgUser, $parserOptions );
3708  $scopedCallback = $parserOptions->setupFakeRevision(
3709  $this->mTitle, $pstContent, $wgUser );
3710  $parserOutput = $pstContent->getParserOutput( $this->mTitle, null, $parserOptions );
3711 
3712  # Try to stash the edit for the final submission step
3713  # @todo: different date format preferences cause cache misses
3715  $this->getArticle(), $content, $pstContent,
3716  $parserOutput, $parserOptions, $parserOptions, wfTimestampNow()
3717  );
3718 
3719  $parserOutput->setEditSectionTokens( false ); // no section edit links
3720  $previewHTML = $parserOutput->getText();
3721  $this->mParserOutput = $parserOutput;
3722  $wgOut->addParserOutputMetadata( $parserOutput );
3723 
3724  if ( count( $parserOutput->getWarnings() ) ) {
3725  $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
3726  }
3727 
3728  ScopedCallback::consume( $scopedCallback );
3729  } catch ( MWContentSerializationException $ex ) {
3730  $m = wfMessage(
3731  'content-failed-to-parse',
3732  $this->contentModel,
3733  $this->contentFormat,
3734  $ex->getMessage()
3735  );
3736  $note .= "\n\n" . $m->parse();
3737  $previewHTML = '';
3738  }
3739 
3740  if ( $this->isConflict ) {
3741  $conflict = '<h2 id="mw-previewconflict">'
3742  . wfMessage( 'previewconflict' )->escaped() . "</h2>\n";
3743  } else {
3744  $conflict = '<hr />';
3745  }
3746 
3747  $previewhead = "<div class='previewnote'>\n" .
3748  '<h2 id="mw-previewheader">' . wfMessage( 'preview' )->escaped() . "</h2>" .
3749  $wgOut->parse( $note, true, /* interface */true ) . $conflict . "</div>\n";
3750 
3751  $pageViewLang = $this->mTitle->getPageViewLanguage();
3752  $attribs = [ 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(),
3753  'class' => 'mw-content-' . $pageViewLang->getDir() ];
3754  $previewHTML = Html::rawElement( 'div', $attribs, $previewHTML );
3755 
3756  return $previewhead . $previewHTML . $this->previewTextAfterContent;
3757  }
3758 
3762  function getTemplates() {
3763  if ( $this->preview || $this->section != '' ) {
3764  $templates = [];
3765  if ( !isset( $this->mParserOutput ) ) {
3766  return $templates;
3767  }
3768  foreach ( $this->mParserOutput->getTemplates() as $ns => $template ) {
3769  foreach ( array_keys( $template ) as $dbk ) {
3770  $templates[] = Title::makeTitle( $ns, $dbk );
3771  }
3772  }
3773  return $templates;
3774  } else {
3775  return $this->mTitle->getTemplateLinksFrom();
3776  }
3777  }
3778 
3786  static function getEditToolbar( $title = null ) {
3789 
3790  $imagesAvailable = $wgEnableUploads || count( $wgForeignFileRepos );
3791  $showSignature = true;
3792  if ( $title ) {
3793  $showSignature = MWNamespace::wantSignatures( $title->getNamespace() );
3794  }
3795 
3805  $toolarray = [
3806  [
3807  'id' => 'mw-editbutton-bold',
3808  'open' => '\'\'\'',
3809  'close' => '\'\'\'',
3810  'sample' => wfMessage( 'bold_sample' )->text(),
3811  'tip' => wfMessage( 'bold_tip' )->text(),
3812  ],
3813  [
3814  'id' => 'mw-editbutton-italic',
3815  'open' => '\'\'',
3816  'close' => '\'\'',
3817  'sample' => wfMessage( 'italic_sample' )->text(),
3818  'tip' => wfMessage( 'italic_tip' )->text(),
3819  ],
3820  [
3821  'id' => 'mw-editbutton-link',
3822  'open' => '[[',
3823  'close' => ']]',
3824  'sample' => wfMessage( 'link_sample' )->text(),
3825  'tip' => wfMessage( 'link_tip' )->text(),
3826  ],
3827  [
3828  'id' => 'mw-editbutton-extlink',
3829  'open' => '[',
3830  'close' => ']',
3831  'sample' => wfMessage( 'extlink_sample' )->text(),
3832  'tip' => wfMessage( 'extlink_tip' )->text(),
3833  ],
3834  [
3835  'id' => 'mw-editbutton-headline',
3836  'open' => "\n== ",
3837  'close' => " ==\n",
3838  'sample' => wfMessage( 'headline_sample' )->text(),
3839  'tip' => wfMessage( 'headline_tip' )->text(),
3840  ],
3841  $imagesAvailable ? [
3842  'id' => 'mw-editbutton-image',
3843  'open' => '[[' . $wgContLang->getNsText( NS_FILE ) . ':',
3844  'close' => ']]',
3845  'sample' => wfMessage( 'image_sample' )->text(),
3846  'tip' => wfMessage( 'image_tip' )->text(),
3847  ] : false,
3848  $imagesAvailable ? [
3849  'id' => 'mw-editbutton-media',
3850  'open' => '[[' . $wgContLang->getNsText( NS_MEDIA ) . ':',
3851  'close' => ']]',
3852  'sample' => wfMessage( 'media_sample' )->text(),
3853  'tip' => wfMessage( 'media_tip' )->text(),
3854  ] : false,
3855  [
3856  'id' => 'mw-editbutton-nowiki',
3857  'open' => "<nowiki>",
3858  'close' => "</nowiki>",
3859  'sample' => wfMessage( 'nowiki_sample' )->text(),
3860  'tip' => wfMessage( 'nowiki_tip' )->text(),
3861  ],
3862  $showSignature ? [
3863  'id' => 'mw-editbutton-signature',
3864  'open' => '--~~~~',
3865  'close' => '',
3866  'sample' => '',
3867  'tip' => wfMessage( 'sig_tip' )->text(),
3868  ] : false,
3869  [
3870  'id' => 'mw-editbutton-hr',
3871  'open' => "\n----\n",
3872  'close' => '',
3873  'sample' => '',
3874  'tip' => wfMessage( 'hr_tip' )->text(),
3875  ]
3876  ];
3877 
3878  $script = 'mw.loader.using("mediawiki.toolbar", function () {';
3879  foreach ( $toolarray as $tool ) {
3880  if ( !$tool ) {
3881  continue;
3882  }
3883 
3884  $params = [
3885  // Images are defined in ResourceLoaderEditToolbarModule
3886  false,
3887  // Note that we use the tip both for the ALT tag and the TITLE tag of the image.
3888  // Older browsers show a "speedtip" type message only for ALT.
3889  // Ideally these should be different, realistically they
3890  // probably don't need to be.
3891  $tool['tip'],
3892  $tool['open'],
3893  $tool['close'],
3894  $tool['sample'],
3895  $tool['id'],
3896  ];
3897 
3898  $script .= Xml::encodeJsCall(
3899  'mw.toolbar.addButton',
3900  $params,
3902  );
3903  }
3904 
3905  $script .= '});';
3906  $wgOut->addScript( ResourceLoader::makeInlineScript( $script ) );
3907 
3908  $toolbar = '<div id="toolbar"></div>';
3909 
3910  Hooks::run( 'EditPageBeforeEditToolbar', [ &$toolbar ] );
3911 
3912  return $toolbar;
3913  }
3914 
3925  public function getCheckboxes( &$tabindex, $checked ) {
3927 
3928  $checkboxes = [];
3929 
3930  // don't show the minor edit checkbox if it's a new page or section
3931  if ( !$this->isNew ) {
3932  $checkboxes['minor'] = '';
3933  $minorLabel = wfMessage( 'minoredit' )->parse();
3934  if ( $wgUser->isAllowed( 'minoredit' ) ) {
3935  $attribs = [
3936  'tabindex' => ++$tabindex,
3937  'accesskey' => wfMessage( 'accesskey-minoredit' )->text(),
3938  'id' => 'wpMinoredit',
3939  ];
3940  $minorEditHtml =
3941  Xml::check( 'wpMinoredit', $checked['minor'], $attribs ) .
3942  "&#160;<label for='wpMinoredit' id='mw-editpage-minoredit'" .
3943  Xml::expandAttributes( [ 'title' => Linker::titleAttrib( 'minoredit', 'withaccess' ) ] ) .
3944  ">{$minorLabel}</label>";
3945 
3946  if ( $wgUseMediaWikiUIEverywhere ) {
3947  $checkboxes['minor'] = Html::openElement( 'div', [ 'class' => 'mw-ui-checkbox' ] ) .
3948  $minorEditHtml .
3949  Html::closeElement( 'div' );
3950  } else {
3951  $checkboxes['minor'] = $minorEditHtml;
3952  }
3953  }
3954  }
3955 
3956  $watchLabel = wfMessage( 'watchthis' )->parse();
3957  $checkboxes['watch'] = '';
3958  if ( $wgUser->isLoggedIn() ) {
3959  $attribs = [
3960  'tabindex' => ++$tabindex,
3961  'accesskey' => wfMessage( 'accesskey-watch' )->text(),
3962  'id' => 'wpWatchthis',
3963  ];
3964  $watchThisHtml =
3965  Xml::check( 'wpWatchthis', $checked['watch'], $attribs ) .
3966  "&#160;<label for='wpWatchthis' id='mw-editpage-watch'" .
3967  Xml::expandAttributes( [ 'title' => Linker::titleAttrib( 'watch', 'withaccess' ) ] ) .
3968  ">{$watchLabel}</label>";
3969  if ( $wgUseMediaWikiUIEverywhere ) {
3970  $checkboxes['watch'] = Html::openElement( 'div', [ 'class' => 'mw-ui-checkbox' ] ) .
3971  $watchThisHtml .
3972  Html::closeElement( 'div' );
3973  } else {
3974  $checkboxes['watch'] = $watchThisHtml;
3975  }
3976  }
3977  Hooks::run( 'EditPageBeforeEditChecks', [ &$this, &$checkboxes, &$tabindex ] );
3978  return $checkboxes;
3979  }
3980 
3989  public function getEditButtons( &$tabindex ) {
3990  $buttons = [];
3991 
3992  $attribs = [
3993  'id' => 'wpSave',
3994  'name' => 'wpSave',
3995  'tabindex' => ++$tabindex,
3996  ] + Linker::tooltipAndAccesskeyAttribs( 'save' );
3997  $buttons['save'] = Html::submitButton( wfMessage( 'savearticle' )->text(),
3998  $attribs, [ 'mw-ui-constructive' ] );
3999 
4000  ++$tabindex; // use the same for preview and live preview
4001  $attribs = [
4002  'id' => 'wpPreview',
4003  'name' => 'wpPreview',
4004  'tabindex' => $tabindex,
4005  ] + Linker::tooltipAndAccesskeyAttribs( 'preview' );
4006  $buttons['preview'] = Html::submitButton( wfMessage( 'showpreview' )->text(),
4007  $attribs );
4008  $buttons['live'] = '';
4009 
4010  $attribs = [
4011  'id' => 'wpDiff',
4012  'name' => 'wpDiff',
4013  'tabindex' => ++$tabindex,
4014  ] + Linker::tooltipAndAccesskeyAttribs( 'diff' );
4015  $buttons['diff'] = Html::submitButton( wfMessage( 'showdiff' )->text(),
4016  $attribs );
4017 
4018  Hooks::run( 'EditPageBeforeEditButtons', [ &$this, &$buttons, &$tabindex ] );
4019  return $buttons;
4020  }
4021 
4026  function noSuchSectionPage() {
4027  global $wgOut;
4028 
4029  $wgOut->prepareErrorPage( wfMessage( 'nosuchsectiontitle' ) );
4030 
4031  $res = wfMessage( 'nosuchsectiontext', $this->section )->parseAsBlock();
4032  Hooks::run( 'EditPageNoSuchSection', [ &$this, &$res ] );
4033  $wgOut->addHTML( $res );
4034 
4035  $wgOut->returnToMain( false, $this->mTitle );
4036  }
4037 
4043  public function spamPageWithContent( $match = false ) {
4045  $this->textbox2 = $this->textbox1;
4046 
4047  if ( is_array( $match ) ) {
4048  $match = $wgLang->listToText( $match );
4049  }
4050  $wgOut->prepareErrorPage( wfMessage( 'spamprotectiontitle' ) );
4051 
4052  $wgOut->addHTML( '<div id="spamprotected">' );
4053  $wgOut->addWikiMsg( 'spamprotectiontext' );
4054  if ( $match ) {
4055  $wgOut->addWikiMsg( 'spamprotectionmatch', wfEscapeWikiText( $match ) );
4056  }
4057  $wgOut->addHTML( '</div>' );
4058 
4059  $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
4060  $this->showDiff();
4061 
4062  $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
4063  $this->showTextbox2();
4064 
4065  $wgOut->addReturnTo( $this->getContextTitle(), [ 'action' => 'edit' ] );
4066  }
4067 
4074  private function checkUnicodeCompliantBrowser() {
4076 
4077  $currentbrowser = $wgRequest->getHeader( 'User-Agent' );
4078  if ( $currentbrowser === false ) {
4079  // No User-Agent header sent? Trust it by default...
4080  return true;
4081  }
4082 
4083  foreach ( $wgBrowserBlackList as $browser ) {
4084  if ( preg_match( $browser, $currentbrowser ) ) {
4085  return false;
4086  }
4087  }
4088  return true;
4089  }
4090 
4099  protected function safeUnicodeInput( $request, $field ) {
4100  $text = rtrim( $request->getText( $field ) );
4101  return $request->getBool( 'safemode' )
4102  ? $this->unmakeSafe( $text )
4103  : $text;
4104  }
4105 
4113  protected function safeUnicodeOutput( $text ) {
4115  $codedText = $wgContLang->recodeForEdit( $text );
4116  return $this->checkUnicodeCompliantBrowser()
4117  ? $codedText
4118  : $this->makeSafe( $codedText );
4119  }
4120 
4133  private function makeSafe( $invalue ) {
4134  // Armor existing references for reversibility.
4135  $invalue = strtr( $invalue, [ "&#x" => "&#x0" ] );
4136 
4137  $bytesleft = 0;
4138  $result = "";
4139  $working = 0;
4140  $valueLength = strlen( $invalue );
4141  for ( $i = 0; $i < $valueLength; $i++ ) {
4142  $bytevalue = ord( $invalue[$i] );
4143  if ( $bytevalue <= 0x7F ) { // 0xxx xxxx
4144  $result .= chr( $bytevalue );
4145  $bytesleft = 0;
4146  } elseif ( $bytevalue <= 0xBF ) { // 10xx xxxx
4147  $working = $working << 6;
4148  $working += ( $bytevalue & 0x3F );
4149  $bytesleft--;
4150  if ( $bytesleft <= 0 ) {
4151  $result .= "&#x" . strtoupper( dechex( $working ) ) . ";";
4152  }
4153  } elseif ( $bytevalue <= 0xDF ) { // 110x xxxx
4154  $working = $bytevalue & 0x1F;
4155  $bytesleft = 1;
4156  } elseif ( $bytevalue <= 0xEF ) { // 1110 xxxx
4157  $working = $bytevalue & 0x0F;
4158  $bytesleft = 2;
4159  } else { // 1111 0xxx
4160  $working = $bytevalue & 0x07;
4161  $bytesleft = 3;
4162  }
4163  }
4164  return $result;
4165  }
4166 
4175  private function unmakeSafe( $invalue ) {
4176  $result = "";
4177  $valueLength = strlen( $invalue );
4178  for ( $i = 0; $i < $valueLength; $i++ ) {
4179  if ( ( substr( $invalue, $i, 3 ) == "&#x" ) && ( $invalue[$i + 3] != '0' ) ) {
4180  $i += 3;
4181  $hexstring = "";
4182  do {
4183  $hexstring .= $invalue[$i];
4184  $i++;
4185  } while ( ctype_xdigit( $invalue[$i] ) && ( $i < strlen( $invalue ) ) );
4186 
4187  // Do some sanity checks. These aren't needed for reversibility,
4188  // but should help keep the breakage down if the editor
4189  // breaks one of the entities whilst editing.
4190  if ( ( substr( $invalue, $i, 1 ) == ";" ) && ( strlen( $hexstring ) <= 6 ) ) {
4191  $codepoint = hexdec( $hexstring );
4192  $result .= UtfNormal\Utils::codepointToUtf8( $codepoint );
4193  } else {
4194  $result .= "&#x" . $hexstring . substr( $invalue, $i, 1 );
4195  }
4196  } else {
4197  $result .= substr( $invalue, $i, 1 );
4198  }
4199  }
4200  // reverse the transform that we made for reversibility reasons.
4201  return strtr( $result, [ "&#x0" => "&#x" ] );
4202  }
4203 }
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:4133
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:3087
bool $allowBlankSummary
Definition: EditPage.php:271
getPreviewText()
Get the rendered text for previewing.
Definition: EditPage.php:3600
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:1411
string $summary
Definition: EditPage.php:330
setHeaders()
Definition: EditPage.php:2226
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:4099
showTextbox2()
Definition: EditPage.php:3141
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:3301
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:3318
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:3989
$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:1367
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:3377
spamPageWithContent($match=false)
Show "your edit contains spam" page with your diff and text.
Definition: EditPage.php:4043
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:3353
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:3046
internalAttemptSave(&$result, $bot=false)
Attempt submission (no UI)
Definition: EditPage.php:1644
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:1061
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:2136
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:2205
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:4113
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:1390
showIntro()
Show all applicable editing introductions.
Definition: EditPage.php:2285
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:1221
isWrongCaseCssJsPage()
Checks whether the user entered a skin name in uppercase, e.g.
Definition: EditPage.php:767
getTemplates()
Definition: EditPage.php:3762
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