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