MediaWiki  1.27.4
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  // Avoid PHP 7.1 warning of passing $this by reference
2499  $editPage = $this;
2500  Hooks::run( 'EditPage::showEditForm:initial', [ &$editPage, &$wgOut ] );
2501 
2502  $this->setHeaders();
2503 
2504  if ( $this->showHeader() === false ) {
2505  return;
2506  }
2507 
2508  $wgOut->addHTML( $this->editFormPageTop );
2509 
2510  if ( $wgUser->getOption( 'previewontop' ) ) {
2511  $this->displayPreviewArea( $previewOutput, true );
2512  }
2513 
2514  $wgOut->addHTML( $this->editFormTextTop );
2515 
2516  $showToolbar = true;
2517  if ( $this->wasDeletedSinceLastEdit() ) {
2518  if ( $this->formtype == 'save' ) {
2519  // Hide the toolbar and edit area, user can click preview to get it back
2520  // Add an confirmation checkbox and explanation.
2521  $showToolbar = false;
2522  } else {
2523  $wgOut->wrapWikiMsg( "<div class='error mw-deleted-while-editing'>\n$1\n</div>",
2524  'deletedwhileediting' );
2525  }
2526  }
2527 
2528  // @todo add EditForm plugin interface and use it here!
2529  // search for textarea1 and textares2, and allow EditForm to override all uses.
2530  $wgOut->addHTML( Html::openElement(
2531  'form',
2532  [
2533  'id' => self::EDITFORM_ID,
2534  'name' => self::EDITFORM_ID,
2535  'method' => 'post',
2536  'action' => $this->getActionURL( $this->getContextTitle() ),
2537  'enctype' => 'multipart/form-data'
2538  ]
2539  ) );
2540 
2541  if ( is_callable( $formCallback ) ) {
2542  wfWarn( 'The $formCallback parameter to ' . __METHOD__ . 'is deprecated' );
2543  call_user_func_array( $formCallback, [ &$wgOut ] );
2544  }
2545 
2546  // Add an empty field to trip up spambots
2547  $wgOut->addHTML(
2548  Xml::openElement( 'div', [ 'id' => 'antispam-container', 'style' => 'display: none;' ] )
2549  . Html::rawElement(
2550  'label',
2551  [ 'for' => 'wpAntispam' ],
2552  wfMessage( 'simpleantispam-label' )->parse()
2553  )
2554  . Xml::element(
2555  'input',
2556  [
2557  'type' => 'text',
2558  'name' => 'wpAntispam',
2559  'id' => 'wpAntispam',
2560  'value' => ''
2561  ]
2562  )
2563  . Xml::closeElement( 'div' )
2564  );
2565 
2566  // Avoid PHP 7.1 warning of passing $this by reference
2567  $editPage = $this;
2568  Hooks::run( 'EditPage::showEditForm:fields', [ &$editPage, &$wgOut ] );
2569 
2570  // Put these up at the top to ensure they aren't lost on early form submission
2571  $this->showFormBeforeText();
2572 
2573  if ( $this->wasDeletedSinceLastEdit() && 'save' == $this->formtype ) {
2574  $username = $this->lastDelete->user_name;
2575  $comment = $this->lastDelete->log_comment;
2576 
2577  // It is better to not parse the comment at all than to have templates expanded in the middle
2578  // TODO: can the checkLabel be moved outside of the div so that wrapWikiMsg could be used?
2579  $key = $comment === ''
2580  ? 'confirmrecreate-noreason'
2581  : 'confirmrecreate';
2582  $wgOut->addHTML(
2583  '<div class="mw-confirm-recreate">' .
2584  wfMessage( $key, $username, "<nowiki>$comment</nowiki>" )->parse() .
2585  Xml::checkLabel( wfMessage( 'recreate' )->text(), 'wpRecreate', 'wpRecreate', false,
2586  [ 'title' => Linker::titleAttrib( 'recreate' ), 'tabindex' => 1, 'id' => 'wpRecreate' ]
2587  ) .
2588  '</div>'
2589  );
2590  }
2591 
2592  # When the summary is hidden, also hide them on preview/show changes
2593  if ( $this->nosummary ) {
2594  $wgOut->addHTML( Html::hidden( 'nosummary', true ) );
2595  }
2596 
2597  # If a blank edit summary was previously provided, and the appropriate
2598  # user preference is active, pass a hidden tag as wpIgnoreBlankSummary. This will stop the
2599  # user being bounced back more than once in the event that a summary
2600  # is not required.
2601  # ####
2602  # For a bit more sophisticated detection of blank summaries, hash the
2603  # automatic one and pass that in the hidden field wpAutoSummary.
2604  if ( $this->missingSummary || ( $this->section == 'new' && $this->nosummary ) ) {
2605  $wgOut->addHTML( Html::hidden( 'wpIgnoreBlankSummary', true ) );
2606  }
2607 
2608  if ( $this->undidRev ) {
2609  $wgOut->addHTML( Html::hidden( 'wpUndidRevision', $this->undidRev ) );
2610  }
2611 
2612  if ( $this->selfRedirect ) {
2613  $wgOut->addHTML( Html::hidden( 'wpIgnoreSelfRedirect', true ) );
2614  }
2615 
2616  if ( $this->hasPresetSummary ) {
2617  // If a summary has been preset using &summary= we don't want to prompt for
2618  // a different summary. Only prompt for a summary if the summary is blanked.
2619  // (Bug 17416)
2620  $this->autoSumm = md5( '' );
2621  }
2622 
2623  $autosumm = $this->autoSumm ? $this->autoSumm : md5( $this->summary );
2624  $wgOut->addHTML( Html::hidden( 'wpAutoSummary', $autosumm ) );
2625 
2626  $wgOut->addHTML( Html::hidden( 'oldid', $this->oldid ) );
2627  $wgOut->addHTML( Html::hidden( 'parentRevId', $this->getParentRevId() ) );
2628 
2629  $wgOut->addHTML( Html::hidden( 'format', $this->contentFormat ) );
2630  $wgOut->addHTML( Html::hidden( 'model', $this->contentModel ) );
2631 
2632  if ( $this->section == 'new' ) {
2633  $this->showSummaryInput( true, $this->summary );
2634  $wgOut->addHTML( $this->getSummaryPreview( true, $this->summary ) );
2635  }
2636 
2637  $wgOut->addHTML( $this->editFormTextBeforeContent );
2638 
2639  if ( !$this->isCssJsSubpage && $showToolbar && $wgUser->getOption( 'showtoolbar' ) ) {
2640  $wgOut->addHTML( EditPage::getEditToolbar( $this->mTitle ) );
2641  }
2642 
2643  if ( $this->blankArticle ) {
2644  $wgOut->addHTML( Html::hidden( 'wpIgnoreBlankArticle', true ) );
2645  }
2646 
2647  if ( $this->isConflict ) {
2648  // In an edit conflict bypass the overridable content form method
2649  // and fallback to the raw wpTextbox1 since editconflicts can't be
2650  // resolved between page source edits and custom ui edits using the
2651  // custom edit ui.
2652  $this->textbox2 = $this->textbox1;
2653 
2654  $content = $this->getCurrentContent();
2655  $this->textbox1 = $this->toEditText( $content );
2656 
2657  $this->showTextbox1();
2658  } else {
2659  $this->showContentForm();
2660  }
2661 
2662  $wgOut->addHTML( $this->editFormTextAfterContent );
2663 
2664  $this->showStandardInputs();
2665 
2666  $this->showFormAfterText();
2667 
2668  $this->showTosSummary();
2669 
2670  $this->showEditTools();
2671 
2672  $wgOut->addHTML( $this->editFormTextAfterTools . "\n" );
2673 
2674  $wgOut->addHTML( Html::rawElement( 'div', [ 'class' => 'templatesUsed' ],
2675  Linker::formatTemplates( $this->getTemplates(), $this->preview, $this->section != '' ) ) );
2676 
2677  $wgOut->addHTML( Html::rawElement( 'div', [ 'class' => 'hiddencats' ],
2678  Linker::formatHiddenCategories( $this->page->getHiddenCategories() ) ) );
2679 
2680  $wgOut->addHTML( Html::rawElement( 'div', [ 'class' => 'limitreport' ],
2681  self::getPreviewLimitReport( $this->mParserOutput ) ) );
2682 
2683  $wgOut->addModules( 'mediawiki.action.edit.collapsibleFooter' );
2684 
2685  if ( $this->isConflict ) {
2686  try {
2687  $this->showConflict();
2688  } catch ( MWContentSerializationException $ex ) {
2689  // this can't really happen, but be nice if it does.
2690  $msg = wfMessage(
2691  'content-failed-to-parse',
2692  $this->contentModel,
2693  $this->contentFormat,
2694  $ex->getMessage()
2695  );
2696  $wgOut->addWikiText( '<div class="error">' . $msg->text() . '</div>' );
2697  }
2698  }
2699 
2700  // Marker for detecting truncated form data. This must be the last
2701  // parameter sent in order to be of use, so do not move me.
2702  $wgOut->addHTML( Html::hidden( 'wpUltimateParam', true ) );
2703  $wgOut->addHTML( $this->editFormTextBottom . "\n</form>\n" );
2704 
2705  if ( !$wgUser->getOption( 'previewontop' ) ) {
2706  $this->displayPreviewArea( $previewOutput, false );
2707  }
2708 
2709  }
2710 
2717  public static function extractSectionTitle( $text ) {
2718  preg_match( "/^(=+)(.+)\\1\\s*(\n|$)/i", $text, $matches );
2719  if ( !empty( $matches[2] ) ) {
2720  global $wgParser;
2721  return $wgParser->stripSectionName( trim( $matches[2] ) );
2722  } else {
2723  return false;
2724  }
2725  }
2726 
2730  protected function showHeader() {
2733 
2734  if ( $this->mTitle->isTalkPage() ) {
2735  $wgOut->addWikiMsg( 'talkpagetext' );
2736  }
2737 
2738  // Add edit notices
2739  $editNotices = $this->mTitle->getEditNotices( $this->oldid );
2740  if ( count( $editNotices ) ) {
2741  $wgOut->addHTML( implode( "\n", $editNotices ) );
2742  } else {
2743  $msg = wfMessage( 'editnotice-notext' );
2744  if ( !$msg->isDisabled() ) {
2745  $wgOut->addHTML(
2746  '<div class="mw-editnotice-notext">'
2747  . $msg->parseAsBlock()
2748  . '</div>'
2749  );
2750  }
2751  }
2752 
2753  if ( $this->isConflict ) {
2754  $wgOut->wrapWikiMsg( "<div class='mw-explainconflict'>\n$1\n</div>", 'explainconflict' );
2755  $this->edittime = $this->page->getTimestamp();
2756  } else {
2757  if ( $this->section != '' && !$this->isSectionEditSupported() ) {
2758  // We use $this->section to much before this and getVal('wgSection') directly in other places
2759  // at this point we can't reset $this->section to '' to fallback to non-section editing.
2760  // Someone is welcome to try refactoring though
2761  $wgOut->showErrorPage( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
2762  return false;
2763  }
2764 
2765  if ( $this->section != '' && $this->section != 'new' ) {
2766  if ( !$this->summary && !$this->preview && !$this->diff ) {
2767  $sectionTitle = self::extractSectionTitle( $this->textbox1 ); // FIXME: use Content object
2768  if ( $sectionTitle !== false ) {
2769  $this->summary = "/* $sectionTitle */ ";
2770  }
2771  }
2772  }
2773 
2774  if ( $this->missingComment ) {
2775  $wgOut->wrapWikiMsg( "<div id='mw-missingcommenttext'>\n$1\n</div>", 'missingcommenttext' );
2776  }
2777 
2778  if ( $this->missingSummary && $this->section != 'new' ) {
2779  $wgOut->wrapWikiMsg( "<div id='mw-missingsummary'>\n$1\n</div>", 'missingsummary' );
2780  }
2781 
2782  if ( $this->missingSummary && $this->section == 'new' ) {
2783  $wgOut->wrapWikiMsg( "<div id='mw-missingcommentheader'>\n$1\n</div>", 'missingcommentheader' );
2784  }
2785 
2786  if ( $this->blankArticle ) {
2787  $wgOut->wrapWikiMsg( "<div id='mw-blankarticle'>\n$1\n</div>", 'blankarticle' );
2788  }
2789 
2790  if ( $this->selfRedirect ) {
2791  $wgOut->wrapWikiMsg( "<div id='mw-selfredirect'>\n$1\n</div>", 'selfredirect' );
2792  }
2793 
2794  if ( $this->hookError !== '' ) {
2795  $wgOut->addWikiText( $this->hookError );
2796  }
2797 
2798  if ( !$this->checkUnicodeCompliantBrowser() ) {
2799  $wgOut->addWikiMsg( 'nonunicodebrowser' );
2800  }
2801 
2802  if ( $this->section != 'new' ) {
2803  $revision = $this->mArticle->getRevisionFetched();
2804  if ( $revision ) {
2805  // Let sysop know that this will make private content public if saved
2806 
2807  if ( !$revision->userCan( Revision::DELETED_TEXT, $wgUser ) ) {
2808  $wgOut->wrapWikiMsg(
2809  "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
2810  'rev-deleted-text-permission'
2811  );
2812  } elseif ( $revision->isDeleted( Revision::DELETED_TEXT ) ) {
2813  $wgOut->wrapWikiMsg(
2814  "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
2815  'rev-deleted-text-view'
2816  );
2817  }
2818 
2819  if ( !$revision->isCurrent() ) {
2820  $this->mArticle->setOldSubtitle( $revision->getId() );
2821  $wgOut->addWikiMsg( 'editingold' );
2822  }
2823  } elseif ( $this->mTitle->exists() ) {
2824  // Something went wrong
2825 
2826  $wgOut->wrapWikiMsg( "<div class='errorbox'>\n$1\n</div>\n",
2827  [ 'missing-revision', $this->oldid ] );
2828  }
2829  }
2830  }
2831 
2832  if ( wfReadOnly() ) {
2833  $wgOut->wrapWikiMsg(
2834  "<div id=\"mw-read-only-warning\">\n$1\n</div>",
2835  [ 'readonlywarning', wfReadOnlyReason() ]
2836  );
2837  } elseif ( $wgUser->isAnon() ) {
2838  if ( $this->formtype != 'preview' ) {
2839  $wgOut->wrapWikiMsg(
2840  "<div id='mw-anon-edit-warning' class='warningbox'>\n$1\n</div>",
2841  [ 'anoneditwarning',
2842  // Log-in link
2843  '{{fullurl:Special:UserLogin|returnto={{FULLPAGENAMEE}}}}',
2844  // Sign-up link
2845  '{{fullurl:Special:CreateAccount|returnto={{FULLPAGENAMEE}}}}' ]
2846  );
2847  } else {
2848  $wgOut->wrapWikiMsg( "<div id=\"mw-anon-preview-warning\" class=\"warningbox\">\n$1</div>",
2849  'anonpreviewwarning'
2850  );
2851  }
2852  } else {
2853  if ( $this->isCssJsSubpage ) {
2854  # Check the skin exists
2855  if ( $this->isWrongCaseCssJsPage ) {
2856  $wgOut->wrapWikiMsg(
2857  "<div class='error' id='mw-userinvalidcssjstitle'>\n$1\n</div>",
2858  [ 'userinvalidcssjstitle', $this->mTitle->getSkinFromCssJsSubpage() ]
2859  );
2860  }
2861  if ( $this->getTitle()->isSubpageOf( $wgUser->getUserPage() ) ) {
2862  if ( $this->formtype !== 'preview' ) {
2863  if ( $this->isCssSubpage && $wgAllowUserCss ) {
2864  $wgOut->wrapWikiMsg(
2865  "<div id='mw-usercssyoucanpreview'>\n$1\n</div>",
2866  [ 'usercssyoucanpreview' ]
2867  );
2868  }
2869 
2870  if ( $this->isJsSubpage && $wgAllowUserJs ) {
2871  $wgOut->wrapWikiMsg(
2872  "<div id='mw-userjsyoucanpreview'>\n$1\n</div>",
2873  [ 'userjsyoucanpreview' ]
2874  );
2875  }
2876  }
2877  }
2878  }
2879  }
2880 
2881  if ( $this->mTitle->isProtected( 'edit' ) &&
2882  MWNamespace::getRestrictionLevels( $this->mTitle->getNamespace() ) !== [ '' ]
2883  ) {
2884  # Is the title semi-protected?
2885  if ( $this->mTitle->isSemiProtected() ) {
2886  $noticeMsg = 'semiprotectedpagewarning';
2887  } else {
2888  # Then it must be protected based on static groups (regular)
2889  $noticeMsg = 'protectedpagewarning';
2890  }
2891  LogEventsList::showLogExtract( $wgOut, 'protect', $this->mTitle, '',
2892  [ 'lim' => 1, 'msgKey' => [ $noticeMsg ] ] );
2893  }
2894  if ( $this->mTitle->isCascadeProtected() ) {
2895  # Is this page under cascading protection from some source pages?
2896 
2897  list( $cascadeSources, /* $restrictions */ ) = $this->mTitle->getCascadeProtectionSources();
2898  $notice = "<div class='mw-cascadeprotectedwarning'>\n$1\n";
2899  $cascadeSourcesCount = count( $cascadeSources );
2900  if ( $cascadeSourcesCount > 0 ) {
2901  # Explain, and list the titles responsible
2902  foreach ( $cascadeSources as $page ) {
2903  $notice .= '* [[:' . $page->getPrefixedText() . "]]\n";
2904  }
2905  }
2906  $notice .= '</div>';
2907  $wgOut->wrapWikiMsg( $notice, [ 'cascadeprotectedwarning', $cascadeSourcesCount ] );
2908  }
2909  if ( !$this->mTitle->exists() && $this->mTitle->getRestrictions( 'create' ) ) {
2910  LogEventsList::showLogExtract( $wgOut, 'protect', $this->mTitle, '',
2911  [ 'lim' => 1,
2912  'showIfEmpty' => false,
2913  'msgKey' => [ 'titleprotectedwarning' ],
2914  'wrap' => "<div class=\"mw-titleprotectedwarning\">\n$1</div>" ] );
2915  }
2916 
2917  if ( $this->kblength === false ) {
2918  $this->kblength = (int)( strlen( $this->textbox1 ) / 1024 );
2919  }
2920 
2921  if ( $this->tooBig || $this->kblength > $wgMaxArticleSize ) {
2922  $wgOut->wrapWikiMsg( "<div class='error' id='mw-edit-longpageerror'>\n$1\n</div>",
2923  [
2924  'longpageerror',
2925  $wgLang->formatNum( $this->kblength ),
2926  $wgLang->formatNum( $wgMaxArticleSize )
2927  ]
2928  );
2929  } else {
2930  if ( !wfMessage( 'longpage-hint' )->isDisabled() ) {
2931  $wgOut->wrapWikiMsg( "<div id='mw-edit-longpage-hint'>\n$1\n</div>",
2932  [
2933  'longpage-hint',
2934  $wgLang->formatSize( strlen( $this->textbox1 ) ),
2935  strlen( $this->textbox1 )
2936  ]
2937  );
2938  }
2939  }
2940  # Add header copyright warning
2941  $this->showHeaderCopyrightWarning();
2942 
2943  return true;
2944  }
2945 
2960  function getSummaryInput( $summary = "", $labelText = null,
2961  $inputAttrs = null, $spanLabelAttrs = null
2962  ) {
2963  // Note: the maxlength is overridden in JS to 255 and to make it use UTF-8 bytes, not characters.
2964  $inputAttrs = ( is_array( $inputAttrs ) ? $inputAttrs : [] ) + [
2965  'id' => 'wpSummary',
2966  'maxlength' => '200',
2967  'tabindex' => '1',
2968  'size' => 60,
2969  'spellcheck' => 'true',
2970  ] + Linker::tooltipAndAccesskeyAttribs( 'summary' );
2971 
2972  $spanLabelAttrs = ( is_array( $spanLabelAttrs ) ? $spanLabelAttrs : [] ) + [
2973  'class' => $this->missingSummary ? 'mw-summarymissed' : 'mw-summary',
2974  'id' => "wpSummaryLabel"
2975  ];
2976 
2977  $label = null;
2978  if ( $labelText ) {
2979  $label = Xml::tags(
2980  'label',
2981  $inputAttrs['id'] ? [ 'for' => $inputAttrs['id'] ] : null,
2982  $labelText
2983  );
2984  $label = Xml::tags( 'span', $spanLabelAttrs, $label );
2985  }
2986 
2987  $input = Html::input( 'wpSummary', $summary, 'text', $inputAttrs );
2988 
2989  return [ $label, $input ];
2990  }
2991 
2998  protected function showSummaryInput( $isSubjectPreview, $summary = "" ) {
3000  # Add a class if 'missingsummary' is triggered to allow styling of the summary line
3001  $summaryClass = $this->missingSummary ? 'mw-summarymissed' : 'mw-summary';
3002  if ( $isSubjectPreview ) {
3003  if ( $this->nosummary ) {
3004  return;
3005  }
3006  } else {
3007  if ( !$this->mShowSummaryField ) {
3008  return;
3009  }
3010  }
3011  $summary = $wgContLang->recodeForEdit( $summary );
3012  $labelText = wfMessage( $isSubjectPreview ? 'subject' : 'summary' )->parse();
3013  list( $label, $input ) = $this->getSummaryInput(
3014  $summary,
3015  $labelText,
3016  [ 'class' => $summaryClass ],
3017  []
3018  );
3019  $wgOut->addHTML( "{$label} {$input}" );
3020  }
3021 
3029  protected function getSummaryPreview( $isSubjectPreview, $summary = "" ) {
3030  // avoid spaces in preview, gets always trimmed on save
3031  $summary = trim( $summary );
3032  if ( !$summary || ( !$this->preview && !$this->diff ) ) {
3033  return "";
3034  }
3035 
3036  global $wgParser;
3037 
3038  if ( $isSubjectPreview ) {
3039  $summary = wfMessage( 'newsectionsummary' )->rawParams( $wgParser->stripSectionName( $summary ) )
3040  ->inContentLanguage()->text();
3041  }
3042 
3043  $message = $isSubjectPreview ? 'subject-preview' : 'summary-preview';
3044 
3045  $summary = wfMessage( $message )->parse()
3046  . Linker::commentBlock( $summary, $this->mTitle, $isSubjectPreview );
3047  return Xml::tags( 'div', [ 'class' => 'mw-summary-preview' ], $summary );
3048  }
3049 
3050  protected function showFormBeforeText() {
3051  global $wgOut;
3052  $section = htmlspecialchars( $this->section );
3053  $wgOut->addHTML( <<<HTML
3054 <input type='hidden' value="{$section}" name="wpSection"/>
3055 <input type='hidden' value="{$this->starttime}" name="wpStarttime" />
3056 <input type='hidden' value="{$this->edittime}" name="wpEdittime" />
3057 <input type='hidden' value="{$this->scrolltop}" name="wpScrolltop" id="wpScrolltop" />
3058 
3059 HTML
3060  );
3061  if ( !$this->checkUnicodeCompliantBrowser() ) {
3062  $wgOut->addHTML( Html::hidden( 'safemode', '1' ) );
3063  }
3064  }
3065 
3066  protected function showFormAfterText() {
3080  $wgOut->addHTML( "\n" . Html::hidden( "wpEditToken", $wgUser->getEditToken() ) . "\n" );
3081  }
3082 
3091  protected function showContentForm() {
3092  $this->showTextbox1();
3093  }
3094 
3103  protected function showTextbox1( $customAttribs = null, $textoverride = null ) {
3104  if ( $this->wasDeletedSinceLastEdit() && $this->formtype == 'save' ) {
3105  $attribs = [ 'style' => 'display:none;' ];
3106  } else {
3107  $classes = []; // Textarea CSS
3108  if ( $this->mTitle->isProtected( 'edit' ) &&
3109  MWNamespace::getRestrictionLevels( $this->mTitle->getNamespace() ) !== [ '' ]
3110  ) {
3111  # Is the title semi-protected?
3112  if ( $this->mTitle->isSemiProtected() ) {
3113  $classes[] = 'mw-textarea-sprotected';
3114  } else {
3115  # Then it must be protected based on static groups (regular)
3116  $classes[] = 'mw-textarea-protected';
3117  }
3118  # Is the title cascade-protected?
3119  if ( $this->mTitle->isCascadeProtected() ) {
3120  $classes[] = 'mw-textarea-cprotected';
3121  }
3122  }
3123 
3124  $attribs = [ 'tabindex' => 1 ];
3125 
3126  if ( is_array( $customAttribs ) ) {
3128  }
3129 
3130  if ( count( $classes ) ) {
3131  if ( isset( $attribs['class'] ) ) {
3132  $classes[] = $attribs['class'];
3133  }
3134  $attribs['class'] = implode( ' ', $classes );
3135  }
3136  }
3137 
3138  $this->showTextbox(
3139  $textoverride !== null ? $textoverride : $this->textbox1,
3140  'wpTextbox1',
3141  $attribs
3142  );
3143  }
3144 
3145  protected function showTextbox2() {
3146  $this->showTextbox( $this->textbox2, 'wpTextbox2', [ 'tabindex' => 6, 'readonly' ] );
3147  }
3148 
3149  protected function showTextbox( $text, $name, $customAttribs = [] ) {
3151 
3152  $wikitext = $this->safeUnicodeOutput( $text );
3153  if ( strval( $wikitext ) !== '' ) {
3154  // Ensure there's a newline at the end, otherwise adding lines
3155  // is awkward.
3156  // But don't add a newline if the ext is empty, or Firefox in XHTML
3157  // mode will show an extra newline. A bit annoying.
3158  $wikitext .= "\n";
3159  }
3160 
3161  $attribs = $customAttribs + [
3162  'accesskey' => ',',
3163  'id' => $name,
3164  'cols' => $wgUser->getIntOption( 'cols' ),
3165  'rows' => $wgUser->getIntOption( 'rows' ),
3166  // Avoid PHP notices when appending preferences
3167  // (appending allows customAttribs['style'] to still work).
3168  'style' => ''
3169  ];
3170 
3171  $pageLang = $this->mTitle->getPageLanguage();
3172  $attribs['lang'] = $pageLang->getHtmlCode();
3173  $attribs['dir'] = $pageLang->getDir();
3174 
3175  $wgOut->addHTML( Html::textarea( $name, $wikitext, $attribs ) );
3176  }
3177 
3178  protected function displayPreviewArea( $previewOutput, $isOnTop = false ) {
3179  global $wgOut;
3180  $classes = [];
3181  if ( $isOnTop ) {
3182  $classes[] = 'ontop';
3183  }
3184 
3185  $attribs = [ 'id' => 'wikiPreview', 'class' => implode( ' ', $classes ) ];
3186 
3187  if ( $this->formtype != 'preview' ) {
3188  $attribs['style'] = 'display: none;';
3189  }
3190 
3191  $wgOut->addHTML( Xml::openElement( 'div', $attribs ) );
3192 
3193  if ( $this->formtype == 'preview' ) {
3194  $this->showPreview( $previewOutput );
3195  } else {
3196  // Empty content container for LivePreview
3197  $pageViewLang = $this->mTitle->getPageViewLanguage();
3198  $attribs = [ 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(),
3199  'class' => 'mw-content-' . $pageViewLang->getDir() ];
3200  $wgOut->addHTML( Html::rawElement( 'div', $attribs ) );
3201  }
3202 
3203  $wgOut->addHTML( '</div>' );
3204 
3205  if ( $this->formtype == 'diff' ) {
3206  try {
3207  $this->showDiff();
3208  } catch ( MWContentSerializationException $ex ) {
3209  $msg = wfMessage(
3210  'content-failed-to-parse',
3211  $this->contentModel,
3212  $this->contentFormat,
3213  $ex->getMessage()
3214  );
3215  $wgOut->addWikiText( '<div class="error">' . $msg->text() . '</div>' );
3216  }
3217  }
3218  }
3219 
3226  protected function showPreview( $text ) {
3227  global $wgOut;
3228  if ( $this->mTitle->getNamespace() == NS_CATEGORY ) {
3229  $this->mArticle->openShowCategory();
3230  }
3231  # This hook seems slightly odd here, but makes things more
3232  # consistent for extensions.
3233  Hooks::run( 'OutputPageBeforeHTML', [ &$wgOut, &$text ] );
3234  $wgOut->addHTML( $text );
3235  if ( $this->mTitle->getNamespace() == NS_CATEGORY ) {
3236  $this->mArticle->closeShowCategory();
3237  }
3238  }
3239 
3247  function showDiff() {
3249 
3250  $oldtitlemsg = 'currentrev';
3251  # if message does not exist, show diff against the preloaded default
3252  if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && !$this->mTitle->exists() ) {
3253  $oldtext = $this->mTitle->getDefaultMessageText();
3254  if ( $oldtext !== false ) {
3255  $oldtitlemsg = 'defaultmessagetext';
3256  $oldContent = $this->toEditContent( $oldtext );
3257  } else {
3258  $oldContent = null;
3259  }
3260  } else {
3261  $oldContent = $this->getCurrentContent();
3262  }
3263 
3264  $textboxContent = $this->toEditContent( $this->textbox1 );
3265 
3266  $newContent = $this->page->replaceSectionContent(
3267  $this->section, $textboxContent,
3268  $this->summary, $this->edittime );
3269 
3270  if ( $newContent ) {
3271  ContentHandler::runLegacyHooks( 'EditPageGetDiffText', [ $this, &$newContent ] );
3272  Hooks::run( 'EditPageGetDiffContent', [ $this, &$newContent ] );
3273 
3274  $popts = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang );
3275  $newContent = $newContent->preSaveTransform( $this->mTitle, $wgUser, $popts );
3276  }
3277 
3278  if ( ( $oldContent && !$oldContent->isEmpty() ) || ( $newContent && !$newContent->isEmpty() ) ) {
3279  $oldtitle = wfMessage( $oldtitlemsg )->parse();
3280  $newtitle = wfMessage( 'yourtext' )->parse();
3281 
3282  if ( !$oldContent ) {
3283  $oldContent = $newContent->getContentHandler()->makeEmptyContent();
3284  }
3285 
3286  if ( !$newContent ) {
3287  $newContent = $oldContent->getContentHandler()->makeEmptyContent();
3288  }
3289 
3290  $de = $oldContent->getContentHandler()->createDifferenceEngine( $this->mArticle->getContext() );
3291  $de->setContent( $oldContent, $newContent );
3292 
3293  $difftext = $de->getDiff( $oldtitle, $newtitle );
3294  $de->showDiffStyle();
3295  } else {
3296  $difftext = '';
3297  }
3298 
3299  $wgOut->addHTML( '<div id="wikiDiff">' . $difftext . '</div>' );
3300  }
3301 
3305  protected function showHeaderCopyrightWarning() {
3306  $msg = 'editpage-head-copy-warn';
3307  if ( !wfMessage( $msg )->isDisabled() ) {
3308  global $wgOut;
3309  $wgOut->wrapWikiMsg( "<div class='editpage-head-copywarn'>\n$1\n</div>",
3310  'editpage-head-copy-warn' );
3311  }
3312  }
3313 
3322  protected function showTosSummary() {
3323  $msg = 'editpage-tos-summary';
3324  Hooks::run( 'EditPageTosSummary', [ $this->mTitle, &$msg ] );
3325  if ( !wfMessage( $msg )->isDisabled() ) {
3326  global $wgOut;
3327  $wgOut->addHTML( '<div class="mw-tos-summary">' );
3328  $wgOut->addWikiMsg( $msg );
3329  $wgOut->addHTML( '</div>' );
3330  }
3331  }
3332 
3333  protected function showEditTools() {
3334  global $wgOut;
3335  $wgOut->addHTML( '<div class="mw-editTools">' .
3336  wfMessage( 'edittools' )->inContentLanguage()->parse() .
3337  '</div>' );
3338  }
3339 
3346  protected function getCopywarn() {
3347  return self::getCopyrightWarning( $this->mTitle );
3348  }
3349 
3357  public static function getCopyrightWarning( $title, $format = 'plain' ) {
3358  global $wgRightsText;
3359  if ( $wgRightsText ) {
3360  $copywarnMsg = [ 'copyrightwarning',
3361  '[[' . wfMessage( 'copyrightpage' )->inContentLanguage()->text() . ']]',
3362  $wgRightsText ];
3363  } else {
3364  $copywarnMsg = [ 'copyrightwarning2',
3365  '[[' . wfMessage( 'copyrightpage' )->inContentLanguage()->text() . ']]' ];
3366  }
3367  // Allow for site and per-namespace customization of contribution/copyright notice.
3368  Hooks::run( 'EditPageCopyrightWarning', [ $title, &$copywarnMsg ] );
3369 
3370  return "<div id=\"editpage-copywarn\">\n" .
3371  call_user_func_array( 'wfMessage', $copywarnMsg )->$format() . "\n</div>";
3372  }
3373 
3381  public static function getPreviewLimitReport( $output ) {
3382  if ( !$output || !$output->getLimitReportData() ) {
3383  return '';
3384  }
3385 
3386  $limitReport = Html::rawElement( 'div', [ 'class' => 'mw-limitReportExplanation' ],
3387  wfMessage( 'limitreport-title' )->parseAsBlock()
3388  );
3389 
3390  // Show/hide animation doesn't work correctly on a table, so wrap it in a div.
3391  $limitReport .= Html::openElement( 'div', [ 'class' => 'preview-limit-report-wrapper' ] );
3392 
3393  $limitReport .= Html::openElement( 'table', [
3394  'class' => 'preview-limit-report wikitable'
3395  ] ) .
3396  Html::openElement( 'tbody' );
3397 
3398  foreach ( $output->getLimitReportData() as $key => $value ) {
3399  if ( Hooks::run( 'ParserLimitReportFormat',
3400  [ $key, &$value, &$limitReport, true, true ]
3401  ) ) {
3402  $keyMsg = wfMessage( $key );
3403  $valueMsg = wfMessage( [ "$key-value-html", "$key-value" ] );
3404  if ( !$valueMsg->exists() ) {
3405  $valueMsg = new RawMessage( '$1' );
3406  }
3407  if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) {
3408  $limitReport .= Html::openElement( 'tr' ) .
3409  Html::rawElement( 'th', null, $keyMsg->parse() ) .
3410  Html::rawElement( 'td', null, $valueMsg->params( $value )->parse() ) .
3411  Html::closeElement( 'tr' );
3412  }
3413  }
3414  }
3415 
3416  $limitReport .= Html::closeElement( 'tbody' ) .
3417  Html::closeElement( 'table' ) .
3418  Html::closeElement( 'div' );
3419 
3420  return $limitReport;
3421  }
3422 
3423  protected function showStandardInputs( &$tabindex = 2 ) {
3424  global $wgOut;
3425  $wgOut->addHTML( "<div class='editOptions'>\n" );
3426 
3427  if ( $this->section != 'new' ) {
3428  $this->showSummaryInput( false, $this->summary );
3429  $wgOut->addHTML( $this->getSummaryPreview( false, $this->summary ) );
3430  }
3431 
3432  $checkboxes = $this->getCheckboxes( $tabindex,
3433  [ 'minor' => $this->minoredit, 'watch' => $this->watchthis ] );
3434  $wgOut->addHTML( "<div class='editCheckboxes'>" . implode( $checkboxes, "\n" ) . "</div>\n" );
3435 
3436  // Show copyright warning.
3437  $wgOut->addWikiText( $this->getCopywarn() );
3438  $wgOut->addHTML( $this->editFormTextAfterWarn );
3439 
3440  $wgOut->addHTML( "<div class='editButtons'>\n" );
3441  $wgOut->addHTML( implode( $this->getEditButtons( $tabindex ), "\n" ) . "\n" );
3442 
3443  $cancel = $this->getCancelLink();
3444  if ( $cancel !== '' ) {
3445  $cancel .= Html::element( 'span',
3446  [ 'class' => 'mw-editButtons-pipe-separator' ],
3447  wfMessage( 'pipe-separator' )->text() );
3448  }
3449 
3450  $message = wfMessage( 'edithelppage' )->inContentLanguage()->text();
3451  $edithelpurl = Skin::makeInternalOrExternalUrl( $message );
3452  $attrs = [
3453  'target' => 'helpwindow',
3454  'href' => $edithelpurl,
3455  ];
3456  $edithelp = Html::linkButton( wfMessage( 'edithelp' )->text(),
3457  $attrs, [ 'mw-ui-quiet' ] ) .
3458  wfMessage( 'word-separator' )->escaped() .
3459  wfMessage( 'newwindow' )->parse();
3460 
3461  $wgOut->addHTML( " <span class='cancelLink'>{$cancel}</span>\n" );
3462  $wgOut->addHTML( " <span class='editHelp'>{$edithelp}</span>\n" );
3463  $wgOut->addHTML( "</div><!-- editButtons -->\n" );
3464 
3465  Hooks::run( 'EditPage::showStandardInputs:options', [ $this, $wgOut, &$tabindex ] );
3466 
3467  $wgOut->addHTML( "</div><!-- editOptions -->\n" );
3468  }
3469 
3474  protected function showConflict() {
3475  global $wgOut;
3476 
3477  // Avoid PHP 7.1 warning of passing $this by reference
3478  $editPage = $this;
3479  if ( Hooks::run( 'EditPageBeforeConflictDiff', [ &$editPage, &$wgOut ] ) ) {
3480  $stats = $wgOut->getContext()->getStats();
3481  $stats->increment( 'edit.failures.conflict' );
3482 
3483  $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
3484 
3485  $content1 = $this->toEditContent( $this->textbox1 );
3486  $content2 = $this->toEditContent( $this->textbox2 );
3487 
3488  $handler = ContentHandler::getForModelID( $this->contentModel );
3489  $de = $handler->createDifferenceEngine( $this->mArticle->getContext() );
3490  $de->setContent( $content2, $content1 );
3491  $de->showDiff(
3492  wfMessage( 'yourtext' )->parse(),
3493  wfMessage( 'storedversion' )->text()
3494  );
3495 
3496  $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
3497  $this->showTextbox2();
3498  }
3499  }
3500 
3504  public function getCancelLink() {
3505  $cancelParams = [];
3506  if ( !$this->isConflict && $this->oldid > 0 ) {
3507  $cancelParams['oldid'] = $this->oldid;
3508  }
3509  $attrs = [ 'id' => 'mw-editform-cancel' ];
3510 
3511  return Linker::linkKnown(
3512  $this->getContextTitle(),
3513  wfMessage( 'cancel' )->parse(),
3514  Html::buttonAttributes( $attrs, [ 'mw-ui-quiet' ] ),
3515  $cancelParams
3516  );
3517  }
3518 
3528  protected function getActionURL( Title $title ) {
3529  return $title->getLocalURL( [ 'action' => $this->action ] );
3530  }
3531 
3539  protected function wasDeletedSinceLastEdit() {
3540  if ( $this->deletedSinceEdit !== null ) {
3541  return $this->deletedSinceEdit;
3542  }
3543 
3544  $this->deletedSinceEdit = false;
3545 
3546  if ( !$this->mTitle->exists() && $this->mTitle->isDeletedQuick() ) {
3547  $this->lastDelete = $this->getLastDelete();
3548  if ( $this->lastDelete ) {
3549  $deleteTime = wfTimestamp( TS_MW, $this->lastDelete->log_timestamp );
3550  if ( $deleteTime > $this->starttime ) {
3551  $this->deletedSinceEdit = true;
3552  }
3553  }
3554  }
3555 
3556  return $this->deletedSinceEdit;
3557  }
3558 
3562  protected function getLastDelete() {
3563  $dbr = wfGetDB( DB_SLAVE );
3564  $data = $dbr->selectRow(
3565  [ 'logging', 'user' ],
3566  [
3567  'log_type',
3568  'log_action',
3569  'log_timestamp',
3570  'log_user',
3571  'log_namespace',
3572  'log_title',
3573  'log_comment',
3574  'log_params',
3575  'log_deleted',
3576  'user_name'
3577  ], [
3578  'log_namespace' => $this->mTitle->getNamespace(),
3579  'log_title' => $this->mTitle->getDBkey(),
3580  'log_type' => 'delete',
3581  'log_action' => 'delete',
3582  'user_id=log_user'
3583  ],
3584  __METHOD__,
3585  [ 'LIMIT' => 1, 'ORDER BY' => 'log_timestamp DESC' ]
3586  );
3587  // Quick paranoid permission checks...
3588  if ( is_object( $data ) ) {
3589  if ( $data->log_deleted & LogPage::DELETED_USER ) {
3590  $data->user_name = wfMessage( 'rev-deleted-user' )->escaped();
3591  }
3592 
3593  if ( $data->log_deleted & LogPage::DELETED_COMMENT ) {
3594  $data->log_comment = wfMessage( 'rev-deleted-comment' )->escaped();
3595  }
3596  }
3597 
3598  return $data;
3599  }
3600 
3606  function getPreviewText() {
3607  global $wgOut, $wgUser, $wgRawHtml, $wgLang;
3609 
3610  $stats = $wgOut->getContext()->getStats();
3611 
3612  if ( $wgRawHtml && !$this->mTokenOk ) {
3613  // Could be an offsite preview attempt. This is very unsafe if
3614  // HTML is enabled, as it could be an attack.
3615  $parsedNote = '';
3616  if ( $this->textbox1 !== '' ) {
3617  // Do not put big scary notice, if previewing the empty
3618  // string, which happens when you initially edit
3619  // a category page, due to automatic preview-on-open.
3620  $parsedNote = $wgOut->parse( "<div class='previewnote'>" .
3621  wfMessage( 'session_fail_preview_html' )->text() . "</div>", true, /* interface */true );
3622  }
3623  $stats->increment( 'edit.failures.session_loss' );
3624  return $parsedNote;
3625  }
3626 
3627  $note = '';
3628 
3629  try {
3630  $content = $this->toEditContent( $this->textbox1 );
3631 
3632  $previewHTML = '';
3633  if ( !Hooks::run(
3634  'AlternateEditPreview',
3635  [ $this, &$content, &$previewHTML, &$this->mParserOutput ] )
3636  ) {
3637  return $previewHTML;
3638  }
3639 
3640  # provide a anchor link to the editform
3641  $continueEditing = '<span class="mw-continue-editing">' .
3642  '[[#' . self::EDITFORM_ID . '|' . $wgLang->getArrow() . ' ' .
3643  wfMessage( 'continue-editing' )->text() . ']]</span>';
3644  if ( $this->mTriedSave && !$this->mTokenOk ) {
3645  if ( $this->mTokenOkExceptSuffix ) {
3646  $note = wfMessage( 'token_suffix_mismatch' )->plain();
3647  $stats->increment( 'edit.failures.bad_token' );
3648  } else {
3649  $note = wfMessage( 'session_fail_preview' )->plain();
3650  $stats->increment( 'edit.failures.session_loss' );
3651  }
3652  } elseif ( $this->incompleteForm ) {
3653  $note = wfMessage( 'edit_form_incomplete' )->plain();
3654  if ( $this->mTriedSave ) {
3655  $stats->increment( 'edit.failures.incomplete_form' );
3656  }
3657  } else {
3658  $note = wfMessage( 'previewnote' )->plain() . ' ' . $continueEditing;
3659  }
3660 
3661  $parserOptions = $this->page->makeParserOptions( $this->mArticle->getContext() );
3662  $parserOptions->setIsPreview( true );
3663  $parserOptions->setIsSectionPreview( !is_null( $this->section ) && $this->section !== '' );
3664 
3665  # don't parse non-wikitext pages, show message about preview
3666  if ( $this->mTitle->isCssJsSubpage() || $this->mTitle->isCssOrJsPage() ) {
3667  if ( $this->mTitle->isCssJsSubpage() ) {
3668  $level = 'user';
3669  } elseif ( $this->mTitle->isCssOrJsPage() ) {
3670  $level = 'site';
3671  } else {
3672  $level = false;
3673  }
3674 
3675  if ( $content->getModel() == CONTENT_MODEL_CSS ) {
3676  $format = 'css';
3677  if ( $level === 'user' && !$wgAllowUserCss ) {
3678  $format = false;
3679  }
3680  } elseif ( $content->getModel() == CONTENT_MODEL_JAVASCRIPT ) {
3681  $format = 'js';
3682  if ( $level === 'user' && !$wgAllowUserJs ) {
3683  $format = false;
3684  }
3685  } else {
3686  $format = false;
3687  }
3688 
3689  # Used messages to make sure grep find them:
3690  # Messages: usercsspreview, userjspreview, sitecsspreview, sitejspreview
3691  if ( $level && $format ) {
3692  $note = "<div id='mw-{$level}{$format}preview'>" .
3693  wfMessage( "{$level}{$format}preview" )->text() .
3694  ' ' . $continueEditing . "</div>";
3695  }
3696  }
3697 
3698  # If we're adding a comment, we need to show the
3699  # summary as the headline
3700  if ( $this->section === "new" && $this->summary !== "" ) {
3701  $content = $content->addSectionHeader( $this->summary );
3702  }
3703 
3704  $hook_args = [ $this, &$content ];
3705  ContentHandler::runLegacyHooks( 'EditPageGetPreviewText', $hook_args );
3706  Hooks::run( 'EditPageGetPreviewContent', $hook_args );
3707 
3708  $parserOptions->enableLimitReport();
3709 
3710  # For CSS/JS pages, we should have called the ShowRawCssJs hook here.
3711  # But it's now deprecated, so never mind
3712 
3713  $pstContent = $content->preSaveTransform( $this->mTitle, $wgUser, $parserOptions );
3714  $scopedCallback = $parserOptions->setupFakeRevision(
3715  $this->mTitle, $pstContent, $wgUser );
3716  $parserOutput = $pstContent->getParserOutput( $this->mTitle, null, $parserOptions );
3717 
3718  # Try to stash the edit for the final submission step
3719  # @todo: different date format preferences cause cache misses
3721  $this->getArticle(), $content, $pstContent,
3722  $parserOutput, $parserOptions, $parserOptions, wfTimestampNow()
3723  );
3724 
3725  $parserOutput->setEditSectionTokens( false ); // no section edit links
3726  $previewHTML = $parserOutput->getText();
3727  $this->mParserOutput = $parserOutput;
3728  $wgOut->addParserOutputMetadata( $parserOutput );
3729 
3730  if ( count( $parserOutput->getWarnings() ) ) {
3731  $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
3732  }
3733 
3734  ScopedCallback::consume( $scopedCallback );
3735  } catch ( MWContentSerializationException $ex ) {
3736  $m = wfMessage(
3737  'content-failed-to-parse',
3738  $this->contentModel,
3739  $this->contentFormat,
3740  $ex->getMessage()
3741  );
3742  $note .= "\n\n" . $m->parse();
3743  $previewHTML = '';
3744  }
3745 
3746  if ( $this->isConflict ) {
3747  $conflict = '<h2 id="mw-previewconflict">'
3748  . wfMessage( 'previewconflict' )->escaped() . "</h2>\n";
3749  } else {
3750  $conflict = '<hr />';
3751  }
3752 
3753  $previewhead = "<div class='previewnote'>\n" .
3754  '<h2 id="mw-previewheader">' . wfMessage( 'preview' )->escaped() . "</h2>" .
3755  $wgOut->parse( $note, true, /* interface */true ) . $conflict . "</div>\n";
3756 
3757  $pageViewLang = $this->mTitle->getPageViewLanguage();
3758  $attribs = [ 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(),
3759  'class' => 'mw-content-' . $pageViewLang->getDir() ];
3760  $previewHTML = Html::rawElement( 'div', $attribs, $previewHTML );
3761 
3762  return $previewhead . $previewHTML . $this->previewTextAfterContent;
3763  }
3764 
3768  function getTemplates() {
3769  if ( $this->preview || $this->section != '' ) {
3770  $templates = [];
3771  if ( !isset( $this->mParserOutput ) ) {
3772  return $templates;
3773  }
3774  foreach ( $this->mParserOutput->getTemplates() as $ns => $template ) {
3775  foreach ( array_keys( $template ) as $dbk ) {
3776  $templates[] = Title::makeTitle( $ns, $dbk );
3777  }
3778  }
3779  return $templates;
3780  } else {
3781  return $this->mTitle->getTemplateLinksFrom();
3782  }
3783  }
3784 
3792  static function getEditToolbar( $title = null ) {
3795 
3796  $imagesAvailable = $wgEnableUploads || count( $wgForeignFileRepos );
3797  $showSignature = true;
3798  if ( $title ) {
3799  $showSignature = MWNamespace::wantSignatures( $title->getNamespace() );
3800  }
3801 
3811  $toolarray = [
3812  [
3813  'id' => 'mw-editbutton-bold',
3814  'open' => '\'\'\'',
3815  'close' => '\'\'\'',
3816  'sample' => wfMessage( 'bold_sample' )->text(),
3817  'tip' => wfMessage( 'bold_tip' )->text(),
3818  ],
3819  [
3820  'id' => 'mw-editbutton-italic',
3821  'open' => '\'\'',
3822  'close' => '\'\'',
3823  'sample' => wfMessage( 'italic_sample' )->text(),
3824  'tip' => wfMessage( 'italic_tip' )->text(),
3825  ],
3826  [
3827  'id' => 'mw-editbutton-link',
3828  'open' => '[[',
3829  'close' => ']]',
3830  'sample' => wfMessage( 'link_sample' )->text(),
3831  'tip' => wfMessage( 'link_tip' )->text(),
3832  ],
3833  [
3834  'id' => 'mw-editbutton-extlink',
3835  'open' => '[',
3836  'close' => ']',
3837  'sample' => wfMessage( 'extlink_sample' )->text(),
3838  'tip' => wfMessage( 'extlink_tip' )->text(),
3839  ],
3840  [
3841  'id' => 'mw-editbutton-headline',
3842  'open' => "\n== ",
3843  'close' => " ==\n",
3844  'sample' => wfMessage( 'headline_sample' )->text(),
3845  'tip' => wfMessage( 'headline_tip' )->text(),
3846  ],
3847  $imagesAvailable ? [
3848  'id' => 'mw-editbutton-image',
3849  'open' => '[[' . $wgContLang->getNsText( NS_FILE ) . ':',
3850  'close' => ']]',
3851  'sample' => wfMessage( 'image_sample' )->text(),
3852  'tip' => wfMessage( 'image_tip' )->text(),
3853  ] : false,
3854  $imagesAvailable ? [
3855  'id' => 'mw-editbutton-media',
3856  'open' => '[[' . $wgContLang->getNsText( NS_MEDIA ) . ':',
3857  'close' => ']]',
3858  'sample' => wfMessage( 'media_sample' )->text(),
3859  'tip' => wfMessage( 'media_tip' )->text(),
3860  ] : false,
3861  [
3862  'id' => 'mw-editbutton-nowiki',
3863  'open' => "<nowiki>",
3864  'close' => "</nowiki>",
3865  'sample' => wfMessage( 'nowiki_sample' )->text(),
3866  'tip' => wfMessage( 'nowiki_tip' )->text(),
3867  ],
3868  $showSignature ? [
3869  'id' => 'mw-editbutton-signature',
3870  'open' => '--~~~~',
3871  'close' => '',
3872  'sample' => '',
3873  'tip' => wfMessage( 'sig_tip' )->text(),
3874  ] : false,
3875  [
3876  'id' => 'mw-editbutton-hr',
3877  'open' => "\n----\n",
3878  'close' => '',
3879  'sample' => '',
3880  'tip' => wfMessage( 'hr_tip' )->text(),
3881  ]
3882  ];
3883 
3884  $script = 'mw.loader.using("mediawiki.toolbar", function () {';
3885  foreach ( $toolarray as $tool ) {
3886  if ( !$tool ) {
3887  continue;
3888  }
3889 
3890  $params = [
3891  // Images are defined in ResourceLoaderEditToolbarModule
3892  false,
3893  // Note that we use the tip both for the ALT tag and the TITLE tag of the image.
3894  // Older browsers show a "speedtip" type message only for ALT.
3895  // Ideally these should be different, realistically they
3896  // probably don't need to be.
3897  $tool['tip'],
3898  $tool['open'],
3899  $tool['close'],
3900  $tool['sample'],
3901  $tool['id'],
3902  ];
3903 
3904  $script .= Xml::encodeJsCall(
3905  'mw.toolbar.addButton',
3906  $params,
3908  );
3909  }
3910 
3911  $script .= '});';
3912  $wgOut->addScript( ResourceLoader::makeInlineScript( $script ) );
3913 
3914  $toolbar = '<div id="toolbar"></div>';
3915 
3916  Hooks::run( 'EditPageBeforeEditToolbar', [ &$toolbar ] );
3917 
3918  return $toolbar;
3919  }
3920 
3931  public function getCheckboxes( &$tabindex, $checked ) {
3933 
3934  $checkboxes = [];
3935 
3936  // don't show the minor edit checkbox if it's a new page or section
3937  if ( !$this->isNew ) {
3938  $checkboxes['minor'] = '';
3939  $minorLabel = wfMessage( 'minoredit' )->parse();
3940  if ( $wgUser->isAllowed( 'minoredit' ) ) {
3941  $attribs = [
3942  'tabindex' => ++$tabindex,
3943  'accesskey' => wfMessage( 'accesskey-minoredit' )->text(),
3944  'id' => 'wpMinoredit',
3945  ];
3946  $minorEditHtml =
3947  Xml::check( 'wpMinoredit', $checked['minor'], $attribs ) .
3948  "&#160;<label for='wpMinoredit' id='mw-editpage-minoredit'" .
3949  Xml::expandAttributes( [ 'title' => Linker::titleAttrib( 'minoredit', 'withaccess' ) ] ) .
3950  ">{$minorLabel}</label>";
3951 
3952  if ( $wgUseMediaWikiUIEverywhere ) {
3953  $checkboxes['minor'] = Html::openElement( 'div', [ 'class' => 'mw-ui-checkbox' ] ) .
3954  $minorEditHtml .
3955  Html::closeElement( 'div' );
3956  } else {
3957  $checkboxes['minor'] = $minorEditHtml;
3958  }
3959  }
3960  }
3961 
3962  $watchLabel = wfMessage( 'watchthis' )->parse();
3963  $checkboxes['watch'] = '';
3964  if ( $wgUser->isLoggedIn() ) {
3965  $attribs = [
3966  'tabindex' => ++$tabindex,
3967  'accesskey' => wfMessage( 'accesskey-watch' )->text(),
3968  'id' => 'wpWatchthis',
3969  ];
3970  $watchThisHtml =
3971  Xml::check( 'wpWatchthis', $checked['watch'], $attribs ) .
3972  "&#160;<label for='wpWatchthis' id='mw-editpage-watch'" .
3973  Xml::expandAttributes( [ 'title' => Linker::titleAttrib( 'watch', 'withaccess' ) ] ) .
3974  ">{$watchLabel}</label>";
3975  if ( $wgUseMediaWikiUIEverywhere ) {
3976  $checkboxes['watch'] = Html::openElement( 'div', [ 'class' => 'mw-ui-checkbox' ] ) .
3977  $watchThisHtml .
3978  Html::closeElement( 'div' );
3979  } else {
3980  $checkboxes['watch'] = $watchThisHtml;
3981  }
3982  }
3983 
3984  // Avoid PHP 7.1 warning of passing $this by reference
3985  $editPage = $this;
3986  Hooks::run( 'EditPageBeforeEditChecks', [ &$editPage, &$checkboxes, &$tabindex ] );
3987  return $checkboxes;
3988  }
3989 
3998  public function getEditButtons( &$tabindex ) {
3999  $buttons = [];
4000 
4001  $attribs = [
4002  'id' => 'wpSave',
4003  'name' => 'wpSave',
4004  'tabindex' => ++$tabindex,
4005  ] + Linker::tooltipAndAccesskeyAttribs( 'save' );
4006  $buttons['save'] = Html::submitButton( wfMessage( 'savearticle' )->text(),
4007  $attribs, [ 'mw-ui-constructive' ] );
4008 
4009  ++$tabindex; // use the same for preview and live preview
4010  $attribs = [
4011  'id' => 'wpPreview',
4012  'name' => 'wpPreview',
4013  'tabindex' => $tabindex,
4014  ] + Linker::tooltipAndAccesskeyAttribs( 'preview' );
4015  $buttons['preview'] = Html::submitButton( wfMessage( 'showpreview' )->text(),
4016  $attribs );
4017  $buttons['live'] = '';
4018 
4019  $attribs = [
4020  'id' => 'wpDiff',
4021  'name' => 'wpDiff',
4022  'tabindex' => ++$tabindex,
4023  ] + Linker::tooltipAndAccesskeyAttribs( 'diff' );
4024  $buttons['diff'] = Html::submitButton( wfMessage( 'showdiff' )->text(),
4025  $attribs );
4026 
4027  // Avoid PHP 7.1 warning of passing $this by reference
4028  $editPage = $this;
4029  Hooks::run( 'EditPageBeforeEditButtons', [ &$editPage, &$buttons, &$tabindex ] );
4030  return $buttons;
4031  }
4032 
4037  function noSuchSectionPage() {
4038  global $wgOut;
4039 
4040  $wgOut->prepareErrorPage( wfMessage( 'nosuchsectiontitle' ) );
4041 
4042  $res = wfMessage( 'nosuchsectiontext', $this->section )->parseAsBlock();
4043  // Avoid PHP 7.1 warning of passing $this by reference
4044  $editPage = $this;
4045  Hooks::run( 'EditPageNoSuchSection', [ &$editPage, &$res ] );
4046  $wgOut->addHTML( $res );
4047 
4048  $wgOut->returnToMain( false, $this->mTitle );
4049  }
4050 
4056  public function spamPageWithContent( $match = false ) {
4058  $this->textbox2 = $this->textbox1;
4059 
4060  if ( is_array( $match ) ) {
4061  $match = $wgLang->listToText( $match );
4062  }
4063  $wgOut->prepareErrorPage( wfMessage( 'spamprotectiontitle' ) );
4064 
4065  $wgOut->addHTML( '<div id="spamprotected">' );
4066  $wgOut->addWikiMsg( 'spamprotectiontext' );
4067  if ( $match ) {
4068  $wgOut->addWikiMsg( 'spamprotectionmatch', wfEscapeWikiText( $match ) );
4069  }
4070  $wgOut->addHTML( '</div>' );
4071 
4072  $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
4073  $this->showDiff();
4074 
4075  $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
4076  $this->showTextbox2();
4077 
4078  $wgOut->addReturnTo( $this->getContextTitle(), [ 'action' => 'edit' ] );
4079  }
4080 
4087  private function checkUnicodeCompliantBrowser() {
4089 
4090  $currentbrowser = $wgRequest->getHeader( 'User-Agent' );
4091  if ( $currentbrowser === false ) {
4092  // No User-Agent header sent? Trust it by default...
4093  return true;
4094  }
4095 
4096  foreach ( $wgBrowserBlackList as $browser ) {
4097  if ( preg_match( $browser, $currentbrowser ) ) {
4098  return false;
4099  }
4100  }
4101  return true;
4102  }
4103 
4112  protected function safeUnicodeInput( $request, $field ) {
4113  $text = rtrim( $request->getText( $field ) );
4114  return $request->getBool( 'safemode' )
4115  ? $this->unmakeSafe( $text )
4116  : $text;
4117  }
4118 
4126  protected function safeUnicodeOutput( $text ) {
4128  $codedText = $wgContLang->recodeForEdit( $text );
4129  return $this->checkUnicodeCompliantBrowser()
4130  ? $codedText
4131  : $this->makeSafe( $codedText );
4132  }
4133 
4146  private function makeSafe( $invalue ) {
4147  // Armor existing references for reversibility.
4148  $invalue = strtr( $invalue, [ "&#x" => "&#x0" ] );
4149 
4150  $bytesleft = 0;
4151  $result = "";
4152  $working = 0;
4153  $valueLength = strlen( $invalue );
4154  for ( $i = 0; $i < $valueLength; $i++ ) {
4155  $bytevalue = ord( $invalue[$i] );
4156  if ( $bytevalue <= 0x7F ) { // 0xxx xxxx
4157  $result .= chr( $bytevalue );
4158  $bytesleft = 0;
4159  } elseif ( $bytevalue <= 0xBF ) { // 10xx xxxx
4160  $working = $working << 6;
4161  $working += ( $bytevalue & 0x3F );
4162  $bytesleft--;
4163  if ( $bytesleft <= 0 ) {
4164  $result .= "&#x" . strtoupper( dechex( $working ) ) . ";";
4165  }
4166  } elseif ( $bytevalue <= 0xDF ) { // 110x xxxx
4167  $working = $bytevalue & 0x1F;
4168  $bytesleft = 1;
4169  } elseif ( $bytevalue <= 0xEF ) { // 1110 xxxx
4170  $working = $bytevalue & 0x0F;
4171  $bytesleft = 2;
4172  } else { // 1111 0xxx
4173  $working = $bytevalue & 0x07;
4174  $bytesleft = 3;
4175  }
4176  }
4177  return $result;
4178  }
4179 
4188  private function unmakeSafe( $invalue ) {
4189  $result = "";
4190  $valueLength = strlen( $invalue );
4191  for ( $i = 0; $i < $valueLength; $i++ ) {
4192  if ( ( substr( $invalue, $i, 3 ) == "&#x" ) && ( $invalue[$i + 3] != '0' ) ) {
4193  $i += 3;
4194  $hexstring = "";
4195  do {
4196  $hexstring .= $invalue[$i];
4197  $i++;
4198  } while ( ctype_xdigit( $invalue[$i] ) && ( $i < strlen( $invalue ) ) );
4199 
4200  // Do some sanity checks. These aren't needed for reversibility,
4201  // but should help keep the breakage down if the editor
4202  // breaks one of the entities whilst editing.
4203  if ( ( substr( $invalue, $i, 1 ) == ";" ) && ( strlen( $hexstring ) <= 6 ) ) {
4204  $codepoint = hexdec( $hexstring );
4205  $result .= UtfNormal\Utils::codepointToUtf8( $codepoint );
4206  } else {
4207  $result .= "&#x" . $hexstring . substr( $invalue, $i, 1 );
4208  }
4209  } else {
4210  $result .= substr( $invalue, $i, 1 );
4211  }
4212  }
4213  // reverse the transform that we made for reversibility reasons.
4214  return strtr( $result, [ "&#x0" => "&#x" ] );
4215  }
4216 }
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:4146
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:1422
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:2325
$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:3091
bool $allowBlankSummary
Definition: EditPage.php:271
getPreviewText()
Get the rendered text for previewing.
Definition: EditPage.php:3606
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:1936
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:4112
showTextbox2()
Definition: EditPage.php:3145
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:3305
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:3322
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:3998
$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:1422
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:3381
spamPageWithContent($match=false)
Show "your edit contains spam" page with your diff and text.
Definition: EditPage.php:4056
it s the revision text itself In either if gzip is the revision text is gzipped $flags
Definition: hooks.txt:2552
static getCopyrightWarning($title, $format= 'plain')
Get the copyright warning, by default returns wikitext.
Definition: EditPage.php:3357
bool $missingComment
Definition: EditPage.php:265
const EDIT_MINOR
Definition: Defines.php:182
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:181
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:766
showFormBeforeText()
Definition: EditPage.php:3050
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:2337
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:469
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:2041
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:1800
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:766
safeUnicodeOutput($text)
Filter an output field through a Unicode armoring process if it is going to an old browser with known...
Definition: EditPage.php:4126
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:1802
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:657
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:3768
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