MediaWiki REL1_35
EditPage.php
Go to the documentation of this file.
1<?php
28use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
36use OOUI\CheckboxInputWidget;
37use OOUI\DropdownInputWidget;
38use OOUI\FieldLayout;
40use Wikimedia\ScopedCallback;
41
62class EditPage implements IEditObject {
64 use ProtectedHookAccessorTrait;
65
69 public const UNICODE_CHECK = 'ℳ𝒲β™₯π“Šπ“ƒπ’Ύπ’Έβ„΄π’Ήβ„―';
70
74 public const EDITFORM_ID = 'editform';
75
80 public const POST_EDIT_COOKIE_KEY_PREFIX = 'PostEditRevision';
81
95 public const POST_EDIT_COOKIE_DURATION = 1200;
96
101 public $mArticle;
103 private $page;
104
109 public $mTitle;
110
112 private $mContextTitle = null;
113
115 public $action = 'submit';
116
121 public $isConflict = false;
122
124 public $isNew = false;
125
131
133 public $formtype;
134
140
146
151 public $mTokenOk = false;
152
157 public $mTokenOkExceptSuffix = false;
158
163 public $mTriedSave = false;
164
169 public $incompleteForm = false;
170
175 public $tooBig = false;
176
181 public $missingComment = false;
182
187 public $missingSummary = false;
188
193 public $allowBlankSummary = false;
194
196 protected $blankArticle = false;
197
199 protected $allowBlankArticle = false;
200
202 protected $selfRedirect = false;
203
205 protected $allowSelfRedirect = false;
206
211 public $autoSumm = '';
212
214 private $hookError = '';
215
221
226 public $hasPresetSummary = false;
227
236 protected $mBaseRevision = false;
237
245
247 public $mShowSummaryField = true;
248
249 # Form values
250
252 public $save = false;
253
255 public $preview = false;
256
258 public $diff = false;
259
264 public $minoredit = false;
265
270 public $watchthis = false;
271
273 private $watchlistExpiryEnabled = false;
274
277
280
285 public $recreate = false;
286
290 public $textbox1 = '';
291
293 public $textbox2 = '';
294
296 public $summary = '';
297
303 public $nosummary = false;
304
309 public $edittime = '';
310
322 private $editRevId = null;
323
325 public $section = '';
326
328 public $sectiontitle = '';
329
333 public $starttime = '';
334
340 public $oldid = 0;
341
349 public $parentRevId = 0;
350
355 public $editintro = '';
356
361 public $scrolltop = null;
362
367 public $markAsBot = true;
368
371
373 public $contentFormat = null;
374
376 private $changeTags = null;
377
378 # Placeholders for text injection by hooks (must be HTML)
379 # extensions should take care to _append_ to the present value
380
382 public $editFormPageTop = '';
383 public $editFormTextTop = '';
390 public $mPreloadContent = null;
391
392 /* $didSave should be set to true whenever an article was successfully altered. */
393 public $didSave = false;
394 public $undidRev = 0;
395 public $undoAfter = 0;
396
397 public $suppressIntro = false;
398
400 protected $edit;
401
403 protected $contentLength = false;
404
408 private $enableApiEditOverride = false;
409
413 protected $context;
414
418 private $isOldRev = false;
419
424
431
436
441
446
451
456 public function __construct( Article $article ) {
457 $this->mArticle = $article;
458 $this->page = $article->getPage(); // model object
459 $this->mTitle = $article->getTitle();
460
461 // Make sure the local context is in sync with other member variables.
462 // Particularly make sure everything is using the same WikiPage instance.
463 // This should probably be the case in Article as well, but it's
464 // particularly important for EditPage, to make use of the in-place caching
465 // facility in WikiPage::prepareContentForEdit.
466 $this->context = new DerivativeContext( $article->getContext() );
467 $this->context->setWikiPage( $this->page );
468 $this->context->setTitle( $this->mTitle );
469
470 $this->contentModel = $this->mTitle->getContentModel();
471
472 $services = MediaWikiServices::getInstance();
473 $this->contentHandlerFactory = $services->getContentHandlerFactory();
474 $this->contentFormat = $this->contentHandlerFactory
475 ->getContentHandler( $this->contentModel )
476 ->getDefaultFormat();
477 $this->editConflictHelperFactory = [ $this, 'newTextConflictHelper' ];
478 $this->permManager = $services->getPermissionManager();
479 $this->revisionStore = $services->getRevisionStore();
480 $this->watchlistExpiryEnabled = $this->getContext()->getConfig() instanceof Config
481 && $this->getContext()->getConfig()->get( 'WatchlistExpiry' );
482 $this->watchedItemStore = $services->getWatchedItemStore();
483
484 $this->deprecatePublicProperty( 'mBaseRevision', '1.35', __CLASS__ );
485 }
486
490 public function getArticle() {
491 return $this->mArticle;
492 }
493
498 public function getContext() {
499 return $this->context;
500 }
501
506 public function getTitle() {
507 return $this->mTitle;
508 }
509
515 public function setContextTitle( $title ) {
516 $this->mContextTitle = $title;
517 }
518
527 public function getContextTitle() {
528 if ( $this->mContextTitle === null ) {
529 wfDeprecated( get_class( $this ) . '::getContextTitle called with no title set', '1.32' );
530 global $wgTitle;
531 return $wgTitle;
532 } else {
534 }
535 }
536
544 public function isSupportedContentModel( $modelId ) {
545 return $this->enableApiEditOverride === true ||
546 $this->contentHandlerFactory->getContentHandler( $modelId )->supportsDirectEditing();
547 }
548
555 public function setApiEditOverride( $enableOverride ) {
556 $this->enableApiEditOverride = $enableOverride;
557 }
558
570 public function edit() {
571 // Allow extensions to modify/prevent this form or submission
572 if ( !$this->getHookRunner()->onAlternateEdit( $this ) ) {
573 return;
574 }
575
576 wfDebug( __METHOD__ . ": enter" );
577
578 $request = $this->context->getRequest();
579 // If they used redlink=1 and the page exists, redirect to the main article
580 if ( $request->getBool( 'redlink' ) && $this->mTitle->exists() ) {
581 $this->context->getOutput()->redirect( $this->mTitle->getFullURL() );
582 return;
583 }
584
585 $this->importFormData( $request );
586 $this->firsttime = false;
587
588 if ( wfReadOnly() && $this->save ) {
589 // Force preview
590 $this->save = false;
591 $this->preview = true;
592 }
593
594 if ( $this->save ) {
595 $this->formtype = 'save';
596 } elseif ( $this->preview ) {
597 $this->formtype = 'preview';
598 } elseif ( $this->diff ) {
599 $this->formtype = 'diff';
600 } else { # First time through
601 $this->firsttime = true;
602 if ( $this->previewOnOpen() ) {
603 $this->formtype = 'preview';
604 } else {
605 $this->formtype = 'initial';
606 }
607 }
608
609 $permErrors = $this->getEditPermissionErrors(
610 $this->save ? PermissionManager::RIGOR_SECURE : PermissionManager::RIGOR_FULL
611 );
612 if ( $permErrors ) {
613 wfDebug( __METHOD__ . ": User can't edit" );
614
615 if ( $this->context->getUser()->getBlock() ) {
616 // Auto-block user's IP if the account was "hard" blocked
617 if ( !wfReadOnly() ) {
618 DeferredUpdates::addCallableUpdate( function () {
619 $this->context->getUser()->spreadAnyEditBlock();
620 } );
621 }
622 }
623 $this->displayPermissionsError( $permErrors );
624
625 return;
626 }
627
628 $revRecord = $this->mArticle->fetchRevisionRecord();
629 // Disallow editing revisions with content models different from the current one
630 // Undo edits being an exception in order to allow reverting content model changes.
631 $revContentModel = $revRecord ?
632 $revRecord->getSlot( SlotRecord::MAIN, RevisionRecord::RAW )->getModel() :
633 false;
634 if ( $revContentModel && $revContentModel !== $this->contentModel ) {
635 $prevRev = null;
636 if ( $this->undidRev ) {
637 $undidRevRecord = $this->revisionStore
638 ->getRevisionById( $this->undidRev );
639 $prevRevRecord = $undidRevRecord ?
640 $this->revisionStore->getPreviousRevision( $undidRevRecord ) :
641 null;
642
643 $prevContentModel = $prevRevRecord ?
644 $prevRevRecord
645 ->getSlot( SlotRecord::MAIN, RevisionRecord::RAW )
646 ->getModel() :
647 '';
648 }
649
650 if ( !$this->undidRev
651 || !$prevRevRecord
652 || $prevContentModel !== $this->contentModel
653 ) {
655 $this->getContentObject(),
656 $this->context->msg(
657 'contentmodelediterror',
658 $revContentModel,
659 $this->contentModel
660 )->plain()
661 );
662 return;
663 }
664 }
665
666 $this->isConflict = false;
667
668 # Show applicable editing introductions
669 if ( $this->formtype == 'initial' || $this->firsttime ) {
670 $this->showIntro();
671 }
672
673 # Attempt submission here. This will check for edit conflicts,
674 # and redundantly check for locked database, blocked IPs, etc.
675 # that edit() already checked just in case someone tries to sneak
676 # in the back door with a hand-edited submission URL.
677
678 if ( $this->formtype == 'save' ) {
679 $resultDetails = null;
680 $status = $this->attemptSave( $resultDetails );
681 if ( !$this->handleStatus( $status, $resultDetails ) ) {
682 return;
683 }
684 }
685
686 # First time through: get contents, set time for conflict
687 # checking, etc.
688 if ( $this->formtype == 'initial' || $this->firsttime ) {
689 if ( $this->initialiseForm() === false ) {
690 return;
691 }
692
693 if ( !$this->mTitle->getArticleID() ) {
694 $this->getHookRunner()->onEditFormPreloadText( $this->textbox1, $this->mTitle );
695 } else {
696 $this->getHookRunner()->onEditFormInitialText( $this );
697 }
698
699 }
700
701 $this->showEditForm();
702 }
703
708 protected function getEditPermissionErrors( $rigor = PermissionManager::RIGOR_SECURE ) {
709 $user = $this->context->getUser();
710 $permErrors = $this->permManager->getPermissionErrors(
711 'edit',
712 $user,
713 $this->mTitle,
714 $rigor
715 );
716 # Can this title be created?
717 if ( !$this->mTitle->exists() ) {
718 $permErrors = array_merge(
719 $permErrors,
721 $this->permManager->getPermissionErrors(
722 'create',
723 $user,
724 $this->mTitle,
725 $rigor
726 ),
727 $permErrors
728 )
729 );
730 }
731 # Ignore some permissions errors when a user is just previewing/viewing diffs
732 $remove = [];
733 foreach ( $permErrors as $error ) {
734 if ( ( $this->preview || $this->diff )
735 && (
736 $error[0] == 'blockedtext' ||
737 $error[0] == 'autoblockedtext' ||
738 $error[0] == 'systemblockedtext'
739 )
740 ) {
741 $remove[] = $error;
742 }
743 }
744 $permErrors = wfArrayDiff2( $permErrors, $remove );
745
746 return $permErrors;
747 }
748
761 protected function displayPermissionsError( array $permErrors ) {
762 $out = $this->context->getOutput();
763 if ( $this->context->getRequest()->getBool( 'redlink' ) ) {
764 // The edit page was reached via a red link.
765 // Redirect to the article page and let them click the edit tab if
766 // they really want a permission error.
767 $out->redirect( $this->mTitle->getFullURL() );
768 return;
769 }
770
771 $content = $this->getContentObject();
772
773 # Use the normal message if there's nothing to display
774 if ( $this->firsttime && ( !$content || $content->isEmpty() ) ) {
775 $action = $this->mTitle->exists() ? 'edit' :
776 ( $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage' );
777 throw new PermissionsError( $action, $permErrors );
778 }
779
781 $content,
782 $out->formatPermissionsErrorMessage( $permErrors, 'edit' )
783 );
784 }
785
791 protected function displayViewSourcePage( Content $content, $errorMessage = '' ) {
792 $out = $this->context->getOutput();
793 $this->getHookRunner()->onEditPage__showReadOnlyForm_initial( $this, $out );
794
795 $out->setRobotPolicy( 'noindex,nofollow' );
796 $out->setPageTitle( $this->context->msg(
797 'viewsource-title',
798 $this->getContextTitle()->getPrefixedText()
799 ) );
800 $out->addBacklinkSubtitle( $this->getContextTitle() );
801 $out->addHTML( $this->editFormPageTop );
802 $out->addHTML( $this->editFormTextTop );
803
804 if ( $errorMessage !== '' ) {
805 $out->addWikiTextAsInterface( $errorMessage );
806 $out->addHTML( "<hr />\n" );
807 }
808
809 # If the user made changes, preserve them when showing the markup
810 # (This happens when a user is blocked during edit, for instance)
811 if ( !$this->firsttime ) {
812 $text = $this->textbox1;
813 $out->addWikiMsg( 'viewyourtext' );
814 } else {
815 try {
816 $text = $this->toEditText( $content );
817 } catch ( MWException $e ) {
818 # Serialize using the default format if the content model is not supported
819 # (e.g. for an old revision with a different model)
820 $text = $content->serialize();
821 }
822 $out->addWikiMsg( 'viewsourcetext' );
823 }
824
825 $out->addHTML( $this->editFormTextBeforeContent );
826 $this->showTextbox( $text, 'wpTextbox1', [ 'readonly' ] );
827 $out->addHTML( $this->editFormTextAfterContent );
828
829 $out->addHTML( $this->makeTemplatesOnThisPageList( $this->getTemplates() ) );
830
831 $out->addModules( 'mediawiki.action.edit.collapsibleFooter' );
832
833 $out->addHTML( $this->editFormTextBottom );
834 if ( $this->mTitle->exists() ) {
835 $out->returnToMain( null, $this->mTitle );
836 }
837 }
838
844 protected function previewOnOpen() {
845 $config = $this->context->getConfig();
846 $previewOnOpenNamespaces = $config->get( 'PreviewOnOpenNamespaces' );
847 $request = $this->context->getRequest();
848 if ( $config->get( 'RawHtml' ) ) {
849 // If raw HTML is enabled, disable preview on open
850 // since it has to be posted with a token for
851 // security reasons
852 return false;
853 }
854 if ( $request->getVal( 'preview' ) == 'yes' ) {
855 // Explicit override from request
856 return true;
857 } elseif ( $request->getVal( 'preview' ) == 'no' ) {
858 // Explicit override from request
859 return false;
860 } elseif ( $this->section == 'new' ) {
861 // Nothing *to* preview for new sections
862 return false;
863 } elseif ( ( $request->getCheck( 'preload' ) || $this->mTitle->exists() )
864 && $this->context->getUser()->getOption( 'previewonfirst' )
865 ) {
866 // Standard preference behavior
867 return true;
868 } elseif ( !$this->mTitle->exists()
869 && isset( $previewOnOpenNamespaces[$this->mTitle->getNamespace()] )
870 && $previewOnOpenNamespaces[$this->mTitle->getNamespace()]
871 ) {
872 // Categories are special
873 return true;
874 } else {
875 return false;
876 }
877 }
878
885 protected function isWrongCaseUserConfigPage() {
886 if ( $this->mTitle->isUserConfigPage() ) {
887 $name = $this->mTitle->getSkinFromConfigSubpage();
888 $skins = array_merge(
889 array_keys( Skin::getSkinNames() ),
890 [ 'common' ]
891 );
892 return !in_array( $name, $skins )
893 && in_array( strtolower( $name ), $skins );
894 } else {
895 return false;
896 }
897 }
898
906 protected function isSectionEditSupported() {
907 return $this->contentHandlerFactory
908 ->getContentHandler( $this->mTitle->getContentModel() )
909 ->supportsSections();
910 }
911
917 public function importFormData( &$request ) {
918 # Section edit can come from either the form or a link
919 $this->section = $request->getVal( 'wpSection', $request->getVal( 'section' ) );
920
921 if ( $this->section !== null && $this->section !== '' && !$this->isSectionEditSupported() ) {
922 throw new ErrorPageError( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
923 }
924
925 $this->isNew = !$this->mTitle->exists() || $this->section == 'new';
926
927 if ( $request->wasPosted() ) {
928 # These fields need to be checked for encoding.
929 # Also remove trailing whitespace, but don't remove _initial_
930 # whitespace from the text boxes. This may be significant formatting.
931 $this->textbox1 = rtrim( $request->getText( 'wpTextbox1' ) );
932 if ( !$request->getCheck( 'wpTextbox2' ) ) {
933 // Skip this if wpTextbox2 has input, it indicates that we came
934 // from a conflict page with raw page text, not a custom form
935 // modified by subclasses
936 $textbox1 = $this->importContentFormData( $request );
937 if ( $textbox1 !== null ) {
938 $this->textbox1 = $textbox1;
939 }
940 }
941
942 $this->unicodeCheck = $request->getText( 'wpUnicodeCheck' );
943
944 $this->summary = $request->getText( 'wpSummary' );
945
946 # If the summary consists of a heading, e.g. '==Foobar==', extract the title from the
947 # header syntax, e.g. 'Foobar'. This is mainly an issue when we are using wpSummary for
948 # section titles.
949 $this->summary = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->summary );
950
951 # Treat sectiontitle the same way as summary.
952 # Note that wpSectionTitle is not yet a part of the actual edit form, as wpSummary is
953 # currently doing double duty as both edit summary and section title. Right now this
954 # is just to allow API edits to work around this limitation, but this should be
955 # incorporated into the actual edit form when EditPage is rewritten (T20654, T28312).
956 $this->sectiontitle = $request->getText( 'wpSectionTitle' );
957 $this->sectiontitle = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->sectiontitle );
958
959 $this->edittime = $request->getVal( 'wpEdittime' );
960 $this->editRevId = $request->getIntOrNull( 'editRevId' );
961 $this->starttime = $request->getVal( 'wpStarttime' );
962
963 $undidRev = $request->getInt( 'wpUndidRevision' );
964 if ( $undidRev ) {
965 $this->undidRev = $undidRev;
966 }
967 $undoAfter = $request->getInt( 'wpUndoAfter' );
968 if ( $undoAfter ) {
969 $this->undoAfter = $undoAfter;
970 }
971
972 $this->scrolltop = $request->getIntOrNull( 'wpScrolltop' );
973
974 if ( $this->textbox1 === '' && !$request->getCheck( 'wpTextbox1' ) ) {
975 // wpTextbox1 field is missing, possibly due to being "too big"
976 // according to some filter rules such as Suhosin's setting for
977 // suhosin.request.max_value_length (d'oh)
978 $this->incompleteForm = true;
979 } else {
980 // If we receive the last parameter of the request, we can fairly
981 // claim the POST request has not been truncated.
982 $this->incompleteForm = !$request->getVal( 'wpUltimateParam' );
983 }
984 if ( $this->incompleteForm ) {
985 # If the form is incomplete, force to preview.
986 wfDebug( __METHOD__ . ": Form data appears to be incomplete" );
987 wfDebug( "POST DATA: " . var_export( $request->getPostValues(), true ) );
988 $this->preview = true;
989 } else {
990 $this->preview = $request->getCheck( 'wpPreview' );
991 $this->diff = $request->getCheck( 'wpDiff' );
992
993 // Remember whether a save was requested, so we can indicate
994 // if we forced preview due to session failure.
995 $this->mTriedSave = !$this->preview;
996
997 if ( $this->tokenOk( $request ) ) {
998 # Some browsers will not report any submit button
999 # if the user hits enter in the comment box.
1000 # The unmarked state will be assumed to be a save,
1001 # if the form seems otherwise complete.
1002 wfDebug( __METHOD__ . ": Passed token check." );
1003 } elseif ( $this->diff ) {
1004 # Failed token check, but only requested "Show Changes".
1005 wfDebug( __METHOD__ . ": Failed token check; Show Changes requested." );
1006 } else {
1007 # Page might be a hack attempt posted from
1008 # an external site. Preview instead of saving.
1009 wfDebug( __METHOD__ . ": Failed token check; forcing preview" );
1010 $this->preview = true;
1011 }
1012 }
1013 $this->save = !$this->preview && !$this->diff;
1014 if ( !$this->edittime || !preg_match( '/^\d{14}$/', $this->edittime ) ) {
1015 $this->edittime = null;
1016 }
1017
1018 if ( !$this->starttime || !preg_match( '/^\d{14}$/', $this->starttime ) ) {
1019 $this->starttime = null;
1020 }
1021
1022 $this->recreate = $request->getCheck( 'wpRecreate' );
1023
1024 $user = $this->getContext()->getUser();
1025
1026 $this->minoredit = $request->getCheck( 'wpMinoredit' );
1027 $this->watchthis = $request->getCheck( 'wpWatchthis' );
1028 $expiry = $request->getText( 'wpWatchlistExpiry' );
1029 if ( $this->watchlistExpiryEnabled && $expiry !== '' ) {
1030 // This parsing of the user-posted expiry is done for both preview and saving. This
1031 // is necessary because ApiEditPage uses preview when it saves (yuck!). Note that it
1032 // only works because the unnormalized value is retrieved again below in
1033 // getCheckboxesDefinitionForWatchlist().
1034 $expiry = ExpiryDef::normalizeExpiry( $expiry, TS_ISO_8601 );
1035 if ( $expiry !== false ) {
1036 $this->watchlistExpiry = $expiry;
1037 }
1038 }
1039
1040 # Don't force edit summaries when a user is editing their own user or talk page
1041 if ( ( $this->mTitle->mNamespace == NS_USER || $this->mTitle->mNamespace == NS_USER_TALK )
1042 && $this->mTitle->getText() == $user->getName()
1043 ) {
1044 $this->allowBlankSummary = true;
1045 } else {
1046 $this->allowBlankSummary = $request->getBool( 'wpIgnoreBlankSummary' )
1047 || !$user->getOption( 'forceeditsummary' );
1048 }
1049
1050 $this->autoSumm = $request->getText( 'wpAutoSummary' );
1051
1052 $this->allowBlankArticle = $request->getBool( 'wpIgnoreBlankArticle' );
1053 $this->allowSelfRedirect = $request->getBool( 'wpIgnoreSelfRedirect' );
1054
1055 $changeTags = $request->getVal( 'wpChangeTags' );
1056 if ( $changeTags === null || $changeTags === '' ) {
1057 $this->changeTags = [];
1058 } else {
1059 $this->changeTags = array_filter( array_map( 'trim', explode( ',',
1060 $changeTags ) ) );
1061 }
1062 } else {
1063 # Not a posted form? Start with nothing.
1064 wfDebug( __METHOD__ . ": Not a posted form." );
1065 $this->textbox1 = '';
1066 $this->summary = '';
1067 $this->sectiontitle = '';
1068 $this->edittime = '';
1069 $this->editRevId = null;
1070 $this->starttime = wfTimestampNow();
1071 $this->edit = false;
1072 $this->preview = false;
1073 $this->save = false;
1074 $this->diff = false;
1075 $this->minoredit = false;
1076 // Watch may be overridden by request parameters
1077 $this->watchthis = $request->getBool( 'watchthis', false );
1078 if ( $this->watchlistExpiryEnabled ) {
1079 $this->watchlistExpiry = null;
1080 }
1081 $this->recreate = false;
1082
1083 // When creating a new section, we can preload a section title by passing it as the
1084 // preloadtitle parameter in the URL (T15100)
1085 if ( $this->section == 'new' && $request->getVal( 'preloadtitle' ) ) {
1086 $this->sectiontitle = $request->getVal( 'preloadtitle' );
1087 // Once wpSummary isn't being use for setting section titles, we should delete this.
1088 $this->summary = $request->getVal( 'preloadtitle' );
1089 } elseif ( $this->section != 'new' && $request->getVal( 'summary' ) !== '' ) {
1090 $this->summary = $request->getText( 'summary' );
1091 if ( $this->summary !== '' ) {
1092 $this->hasPresetSummary = true;
1093 }
1094 }
1095
1096 if ( $request->getVal( 'minor' ) ) {
1097 $this->minoredit = true;
1098 }
1099 }
1100
1101 $this->oldid = $request->getInt( 'oldid' );
1102 $this->parentRevId = $request->getInt( 'parentRevId' );
1103
1104 $this->markAsBot = $request->getBool( 'bot', true );
1105 $this->nosummary = $request->getBool( 'nosummary' );
1106
1107 // May be overridden by revision.
1108 $this->contentModel = $request->getText( 'model', $this->contentModel );
1109 // May be overridden by revision.
1110 $this->contentFormat = $request->getText( 'format', $this->contentFormat );
1111
1112 try {
1113 $handler = $this->contentHandlerFactory->getContentHandler( $this->contentModel );
1114 } catch ( MWUnknownContentModelException $e ) {
1115 throw new ErrorPageError(
1116 'editpage-invalidcontentmodel-title',
1117 'editpage-invalidcontentmodel-text',
1118 [ wfEscapeWikiText( $this->contentModel ) ]
1119 );
1120 }
1121
1122 if ( !$handler->isSupportedFormat( $this->contentFormat ) ) {
1123 throw new ErrorPageError(
1124 'editpage-notsupportedcontentformat-title',
1125 'editpage-notsupportedcontentformat-text',
1126 [
1127 wfEscapeWikiText( $this->contentFormat ),
1128 wfEscapeWikiText( ContentHandler::getLocalizedName( $this->contentModel ) )
1129 ]
1130 );
1131 }
1132
1139 $this->editintro = $request->getText( 'editintro',
1140 // Custom edit intro for new sections
1141 $this->section === 'new' ? 'MediaWiki:addsection-editintro' : '' );
1142
1143 // Allow extensions to modify form data
1144 $this->getHookRunner()->onEditPage__importFormData( $this, $request );
1145 }
1146
1156 protected function importContentFormData( &$request ) {
1157 return null; // Don't do anything, EditPage already extracted wpTextbox1
1158 }
1159
1165 public function initialiseForm() {
1166 $this->edittime = $this->page->getTimestamp();
1167 $this->editRevId = $this->page->getLatest();
1168
1169 $dummy = $this->contentHandlerFactory
1170 ->getContentHandler( $this->contentModel )
1171 ->makeEmptyContent();
1172 $content = $this->getContentObject( $dummy ); # TODO: track content object?!
1173 if ( $content === $dummy ) { // Invalid section
1174 $this->noSuchSectionPage();
1175 return false;
1176 }
1177
1178 if ( !$content ) {
1179 $out = $this->context->getOutput();
1180 $this->editFormPageTop .= Html::rawElement(
1181 'div', [ 'class' => 'errorbox' ],
1182 $out->parseAsInterface( $this->context->msg( 'missing-revision-content',
1183 $this->oldid,
1184 Message::plaintextParam( $this->mTitle->getPrefixedText() )
1185 ) )
1186 );
1187 } elseif ( !$this->isSupportedContentModel( $content->getModel() ) ) {
1188 $modelMsg = $this->getContext()->msg( 'content-model-' . $content->getModel() );
1189 $modelName = $modelMsg->exists() ? $modelMsg->text() : $content->getModel();
1190
1191 $out = $this->context->getOutput();
1192 $out->showErrorPage(
1193 'modeleditnotsupported-title',
1194 'modeleditnotsupported-text',
1195 [ $modelName ]
1196 );
1197 return false;
1198 }
1199
1200 $this->textbox1 = $this->toEditText( $content );
1201
1202 $user = $this->context->getUser();
1203 // activate checkboxes if user wants them to be always active
1204 # Sort out the "watch" checkbox
1205 if ( $user->getOption( 'watchdefault' ) ) {
1206 # Watch all edits
1207 $this->watchthis = true;
1208 } elseif ( $user->getOption( 'watchcreations' ) && !$this->mTitle->exists() ) {
1209 # Watch creations
1210 $this->watchthis = true;
1211 } elseif ( $user->isWatched( $this->mTitle ) ) {
1212 # Already watched
1213 $this->watchthis = true;
1214 }
1215 if ( $this->watchthis && $this->watchlistExpiryEnabled ) {
1216 $watchedItem = $this->watchedItemStore->getWatchedItem( $user, $this->getTitle() );
1217 $this->watchlistExpiry = $watchedItem ? $watchedItem->getExpiry() : null;
1218 }
1219 if ( $user->getOption( 'minordefault' ) && !$this->isNew ) {
1220 $this->minoredit = true;
1221 }
1222 if ( $this->textbox1 === false ) {
1223 return false;
1224 }
1225 return true;
1226 }
1227
1235 protected function getContentObject( $def_content = null ) {
1236 global $wgDisableAnonTalk;
1237
1238 $content = false;
1239
1240 $user = $this->context->getUser();
1241 $request = $this->context->getRequest();
1242 // For message page not locally set, use the i18n message.
1243 // For other non-existent articles, use preload text if any.
1244 if ( !$this->mTitle->exists() || $this->section == 'new' ) {
1245 if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && $this->section != 'new' ) {
1246 # If this is a system message, get the default text.
1247 $msg = $this->mTitle->getDefaultMessageText();
1248
1249 $content = $this->toEditContent( $msg );
1250 }
1251 if ( $content === false ) {
1252 # If requested, preload some text.
1253 $preload = $request->getVal( 'preload',
1254 // Custom preload text for new sections
1255 $this->section === 'new' ? 'MediaWiki:addsection-preload' : '' );
1256 $params = $request->getArray( 'preloadparams', [] );
1257
1258 $content = $this->getPreloadedContent( $preload, $params );
1259 }
1260 // For existing pages, get text based on "undo" or section parameters.
1261 } elseif ( $this->section != '' ) {
1262 // Get section edit text (returns $def_text for invalid sections)
1263 $orig = $this->getOriginalContent( $user );
1264 $content = $orig ? $orig->getSection( $this->section ) : null;
1265
1266 if ( !$content ) {
1267 $content = $def_content;
1268 }
1269 } else {
1270 $undoafter = $request->getInt( 'undoafter' );
1271 $undo = $request->getInt( 'undo' );
1272
1273 if ( $undo > 0 && $undoafter > 0 ) {
1274 // The use of getRevisionByTitle() is intentional, as allowing access to
1275 // arbitrary revisions on arbitrary pages bypass partial visibility restrictions (T297322).
1276 $undorev = $this->revisionStore->getRevisionByTitle( $this->mTitle, $undo );
1277 $oldrev = $this->revisionStore->getRevisionByTitle( $this->mTitle, $undoafter );
1278 $undoMsg = null;
1279
1280 # Sanity check, make sure it's the right page,
1281 # the revisions exist and they were not deleted.
1282 # Otherwise, $content will be left as-is.
1283 if ( $undorev !== null && $oldrev !== null &&
1284 !$undorev->isDeleted( RevisionRecord::DELETED_TEXT ) &&
1285 !$oldrev->isDeleted( RevisionRecord::DELETED_TEXT )
1286 ) {
1287 if ( WikiPage::hasDifferencesOutsideMainSlot( $undorev, $oldrev )
1288 || !$this->isSupportedContentModel(
1289 $oldrev->getSlot( SlotRecord::MAIN, RevisionRecord::RAW )->getModel()
1290 )
1291 ) {
1292 // Hack for undo while EditPage can't handle multi-slot editing
1293 $this->context->getOutput()->redirect( $this->mTitle->getFullURL( [
1294 'action' => 'mcrundo',
1295 'undo' => $undo,
1296 'undoafter' => $undoafter,
1297 ] ) );
1298 return false;
1299 } else {
1300 $handler = $this->contentHandlerFactory
1301 ->getContentHandler( $undorev->getSlot(
1302 SlotRecord::MAIN,
1303 RevisionRecord::RAW
1304 )->getModel() );
1305 $currentContent = $this->page->getRevisionRecord()
1306 ->getContent( SlotRecord::MAIN );
1307 $undoContent = $undorev->getContent( SlotRecord::MAIN );
1308 $undoAfterContent = $oldrev->getContent( SlotRecord::MAIN );
1309 $undoIsLatest = $this->page->getRevisionRecord()->getId() === $undorev->getId();
1310 $content = $handler->getUndoContent(
1311 $currentContent,
1312 $undoContent,
1313 $undoAfterContent,
1314 $undoIsLatest
1315 );
1316
1317 if ( $content === false ) {
1318 # Warn the user that something went wrong
1319 $undoMsg = 'failure';
1320 }
1321 }
1322
1323 if ( $undoMsg === null ) {
1324 $oldContent = $this->page->getContent( RevisionRecord::RAW );
1325 $popts = ParserOptions::newFromUserAndLang(
1326 $user, MediaWikiServices::getInstance()->getContentLanguage() );
1327 $newContent = $content->preSaveTransform( $this->mTitle, $user, $popts );
1328 if ( $newContent->getModel() !== $oldContent->getModel() ) {
1329 // The undo may change content
1330 // model if its reverting the top
1331 // edit. This can result in
1332 // mismatched content model/format.
1333 $this->contentModel = $newContent->getModel();
1334 $oldMainSlot = $oldrev->getSlot(
1335 SlotRecord::MAIN,
1336 RevisionRecord::RAW
1337 );
1338 $this->contentFormat = $oldMainSlot->getFormat();
1339 if ( $this->contentFormat === null ) {
1340 $this->contentFormat = $this->contentHandlerFactory
1341 ->getContentHandler( $oldMainSlot->getModel() )
1342 ->getDefaultFormat();
1343 }
1344 }
1345
1346 if ( $newContent->equals( $oldContent ) ) {
1347 # Tell the user that the undo results in no change,
1348 # i.e. the revisions were already undone.
1349 $undoMsg = 'nochange';
1350 $content = false;
1351 } else {
1352 # Inform the user of our success and set an automatic edit summary
1353 $undoMsg = 'success';
1354
1355 # If we just undid one rev, use an autosummary
1356 $firstrev = $this->revisionStore->getNextRevision( $oldrev );
1357 if ( $firstrev && $firstrev->getId() == $undo ) {
1358 $userText = $undorev->getUser() ?
1359 $undorev->getUser()->getName() :
1360 '';
1361 if ( $userText === '' ) {
1362 $undoSummary = $this->context->msg(
1363 'undo-summary-username-hidden',
1364 $undo
1365 )->inContentLanguage()->text();
1366 // Handle external users (imported revisions)
1367 } elseif ( ExternalUserNames::isExternal( $userText ) ) {
1368 $userLinkTitle = ExternalUserNames::getUserLinkTitle( $userText );
1369 if ( $userLinkTitle ) {
1370 $userLink = $userLinkTitle->getPrefixedText();
1371 $undoSummary = $this->context->msg(
1372 'undo-summary-import',
1373 $undo,
1374 $userLink,
1375 $userText
1376 )->inContentLanguage()->text();
1377 } else {
1378 $undoSummary = $this->context->msg(
1379 'undo-summary-import2',
1380 $undo,
1381 $userText
1382 )->inContentLanguage()->text();
1383 }
1384 } else {
1385 $undoIsAnon = $undorev->getUser() ?
1386 !$undorev->getUser()->isRegistered() :
1387 true;
1388 $undoMessage = ( $undoIsAnon && $wgDisableAnonTalk ) ?
1389 'undo-summary-anon' :
1390 'undo-summary';
1391 $undoSummary = $this->context->msg(
1392 $undoMessage,
1393 $undo,
1394 $userText
1395 )->inContentLanguage()->text();
1396 }
1397 if ( $this->summary === '' ) {
1398 $this->summary = $undoSummary;
1399 } else {
1400 $this->summary = $undoSummary . $this->context->msg( 'colon-separator' )
1401 ->inContentLanguage()->text() . $this->summary;
1402 }
1403 }
1404 $this->undidRev = $undo;
1405 $this->undoAfter = $undoafter;
1406 $this->formtype = 'diff';
1407 }
1408 }
1409 } else {
1410 // Failed basic sanity checks.
1411 // Older revisions may have been removed since the link
1412 // was created, or we may simply have got bogus input.
1413 $undoMsg = 'norev';
1414 }
1415
1416 $out = $this->context->getOutput();
1417 // Messages: undo-success, undo-failure, undo-main-slot-only, undo-norev,
1418 // undo-nochange.
1419 $class = ( $undoMsg == 'success' ? '' : 'error ' ) . "mw-undo-{$undoMsg}";
1420 $this->editFormPageTop .= Html::rawElement(
1421 'div', [ 'class' => $class ],
1422 $out->parseAsInterface(
1423 $this->context->msg( 'undo-' . $undoMsg )->plain()
1424 )
1425 );
1426 }
1427
1428 if ( $content === false ) {
1429 // Hack for restoring old revisions while EditPage
1430 // can't handle multi-slot editing.
1431 $curRevisionRecord = $this->page->getRevisionRecord();
1432 $oldRevisionRecord = $this->mArticle->fetchRevisionRecord();
1433
1434 if ( $curRevisionRecord
1435 && $oldRevisionRecord
1436 && $curRevisionRecord->getId() !== $oldRevisionRecord->getId()
1437 && ( WikiPage::hasDifferencesOutsideMainSlot(
1438 $oldRevisionRecord,
1439 $curRevisionRecord
1440 ) || !$this->isSupportedContentModel(
1441 $oldRevisionRecord->getSlot(
1442 SlotRecord::MAIN,
1443 RevisionRecord::RAW
1444 )->getModel()
1445 ) )
1446 ) {
1447 $this->context->getOutput()->redirect(
1448 $this->mTitle->getFullURL(
1449 [
1450 'action' => 'mcrrestore',
1451 'restore' => $oldRevisionRecord->getId(),
1452 ]
1453 )
1454 );
1455
1456 return false;
1457 }
1458 }
1459
1460 if ( $content === false ) {
1461 $content = $this->getOriginalContent( $user );
1462 }
1463 }
1464
1465 return $content;
1466 }
1467
1483 private function getOriginalContent( User $user ) {
1484 if ( $this->section == 'new' ) {
1485 return $this->getCurrentContent();
1486 }
1487 $revRecord = $this->mArticle->fetchRevisionRecord();
1488 if ( $revRecord === null ) {
1489 return $this->contentHandlerFactory
1490 ->getContentHandler( $this->contentModel )
1491 ->makeEmptyContent();
1492 }
1493 return $revRecord->getContent( SlotRecord::MAIN, RevisionRecord::FOR_THIS_USER, $user );
1494 }
1495
1508 public function getParentRevId() {
1509 if ( $this->parentRevId ) {
1510 return $this->parentRevId;
1511 } else {
1512 return $this->mArticle->getRevIdFetched();
1513 }
1514 }
1515
1524 protected function getCurrentContent() {
1525 $revRecord = $this->page->getRevisionRecord();
1526 $content = $revRecord ? $revRecord->getContent(
1527 SlotRecord::MAIN,
1528 RevisionRecord::RAW
1529 ) : null;
1530
1531 if ( $content === false || $content === null ) {
1532 return $this->contentHandlerFactory
1533 ->getContentHandler( $this->contentModel )
1534 ->makeEmptyContent();
1535 } elseif ( !$this->undidRev ) {
1536 $mainSlot = $revRecord->getSlot( SlotRecord::MAIN, RevisionRecord::RAW );
1537
1538 // Content models should always be the same since we error
1539 // out if they are different before this point (in ->edit()).
1540 // The exception being, during an undo, the current revision might
1541 // differ from the prior revision.
1542 $logger = LoggerFactory::getInstance( 'editpage' );
1543 if ( $this->contentModel !== $mainSlot->getModel() ) {
1544 $logger->warning( "Overriding content model from current edit {prev} to {new}", [
1545 'prev' => $this->contentModel,
1546 'new' => $mainSlot->getModel(),
1547 'title' => $this->getTitle()->getPrefixedDBkey(),
1548 'method' => __METHOD__
1549 ] );
1550 $this->contentModel = $mainSlot->getModel();
1551 }
1552
1553 // Given that the content models should match, the current selected
1554 // format should be supported.
1555 if ( !$content->isSupportedFormat( $this->contentFormat ) ) {
1556 $revFormat = $mainSlot->getFormat();
1557 if ( $revFormat === null ) {
1558 $revFormat = $this->contentHandlerFactory
1559 ->getContentHandler( $mainSlot->getModel() )
1560 ->getDefaultFormat();
1561 }
1562
1563 $logger->warning( "Current revision content format unsupported. Overriding {prev} to {new}", [
1564 'prev' => $this->contentFormat,
1565 'new' => $revFormat,
1566 'title' => $this->getTitle()->getPrefixedDBkey(),
1567 'method' => __METHOD__
1568 ] );
1569 $this->contentFormat = $revFormat;
1570 }
1571 }
1572 return $content;
1573 }
1574
1583 $this->mPreloadContent = $content;
1584 }
1585
1597 protected function getPreloadedContent( $preload, $params = [] ) {
1598 if ( !empty( $this->mPreloadContent ) ) {
1600 }
1601
1602 $handler = $this->contentHandlerFactory->getContentHandler( $this->contentModel );
1603
1604 if ( $preload === '' ) {
1605 return $handler->makeEmptyContent();
1606 }
1607
1608 $user = $this->context->getUser();
1609 $title = Title::newFromText( $preload );
1610
1611 # Check for existence to avoid getting MediaWiki:Noarticletext
1612 if ( !$this->isPageExistingAndViewable( $title, $user ) ) {
1613 // TODO: somehow show a warning to the user!
1614 return $handler->makeEmptyContent();
1615 }
1616
1617 $page = WikiPage::factory( $title );
1618 if ( $page->isRedirect() ) {
1620 # Same as before
1621 if ( !$this->isPageExistingAndViewable( $title, $user ) ) {
1622 // TODO: somehow show a warning to the user!
1623 return $handler->makeEmptyContent();
1624 }
1625 $page = WikiPage::factory( $title );
1626 }
1627
1628 $parserOptions = ParserOptions::newFromUser( $user );
1629 $content = $page->getContent( RevisionRecord::RAW );
1630
1631 if ( !$content ) {
1632 // TODO: somehow show a warning to the user!
1633 return $handler->makeEmptyContent();
1634 }
1635
1636 if ( $content->getModel() !== $handler->getModelID() ) {
1637 $converted = $content->convert( $handler->getModelID() );
1638
1639 if ( !$converted ) {
1640 // TODO: somehow show a warning to the user!
1641 wfDebug( "Attempt to preload incompatible content: " .
1642 "can't convert " . $content->getModel() .
1643 " to " . $handler->getModelID() );
1644
1645 return $handler->makeEmptyContent();
1646 }
1647
1648 $content = $converted;
1649 }
1650
1651 return $content->preloadTransform( $title, $parserOptions, $params );
1652 }
1653
1663 private function isPageExistingAndViewable( $title, User $user ) {
1664 return $title && $title->exists() && $this->permManager->userCan( 'read', $user, $title );
1665 }
1666
1674 public function tokenOk( &$request ) {
1675 $token = $request->getVal( 'wpEditToken' );
1676 $user = $this->context->getUser();
1677 $this->mTokenOk = $user->matchEditToken( $token );
1678 $this->mTokenOkExceptSuffix = $user->matchEditTokenNoSuffix( $token );
1679 return $this->mTokenOk;
1680 }
1681
1696 protected function setPostEditCookie( $statusValue ) {
1697 $revisionId = $this->page->getLatest();
1698 $postEditKey = self::POST_EDIT_COOKIE_KEY_PREFIX . $revisionId;
1699
1700 $val = 'saved';
1701 if ( $statusValue == self::AS_SUCCESS_NEW_ARTICLE ) {
1702 $val = 'created';
1703 } elseif ( $this->oldid ) {
1704 $val = 'restored';
1705 }
1706
1707 $response = $this->context->getRequest()->response();
1708 $response->setCookie( $postEditKey, $val, time() + self::POST_EDIT_COOKIE_DURATION );
1709 }
1710
1717 public function attemptSave( &$resultDetails = false ) {
1718 // TODO: MCR:
1719 // * treat $this->minoredit like $this->markAsBot and check isAllowed( 'minoredit' )!
1720 // * add $this->autopatrol like $this->markAsBot and check isAllowed( 'autopatrol' )!
1721 // This is needed since PageUpdater no longer checks these rights!
1722
1723 // Allow bots to exempt some edits from bot flagging
1724 $markAsBot = $this->markAsBot
1725 && $this->permManager->userHasRight( $this->context->getUser(), 'bot' );
1726 $status = $this->internalAttemptSave( $resultDetails, $markAsBot );
1727
1728 $this->getHookRunner()->onEditPage__attemptSave_after( $this, $status, $resultDetails );
1729
1730 return $status;
1731 }
1732
1736 private function incrementResolvedConflicts() {
1737 if ( $this->context->getRequest()->getText( 'mode' ) !== 'conflict' ) {
1738 return;
1739 }
1740
1741 $this->getEditConflictHelper()->incrementResolvedStats( $this->context->getUser() );
1742 }
1743
1753 private function handleStatus( Status $status, $resultDetails ) {
1758 if ( $status->value == self::AS_SUCCESS_UPDATE
1759 || $status->value == self::AS_SUCCESS_NEW_ARTICLE
1760 ) {
1762
1763 $this->didSave = true;
1764 if ( !$resultDetails['nullEdit'] ) {
1765 $this->setPostEditCookie( $status->value );
1766 }
1767 }
1768
1769 $out = $this->context->getOutput();
1770
1771 // "wpExtraQueryRedirect" is a hidden input to modify
1772 // after save URL and is not used by actual edit form
1773 $request = $this->context->getRequest();
1774 $extraQueryRedirect = $request->getVal( 'wpExtraQueryRedirect' );
1775
1776 switch ( $status->value ) {
1784 case self::AS_END:
1787 return true;
1788
1790 return false;
1791
1794 __METHOD__ . ' with $status->value == AS_CANNOT_USE_CUSTOM_MODEL',
1795 '1.35'
1796 );
1797 // ...and fall through to next case
1800 $out->wrapWikiTextAsInterface( 'error',
1801 $status->getWikiText( false, false, $this->context->getLanguage() )
1802 );
1803 return true;
1804
1806 $query = $resultDetails['redirect'] ? 'redirect=no' : '';
1807 if ( $extraQueryRedirect ) {
1808 if ( $query !== '' ) {
1809 $query .= '&';
1810 }
1811 $query .= $extraQueryRedirect;
1812 }
1813 $anchor = $resultDetails['sectionanchor'] ?? '';
1814 $out->redirect( $this->mTitle->getFullURL( $query ) . $anchor );
1815 return false;
1816
1818 $extraQuery = '';
1819 $sectionanchor = $resultDetails['sectionanchor'];
1820
1821 // Give extensions a chance to modify URL query on update
1822 $this->getHookRunner()->onArticleUpdateBeforeRedirect( $this->mArticle,
1823 $sectionanchor, $extraQuery );
1824
1825 if ( $resultDetails['redirect'] ) {
1826 if ( $extraQuery !== '' ) {
1827 $extraQuery = '&' . $extraQuery;
1828 }
1829 $extraQuery = 'redirect=no' . $extraQuery;
1830 }
1831 if ( $extraQueryRedirect ) {
1832 if ( $extraQuery !== '' ) {
1833 $extraQuery .= '&';
1834 }
1835 $extraQuery .= $extraQueryRedirect;
1836 }
1837
1838 $out->redirect( $this->mTitle->getFullURL( $extraQuery ) . $sectionanchor );
1839 return false;
1840
1842 $this->spamPageWithContent( $resultDetails['spam'] ?? false );
1843 return false;
1844
1846 throw new UserBlockedError(
1847 $this->context->getUser()->getBlock(),
1848 $this->context->getUser(),
1849 $this->context->getLanguage(),
1850 $request->getIP()
1851 );
1852
1855 throw new PermissionsError( 'upload' );
1856
1859 throw new PermissionsError( 'edit' );
1860
1862 throw new ReadOnlyError;
1863
1865 throw new ThrottledError();
1866
1868 $permission = $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage';
1869 throw new PermissionsError( $permission );
1870
1872 throw new PermissionsError( 'editcontentmodel' );
1873
1874 default:
1875 // We don't recognize $status->value. The only way that can happen
1876 // is if an extension hook aborted from inside ArticleSave.
1877 // Render the status object into $this->hookError
1878 // FIXME this sucks, we should just use the Status object throughout
1879 $this->hookError = '<div class="error">' . "\n" .
1880 $status->getWikiText( false, false, $this->context->getLanguage() ) .
1881 '</div>';
1882 return true;
1883 }
1884 }
1885
1895 protected function runPostMergeFilters( Content $content, Status $status, User $user ) {
1896 // Run old style post-section-merge edit filter
1897 if ( $this->hookError != '' ) {
1898 # ...or the hook could be expecting us to produce an error
1899 $status->fatal( 'hookaborted' );
1900 $status->value = self::AS_HOOK_ERROR_EXPECTED;
1901 return false;
1902 }
1903
1904 // Run new style post-section-merge edit filter
1905 if ( !$this->getHookRunner()->onEditFilterMergedContent( $this->context, $content,
1906 $status, $this->summary, $user, $this->minoredit )
1907 ) {
1908 # Error messages etc. could be handled within the hook...
1909 if ( $status->isGood() ) {
1910 $status->fatal( 'hookaborted' );
1911 // Not setting $this->hookError here is a hack to allow the hook
1912 // to cause a return to the edit page without $this->hookError
1913 // being set. This is used by ConfirmEdit to display a captcha
1914 // without any error message cruft.
1915 } else {
1916 $this->hookError = $this->formatStatusErrors( $status );
1917 }
1918 // Use the existing $status->value if the hook set it
1919 if ( !$status->value ) {
1920 $status->value = self::AS_HOOK_ERROR;
1921 }
1922 return false;
1923 } elseif ( !$status->isOK() ) {
1924 # ...or the hook could be expecting us to produce an error
1925 // FIXME this sucks, we should just use the Status object throughout
1926 if ( !$status->getErrors() ) {
1927 // Provide a fallback error message if none was set
1928 $status->fatal( 'hookaborted' );
1929 }
1930 $this->hookError = $this->formatStatusErrors( $status );
1931 $status->value = self::AS_HOOK_ERROR_EXPECTED;
1932 return false;
1933 }
1934
1935 return true;
1936 }
1937
1944 private function formatStatusErrors( Status $status ) {
1945 $errmsg = $status->getWikiText(
1946 'edit-error-short',
1947 'edit-error-long',
1948 $this->context->getLanguage()
1949 );
1950 return <<<ERROR
1951<div class="errorbox">
1952{$errmsg}
1953</div>
1954<br clear="all" />
1955ERROR;
1956 }
1957
1964 private function newSectionSummary( &$sectionanchor = null ) {
1965 if ( $this->sectiontitle !== '' ) {
1966 $sectionanchor = $this->guessSectionName( $this->sectiontitle );
1967 // If no edit summary was specified, create one automatically from the section
1968 // title and have it link to the new section. Otherwise, respect the summary as
1969 // passed.
1970 if ( $this->summary === '' ) {
1971 $cleanSectionTitle = MediaWikiServices::getInstance()->getParser()
1972 ->stripSectionName( $this->sectiontitle );
1973 return $this->context->msg( 'newsectionsummary' )
1974 ->plaintextParams( $cleanSectionTitle )->inContentLanguage()->text();
1975 }
1976 } elseif ( $this->summary !== '' ) {
1977 $sectionanchor = $this->guessSectionName( $this->summary );
1978 # This is a new section, so create a link to the new section
1979 # in the revision summary.
1980 $cleanSummary = MediaWikiServices::getInstance()->getParser()
1981 ->stripSectionName( $this->summary );
1982 return $this->context->msg( 'newsectionsummary' )
1983 ->plaintextParams( $cleanSummary )->inContentLanguage()->text();
1984 }
1985 return $this->summary;
1986 }
1987
2013 public function internalAttemptSave( &$result, $markAsBot = false ) {
2014 $status = Status::newGood();
2015 $user = $this->context->getUser();
2016
2017 if ( !$this->getHookRunner()->onEditPage__attemptSave( $this ) ) {
2018 wfDebug( "Hook 'EditPage::attemptSave' aborted article saving" );
2019 $status->fatal( 'hookaborted' );
2020 $status->value = self::AS_HOOK_ERROR;
2021 return $status;
2022 }
2023
2024 if ( $this->unicodeCheck !== self::UNICODE_CHECK ) {
2025 $status->fatal( 'unicode-support-fail' );
2026 $status->value = self::AS_UNICODE_NOT_SUPPORTED;
2027 return $status;
2028 }
2029
2030 $request = $this->context->getRequest();
2031 $spam = $request->getText( 'wpAntispam' );
2032 if ( $spam !== '' ) {
2033 wfDebugLog(
2034 'SimpleAntiSpam',
2035 $user->getName() .
2036 ' editing "' .
2037 $this->mTitle->getPrefixedText() .
2038 '" submitted bogus field "' .
2039 $spam .
2040 '"'
2041 );
2042 $status->fatal( 'spamprotectionmatch', false );
2043 $status->value = self::AS_SPAM_ERROR;
2044 return $status;
2045 }
2046
2047 try {
2048 # Construct Content object
2049 $textbox_content = $this->toEditContent( $this->textbox1 );
2050 } catch ( MWContentSerializationException $ex ) {
2051 $status->fatal(
2052 'content-failed-to-parse',
2053 $this->contentModel,
2054 $this->contentFormat,
2055 $ex->getMessage()
2056 );
2057 $status->value = self::AS_PARSE_ERROR;
2058 return $status;
2059 }
2060
2061 # Check image redirect
2062 if ( $this->mTitle->getNamespace() == NS_FILE &&
2063 $textbox_content->isRedirect() &&
2064 !$this->permManager->userHasRight( $user, 'upload' )
2065 ) {
2067 $status->setResult( false, $code );
2068
2069 return $status;
2070 }
2071
2072 # Check for spam
2073 $spamRegexChecker = MediaWikiServices::getInstance()->getSpamChecker();
2074 $match = $spamRegexChecker->checkSummary( $this->summary );
2075 if ( $match === false && $this->section == 'new' ) {
2076 # $wgSpamRegex is enforced on this new heading/summary because, unlike
2077 # regular summaries, it is added to the actual wikitext.
2078 if ( $this->sectiontitle !== '' ) {
2079 # This branch is taken when the API is used with the 'sectiontitle' parameter.
2080 $match = $spamRegexChecker->checkContent( $this->sectiontitle );
2081 } else {
2082 # This branch is taken when the "Add Topic" user interface is used, or the API
2083 # is used with the 'summary' parameter.
2084 $match = $spamRegexChecker->checkContent( $this->summary );
2085 }
2086 }
2087 if ( $match === false ) {
2088 $match = $spamRegexChecker->checkContent( $this->textbox1 );
2089 }
2090 if ( $match !== false ) {
2091 $result['spam'] = $match;
2092 $ip = $request->getIP();
2093 $pdbk = $this->mTitle->getPrefixedDBkey();
2094 $match = str_replace( "\n", '', $match );
2095 wfDebugLog( 'SpamRegex', "$ip spam regex hit [[$pdbk]]: \"$match\"" );
2096 $status->fatal( 'spamprotectionmatch', $match );
2097 $status->value = self::AS_SPAM_ERROR;
2098 return $status;
2099 }
2100 if ( !$this->getHookRunner()->onEditFilter( $this, $this->textbox1, $this->section,
2101 $this->hookError, $this->summary )
2102 ) {
2103 # Error messages etc. could be handled within the hook...
2104 $status->fatal( 'hookaborted' );
2105 $status->value = self::AS_HOOK_ERROR;
2106 return $status;
2107 } elseif ( $this->hookError != '' ) {
2108 # ...or the hook could be expecting us to produce an error
2109 $status->fatal( 'hookaborted' );
2110 $status->value = self::AS_HOOK_ERROR_EXPECTED;
2111 return $status;
2112 }
2113
2114 if ( $this->permManager->isBlockedFrom( $user, $this->mTitle ) ) {
2115 // Auto-block user's IP if the account was "hard" blocked
2116 if ( !wfReadOnly() ) {
2117 $user->spreadAnyEditBlock();
2118 }
2119 # Check block state against master, thus 'false'.
2120 $status->setResult( false, self::AS_BLOCKED_PAGE_FOR_USER );
2121 return $status;
2122 }
2123
2124 $this->contentLength = strlen( $this->textbox1 );
2125 $config = $this->context->getConfig();
2126 $maxArticleSize = $config->get( 'MaxArticleSize' );
2127 if ( $this->contentLength > $maxArticleSize * 1024 ) {
2128 // Error will be displayed by showEditForm()
2129 $this->tooBig = true;
2130 $status->setResult( false, self::AS_CONTENT_TOO_BIG );
2131 return $status;
2132 }
2133
2134 if ( !$this->permManager->userHasRight( $user, 'edit' ) ) {
2135 if ( $user->isAnon() ) {
2136 $status->setResult( false, self::AS_READ_ONLY_PAGE_ANON );
2137 return $status;
2138 } else {
2139 $status->fatal( 'readonlytext' );
2140 $status->value = self::AS_READ_ONLY_PAGE_LOGGED;
2141 return $status;
2142 }
2143 }
2144
2145 $changingContentModel = false;
2146 if ( $this->contentModel !== $this->mTitle->getContentModel() ) {
2147 if ( !$this->permManager->userHasRight( $user, 'editcontentmodel' ) ) {
2148 $status->setResult( false, self::AS_NO_CHANGE_CONTENT_MODEL );
2149 return $status;
2150 }
2151 // Make sure the user can edit the page under the new content model too
2152 $titleWithNewContentModel = clone $this->mTitle;
2153 $titleWithNewContentModel->setContentModel( $this->contentModel );
2154
2155 $canEditModel = $this->permManager->userCan(
2156 'editcontentmodel',
2157 $user,
2158 $titleWithNewContentModel
2159 );
2160
2161 if (
2162 !$canEditModel
2163 || !$this->permManager->userCan( 'edit', $user, $titleWithNewContentModel )
2164 ) {
2165 $status->setResult( false, self::AS_NO_CHANGE_CONTENT_MODEL );
2166
2167 return $status;
2168 }
2169
2170 $changingContentModel = true;
2171 $oldContentModel = $this->mTitle->getContentModel();
2172 }
2173
2174 if ( $this->changeTags ) {
2175 $changeTagsStatus = ChangeTags::canAddTagsAccompanyingChange(
2176 $this->changeTags, $user );
2177 if ( !$changeTagsStatus->isOK() ) {
2178 $changeTagsStatus->value = self::AS_CHANGE_TAG_ERROR;
2179 return $changeTagsStatus;
2180 }
2181 }
2182
2183 if ( wfReadOnly() ) {
2184 $status->fatal( 'readonlytext' );
2185 $status->value = self::AS_READ_ONLY_PAGE;
2186 return $status;
2187 }
2188 if ( $user->pingLimiter() || $user->pingLimiter( 'linkpurge', 0 )
2189 || ( $changingContentModel && $user->pingLimiter( 'editcontentmodel' ) )
2190 ) {
2191 $status->fatal( 'actionthrottledtext' );
2192 $status->value = self::AS_RATE_LIMITED;
2193 return $status;
2194 }
2195
2196 # If the article has been deleted while editing, don't save it without
2197 # confirmation
2198 if ( $this->wasDeletedSinceLastEdit() && !$this->recreate ) {
2199 $status->setResult( false, self::AS_ARTICLE_WAS_DELETED );
2200 return $status;
2201 }
2202
2203 # Load the page data from the master. If anything changes in the meantime,
2204 # we detect it by using page_latest like a token in a 1 try compare-and-swap.
2205 $this->page->loadPageData( 'fromdbmaster' );
2206 $new = !$this->page->exists();
2207
2208 if ( $new ) {
2209 // Late check for create permission, just in case *PARANOIA*
2210 if ( !$this->permManager->userCan( 'create', $user, $this->mTitle ) ) {
2211 $status->fatal( 'nocreatetext' );
2212 $status->value = self::AS_NO_CREATE_PERMISSION;
2213 wfDebug( __METHOD__ . ": no create permission" );
2214 return $status;
2215 }
2216
2217 // Don't save a new page if it's blank or if it's a MediaWiki:
2218 // message with content equivalent to default (allow empty pages
2219 // in this case to disable messages, see T52124)
2220 $defaultMessageText = $this->mTitle->getDefaultMessageText();
2221 if ( $this->mTitle->getNamespace() === NS_MEDIAWIKI && $defaultMessageText !== false ) {
2222 $defaultText = $defaultMessageText;
2223 } else {
2224 $defaultText = '';
2225 }
2226
2227 if ( !$this->allowBlankArticle && $this->textbox1 === $defaultText ) {
2228 $this->blankArticle = true;
2229 $status->fatal( 'blankarticle' );
2230 $status->setResult( false, self::AS_BLANK_ARTICLE );
2231 return $status;
2232 }
2233
2234 if ( !$this->runPostMergeFilters( $textbox_content, $status, $user ) ) {
2235 return $status;
2236 }
2237
2238 $content = $textbox_content;
2239
2240 $result['sectionanchor'] = '';
2241 if ( $this->section == 'new' ) {
2242 // @phan-suppress-next-line PhanSuspiciousValueComparison
2243 if ( $this->sectiontitle !== '' ) {
2244 // Insert the section title above the content.
2245 $content = $content->addSectionHeader( $this->sectiontitle );
2246 } elseif ( $this->summary !== '' ) {
2247 // Insert the section title above the content.
2248 $content = $content->addSectionHeader( $this->summary );
2249 }
2250 $this->summary = $this->newSectionSummary( $result['sectionanchor'] );
2251 }
2252
2253 $status->value = self::AS_SUCCESS_NEW_ARTICLE;
2254
2255 } else { # not $new
2256
2257 # Article exists. Check for edit conflict.
2258
2259 $this->page->clear(); # Force reload of dates, etc.
2260 $timestamp = $this->page->getTimestamp();
2261 $latest = $this->page->getLatest();
2262
2263 wfDebug( "timestamp: {$timestamp}, edittime: {$this->edittime}" );
2264 wfDebug( "revision: {$latest}, editRevId: {$this->editRevId}" );
2265
2266 // An edit conflict is detected if the current revision is different from the
2267 // revision that was current when editing was initiated on the client.
2268 // This is checked based on the timestamp and revision ID.
2269 // TODO: the timestamp based check can probably go away now.
2270 if ( ( $this->edittime !== null && $this->edittime != $timestamp )
2271 || ( $this->editRevId !== null && $this->editRevId != $latest )
2272 ) {
2273 $this->isConflict = true;
2274 if ( $this->section == 'new' ) {
2275 if ( $this->page->getUserText() == $user->getName() &&
2276 $this->page->getComment() == $this->newSectionSummary()
2277 ) {
2278 // Probably a duplicate submission of a new comment.
2279 // This can happen when CDN resends a request after
2280 // a timeout but the first one actually went through.
2281 wfDebug( __METHOD__
2282 . ": duplicate new section submission; trigger edit conflict!" );
2283 } else {
2284 // New comment; suppress conflict.
2285 $this->isConflict = false;
2286 wfDebug( __METHOD__ . ": conflict suppressed; new section" );
2287 }
2288 } elseif ( $this->section == ''
2289 && $this->edittime
2290 && $this->revisionStore->userWasLastToEdit(
2291 wfGetDB( DB_MASTER ),
2292 $this->mTitle->getArticleID(),
2293 $user->getId(),
2294 $this->edittime
2295 )
2296 ) {
2297 # Suppress edit conflict with self, except for section edits where merging is required.
2298 wfDebug( __METHOD__ . ": Suppressing edit conflict, same user." );
2299 $this->isConflict = false;
2300 }
2301 }
2302
2303 // If sectiontitle is set, use it, otherwise use the summary as the section title.
2304 // @phan-suppress-next-line PhanSuspiciousValueComparison
2305 if ( $this->sectiontitle !== '' ) {
2306 $sectionTitle = $this->sectiontitle;
2307 } else {
2308 $sectionTitle = $this->summary;
2309 }
2310
2311 $content = null;
2312
2313 if ( $this->isConflict ) {
2314 wfDebug( __METHOD__
2315 . ": conflict! getting section '{$this->section}' for time '{$this->edittime}'"
2316 . " (id '{$this->editRevId}') (article time '{$timestamp}')" );
2317 // @TODO: replaceSectionAtRev() with base ID (not prior current) for ?oldid=X case
2318 // ...or disable section editing for non-current revisions (not exposed anyway).
2319 if ( $this->editRevId !== null ) {
2320 $content = $this->page->replaceSectionAtRev(
2321 $this->section,
2322 $textbox_content,
2323 $sectionTitle,
2324 $this->editRevId
2325 );
2326 } else {
2327 $content = $this->page->replaceSectionContent(
2328 $this->section,
2329 $textbox_content,
2330 $sectionTitle,
2331 $this->edittime
2332 );
2333 }
2334 } else {
2335 wfDebug( __METHOD__ . ": getting section '{$this->section}'" );
2336 $content = $this->page->replaceSectionContent(
2337 $this->section,
2338 $textbox_content,
2339 $sectionTitle
2340 );
2341 }
2342
2343 if ( $content === null ) {
2344 wfDebug( __METHOD__ . ": activating conflict; section replace failed." );
2345 $this->isConflict = true;
2346 $content = $textbox_content; // do not try to merge here!
2347 } elseif ( $this->isConflict ) {
2348 # Attempt merge
2349 if ( $this->mergeChangesIntoContent( $content ) ) {
2350 // Successful merge! Maybe we should tell the user the good news?
2351 $this->isConflict = false;
2352 wfDebug( __METHOD__ . ": Suppressing edit conflict, successful merge." );
2353 } else {
2354 $this->section = '';
2355 $this->textbox1 = ContentHandler::getContentText( $content );
2356 wfDebug( __METHOD__ . ": Keeping edit conflict, failed merge." );
2357 }
2358 }
2359
2360 if ( $this->isConflict ) {
2361 $status->setResult( false, self::AS_CONFLICT_DETECTED );
2362 return $status;
2363 }
2364
2365 if ( !$this->runPostMergeFilters( $content, $status, $user ) ) {
2366 return $status;
2367 }
2368
2369 if ( $this->section == 'new' ) {
2370 // Handle the user preference to force summaries here
2371 if ( !$this->allowBlankSummary && trim( $this->summary ) == '' ) {
2372 $this->missingSummary = true;
2373 $status->fatal( 'missingsummary' ); // or 'missingcommentheader' if $section == 'new'. Blegh
2374 $status->value = self::AS_SUMMARY_NEEDED;
2375 return $status;
2376 }
2377
2378 // Do not allow the user to post an empty comment
2379 if ( $this->textbox1 == '' ) {
2380 $this->missingComment = true;
2381 $status->fatal( 'missingcommenttext' );
2382 $status->value = self::AS_TEXTBOX_EMPTY;
2383 return $status;
2384 }
2385 } elseif ( !$this->allowBlankSummary
2386 && !$content->equals( $this->getOriginalContent( $user ) )
2387 && !$content->isRedirect()
2388 && md5( $this->summary ) == $this->autoSumm
2389 ) {
2390 $this->missingSummary = true;
2391 $status->fatal( 'missingsummary' );
2392 $status->value = self::AS_SUMMARY_NEEDED;
2393 return $status;
2394 }
2395
2396 # All's well
2397 $sectionanchor = '';
2398 if ( $this->section == 'new' ) {
2399 $this->summary = $this->newSectionSummary( $sectionanchor );
2400 } elseif ( $this->section != '' ) {
2401 # Try to get a section anchor from the section source, redirect
2402 # to edited section if header found.
2403 # XXX: Might be better to integrate this into Article::replaceSectionAtRev
2404 # for duplicate heading checking and maybe parsing.
2405 $hasmatch = preg_match( "/^ *([=]{1,6})(.*?)(\\1) *\\n/i", $this->textbox1, $matches );
2406 # We can't deal with anchors, includes, html etc in the header for now,
2407 # headline would need to be parsed to improve this.
2408 if ( $hasmatch && strlen( $matches[2] ) > 0 ) {
2409 $sectionanchor = $this->guessSectionName( $matches[2] );
2410 }
2411 }
2412 $result['sectionanchor'] = $sectionanchor;
2413
2414 // Save errors may fall down to the edit form, but we've now
2415 // merged the section into full text. Clear the section field
2416 // so that later submission of conflict forms won't try to
2417 // replace that into a duplicated mess.
2418 $this->textbox1 = $this->toEditText( $content );
2419 $this->section = '';
2420
2421 $status->value = self::AS_SUCCESS_UPDATE;
2422 }
2423
2424 if ( !$this->allowSelfRedirect
2425 && $content->isRedirect()
2426 && $content->getRedirectTarget()->equals( $this->getTitle() )
2427 ) {
2428 // If the page already redirects to itself, don't warn.
2429 $currentTarget = $this->getCurrentContent()->getRedirectTarget();
2430 if ( !$currentTarget || !$currentTarget->equals( $this->getTitle() ) ) {
2431 $this->selfRedirect = true;
2432 $status->fatal( 'selfredirect' );
2433 $status->value = self::AS_SELF_REDIRECT;
2434 return $status;
2435 }
2436 }
2437
2438 // Check for length errors again now that the section is merged in
2439 $this->contentLength = strlen( $this->toEditText( $content ) );
2440 if ( $this->contentLength > $maxArticleSize * 1024 ) {
2441 $this->tooBig = true;
2442 $status->setResult( false, self::AS_MAX_ARTICLE_SIZE_EXCEEDED );
2443 return $status;
2444 }
2445
2446 $flags = EDIT_AUTOSUMMARY |
2447 ( $new ? EDIT_NEW : EDIT_UPDATE ) |
2448 ( ( $this->minoredit && !$this->isNew ) ? EDIT_MINOR : 0 ) |
2449 ( $markAsBot ? EDIT_FORCE_BOT : 0 );
2450
2451 $doEditStatus = $this->page->doEditContent(
2452 $content,
2453 $this->summary,
2454 $flags,
2455 $this->undoAfter ?: false,
2456 $user,
2457 $content->getDefaultFormat(),
2460 );
2461
2462 if ( !$doEditStatus->isOK() ) {
2463 // Failure from doEdit()
2464 // Show the edit conflict page for certain recognized errors from doEdit(),
2465 // but don't show it for errors from extension hooks
2466 $errors = $doEditStatus->getErrorsArray();
2467 if ( in_array( $errors[0][0],
2468 [ 'edit-gone-missing', 'edit-conflict', 'edit-already-exists' ] )
2469 ) {
2470 $this->isConflict = true;
2471 }
2472 // Destroys data doEdit() put in $status->value but who cares
2473 $doEditStatus->value = self::AS_END;
2474 return $doEditStatus;
2475 }
2476
2477 $result['nullEdit'] = $doEditStatus->hasMessage( 'edit-no-change' );
2478 if ( $result['nullEdit'] ) {
2479 // We don't know if it was a null edit until now, so increment here
2480 $user->pingLimiter( 'linkpurge' );
2481 }
2482 $result['redirect'] = $content->isRedirect();
2483
2484 $this->updateWatchlist();
2485
2486 // If the content model changed, add a log entry
2487 if ( $changingContentModel ) {
2489 $user,
2490 $new ? false : $oldContentModel,
2493 );
2494 }
2495
2496 return $status;
2497 }
2498
2505 protected function addContentModelChangeLogEntry( User $user, $oldModel, $newModel, $reason ) {
2506 $new = $oldModel === false;
2507 $log = new ManualLogEntry( 'contentmodel', $new ? 'new' : 'change' );
2508 $log->setPerformer( $user );
2509 $log->setTarget( $this->mTitle );
2510 $log->setComment( $reason );
2511 $log->setParameters( [
2512 '4::oldmodel' => $oldModel,
2513 '5::newmodel' => $newModel
2514 ] );
2515 $logid = $log->insert();
2516 $log->publish( $logid );
2517 }
2518
2522 protected function updateWatchlist() {
2523 $user = $this->context->getUser();
2524 if ( !$user->isLoggedIn() ) {
2525 return;
2526 }
2527
2529 $watch = $this->watchthis;
2531
2532 // This can't run as a DeferredUpdate due to a possible race condition
2533 // when the post-edit redirect happens if the pendingUpdates queue is
2534 // too large to finish in time (T259564)
2536
2537 // Add a job to purge expired watchlist items. Jobs will only be added at the rate
2538 // specified by $wgWatchlistPurgeRate, which by default is every tenth edit.
2539 if ( $this->watchlistExpiryEnabled ) {
2540 $purgeRate = $this->getContext()->getConfig()->get( 'WatchlistPurgeRate' );
2541 $this->watchedItemStore->enqueueWatchlistExpiryJob( $purgeRate );
2542 }
2543 }
2544
2556 private function mergeChangesIntoContent( &$editContent ) {
2557 // This is the revision that was current at the time editing was initiated on the client,
2558 // even if the edit was based on an old revision.
2559 $baseRevRecord = $this->getExpectedParentRevision();
2560 $baseContent = $baseRevRecord ?
2561 $baseRevRecord->getContent( SlotRecord::MAIN ) :
2562 null;
2563
2564 if ( $baseContent === null ) {
2565 return false;
2566 }
2567
2568 // The current state, we want to merge updates into it
2569 $currentRevisionRecord = $this->revisionStore->getRevisionByTitle(
2570 $this->mTitle,
2571 0,
2572 RevisionStore::READ_LATEST
2573 );
2574 $currentContent = $currentRevisionRecord
2575 ? $currentRevisionRecord->getContent( SlotRecord::MAIN )
2576 : null;
2577
2578 if ( $currentContent === null ) {
2579 return false;
2580 }
2581
2582 $result = $this->contentHandlerFactory
2583 ->getContentHandler( $baseContent->getModel() )
2584 ->merge3( $baseContent, $editContent, $currentContent );
2585
2586 if ( $result ) {
2587 $editContent = $result;
2588 // Update parentRevId to what we just merged.
2589 $this->parentRevId = $currentRevisionRecord->getId();
2590 return true;
2591 }
2592
2593 return false;
2594 }
2595
2610 public function getBaseRevision() {
2611 wfDeprecated( __METHOD__, '1.35' );
2612 if ( $this->mBaseRevision === false ) {
2613 $revRecord = $this->getExpectedParentRevision();
2614 $this->mBaseRevision = $revRecord ? new Revision( $revRecord ) : null;
2615 }
2616 return $this->mBaseRevision;
2617 }
2618
2626 public function getExpectedParentRevision() {
2627 if ( $this->mExpectedParentRevision === false ) {
2628 $revRecord = null;
2629 if ( $this->editRevId ) {
2630 $revRecord = $this->revisionStore->getRevisionById(
2631 $this->editRevId,
2632 RevisionStore::READ_LATEST
2633 );
2634 } else {
2635 $revRecord = $this->revisionStore->getRevisionByTimestamp(
2636 $this->getTitle(),
2637 $this->edittime,
2638 RevisionStore::READ_LATEST
2639 );
2640 }
2641 $this->mExpectedParentRevision = $revRecord;
2642 }
2644 }
2645
2655 public static function matchSpamRegex( $text ) {
2656 wfDeprecated( __METHOD__, '1.35' );
2657 return MediaWikiServices::getInstance()->getSpamChecker()->checkContent( $text );
2658 }
2659
2669 public static function matchSummarySpamRegex( $text ) {
2670 wfDeprecated( __METHOD__, '1.35' );
2671 return MediaWikiServices::getInstance()->getSpamChecker()->checkSummary( $text );
2672 }
2673
2674 public function setHeaders() {
2675 $out = $this->context->getOutput();
2676
2677 $out->addModules( 'mediawiki.action.edit' );
2678 $out->addModuleStyles( 'mediawiki.action.edit.styles' );
2679 $out->addModuleStyles( 'mediawiki.editfont.styles' );
2680
2681 $user = $this->context->getUser();
2682
2683 if ( $user->getOption( 'uselivepreview' ) ) {
2684 $out->addModules( 'mediawiki.action.edit.preview' );
2685 }
2686
2687 if ( $user->getOption( 'useeditwarning' ) ) {
2688 $out->addModules( 'mediawiki.action.edit.editWarning' );
2689 }
2690
2691 if ( $this->watchlistExpiryEnabled && $user->isRegistered() ) {
2692 $out->addModules( 'mediawiki.action.edit.watchlistExpiry' );
2693 }
2694
2695 # Enabled article-related sidebar, toplinks, etc.
2696 $out->setArticleRelated( true );
2697
2698 $contextTitle = $this->getContextTitle();
2699 if ( $this->isConflict ) {
2700 $msg = 'editconflict';
2701 } elseif ( $contextTitle->exists() && $this->section != '' ) {
2702 $msg = $this->section == 'new' ? 'editingcomment' : 'editingsection';
2703 } else {
2704 $msg = $contextTitle->exists()
2705 || ( $contextTitle->getNamespace() == NS_MEDIAWIKI
2706 && $contextTitle->getDefaultMessageText() !== false
2707 )
2708 ? 'editing'
2709 : 'creating';
2710 }
2711
2712 # Use the title defined by DISPLAYTITLE magic word when present
2713 # NOTE: getDisplayTitle() returns HTML while getPrefixedText() returns plain text.
2714 # setPageTitle() treats the input as wikitext, which should be safe in either case.
2715 $displayTitle = isset( $this->mParserOutput ) ? $this->mParserOutput->getDisplayTitle() : false;
2716 if ( $displayTitle === false ) {
2717 $displayTitle = $contextTitle->getPrefixedText();
2718 } else {
2719 $out->setDisplayTitle( $displayTitle );
2720 }
2721 $out->setPageTitle( $this->context->msg( $msg, $displayTitle ) );
2722
2723 $config = $this->context->getConfig();
2724
2725 # Transmit the name of the message to JavaScript for live preview
2726 # Keep Resources.php/mediawiki.action.edit.preview in sync with the possible keys
2727 $out->addJsConfigVars( [
2728 'wgEditMessage' => $msg,
2729 'wgAjaxEditStash' => $config->get( 'AjaxEditStash' ),
2730 ] );
2731
2732 // Add whether to use 'save' or 'publish' messages to JavaScript for post-edit, other
2733 // editors, etc.
2734 $out->addJsConfigVars(
2735 'wgEditSubmitButtonLabelPublish',
2736 $config->get( 'EditSubmitButtonLabelPublish' )
2737 );
2738 }
2739
2743 protected function showIntro() {
2744 if ( $this->suppressIntro ) {
2745 return;
2746 }
2747
2748 $out = $this->context->getOutput();
2749 $namespace = $this->mTitle->getNamespace();
2750
2751 if ( $namespace == NS_MEDIAWIKI ) {
2752 # Show a warning if editing an interface message
2753 $out->wrapWikiMsg( "<div class='mw-editinginterface'>\n$1\n</div>", 'editinginterface' );
2754 # If this is a default message (but not css, json, or js),
2755 # show a hint that it is translatable on translatewiki.net
2756 if (
2757 !$this->mTitle->hasContentModel( CONTENT_MODEL_CSS )
2758 && !$this->mTitle->hasContentModel( CONTENT_MODEL_JSON )
2759 && !$this->mTitle->hasContentModel( CONTENT_MODEL_JAVASCRIPT )
2760 ) {
2761 $defaultMessageText = $this->mTitle->getDefaultMessageText();
2762 if ( $defaultMessageText !== false ) {
2763 $out->wrapWikiMsg( "<div class='mw-translateinterface'>\n$1\n</div>",
2764 'translateinterface' );
2765 }
2766 }
2767 } elseif ( $namespace == NS_FILE ) {
2768 # Show a hint to shared repo
2769 $file = MediaWikiServices::getInstance()->getRepoGroup()->findFile( $this->mTitle );
2770 if ( $file && !$file->isLocal() ) {
2771 $descUrl = $file->getDescriptionUrl();
2772 # there must be a description url to show a hint to shared repo
2773 if ( $descUrl ) {
2774 if ( !$this->mTitle->exists() ) {
2775 $out->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-create\">\n$1\n</div>", [
2776 'sharedupload-desc-create', $file->getRepo()->getDisplayName(), $descUrl
2777 ] );
2778 } else {
2779 $out->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-edit\">\n$1\n</div>", [
2780 'sharedupload-desc-edit', $file->getRepo()->getDisplayName(), $descUrl
2781 ] );
2782 }
2783 }
2784 }
2785 }
2786
2787 # Show a warning message when someone creates/edits a user (talk) page but the user does not exist
2788 # Show log extract when the user is currently blocked
2789 if ( $namespace == NS_USER || $namespace == NS_USER_TALK ) {
2790 $username = explode( '/', $this->mTitle->getText(), 2 )[0];
2791 $user = User::newFromName( $username, false /* allow IP users */ );
2792 $ip = User::isIP( $username );
2793 $block = DatabaseBlock::newFromTarget( $user, $user );
2794
2795 $userExists = ( $user && $user->isLoggedIn() );
2796 if ( $userExists && $user->isHidden() &&
2797 !$this->permManager->userHasRight( $this->context->getUser(), 'hideuser' )
2798 ) {
2799 // If the user exists, but is hidden, and the viewer cannot see hidden
2800 // users, pretend like they don't exist at all. See T120883
2801 $userExists = false;
2802 }
2803
2804 if ( !$userExists && !$ip ) { # User does not exist
2805 $out->wrapWikiMsg( "<div class=\"mw-userpage-userdoesnotexist error\">\n$1\n</div>",
2806 [ 'userpage-userdoesnotexist', wfEscapeWikiText( $username ) ] );
2807 } elseif (
2808 $block !== null &&
2809 $block->getType() != DatabaseBlock::TYPE_AUTO &&
2810 ( $block->isSitewide() || $user->isBlockedFrom( $this->mTitle ) )
2811 ) {
2812 // Show log extract if the user is sitewide blocked or is partially
2813 // blocked and not allowed to edit their user page or user talk page
2814 LogEventsList::showLogExtract(
2815 $out,
2816 'block',
2817 MediaWikiServices::getInstance()->getNamespaceInfo()->
2818 getCanonicalName( NS_USER ) . ':' . $block->getTarget(),
2819 '',
2820 [
2821 'lim' => 1,
2822 'showIfEmpty' => false,
2823 'msgKey' => [
2824 'blocked-notice-logextract',
2825 $user->getName() # Support GENDER in notice
2826 ]
2827 ]
2828 );
2829 }
2830 }
2831 # Try to add a custom edit intro, or use the standard one if this is not possible.
2832 if ( !$this->showCustomIntro() && !$this->mTitle->exists() ) {
2833 $helpLink = wfExpandUrl( Skin::makeInternalOrExternalUrl(
2834 $this->context->msg( 'helppage' )->inContentLanguage()->text()
2835 ) );
2836 if ( $this->context->getUser()->isLoggedIn() ) {
2837 $out->wrapWikiMsg(
2838 // Suppress the external link icon, consider the help url an internal one
2839 "<div class=\"mw-newarticletext plainlinks\">\n$1\n</div>",
2840 [
2841 'newarticletext',
2842 $helpLink
2843 ]
2844 );
2845 } else {
2846 $out->wrapWikiMsg(
2847 // Suppress the external link icon, consider the help url an internal one
2848 "<div class=\"mw-newarticletextanon plainlinks\">\n$1\n</div>",
2849 [
2850 'newarticletextanon',
2851 $helpLink
2852 ]
2853 );
2854 }
2855 }
2856 # Give a notice if the user is editing a deleted/moved page...
2857 if ( !$this->mTitle->exists() ) {
2858 $dbr = wfGetDB( DB_REPLICA );
2859
2860 LogEventsList::showLogExtract( $out, [ 'delete', 'move' ], $this->mTitle,
2861 '',
2862 [
2863 'lim' => 10,
2864 'conds' => [ 'log_action != ' . $dbr->addQuotes( 'revision' ) ],
2865 'showIfEmpty' => false,
2866 'msgKey' => [ 'recreate-moveddeleted-warn' ]
2867 ]
2868 );
2869 }
2870 }
2871
2877 protected function showCustomIntro() {
2878 if ( $this->editintro ) {
2879 $title = Title::newFromText( $this->editintro );
2880 if ( $this->isPageExistingAndViewable( $title, $this->context->getUser() ) ) {
2881 // Added using template syntax, to take <noinclude>'s into account.
2882 $this->context->getOutput()->addWikiTextAsContent(
2883 '<div class="mw-editintro">{{:' . $title->getFullText() . '}}</div>',
2884 /*linestart*/true,
2885 $this->mTitle
2886 );
2887 return true;
2888 }
2889 }
2890 return false;
2891 }
2892
2911 protected function toEditText( $content ) {
2912 if ( $content === null || $content === false || is_string( $content ) ) {
2913 return $content;
2914 }
2915
2916 if ( !$this->isSupportedContentModel( $content->getModel() ) ) {
2917 throw new MWException( 'This content model is not supported: ' . $content->getModel() );
2918 }
2919
2920 return $content->serialize( $this->contentFormat );
2921 }
2922
2939 protected function toEditContent( $text ) {
2940 if ( $text === false || $text === null ) {
2941 return $text;
2942 }
2943
2944 $content = ContentHandler::makeContent( $text, $this->getTitle(),
2945 $this->contentModel, $this->contentFormat );
2946
2947 if ( !$this->isSupportedContentModel( $content->getModel() ) ) {
2948 throw new MWException( 'This content model is not supported: ' . $content->getModel() );
2949 }
2950
2951 return $content;
2952 }
2953
2962 public function showEditForm( $formCallback = null ) {
2963 # need to parse the preview early so that we know which templates are used,
2964 # otherwise users with "show preview after edit box" will get a blank list
2965 # we parse this near the beginning so that setHeaders can do the title
2966 # setting work instead of leaving it in getPreviewText
2967 $previewOutput = '';
2968 if ( $this->formtype == 'preview' ) {
2969 $previewOutput = $this->getPreviewText();
2970 }
2971
2972 $out = $this->context->getOutput();
2973
2974 $this->getHookRunner()->onEditPage__showEditForm_initial( $this, $out );
2975
2976 $this->setHeaders();
2977
2978 $this->addTalkPageText();
2979 $this->addEditNotices();
2980
2981 if ( !$this->isConflict &&
2982 $this->section != '' &&
2983 !$this->isSectionEditSupported() ) {
2984 // We use $this->section to much before this and getVal('wgSection') directly in other places
2985 // at this point we can't reset $this->section to '' to fallback to non-section editing.
2986 // Someone is welcome to try refactoring though
2987 $out->showErrorPage( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
2988 return;
2989 }
2990
2991 $this->showHeader();
2992
2993 $out->addHTML( $this->editFormPageTop );
2994
2995 $user = $this->context->getUser();
2996 if ( $user->getOption( 'previewontop' ) ) {
2997 $this->displayPreviewArea( $previewOutput, true );
2998 }
2999
3000 $out->addHTML( $this->editFormTextTop );
3001
3002 if ( $this->wasDeletedSinceLastEdit() && $this->formtype !== 'save' ) {
3003 $out->wrapWikiMsg( "<div class='error mw-deleted-while-editing'>\n$1\n</div>",
3004 'deletedwhileediting' );
3005 }
3006
3007 // @todo add EditForm plugin interface and use it here!
3008 // search for textarea1 and textarea2, and allow EditForm to override all uses.
3009 $out->addHTML( Html::openElement(
3010 'form',
3011 [
3012 'class' => 'mw-editform',
3013 'id' => self::EDITFORM_ID,
3014 'name' => self::EDITFORM_ID,
3015 'method' => 'post',
3016 'action' => $this->getActionURL( $this->getContextTitle() ),
3017 'enctype' => 'multipart/form-data'
3018 ]
3019 ) );
3020
3021 if ( is_callable( $formCallback ) ) {
3022 wfWarn( 'The $formCallback parameter to ' . __METHOD__ . 'is deprecated' );
3023 call_user_func_array( $formCallback, [ &$out ] );
3024 }
3025
3026 // Add a check for Unicode support
3027 $out->addHTML( Html::hidden( 'wpUnicodeCheck', self::UNICODE_CHECK ) );
3028
3029 // Add an empty field to trip up spambots
3030 $out->addHTML(
3031 Xml::openElement( 'div', [ 'id' => 'antispam-container', 'style' => 'display: none;' ] )
3032 . Html::rawElement(
3033 'label',
3034 [ 'for' => 'wpAntispam' ],
3035 $this->context->msg( 'simpleantispam-label' )->parse()
3036 )
3037 . Xml::element(
3038 'input',
3039 [
3040 'type' => 'text',
3041 'name' => 'wpAntispam',
3042 'id' => 'wpAntispam',
3043 'value' => ''
3044 ]
3045 )
3046 . Xml::closeElement( 'div' )
3047 );
3048
3049 $this->getHookRunner()->onEditPage__showEditForm_fields( $this, $out );
3050
3051 // Put these up at the top to ensure they aren't lost on early form submission
3052 $this->showFormBeforeText();
3053
3054 if ( $this->wasDeletedSinceLastEdit() && $this->formtype == 'save' ) {
3055 $username = $this->lastDelete->user_name;
3056 $comment = CommentStore::getStore()
3057 ->getComment( 'log_comment', $this->lastDelete )->text;
3058
3059 // It is better to not parse the comment at all than to have templates expanded in the middle
3060 // TODO: can the checkLabel be moved outside of the div so that wrapWikiMsg could be used?
3061 $key = $comment === ''
3062 ? 'confirmrecreate-noreason'
3063 : 'confirmrecreate';
3064 $out->addHTML(
3065 '<div class="mw-confirm-recreate">' .
3066 $this->context->msg( $key, $username, "<nowiki>$comment</nowiki>" )->parse() .
3067 Xml::checkLabel( $this->context->msg( 'recreate' )->text(), 'wpRecreate', 'wpRecreate', false,
3068 [ 'title' => Linker::titleAttrib( 'recreate' ), 'tabindex' => 1, 'id' => 'wpRecreate' ]
3069 ) .
3070 '</div>'
3071 );
3072 }
3073
3074 # When the summary is hidden, also hide them on preview/show changes
3075 if ( $this->nosummary ) {
3076 $out->addHTML( Html::hidden( 'nosummary', true ) );
3077 }
3078
3079 # If a blank edit summary was previously provided, and the appropriate
3080 # user preference is active, pass a hidden tag as wpIgnoreBlankSummary. This will stop the
3081 # user being bounced back more than once in the event that a summary
3082 # is not required.
3083 # ####
3084 # For a bit more sophisticated detection of blank summaries, hash the
3085 # automatic one and pass that in the hidden field wpAutoSummary.
3086 if ( $this->missingSummary || ( $this->section == 'new' && $this->nosummary ) ) {
3087 $out->addHTML( Html::hidden( 'wpIgnoreBlankSummary', true ) );
3088 }
3089
3090 if ( $this->undidRev ) {
3091 $out->addHTML( Html::hidden( 'wpUndidRevision', $this->undidRev ) );
3092 }
3093 if ( $this->undoAfter ) {
3094 $out->addHTML( Html::hidden( 'wpUndoAfter', $this->undoAfter ) );
3095 }
3096
3097 if ( $this->selfRedirect ) {
3098 $out->addHTML( Html::hidden( 'wpIgnoreSelfRedirect', true ) );
3099 }
3100
3101 if ( $this->hasPresetSummary ) {
3102 // If a summary has been preset using &summary= we don't want to prompt for
3103 // a different summary. Only prompt for a summary if the summary is blanked.
3104 // (T19416)
3105 $this->autoSumm = md5( '' );
3106 }
3107
3108 $autosumm = $this->autoSumm !== '' ? $this->autoSumm : md5( $this->summary );
3109 $out->addHTML( Html::hidden( 'wpAutoSummary', $autosumm ) );
3110
3111 $out->addHTML( Html::hidden( 'oldid', $this->oldid ) );
3112 $out->addHTML( Html::hidden( 'parentRevId', $this->getParentRevId() ) );
3113
3114 $out->addHTML( Html::hidden( 'format', $this->contentFormat ) );
3115 $out->addHTML( Html::hidden( 'model', $this->contentModel ) );
3116
3117 $out->enableOOUI();
3118
3119 if ( $this->section == 'new' ) {
3120 $this->showSummaryInput( true, $this->summary );
3121 $out->addHTML( $this->getSummaryPreview( true, $this->summary ) );
3122 }
3123
3124 $out->addHTML( $this->editFormTextBeforeContent );
3125 if ( $this->isConflict ) {
3126 // In an edit conflict, we turn textbox2 into the user's text,
3127 // and textbox1 into the stored version
3128 $this->textbox2 = $this->textbox1;
3129
3130 $content = $this->getCurrentContent();
3131 $this->textbox1 = $this->toEditText( $content );
3132
3134 $editConflictHelper->setTextboxes( $this->textbox2, $this->textbox1 );
3135 $editConflictHelper->setContentModel( $this->contentModel );
3136 $editConflictHelper->setContentFormat( $this->contentFormat );
3138 }
3139
3140 if ( !$this->mTitle->isUserConfigPage() ) {
3141 $out->addHTML( self::getEditToolbar() );
3142 }
3143
3144 if ( $this->blankArticle ) {
3145 $out->addHTML( Html::hidden( 'wpIgnoreBlankArticle', true ) );
3146 }
3147
3148 if ( $this->isConflict ) {
3149 // In an edit conflict bypass the overridable content form method
3150 // and fallback to the raw wpTextbox1 since editconflicts can't be
3151 // resolved between page source edits and custom ui edits using the
3152 // custom edit ui.
3153 $conflictTextBoxAttribs = [];
3154 if ( $this->wasDeletedSinceLastEdit() ) {
3155 $conflictTextBoxAttribs['style'] = 'display:none;';
3156 } elseif ( $this->isOldRev ) {
3157 $conflictTextBoxAttribs['class'] = 'mw-textarea-oldrev';
3158 }
3159
3160 $out->addHTML( $editConflictHelper->getEditConflictMainTextBox( $conflictTextBoxAttribs ) );
3162 } else {
3163 $this->showContentForm();
3164 }
3165
3166 $out->addHTML( $this->editFormTextAfterContent );
3167
3168 $this->showStandardInputs();
3169
3170 $this->showFormAfterText();
3171
3172 $this->showTosSummary();
3173
3174 $this->showEditTools();
3175
3176 $out->addHTML( $this->editFormTextAfterTools . "\n" );
3177
3178 $out->addHTML( $this->makeTemplatesOnThisPageList( $this->getTemplates() ) );
3179
3180 $out->addHTML( Html::rawElement( 'div', [ 'class' => 'hiddencats' ],
3181 Linker::formatHiddenCategories( $this->page->getHiddenCategories() ) ) );
3182
3183 $out->addHTML( Html::rawElement( 'div', [ 'class' => 'limitreport' ],
3184 self::getPreviewLimitReport( $this->mParserOutput ) ) );
3185
3186 $out->addModules( 'mediawiki.action.edit.collapsibleFooter' );
3187
3188 if ( $this->isConflict ) {
3189 try {
3190 $this->showConflict();
3191 } catch ( MWContentSerializationException $ex ) {
3192 // this can't really happen, but be nice if it does.
3193 $msg = $this->context->msg(
3194 'content-failed-to-parse',
3195 $this->contentModel,
3196 $this->contentFormat,
3197 $ex->getMessage()
3198 );
3199 $out->wrapWikiTextAsInterface( 'error', $msg->plain() );
3200 }
3201 }
3202
3203 // Set a hidden field so JS knows what edit form mode we are in
3204 if ( $this->isConflict ) {
3205 $mode = 'conflict';
3206 } elseif ( $this->preview ) {
3207 $mode = 'preview';
3208 } elseif ( $this->diff ) {
3209 $mode = 'diff';
3210 } else {
3211 $mode = 'text';
3212 }
3213 $out->addHTML( Html::hidden( 'mode', $mode, [ 'id' => 'mw-edit-mode' ] ) );
3214
3215 // Marker for detecting truncated form data. This must be the last
3216 // parameter sent in order to be of use, so do not move me.
3217 $out->addHTML( Html::hidden( 'wpUltimateParam', true ) );
3218 $out->addHTML( $this->editFormTextBottom . "\n</form>\n" );
3219
3220 if ( !$user->getOption( 'previewontop' ) ) {
3221 $this->displayPreviewArea( $previewOutput, false );
3222 }
3223 }
3224
3232 public function makeTemplatesOnThisPageList( array $templates ) {
3233 $templateListFormatter = new TemplatesOnThisPageFormatter(
3234 $this->context, MediaWikiServices::getInstance()->getLinkRenderer()
3235 );
3236
3237 // preview if preview, else section if section, else false
3238 $type = false;
3239 if ( $this->preview ) {
3240 $type = 'preview';
3241 } elseif ( $this->section != '' ) {
3242 $type = 'section';
3243 }
3244
3245 return Html::rawElement( 'div', [ 'class' => 'templatesUsed' ],
3246 $templateListFormatter->format( $templates, $type )
3247 );
3248 }
3249
3256 public static function extractSectionTitle( $text ) {
3257 preg_match( "/^(=+)(.+)\\1\\s*(\n|$)/i", $text, $matches );
3258 if ( !empty( $matches[2] ) ) {
3259 return MediaWikiServices::getInstance()->getParser()
3260 ->stripSectionName( trim( $matches[2] ) );
3261 } else {
3262 return false;
3263 }
3264 }
3265
3266 protected function showHeader() {
3267 $out = $this->context->getOutput();
3268 $user = $this->context->getUser();
3269 if ( $this->isConflict ) {
3270 $this->addExplainConflictHeader( $out );
3271 $this->editRevId = $this->page->getLatest();
3272 } else {
3273 if ( $this->section != '' && $this->section != 'new' && !$this->summary &&
3274 !$this->preview && !$this->diff
3275 ) {
3276 $sectionTitle = self::extractSectionTitle( $this->textbox1 ); // FIXME: use Content object
3277 if ( $sectionTitle !== false ) {
3278 $this->summary = "/* $sectionTitle */ ";
3279 }
3280 }
3281
3282 $buttonLabel = $this->context->msg( $this->getSubmitButtonLabel() )->text();
3283
3284 if ( $this->missingComment ) {
3285 $out->wrapWikiMsg( "<div id='mw-missingcommenttext'>\n$1\n</div>", 'missingcommenttext' );
3286 }
3287
3288 if ( $this->missingSummary && $this->section != 'new' ) {
3289 $out->wrapWikiMsg(
3290 "<div id='mw-missingsummary'>\n$1\n</div>",
3291 [ 'missingsummary', $buttonLabel ]
3292 );
3293 }
3294
3295 if ( $this->missingSummary && $this->section == 'new' ) {
3296 $out->wrapWikiMsg(
3297 "<div id='mw-missingcommentheader'>\n$1\n</div>",
3298 [ 'missingcommentheader', $buttonLabel ]
3299 );
3300 }
3301
3302 if ( $this->blankArticle ) {
3303 $out->wrapWikiMsg(
3304 "<div id='mw-blankarticle'>\n$1\n</div>",
3305 [ 'blankarticle', $buttonLabel ]
3306 );
3307 }
3308
3309 if ( $this->selfRedirect ) {
3310 $out->wrapWikiMsg(
3311 "<div id='mw-selfredirect'>\n$1\n</div>",
3312 [ 'selfredirect', $buttonLabel ]
3313 );
3314 }
3315
3316 if ( $this->hookError !== '' ) {
3317 $out->addWikiTextAsInterface( $this->hookError );
3318 }
3319
3320 if ( $this->section != 'new' ) {
3321 $revRecord = $this->mArticle->fetchRevisionRecord();
3322 if ( $revRecord && $revRecord instanceof RevisionStoreRecord ) {
3323 // Let sysop know that this will make private content public if saved
3324
3325 if ( !RevisionRecord::userCanBitfield(
3326 $revRecord->getVisibility(),
3327 RevisionRecord::DELETED_TEXT,
3328 $user
3329 ) ) {
3330 $out->wrapWikiMsg(
3331 "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
3332 'rev-deleted-text-permission'
3333 );
3334 } elseif ( $revRecord->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
3335 $out->wrapWikiMsg(
3336 "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
3337 'rev-deleted-text-view'
3338 );
3339 }
3340
3341 if ( !$revRecord->isCurrent() ) {
3342 $this->mArticle->setOldSubtitle( $revRecord->getId() );
3343 $out->wrapWikiMsg(
3344 Html::warningBox( "\n$1\n" ),
3345 'editingold'
3346 );
3347 $this->isOldRev = true;
3348 }
3349 } elseif ( $this->mTitle->exists() ) {
3350 // Something went wrong
3351
3352 $out->wrapWikiMsg( "<div class='errorbox'>\n$1\n</div>\n",
3353 [ 'missing-revision', $this->oldid ] );
3354 }
3355 }
3356 }
3357
3358 if ( wfReadOnly() ) {
3359 $out->wrapWikiMsg(
3360 "<div id=\"mw-read-only-warning\">\n$1\n</div>",
3361 [ 'readonlywarning', wfReadOnlyReason() ]
3362 );
3363 } elseif ( $user->isAnon() ) {
3364 if ( $this->formtype != 'preview' ) {
3365 $returntoquery = array_diff_key(
3366 $this->context->getRequest()->getValues(),
3367 [ 'title' => true, 'returnto' => true, 'returntoquery' => true ]
3368 );
3369 $out->wrapWikiMsg(
3370 "<div id='mw-anon-edit-warning' class='warningbox'>\n$1\n</div>",
3371 [ 'anoneditwarning',
3372 // Log-in link
3373 SpecialPage::getTitleFor( 'Userlogin' )->getFullURL( [
3374 'returnto' => $this->getTitle()->getPrefixedDBkey(),
3375 'returntoquery' => wfArrayToCgi( $returntoquery ),
3376 ] ),
3377 // Sign-up link
3378 SpecialPage::getTitleFor( 'CreateAccount' )->getFullURL( [
3379 'returnto' => $this->getTitle()->getPrefixedDBkey(),
3380 'returntoquery' => wfArrayToCgi( $returntoquery ),
3381 ] )
3382 ]
3383 );
3384 } else {
3385 $out->wrapWikiMsg( "<div id=\"mw-anon-preview-warning\" class=\"warningbox\">\n$1</div>",
3386 'anonpreviewwarning'
3387 );
3388 }
3389 } elseif ( $this->mTitle->isUserConfigPage() ) {
3390 # Check the skin exists
3391 if ( $this->isWrongCaseUserConfigPage() ) {
3392 $out->wrapWikiMsg(
3393 "<div class='error' id='mw-userinvalidconfigtitle'>\n$1\n</div>",
3394 [ 'userinvalidconfigtitle', $this->mTitle->getSkinFromConfigSubpage() ]
3395 );
3396 }
3397 if ( $this->getTitle()->isSubpageOf( $user->getUserPage() ) ) {
3398 $isUserCssConfig = $this->mTitle->isUserCssConfigPage();
3399 $isUserJsonConfig = $this->mTitle->isUserJsonConfigPage();
3400 $isUserJsConfig = $this->mTitle->isUserJsConfigPage();
3401
3402 $warning = $isUserCssConfig
3403 ? 'usercssispublic'
3404 : ( $isUserJsonConfig ? 'userjsonispublic' : 'userjsispublic' );
3405
3406 $out->wrapWikiMsg( '<div class="mw-userconfigpublic">$1</div>', $warning );
3407
3408 if ( $isUserJsConfig ) {
3409 $out->wrapWikiMsg( '<div class="mw-userconfigdangerous">$1</div>', 'userjsdangerous' );
3410 }
3411
3412 if ( $this->formtype !== 'preview' ) {
3413 $config = $this->context->getConfig();
3414 if ( $isUserCssConfig && $config->get( 'AllowUserCss' ) ) {
3415 $out->wrapWikiMsg(
3416 "<div id='mw-usercssyoucanpreview'>\n$1\n</div>",
3417 [ 'usercssyoucanpreview' ]
3418 );
3419 } elseif ( $isUserJsonConfig /* No comparable 'AllowUserJson' */ ) {
3420 $out->wrapWikiMsg(
3421 "<div id='mw-userjsonyoucanpreview'>\n$1\n</div>",
3422 [ 'userjsonyoucanpreview' ]
3423 );
3424 } elseif ( $isUserJsConfig && $config->get( 'AllowUserJs' ) ) {
3425 $out->wrapWikiMsg(
3426 "<div id='mw-userjsyoucanpreview'>\n$1\n</div>",
3427 [ 'userjsyoucanpreview' ]
3428 );
3429 }
3430 }
3431 }
3432 }
3433
3435
3436 $this->addLongPageWarningHeader();
3437
3438 # Add header copyright warning
3440 }
3441
3449 private function getSummaryInputAttributes( array $inputAttrs = null ) {
3450 // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
3451 // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
3452 // Unicode codepoints.
3453 return ( is_array( $inputAttrs ) ? $inputAttrs : [] ) + [
3454 'id' => 'wpSummary',
3455 'name' => 'wpSummary',
3456 'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT,
3457 'tabindex' => 1,
3458 'size' => 60,
3459 'spellcheck' => 'true',
3460 ];
3461 }
3462
3472 public function getSummaryInputWidget( $summary = "", $labelText = null, $inputAttrs = null ) {
3473 $inputAttrs = OOUI\Element::configFromHtmlAttributes(
3474 $this->getSummaryInputAttributes( $inputAttrs )
3475 );
3476 $inputAttrs += [
3477 'title' => Linker::titleAttrib( 'summary' ),
3478 'accessKey' => Linker::accesskey( 'summary' ),
3479 ];
3480
3481 // For compatibility with old scripts and extensions, we want the legacy 'id' on the `<input>`
3482 $inputAttrs['inputId'] = $inputAttrs['id'];
3483 $inputAttrs['id'] = 'wpSummaryWidget';
3484
3485 return new OOUI\FieldLayout(
3486 new OOUI\TextInputWidget( [
3487 'value' => $summary,
3488 'infusable' => true,
3489 ] + $inputAttrs ),
3490 [
3491 'label' => new OOUI\HtmlSnippet( $labelText ),
3492 'align' => 'top',
3493 'id' => 'wpSummaryLabel',
3494 'classes' => [ $this->missingSummary ? 'mw-summarymissed' : 'mw-summary' ],
3495 ]
3496 );
3497 }
3498
3505 protected function showSummaryInput( $isSubjectPreview, $summary = "" ) {
3506 # Add a class if 'missingsummary' is triggered to allow styling of the summary line
3507 $summaryClass = $this->missingSummary ? 'mw-summarymissed' : 'mw-summary';
3508 if ( $isSubjectPreview ) {
3509 if ( $this->nosummary ) {
3510 return;
3511 }
3512 } elseif ( !$this->mShowSummaryField ) {
3513 return;
3514 }
3515
3516 $labelText = $this->context->msg( $isSubjectPreview ? 'subject' : 'summary' )->parse();
3517 $this->context->getOutput()->addHTML( $this->getSummaryInputWidget(
3518 $summary,
3519 $labelText,
3520 [ 'class' => $summaryClass ]
3521 ) );
3522 }
3523
3531 protected function getSummaryPreview( $isSubjectPreview, $summary = "" ) {
3532 // avoid spaces in preview, gets always trimmed on save
3533 $summary = trim( $summary );
3534 if ( !$summary || ( !$this->preview && !$this->diff ) ) {
3535 return "";
3536 }
3537
3538 if ( $isSubjectPreview ) {
3539 $summary = $this->context->msg( 'newsectionsummary' )
3540 ->rawParams( MediaWikiServices::getInstance()->getParser()
3541 ->stripSectionName( $summary ) )
3542 ->inContentLanguage()->text();
3543 }
3544
3545 $message = $isSubjectPreview ? 'subject-preview' : 'summary-preview';
3546
3547 $summary = $this->context->msg( $message )->parse()
3548 . Linker::commentBlock( $summary, $this->mTitle, $isSubjectPreview );
3549 return Xml::tags( 'div', [ 'class' => 'mw-summary-preview' ], $summary );
3550 }
3551
3552 protected function showFormBeforeText() {
3553 $out = $this->context->getOutput();
3554 $out->addHTML( Html::hidden( 'wpSection', $this->section ) );
3555 $out->addHTML( Html::hidden( 'wpStarttime', $this->starttime ) );
3556 $out->addHTML( Html::hidden( 'wpEdittime', $this->edittime ) );
3557 $out->addHTML( Html::hidden( 'editRevId', $this->editRevId ) );
3558 $out->addHTML( Html::hidden( 'wpScrolltop', $this->scrolltop, [ 'id' => 'wpScrolltop' ] ) );
3559 }
3560
3561 protected function showFormAfterText() {
3574 $this->context->getOutput()->addHTML(
3575 "\n" .
3576 Html::hidden( "wpEditToken", $this->context->getUser()->getEditToken() ) .
3577 "\n"
3578 );
3579 }
3580
3589 protected function showContentForm() {
3590 $this->showTextbox1();
3591 }
3592
3601 protected function showTextbox1( $customAttribs = null, $textoverride = null ) {
3602 if ( $this->wasDeletedSinceLastEdit() && $this->formtype == 'save' ) {
3603 $attribs = [ 'style' => 'display:none;' ];
3604 } else {
3605 $builder = new TextboxBuilder();
3606 $classes = $builder->getTextboxProtectionCSSClasses( $this->getTitle() );
3607
3608 # Is an old revision being edited?
3609 if ( $this->isOldRev ) {
3610 $classes[] = 'mw-textarea-oldrev';
3611 }
3612
3613 $attribs = [
3614 'aria-label' => $this->context->msg( 'edit-textarea-aria-label' )->text(),
3615 'tabindex' => 1
3616 ];
3617
3618 if ( is_array( $customAttribs ) ) {
3619 $attribs += $customAttribs;
3620 }
3621
3622 $attribs = $builder->mergeClassesIntoAttributes( $classes, $attribs );
3623 }
3624
3625 $this->showTextbox(
3626 $textoverride ?? $this->textbox1,
3627 'wpTextbox1',
3628 $attribs
3629 );
3630 }
3631
3632 protected function showTextbox2() {
3633 $this->showTextbox( $this->textbox2, 'wpTextbox2', [ 'tabindex' => 6, 'readonly' ] );
3634 }
3635
3636 protected function showTextbox( $text, $name, $customAttribs = [] ) {
3637 $builder = new TextboxBuilder();
3638 $attribs = $builder->buildTextboxAttribs(
3639 $name,
3640 $customAttribs,
3641 $this->context->getUser(),
3642 $this->mTitle
3643 );
3644
3645 $this->context->getOutput()->addHTML(
3646 Html::textarea( $name, $builder->addNewLineAtEnd( $text ), $attribs )
3647 );
3648 }
3649
3650 protected function displayPreviewArea( $previewOutput, $isOnTop = false ) {
3651 $classes = [];
3652 if ( $isOnTop ) {
3653 $classes[] = 'ontop';
3654 }
3655
3656 $attribs = [ 'id' => 'wikiPreview', 'class' => implode( ' ', $classes ) ];
3657
3658 if ( $this->formtype != 'preview' ) {
3659 $attribs['style'] = 'display: none;';
3660 }
3661
3662 $out = $this->context->getOutput();
3663 $out->addHTML( Xml::openElement( 'div', $attribs ) );
3664
3665 if ( $this->formtype == 'preview' ) {
3666 $this->showPreview( $previewOutput );
3667 } else {
3668 // Empty content container for LivePreview
3669 $pageViewLang = $this->mTitle->getPageViewLanguage();
3670 $attribs = [ 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(),
3671 'class' => 'mw-content-' . $pageViewLang->getDir() ];
3672 $out->addHTML( Html::rawElement( 'div', $attribs ) );
3673 }
3674
3675 $out->addHTML( '</div>' );
3676
3677 if ( $this->formtype == 'diff' ) {
3678 try {
3679 $this->showDiff();
3680 } catch ( MWContentSerializationException $ex ) {
3681 $msg = $this->context->msg(
3682 'content-failed-to-parse',
3683 $this->contentModel,
3684 $this->contentFormat,
3685 $ex->getMessage()
3686 );
3687 $out->wrapWikiTextAsInterface( 'error', $msg->plain() );
3688 }
3689 }
3690 }
3691
3698 protected function showPreview( $text ) {
3699 if ( $this->mArticle instanceof CategoryPage ) {
3700 $this->mArticle->openShowCategory();
3701 }
3702 # This hook seems slightly odd here, but makes things more
3703 # consistent for extensions.
3704 $out = $this->context->getOutput();
3705 $this->getHookRunner()->onOutputPageBeforeHTML( $out, $text );
3706 $out->addHTML( $text );
3707 if ( $this->mArticle instanceof CategoryPage ) {
3708 $this->mArticle->closeShowCategory();
3709 }
3710 }
3711
3719 public function showDiff() {
3720 $oldtitlemsg = 'currentrev';
3721 # if message does not exist, show diff against the preloaded default
3722 if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && !$this->mTitle->exists() ) {
3723 $oldtext = $this->mTitle->getDefaultMessageText();
3724 if ( $oldtext !== false ) {
3725 $oldtitlemsg = 'defaultmessagetext';
3726 $oldContent = $this->toEditContent( $oldtext );
3727 } else {
3728 $oldContent = null;
3729 }
3730 } else {
3731 $oldContent = $this->getCurrentContent();
3732 }
3733
3734 $textboxContent = $this->toEditContent( $this->textbox1 );
3735 if ( $this->editRevId !== null ) {
3736 $newContent = $this->page->replaceSectionAtRev(
3737 $this->section, $textboxContent, $this->summary, $this->editRevId
3738 );
3739 } else {
3740 $newContent = $this->page->replaceSectionContent(
3741 $this->section, $textboxContent, $this->summary, $this->edittime
3742 );
3743 }
3744
3745 if ( $newContent ) {
3746 $this->getHookRunner()->onEditPageGetDiffContent( $this, $newContent );
3747
3748 $user = $this->context->getUser();
3749 $popts = ParserOptions::newFromUserAndLang( $user,
3750 MediaWikiServices::getInstance()->getContentLanguage() );
3751 $newContent = $newContent->preSaveTransform( $this->mTitle, $user, $popts );
3752 }
3753
3754 if ( ( $oldContent && !$oldContent->isEmpty() ) || ( $newContent && !$newContent->isEmpty() ) ) {
3755 $oldtitle = $this->context->msg( $oldtitlemsg )->parse();
3756 $newtitle = $this->context->msg( 'yourtext' )->parse();
3757
3758 if ( !$oldContent ) {
3759 $oldContent = $newContent->getContentHandler()->makeEmptyContent();
3760 }
3761
3762 if ( !$newContent ) {
3763 $newContent = $oldContent->getContentHandler()->makeEmptyContent();
3764 }
3765
3766 $de = $oldContent->getContentHandler()->createDifferenceEngine( $this->context );
3767 $de->setContent( $oldContent, $newContent );
3768
3769 $difftext = $de->getDiff( $oldtitle, $newtitle );
3770 $de->showDiffStyle();
3771 } else {
3772 $difftext = '';
3773 }
3774
3775 $this->context->getOutput()->addHTML( '<div id="wikiDiff">' . $difftext . '</div>' );
3776 }
3777
3781 protected function showHeaderCopyrightWarning() {
3782 $msg = 'editpage-head-copy-warn';
3783 if ( !$this->context->msg( $msg )->isDisabled() ) {
3784 $this->context->getOutput()->wrapWikiMsg( "<div class='editpage-head-copywarn'>\n$1\n</div>",
3785 'editpage-head-copy-warn' );
3786 }
3787 }
3788
3797 protected function showTosSummary() {
3798 $msg = 'editpage-tos-summary';
3799 $this->getHookRunner()->onEditPageTosSummary( $this->mTitle, $msg );
3800 if ( !$this->context->msg( $msg )->isDisabled() ) {
3801 $out = $this->context->getOutput();
3802 $out->addHTML( '<div class="mw-tos-summary">' );
3803 $out->addWikiMsg( $msg );
3804 $out->addHTML( '</div>' );
3805 }
3806 }
3807
3812 protected function showEditTools() {
3813 $this->context->getOutput()->addHTML( '<div class="mw-editTools">' .
3814 $this->context->msg( 'edittools' )->inContentLanguage()->parse() .
3815 '</div>' );
3816 }
3817
3824 protected function getCopywarn() {
3825 return self::getCopyrightWarning( $this->mTitle );
3826 }
3827
3836 public static function getCopyrightWarning( $title, $format = 'plain', $langcode = null ) {
3837 global $wgRightsText;
3838 if ( $wgRightsText ) {
3839 $copywarnMsg = [ 'copyrightwarning',
3840 '[[' . wfMessage( 'copyrightpage' )->inContentLanguage()->text() . ']]',
3841 $wgRightsText ];
3842 } else {
3843 $copywarnMsg = [ 'copyrightwarning2',
3844 '[[' . wfMessage( 'copyrightpage' )->inContentLanguage()->text() . ']]' ];
3845 }
3846 // Allow for site and per-namespace customization of contribution/copyright notice.
3847 Hooks::runner()->onEditPageCopyrightWarning( $title, $copywarnMsg );
3848
3849 $msg = wfMessage( ...$copywarnMsg )->title( $title );
3850 if ( $langcode ) {
3851 $msg->inLanguage( $langcode );
3852 }
3853 return "<div id=\"editpage-copywarn\">\n" .
3854 $msg->$format() . "\n</div>";
3855 }
3856
3864 public static function getPreviewLimitReport( ParserOutput $output = null ) {
3865 global $wgLang;
3866
3867 if ( !$output || !$output->getLimitReportData() ) {
3868 return '';
3869 }
3870
3871 $limitReport = Html::rawElement( 'div', [ 'class' => 'mw-limitReportExplanation' ],
3872 wfMessage( 'limitreport-title' )->parseAsBlock()
3873 );
3874
3875 // Show/hide animation doesn't work correctly on a table, so wrap it in a div.
3876 $limitReport .= Html::openElement( 'div', [ 'class' => 'preview-limit-report-wrapper' ] );
3877
3878 $limitReport .= Html::openElement( 'table', [
3879 'class' => 'preview-limit-report wikitable'
3880 ] ) .
3881 Html::openElement( 'tbody' );
3882
3883 foreach ( $output->getLimitReportData() as $key => $value ) {
3884 if ( Hooks::runner()->onParserLimitReportFormat( $key, $value, $limitReport, true, true ) ) {
3885 $keyMsg = wfMessage( $key );
3886 $valueMsg = wfMessage( [ "$key-value-html", "$key-value" ] );
3887 if ( !$valueMsg->exists() ) {
3888 $valueMsg = new RawMessage( '$1' );
3889 }
3890 if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) {
3891 $limitReport .= Html::openElement( 'tr' ) .
3892 Html::rawElement( 'th', null, $keyMsg->parse() ) .
3893 Html::rawElement( 'td', null,
3894 $wgLang->formatNum( $valueMsg->params( $value )->parse() )
3895 ) .
3896 Html::closeElement( 'tr' );
3897 }
3898 }
3899 }
3900
3901 $limitReport .= Html::closeElement( 'tbody' ) .
3902 Html::closeElement( 'table' ) .
3903 Html::closeElement( 'div' );
3904
3905 return $limitReport;
3906 }
3907
3908 protected function showStandardInputs( &$tabindex = 2 ) {
3909 $out = $this->context->getOutput();
3910 $out->addHTML( "<div class='editOptions'>\n" );
3911
3912 if ( $this->section != 'new' ) {
3913 $this->showSummaryInput( false, $this->summary );
3914 $out->addHTML( $this->getSummaryPreview( false, $this->summary ) );
3915 }
3916
3917 $checkboxes = $this->getCheckboxesWidget(
3918 $tabindex,
3919 [ 'minor' => $this->minoredit, 'watch' => $this->watchthis ]
3920 );
3921 $checkboxesHTML = new OOUI\HorizontalLayout( [ 'items' => $checkboxes ] );
3922
3923 $out->addHTML( "<div class='editCheckboxes'>" . $checkboxesHTML . "</div>\n" );
3924
3925 // Show copyright warning.
3926 $out->addWikiTextAsInterface( $this->getCopywarn() );
3927 $out->addHTML( $this->editFormTextAfterWarn );
3928
3929 $out->addHTML( "<div class='editButtons'>\n" );
3930 $out->addHTML( implode( "\n", $this->getEditButtons( $tabindex ) ) . "\n" );
3931
3932 $cancel = $this->getCancelLink( $tabindex++ );
3933
3934 $message = $this->context->msg( 'edithelppage' )->inContentLanguage()->text();
3935 $edithelpurl = Skin::makeInternalOrExternalUrl( $message );
3936 $edithelp =
3937 Html::linkButton(
3938 $this->context->msg( 'edithelp' )->text(),
3939 [ 'target' => 'helpwindow', 'href' => $edithelpurl ],
3940 [ 'mw-ui-quiet' ]
3941 ) .
3942 $this->context->msg( 'word-separator' )->escaped() .
3943 $this->context->msg( 'newwindow' )->parse();
3944
3945 $out->addHTML( " <span class='cancelLink'>{$cancel}</span>\n" );
3946 $out->addHTML( " <span class='editHelp'>{$edithelp}</span>\n" );
3947 $out->addHTML( "</div><!-- editButtons -->\n" );
3948
3949 $this->getHookRunner()->onEditPage__showStandardInputs_options( $this, $out, $tabindex );
3950
3951 $out->addHTML( "</div><!-- editOptions -->\n" );
3952 }
3953
3958 protected function showConflict() {
3959 $out = $this->context->getOutput();
3960 // Avoid PHP 7.1 warning of passing $this by reference
3961 $editPage = $this;
3962 if ( $this->getHookRunner()->onEditPageBeforeConflictDiff( $editPage, $out ) ) {
3963 $this->incrementConflictStats();
3964
3965 $this->getEditConflictHelper()->showEditFormTextAfterFooters();
3966 }
3967 }
3968
3969 protected function incrementConflictStats() {
3970 $this->getEditConflictHelper()->incrementConflictStats( $this->context->getUser() );
3971 }
3972
3977 public function getCancelLink( $tabindex = 0 ) {
3978 $cancelParams = [];
3979 if ( !$this->isConflict && $this->oldid > 0 ) {
3980 $cancelParams['oldid'] = $this->oldid;
3981 } elseif ( $this->getContextTitle()->isRedirect() ) {
3982 $cancelParams['redirect'] = 'no';
3983 }
3984
3985 return new OOUI\ButtonWidget( [
3986 'id' => 'mw-editform-cancel',
3987 'tabIndex' => $tabindex,
3988 'href' => $this->getContextTitle()->getLinkURL( $cancelParams ),
3989 'label' => new OOUI\HtmlSnippet( $this->context->msg( 'cancel' )->parse() ),
3990 'framed' => false,
3991 'infusable' => true,
3992 'flags' => 'destructive',
3993 ] );
3994 }
3995
4005 protected function getActionURL( Title $title ) {
4006 return $title->getLocalURL( [ 'action' => $this->action ] );
4007 }
4008
4016 protected function wasDeletedSinceLastEdit() {
4017 if ( $this->deletedSinceEdit !== null ) {
4019 }
4020
4021 $this->deletedSinceEdit = false;
4022
4023 if ( !$this->mTitle->exists() && $this->mTitle->isDeletedQuick() ) {
4024 $this->lastDelete = $this->getLastDelete();
4025 if ( $this->lastDelete ) {
4026 $deleteTime = wfTimestamp( TS_MW, $this->lastDelete->log_timestamp );
4027 if ( $deleteTime > $this->starttime ) {
4028 $this->deletedSinceEdit = true;
4029 }
4030 }
4031 }
4032
4034 }
4035
4041 protected function getLastDelete() {
4042 $dbr = wfGetDB( DB_REPLICA );
4043 $commentQuery = CommentStore::getStore()->getJoin( 'log_comment' );
4044 $actorQuery = ActorMigration::newMigration()->getJoin( 'log_user' );
4045 $data = $dbr->selectRow(
4046 array_merge( [ 'logging' ], $commentQuery['tables'], $actorQuery['tables'], [ 'user' ] ),
4047 [
4048 'log_type',
4049 'log_action',
4050 'log_timestamp',
4051 'log_namespace',
4052 'log_title',
4053 'log_params',
4054 'log_deleted',
4055 'user_name'
4056 ] + $commentQuery['fields'] + $actorQuery['fields'],
4057 [
4058 'log_namespace' => $this->mTitle->getNamespace(),
4059 'log_title' => $this->mTitle->getDBkey(),
4060 'log_type' => 'delete',
4061 'log_action' => 'delete',
4062 ],
4063 __METHOD__,
4064 [ 'LIMIT' => 1, 'ORDER BY' => 'log_timestamp DESC' ],
4065 [
4066 'user' => [ 'JOIN', 'user_id=' . $actorQuery['fields']['log_user'] ],
4067 ] + $commentQuery['joins'] + $actorQuery['joins']
4068 );
4069 // Quick paranoid permission checks...
4070 if ( is_object( $data ) ) {
4071 if ( $data->log_deleted & LogPage::DELETED_USER ) {
4072 $data->user_name = $this->context->msg( 'rev-deleted-user' )->escaped();
4073 }
4074
4075 if ( $data->log_deleted & LogPage::DELETED_COMMENT ) {
4076 $data->log_comment_text = $this->context->msg( 'rev-deleted-comment' )->escaped();
4077 $data->log_comment_data = null;
4078 }
4079 }
4080
4081 return $data;
4082 }
4083
4089 public function getPreviewText() {
4090 $out = $this->context->getOutput();
4091 $config = $this->context->getConfig();
4092
4093 if ( $config->get( 'RawHtml' ) && !$this->mTokenOk ) {
4094 // Could be an offsite preview attempt. This is very unsafe if
4095 // HTML is enabled, as it could be an attack.
4096 $parsedNote = '';
4097 if ( $this->textbox1 !== '' ) {
4098 // Do not put big scary notice, if previewing the empty
4099 // string, which happens when you initially edit
4100 // a category page, due to automatic preview-on-open.
4101 $parsedNote = Html::rawElement( 'div', [ 'class' => 'previewnote' ],
4102 $out->parseAsInterface(
4103 $this->context->msg( 'session_fail_preview_html' )->plain()
4104 ) );
4105 }
4106 $this->incrementEditFailureStats( 'session_loss' );
4107 return $parsedNote;
4108 }
4109
4110 $note = '';
4111
4112 try {
4113 $content = $this->toEditContent( $this->textbox1 );
4114
4115 $previewHTML = '';
4116 if ( !$this->getHookRunner()->onAlternateEditPreview(
4117 $this, $content, $previewHTML, $this->mParserOutput )
4118 ) {
4119 return $previewHTML;
4120 }
4121
4122 # provide a anchor link to the editform
4123 $continueEditing = '<span class="mw-continue-editing">' .
4124 '[[#' . self::EDITFORM_ID . '|' .
4125 $this->context->getLanguage()->getArrow() . ' ' .
4126 $this->context->msg( 'continue-editing' )->text() . ']]</span>';
4127 if ( $this->mTriedSave && !$this->mTokenOk ) {
4128 if ( $this->mTokenOkExceptSuffix ) {
4129 $note = $this->context->msg( 'token_suffix_mismatch' )->plain();
4130 $this->incrementEditFailureStats( 'bad_token' );
4131 } else {
4132 $note = $this->context->msg( 'session_fail_preview' )->plain();
4133 $this->incrementEditFailureStats( 'session_loss' );
4134 }
4135 } elseif ( $this->incompleteForm ) {
4136 $note = $this->context->msg( 'edit_form_incomplete' )->plain();
4137 if ( $this->mTriedSave ) {
4138 $this->incrementEditFailureStats( 'incomplete_form' );
4139 }
4140 } else {
4141 $note = $this->context->msg( 'previewnote' )->plain() . ' ' . $continueEditing;
4142 }
4143
4144 # don't parse non-wikitext pages, show message about preview
4145 if ( $this->mTitle->isUserConfigPage() || $this->mTitle->isSiteConfigPage() ) {
4146 if ( $this->mTitle->isUserConfigPage() ) {
4147 $level = 'user';
4148 } elseif ( $this->mTitle->isSiteConfigPage() ) {
4149 $level = 'site';
4150 } else {
4151 $level = false;
4152 }
4153
4154 if ( $content->getModel() == CONTENT_MODEL_CSS ) {
4155 $format = 'css';
4156 if ( $level === 'user' && !$config->get( 'AllowUserCss' ) ) {
4157 $format = false;
4158 }
4159 } elseif ( $content->getModel() == CONTENT_MODEL_JSON ) {
4160 $format = 'json';
4161 if ( $level === 'user' /* No comparable 'AllowUserJson' */ ) {
4162 $format = false;
4163 }
4164 } elseif ( $content->getModel() == CONTENT_MODEL_JAVASCRIPT ) {
4165 $format = 'js';
4166 if ( $level === 'user' && !$config->get( 'AllowUserJs' ) ) {
4167 $format = false;
4168 }
4169 } else {
4170 $format = false;
4171 }
4172
4173 # Used messages to make sure grep find them:
4174 # Messages: usercsspreview, userjsonpreview, userjspreview,
4175 # sitecsspreview, sitejsonpreview, sitejspreview
4176 if ( $level && $format ) {
4177 $note = "<div id='mw-{$level}{$format}preview'>" .
4178 $this->context->msg( "{$level}{$format}preview" )->plain() .
4179 ' ' . $continueEditing . "</div>";
4180 }
4181 }
4182
4183 # If we're adding a comment, we need to show the
4184 # summary as the headline
4185 if ( $this->section === "new" && $this->summary !== "" ) {
4186 $content = $content->addSectionHeader( $this->summary );
4187 }
4188
4189 $this->getHookRunner()->onEditPageGetPreviewContent( $this, $content );
4190
4191 $parserResult = $this->doPreviewParse( $content );
4192 $parserOutput = $parserResult['parserOutput'];
4193 $previewHTML = $parserResult['html'];
4194 $this->mParserOutput = $parserOutput;
4195 $out->addParserOutputMetadata( $parserOutput );
4196 if ( $out->userCanPreview() ) {
4197 $out->addContentOverride( $this->getTitle(), $content );
4198 }
4199
4200 if ( count( $parserOutput->getWarnings() ) ) {
4201 $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
4202 }
4203
4204 } catch ( MWContentSerializationException $ex ) {
4205 $m = $this->context->msg(
4206 'content-failed-to-parse',
4207 $this->contentModel,
4208 $this->contentFormat,
4209 $ex->getMessage()
4210 );
4211 $note .= "\n\n" . $m->plain(); # gets parsed down below
4212 $previewHTML = '';
4213 }
4214
4215 if ( $this->isConflict ) {
4216 $conflict = Html::rawElement(
4217 'div', [ 'id' => 'mw-previewconflict', 'class' => 'warningbox' ],
4218 $this->context->msg( 'previewconflict' )->escaped()
4219 );
4220 } else {
4221 $conflict = '';
4222 }
4223
4224 $previewhead = Html::rawElement(
4225 'div', [ 'class' => 'previewnote' ],
4226 Html::rawElement(
4227 'h2', [ 'id' => 'mw-previewheader' ],
4228 $this->context->msg( 'preview' )->escaped()
4229 ) .
4230 Html::rawElement( 'div', [ 'class' => 'warningbox' ],
4231 $out->parseAsInterface( $note )
4232 ) . $conflict
4233 );
4234
4235 $pageViewLang = $this->mTitle->getPageViewLanguage();
4236 $attribs = [ 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(),
4237 'class' => 'mw-content-' . $pageViewLang->getDir() ];
4238 $previewHTML = Html::rawElement( 'div', $attribs, $previewHTML );
4239
4240 return $previewhead . $previewHTML . $this->previewTextAfterContent;
4241 }
4242
4243 private function incrementEditFailureStats( $failureType ) {
4244 $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
4245 $stats->increment( 'edit.failures.' . $failureType );
4246 }
4247
4252 protected function getPreviewParserOptions() {
4253 $parserOptions = $this->page->makeParserOptions( $this->context );
4254 $parserOptions->setIsPreview( true );
4255 $parserOptions->setIsSectionPreview( $this->section !== null && $this->section !== '' );
4256 $parserOptions->enableLimitReport();
4257
4258 // XXX: we could call $parserOptions->setCurrentRevisionCallback here to force the
4259 // current revision to be null during PST, until setupFakeRevision is called on
4260 // the ParserOptions. Currently, we rely on Parser::getRevisionObject() to ignore
4261 // existing revisions in preview mode.
4262
4263 return $parserOptions;
4264 }
4265
4275 protected function doPreviewParse( Content $content ) {
4276 $user = $this->context->getUser();
4277 $parserOptions = $this->getPreviewParserOptions();
4278
4279 // NOTE: preSaveTransform doesn't have a fake revision to operate on.
4280 // Parser::getRevisionObject() will return null in preview mode,
4281 // causing the context user to be used for {{subst:REVISIONUSER}}.
4282 // XXX: Alternatively, we could also call setupFakeRevision() a second time:
4283 // once before PST with $content, and then after PST with $pstContent.
4284 $pstContent = $content->preSaveTransform( $this->mTitle, $user, $parserOptions );
4285 $scopedCallback = $parserOptions->setupFakeRevision( $this->mTitle, $pstContent, $user );
4286 $parserOutput = $pstContent->getParserOutput( $this->mTitle, null, $parserOptions );
4287 ScopedCallback::consume( $scopedCallback );
4288 return [
4289 'parserOutput' => $parserOutput,
4290 'html' => $parserOutput->getText( [
4291 'enableSectionEditLinks' => false
4292 ] )
4293 ];
4294 }
4295
4299 public function getTemplates() {
4300 if ( $this->preview || $this->section != '' ) {
4301 $templates = [];
4302 if ( !isset( $this->mParserOutput ) ) {
4303 return $templates;
4304 }
4305 foreach ( $this->mParserOutput->getTemplates() as $ns => $template ) {
4306 foreach ( array_keys( $template ) as $dbk ) {
4307 $templates[] = Title::makeTitle( $ns, $dbk );
4308 }
4309 }
4310 return $templates;
4311 } else {
4312 return $this->mTitle->getTemplateLinksFrom();
4313 }
4314 }
4315
4321 public static function getEditToolbar() {
4322 $startingToolbar = '<div id="toolbar"></div>';
4323 $toolbar = $startingToolbar;
4324
4325 if ( !Hooks::runner()->onEditPageBeforeEditToolbar( $toolbar ) ) {
4326 return null;
4327 }
4328 // Don't add a pointless `<div>` to the page unless a hook caller populated it
4329 return ( $toolbar === $startingToolbar ) ? null : $toolbar;
4330 }
4331
4357 public function getCheckboxesDefinition( $checked ) {
4358 $checkboxes = [];
4359
4360 $user = $this->context->getUser();
4361 // don't show the minor edit checkbox if it's a new page or section
4362 if ( !$this->isNew && $this->permManager->userHasRight( $user, 'minoredit' ) ) {
4363 $checkboxes['wpMinoredit'] = [
4364 'id' => 'wpMinoredit',
4365 'label-message' => 'minoredit',
4366 // Uses messages: tooltip-minoredit, accesskey-minoredit
4367 'tooltip' => 'minoredit',
4368 'label-id' => 'mw-editpage-minoredit',
4369 'legacy-name' => 'minor',
4370 'default' => $checked['minor'],
4371 ];
4372 }
4373
4374 if ( $user->isLoggedIn() ) {
4375 $checkboxes = array_merge(
4376 $checkboxes,
4377 $this->getCheckboxesDefinitionForWatchlist( $checked['watch'] )
4378 );
4379 }
4380
4381 $this->getHookRunner()->onEditPageGetCheckboxesDefinition( $this, $checkboxes );
4382
4383 return $checkboxes;
4384 }
4385
4393 private function getCheckboxesDefinitionForWatchlist( $watch ) {
4394 $fieldDefs = [
4395 'wpWatchthis' => [
4396 'id' => 'wpWatchthis',
4397 'label-message' => 'watchthis',
4398 // Uses messages: tooltip-watch, accesskey-watch
4399 'tooltip' => 'watch',
4400 'label-id' => 'mw-editpage-watch',
4401 'legacy-name' => 'watch',
4402 'default' => $watch,
4403 ]
4404 ];
4405 if ( $this->watchlistExpiryEnabled ) {
4406 $watchedItem = $this->watchedItemStore->getWatchedItem( $this->getContext()->getUser(), $this->getTitle() );
4407 $expiryOptions = WatchAction::getExpiryOptions( $this->getContext(), $watchedItem );
4408 // When previewing, override the selected dropdown option to select whatever was posted
4409 // (if it's a valid option) rather than the current value for watchlistExpiry.
4410 // See also above in $this->importFormData().
4411 $expiryFromRequest = $this->getContext()->getRequest()->getText( 'wpWatchlistExpiry' );
4412 if ( $this->preview && in_array( $expiryFromRequest, $expiryOptions['options'] ) ) {
4413 $expiryOptions['default'] = $expiryFromRequest;
4414 }
4415 // Reformat the options to match what DropdownInputWidget wants.
4416 $options = [];
4417 foreach ( $expiryOptions['options'] as $label => $value ) {
4418 $options[] = [ 'data' => $value, 'label' => $label ];
4419 }
4420 $fieldDefs['wpWatchlistExpiry'] = [
4421 'id' => 'wpWatchlistExpiry',
4422 'label-message' => 'confirm-watch-label',
4423 // Uses messages: tooltip-watchlist-expiry, accesskey-watchlist-expiry
4424 'tooltip' => 'watchlist-expiry',
4425 'label-id' => 'mw-editpage-watchlist-expiry',
4426 'default' => $expiryOptions['default'],
4427 'value-attr' => 'value',
4428 'class' => DropdownInputWidget::class,
4429 'options' => $options,
4430 'invisibleLabel' => true,
4431 ];
4432 }
4433 return $fieldDefs;
4434 }
4435
4447 public function getCheckboxesWidget( &$tabindex, $checked ) {
4448 $checkboxes = [];
4449 $checkboxesDef = $this->getCheckboxesDefinition( $checked );
4450
4451 foreach ( $checkboxesDef as $name => $options ) {
4452 $legacyName = $options['legacy-name'] ?? $name;
4453
4454 $title = null;
4455 $accesskey = null;
4456 if ( isset( $options['tooltip'] ) ) {
4457 $accesskey = $this->context->msg( "accesskey-{$options['tooltip']}" )->text();
4458 $title = Linker::titleAttrib( $options['tooltip'] );
4459 }
4460 if ( isset( $options['title-message'] ) ) {
4461 $title = $this->context->msg( $options['title-message'] )->text();
4462 }
4463 // Allow checkbox definitions to set their own class and value-attribute names.
4464 // See $this->getCheckboxesDefinition() for details.
4465 $className = $options['class'] ?? CheckboxInputWidget::class;
4466 $valueAttr = $options['value-attr'] ?? 'selected';
4467 $checkboxes[ $legacyName ] = new FieldLayout(
4468 new $className( [
4469 'tabIndex' => ++$tabindex,
4470 'accessKey' => $accesskey,
4471 'id' => $options['id'] . 'Widget',
4472 'inputId' => $options['id'],
4473 'name' => $name,
4474 $valueAttr => $options['default'],
4475 'infusable' => true,
4476 'options' => $options['options'] ?? null,
4477 ] ),
4478 [
4479 'align' => 'inline',
4480 'label' => new OOUI\HtmlSnippet( $this->context->msg( $options['label-message'] )->parse() ),
4481 'title' => $title,
4482 'id' => $options['label-id'] ?? null,
4483 'invisibleLabel' => $options['invisibleLabel'] ?? null,
4484 ]
4485 );
4486 }
4487
4488 return $checkboxes;
4489 }
4490
4497 protected function getSubmitButtonLabel() {
4498 $labelAsPublish =
4499 $this->context->getConfig()->get( 'EditSubmitButtonLabelPublish' );
4500
4501 // Can't use $this->isNew as that's also true if we're adding a new section to an extant page
4502 $newPage = !$this->mTitle->exists();
4503
4504 if ( $labelAsPublish ) {
4505 $buttonLabelKey = $newPage ? 'publishpage' : 'publishchanges';
4506 } else {
4507 $buttonLabelKey = $newPage ? 'savearticle' : 'savechanges';
4508 }
4509
4510 return $buttonLabelKey;
4511 }
4512
4521 public function getEditButtons( &$tabindex ) {
4522 $buttons = [];
4523
4524 $labelAsPublish =
4525 $this->context->getConfig()->get( 'EditSubmitButtonLabelPublish' );
4526
4527 $buttonLabel = $this->context->msg( $this->getSubmitButtonLabel() )->text();
4528 $buttonTooltip = $labelAsPublish ? 'publish' : 'save';
4529
4530 $buttons['save'] = new OOUI\ButtonInputWidget( [
4531 'name' => 'wpSave',
4532 'tabIndex' => ++$tabindex,
4533 'id' => 'wpSaveWidget',
4534 'inputId' => 'wpSave',
4535 // Support: IE 6 – Use <input>, otherwise it can't distinguish which button was clicked
4536 'useInputTag' => true,
4537 'flags' => [ 'progressive', 'primary' ],
4538 'label' => $buttonLabel,
4539 'infusable' => true,
4540 'type' => 'submit',
4541 // Messages used: tooltip-save, tooltip-publish
4542 'title' => Linker::titleAttrib( $buttonTooltip ),
4543 // Messages used: accesskey-save, accesskey-publish
4544 'accessKey' => Linker::accesskey( $buttonTooltip ),
4545 ] );
4546
4547 $buttons['preview'] = new OOUI\ButtonInputWidget( [
4548 'name' => 'wpPreview',
4549 'tabIndex' => ++$tabindex,
4550 'id' => 'wpPreviewWidget',
4551 'inputId' => 'wpPreview',
4552 // Support: IE 6 – Use <input>, otherwise it can't distinguish which button was clicked
4553 'useInputTag' => true,
4554 'label' => $this->context->msg( 'showpreview' )->text(),
4555 'infusable' => true,
4556 'type' => 'submit',
4557 // Message used: tooltip-preview
4558 'title' => Linker::titleAttrib( 'preview' ),
4559 // Message used: accesskey-preview
4560 'accessKey' => Linker::accesskey( 'preview' ),
4561 ] );
4562
4563 $buttons['diff'] = new OOUI\ButtonInputWidget( [
4564 'name' => 'wpDiff',
4565 'tabIndex' => ++$tabindex,
4566 'id' => 'wpDiffWidget',
4567 'inputId' => 'wpDiff',
4568 // Support: IE 6 – Use <input>, otherwise it can't distinguish which button was clicked
4569 'useInputTag' => true,
4570 'label' => $this->context->msg( 'showdiff' )->text(),
4571 'infusable' => true,
4572 'type' => 'submit',
4573 // Message used: tooltip-diff
4574 'title' => Linker::titleAttrib( 'diff' ),
4575 // Message used: accesskey-diff
4576 'accessKey' => Linker::accesskey( 'diff' ),
4577 ] );
4578
4579 $this->getHookRunner()->onEditPageBeforeEditButtons( $this, $buttons, $tabindex );
4580
4581 return $buttons;
4582 }
4583
4588 public function noSuchSectionPage() {
4589 $out = $this->context->getOutput();
4590 $out->prepareErrorPage( $this->context->msg( 'nosuchsectiontitle' ) );
4591
4592 $res = $this->context->msg( 'nosuchsectiontext', $this->section )->parseAsBlock();
4593
4594 $this->getHookRunner()->onEditPageNoSuchSection( $this, $res );
4595 $out->addHTML( $res );
4596
4597 $out->returnToMain( false, $this->mTitle );
4598 }
4599
4605 public function spamPageWithContent( $match = false ) {
4606 $this->textbox2 = $this->textbox1;
4607
4608 if ( is_array( $match ) ) {
4609 $match = $this->context->getLanguage()->listToText( $match );
4610 }
4611 $out = $this->context->getOutput();
4612 $out->prepareErrorPage( $this->context->msg( 'spamprotectiontitle' ) );
4613
4614 $out->addHTML( '<div id="spamprotected">' );
4615 $out->addWikiMsg( 'spamprotectiontext' );
4616 if ( $match ) {
4617 $out->addWikiMsg( 'spamprotectionmatch', wfEscapeWikiText( $match ) );
4618 }
4619 $out->addHTML( '</div>' );
4620
4621 $out->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
4622 $this->showDiff();
4623
4624 $out->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
4625 $this->showTextbox2();
4626
4627 $out->addReturnTo( $this->getContextTitle(), [ 'action' => 'edit' ] );
4628 }
4629
4633 protected function addEditNotices() {
4634 $out = $this->context->getOutput();
4635 $editNotices = $this->mTitle->getEditNotices( $this->oldid );
4636 if ( count( $editNotices ) ) {
4637 $out->addHTML( implode( "\n", $editNotices ) );
4638 } else {
4639 $msg = $this->context->msg( 'editnotice-notext' );
4640 if ( !$msg->isDisabled() ) {
4641 $out->addHTML(
4642 '<div class="mw-editnotice-notext">'
4643 . $msg->parseAsBlock()
4644 . '</div>'
4645 );
4646 }
4647 }
4648 }
4649
4653 protected function addTalkPageText() {
4654 if ( $this->mTitle->isTalkPage() ) {
4655 $this->context->getOutput()->addWikiMsg( 'talkpagetext' );
4656 }
4657 }
4658
4662 protected function addLongPageWarningHeader() {
4663 if ( $this->contentLength === false ) {
4664 $this->contentLength = strlen( $this->textbox1 );
4665 }
4666
4667 $out = $this->context->getOutput();
4668 $lang = $this->context->getLanguage();
4669 $maxArticleSize = $this->context->getConfig()->get( 'MaxArticleSize' );
4670 if ( $this->tooBig || $this->contentLength > $maxArticleSize * 1024 ) {
4671 $out->wrapWikiMsg( "<div class='error' id='mw-edit-longpageerror'>\n$1\n</div>",
4672 [
4673 'longpageerror',
4674 $lang->formatNum( round( $this->contentLength / 1024, 3 ) ),
4675 $lang->formatNum( $maxArticleSize )
4676 ]
4677 );
4678 } elseif ( !$this->context->msg( 'longpage-hint' )->isDisabled() ) {
4679 $out->wrapWikiMsg( "<div id='mw-edit-longpage-hint'>\n$1\n</div>",
4680 [
4681 'longpage-hint',
4682 $lang->formatSize( strlen( $this->textbox1 ) ),
4683 strlen( $this->textbox1 )
4684 ]
4685 );
4686 }
4687 }
4688
4692 protected function addPageProtectionWarningHeaders() {
4693 $out = $this->context->getOutput();
4694 if ( $this->mTitle->isProtected( 'edit' ) &&
4695 $this->permManager->getNamespaceRestrictionLevels(
4696 $this->getTitle()->getNamespace()
4697 ) !== [ '' ]
4698 ) {
4699 # Is the title semi-protected?
4700 if ( $this->mTitle->isSemiProtected() ) {
4701 $noticeMsg = 'semiprotectedpagewarning';
4702 } else {
4703 # Then it must be protected based on static groups (regular)
4704 $noticeMsg = 'protectedpagewarning';
4705 }
4706 LogEventsList::showLogExtract( $out, 'protect', $this->mTitle, '',
4707 [ 'lim' => 1, 'msgKey' => [ $noticeMsg ] ] );
4708 }
4709 if ( $this->mTitle->isCascadeProtected() ) {
4710 # Is this page under cascading protection from some source pages?
4712 list( $cascadeSources, /* $restrictions */ ) = $this->mTitle->getCascadeProtectionSources();
4713 $notice = "<div class='warningbox mw-cascadeprotectedwarning'>\n$1\n";
4714 $cascadeSourcesCount = count( $cascadeSources );
4715 if ( $cascadeSourcesCount > 0 ) {
4716 # Explain, and list the titles responsible
4717 foreach ( $cascadeSources as $page ) {
4718 $notice .= '* [[:' . $page->getPrefixedText() . "]]\n";
4719 }
4720 }
4721 $notice .= '</div>';
4722 $out->wrapWikiMsg( $notice, [ 'cascadeprotectedwarning', $cascadeSourcesCount ] );
4723 }
4724 if ( !$this->mTitle->exists() && $this->mTitle->getRestrictions( 'create' ) ) {
4725 LogEventsList::showLogExtract( $out, 'protect', $this->mTitle, '',
4726 [ 'lim' => 1,
4727 'showIfEmpty' => false,
4728 'msgKey' => [ 'titleprotectedwarning' ],
4729 'wrap' => "<div class=\"mw-titleprotectedwarning\">\n$1</div>" ] );
4730 }
4731 }
4732
4737 protected function addExplainConflictHeader( OutputPage $out ) {
4738 $out->addHTML(
4739 $this->getEditConflictHelper()->getExplainHeader()
4740 );
4741 }
4742
4750 protected function buildTextboxAttribs( $name, array $customAttribs, User $user ) {
4751 return ( new TextboxBuilder() )->buildTextboxAttribs(
4752 $name, $customAttribs, $user, $this->mTitle
4753 );
4754 }
4755
4761 protected function addNewLineAtEnd( $wikitext ) {
4762 return ( new TextboxBuilder() )->addNewLineAtEnd( $wikitext );
4763 }
4764
4775 private function guessSectionName( $text ) {
4776 // Detect Microsoft browsers
4777 $userAgent = $this->context->getRequest()->getHeader( 'User-Agent' );
4778 $parser = MediaWikiServices::getInstance()->getParser();
4779 if ( $userAgent && preg_match( '/MSIE|Edge/', $userAgent ) ) {
4780 // ...and redirect them to legacy encoding, if available
4781 return $parser->guessLegacySectionNameFromWikiText( $text );
4782 }
4783 // Meanwhile, real browsers get real anchors
4784 $name = $parser->guessSectionNameFromWikiText( $text );
4785 // With one little caveat: per T216029, fragments in HTTP redirects need to be urlencoded,
4786 // otherwise Chrome double-escapes the rest of the URL.
4787 return '#' . urlencode( mb_substr( $name, 1 ) );
4788 }
4789
4796 public function setEditConflictHelperFactory( callable $factory ) {
4797 $this->editConflictHelperFactory = $factory;
4798 $this->editConflictHelper = null;
4799 }
4800
4804 private function getEditConflictHelper() {
4805 if ( !$this->editConflictHelper ) {
4806 $this->editConflictHelper = call_user_func(
4807 $this->editConflictHelperFactory,
4808 $this->getSubmitButtonLabel()
4809 );
4810 }
4811
4813 }
4814
4820 private function newTextConflictHelper( $submitButtonLabel ) {
4821 return new TextConflictHelper(
4822 $this->getTitle(),
4823 $this->getContext()->getOutput(),
4824 MediaWikiServices::getInstance()->getStatsdDataFactory(),
4825 $submitButtonLabel,
4826 MediaWikiServices::getInstance()->getContentHandlerFactory()
4827 );
4828 }
4829}
getUser()
$wgDisableAnonTalk
Disable links to talk pages of anonymous users (IPs) in listings on special pages like page history,...
$wgRightsText
If either $wgRightsUrl or $wgRightsPage is specified then this variable gives the text for the link.
deprecatePublicProperty( $property, $version, $class=null, $component=null)
Mark a property as deprecated.
trait DeprecationHelper
Use this trait in classes which have properties for which public access is deprecated.
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfWarn( $msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
wfReadOnly()
Check whether the wiki is in read-only mode.
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
wfExpandUrl( $url, $defaultProto=PROTO_CURRENT)
Expand a potentially local URL to a fully-qualified URL.
wfArrayDiff2( $a, $b)
Like array_diff( $a, $b ) except that it works with two-dimensional arrays.
wfReadOnlyReason()
Check if the site is in read-only mode and return the message if so.
wfDebugLog( $logGroup, $text, $dest='all', array $context=[])
Send a line to a supplementary debug log file, if configured, or main debug log if not.
wfArrayToCgi( $array1, $array2=null, $prefix='')
This function takes one or two arrays as input, and returns a CGI-style string, e....
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that $function is deprecated.
$wgTitle
Definition Setup.php:799
$wgLang
Definition Setup.php:781
Class for viewing MediaWiki article and history.
Definition Article.php:46
getContext()
Gets the context this Article is executed in.
Definition Article.php:2345
getTitle()
Get the title object of the article.
Definition Article.php:255
getPage()
Get the WikiPage object of this instance.
Definition Article.php:265
Special handling for category description pages, showing pages, subcategories and file that belong to...
static canAddTagsAccompanyingChange(array $tags, User $user=null)
Is it OK to allow the user to apply all the specified tags at the same time as they edit/make the cha...
An IContextSource implementation which will inherit context from another source but allow individual ...
The edit page/HTML interface (split from Article) The actual database and text munging is still in Ar...
Definition EditPage.php:62
getPreviewParserOptions()
Get parser options for a preview.
string $sectiontitle
Definition EditPage.php:328
bool $markAsBot
Definition EditPage.php:367
getActionURL(Title $title)
Returns the URL to use in the form's action attribute.
getEditConflictHelper()
bool stdClass $lastDelete
Definition EditPage.php:145
showTextbox( $text, $name, $customAttribs=[])
string $hookError
Definition EditPage.php:214
attemptSave(&$resultDetails=false)
Attempt submission.
$editFormTextAfterTools
Definition EditPage.php:386
isSectionEditSupported()
Returns whether section editing is supported for the current page.
Definition EditPage.php:906
getCheckboxesWidget(&$tabindex, $checked)
Returns an array of checkboxes for the edit form, including 'minor' and 'watch' checkboxes and any ot...
WikiPage $page
Definition EditPage.php:103
updateWatchlist()
Register the change of watch status.
getOriginalContent(User $user)
Get the content of the wanted revision, without section extraction.
null string $contentFormat
Definition EditPage.php:373
bool $allowBlankSummary
Definition EditPage.php:193
showCustomIntro()
Attempt to show a custom editing introduction, if supplied.
displayPermissionsError(array $permErrors)
Display a permissions error page, like OutputPage::showPermissionsErrorPage(), but with the following...
Definition EditPage.php:761
bool $firsttime
True the first time the edit form is rendered, false after re-rendering with diff,...
Definition EditPage.php:139
runPostMergeFilters(Content $content, Status $status, User $user)
Run hooks that can filter edits just before they get saved.
showPreview( $text)
Append preview output to OutputPage.
addTalkPageText()
string $textbox2
Definition EditPage.php:293
bool $tooBig
Definition EditPage.php:175
getCheckboxesDefinition( $checked)
Return an array of checkbox definitions.
showEditTools()
Inserts optional text shown below edit and upload forms.
isSupportedContentModel( $modelId)
Returns if the given content model is editable.
Definition EditPage.php:544
$editFormTextAfterContent
Definition EditPage.php:388
$editFormTextBottom
Definition EditPage.php:387
int $editRevId
Revision ID of the latest revision of the page when editing was initiated on the client.
Definition EditPage.php:322
$editFormTextBeforeContent
Definition EditPage.php:384
string $contentModel
Definition EditPage.php:370
bool $deletedSinceEdit
Definition EditPage.php:130
internalAttemptSave(&$result, $markAsBot=false)
Attempt submission (no UI)
__construct(Article $article)
Stable to call.
Definition EditPage.php:456
toEditContent( $text)
Turns the given text into a Content object by unserializing it.
int $oldid
Revision ID the edit is based on, or 0 if it's the current revision.
Definition EditPage.php:340
const POST_EDIT_COOKIE_KEY_PREFIX
Prefix of key for cookie used to pass post-edit state.
Definition EditPage.php:80
addExplainConflictHeader(OutputPage $out)
$editFormTextAfterWarn
Definition EditPage.php:385
bool $mTokenOk
Definition EditPage.php:151
getSummaryInputAttributes(array $inputAttrs=null)
Helper function for summary input functions, which returns the necessary attributes for the input.
incrementEditFailureStats( $failureType)
setPostEditCookie( $statusValue)
Sets post-edit cookie indicating the user just saved a particular revision.
showFormBeforeText()
getCancelLink( $tabindex=0)
wasDeletedSinceLastEdit()
Check if a page was deleted while the user was editing it, before submit.
setEditConflictHelperFactory(callable $factory)
Set a factory function to create an EditConflictHelper.
string null $unicodeCheck
What the user submitted in the 'wpUnicodeCheck' field.
Definition EditPage.php:423
static getEditToolbar()
Allow extensions to provide a toolbar.
bool $mShowSummaryField
Definition EditPage.php:247
showFormAfterText()
bool $recreate
Definition EditPage.php:285
callable $editConflictHelperFactory
Factory function to create an edit conflict helper.
Definition EditPage.php:430
int $parentRevId
Revision ID the edit is based on, adjusted when an edit conflict is resolved.
Definition EditPage.php:349
isWrongCaseUserConfigPage()
Checks whether the user entered a skin name in uppercase, e.g.
Definition EditPage.php:885
initialiseForm()
Initialise form fields in the object Called on the first invocation, e.g.
static getPreviewLimitReport(ParserOutput $output=null)
Get the Limit report for page previews.
bool $hasPresetSummary
Has a summary been preset using GET parameter &summary= ?
Definition EditPage.php:226
static matchSummarySpamRegex( $text)
Check given input text against $wgSummarySpamRegex, and return the text of the first match.
handleStatus(Status $status, $resultDetails)
Handle status, such as after attempt save.
importFormData(&$request)
This function collects the form data and uses it to populate various member variables.
Definition EditPage.php:917
static extractSectionTitle( $text)
Extract the section title from current section text, if any.
bool $missingSummary
Definition EditPage.php:187
showTextbox1( $customAttribs=null, $textoverride=null)
Method to output wpTextbox1 The $textoverride method can be used by subclasses overriding showContent...
buildTextboxAttribs( $name, array $customAttribs, User $user)
showDiff()
Get a diff between the current contents of the edit box and the version of the page we're editing fro...
addEditNotices()
Title $mTitle
Definition EditPage.php:109
Revision bool null $mBaseRevision
A revision object corresponding to $this->editRevId.
Definition EditPage.php:236
bool int $contentLength
Definition EditPage.php:403
bool $blankArticle
Definition EditPage.php:196
noSuchSectionPage()
Creates a basic error page which informs the user that they have attempted to edit a nonexistent sect...
bool $mTriedSave
Definition EditPage.php:163
incrementResolvedConflicts()
Log when a page was successfully saved after the edit conflict view.
showConflict()
Show an edit conflict.
getCheckboxesDefinitionForWatchlist( $watch)
Get the watchthis and watchlistExpiry form field definitions.
guessSectionName( $text)
Turns section name wikitext into anchors for use in HTTP redirects.
setPreloadedContent(Content $content)
Use this method before edit() to preload some content into the edit box.
static getCopyrightWarning( $title, $format='plain', $langcode=null)
Get the copyright warning, by default returns wikitext.
getParentRevId()
Get the edit's parent revision ID.
tokenOk(&$request)
Make sure the form isn't faking a user's credentials.
makeTemplatesOnThisPageList(array $templates)
Wrapper around TemplatesOnThisPageFormatter to make a "templates on this page" list.
string $section
Definition EditPage.php:325
ParserOutput $mParserOutput
Definition EditPage.php:220
previewOnOpen()
Should we show a preview when the edit form is first shown?
Definition EditPage.php:844
displayPreviewArea( $previewOutput, $isOnTop=false)
doPreviewParse(Content $content)
Parse the page for a preview.
string $formtype
Definition EditPage.php:133
bool $allowBlankArticle
Definition EditPage.php:199
RevisionStore $revisionStore
Definition EditPage.php:450
setApiEditOverride( $enableOverride)
Allow editing of content that supports API direct editing, but not general direct editing.
Definition EditPage.php:555
PermissionManager $permManager
Definition EditPage.php:445
string $summary
Definition EditPage.php:296
getSubmitButtonLabel()
Get the message key of the label for the button to save the page.
getEditPermissionErrors( $rigor=PermissionManager::RIGOR_SECURE)
Definition EditPage.php:708
getSummaryInputWidget( $summary="", $labelText=null, $inputAttrs=null)
Builds a standard summary input with a label.
bool $save
Definition EditPage.php:252
bool $diff
Definition EditPage.php:258
showStandardInputs(&$tabindex=2)
getCurrentContent()
Get the current content of the page.
bool $selfRedirect
Definition EditPage.php:202
getExpectedParentRevision()
Returns the RevisionRecord corresponding to the revision that was current at the time editing was ini...
string $textbox1
Page content input field.
Definition EditPage.php:290
addContentModelChangeLogEntry(User $user, $oldModel, $newModel, $reason)
const EDITFORM_ID
HTML id and name for the beginning of the edit form.
Definition EditPage.php:74
string $editFormPageTop
Before even the preview.
Definition EditPage.php:382
getCopywarn()
Get the copyright warning.
const POST_EDIT_COOKIE_DURATION
Duration of PostEdit cookie, in seconds.
Definition EditPage.php:95
static matchSpamRegex( $text)
Check given input text against $wgSpamRegex, and return the text of the first match.
getContextTitle()
Get the context title object.
Definition EditPage.php:527
toEditText( $content)
Gets an editable textual representation of $content.
null Title $mContextTitle
Definition EditPage.php:112
string $autoSumm
Definition EditPage.php:211
RevisionRecord bool null $mExpectedParentRevision
A RevisionRecord corresponding to $this->editRevId or $this->edittime Replaced $mBaseRevision.
Definition EditPage.php:244
bool $isOldRev
Whether an old revision is edited.
Definition EditPage.php:418
bool $watchlistExpiryEnabled
Corresponds to $wgWatchlistExpiry.
Definition EditPage.php:273
string null $watchlistExpiry
The expiry time of the watch item, or null if it is not watched temporarily.
Definition EditPage.php:279
bool $mTokenOkExceptSuffix
Definition EditPage.php:157
newSectionSummary(&$sectionanchor=null)
Return the summary to be used for a new section.
getBaseRevision()
Returns the revision that was current at the time editing was initiated on the client,...
string $starttime
Timestamp from the first time the edit form was rendered.
Definition EditPage.php:333
bool $isNew
New page or new section.
Definition EditPage.php:124
showIntro()
Show all applicable editing introductions.
Article $mArticle
Definition EditPage.php:101
bool $allowSelfRedirect
Definition EditPage.php:205
getContentObject( $def_content=null)
bool $isConflict
Whether an edit conflict needs to be resolved.
Definition EditPage.php:121
getLastDelete()
Get the last log record of this page being deleted, if ever.
string $action
Definition EditPage.php:115
getEditButtons(&$tabindex)
Returns an array of html code of the following buttons: save, diff and preview.
edit()
This is the function that gets called for "action=edit".
Definition EditPage.php:570
string $editintro
Definition EditPage.php:355
int null $scrolltop
Definition EditPage.php:361
newTextConflictHelper( $submitButtonLabel)
bool $enableApiEditOverride
Set in ApiEditPage, based on ContentHandler::allowsDirectApiEditing.
Definition EditPage.php:408
displayViewSourcePage(Content $content, $errorMessage='')
Display a read-only View Source page.
Definition EditPage.php:791
bool $watchthis
Definition EditPage.php:270
addPageProtectionWarningHeaders()
addLongPageWarningHeader()
WatchedItemStoreInterface $watchedItemStore
Definition EditPage.php:276
addNewLineAtEnd( $wikitext)
importContentFormData(&$request)
Subpage overridable method for extracting the page content data from the posted form to be placed in ...
showHeaderCopyrightWarning()
Show the header copyright warning.
bool $minoredit
Definition EditPage.php:264
$previewTextAfterContent
Definition EditPage.php:389
getSummaryPreview( $isSubjectPreview, $summary="")
bool $nosummary
Definition EditPage.php:303
bool $incompleteForm
Definition EditPage.php:169
showSummaryInput( $isSubjectPreview, $summary="")
mergeChangesIntoContent(&$editContent)
Attempts to do 3-way merge of edit content with a base revision and current content,...
isPageExistingAndViewable( $title, User $user)
Verify if a given title exists and the given user is allowed to view it.
bool $missingComment
Definition EditPage.php:181
string $edittime
Timestamp of the latest revision of the page when editing was initiated on the client.
Definition EditPage.php:309
showTosSummary()
Give a chance for site and per-namespace customizations of terms of service summary link that might e...
const UNICODE_CHECK
Used for Unicode support checks.
Definition EditPage.php:69
getPreviewText()
Get the rendered text for previewing.
formatStatusErrors(Status $status)
Wrap status errors in an errorbox for increased visibility.
IContentHandlerFactory $contentHandlerFactory
Definition EditPage.php:440
showContentForm()
Subpage overridable method for printing the form for page content editing By default this simply outp...
bool $edit
Definition EditPage.php:400
incrementConflictStats()
IContextSource $context
Definition EditPage.php:413
bool $preview
Definition EditPage.php:255
getPreloadedContent( $preload, $params=[])
Get the contents to be preloaded into the box, either set by an earlier setPreloadText() or by loadin...
TextConflictHelper null $editConflictHelper
Definition EditPage.php:435
showEditForm( $formCallback=null)
Send the edit form and related headers to OutputPage.
null array $changeTags
Definition EditPage.php:376
setContextTitle( $title)
Set the context Title object.
Definition EditPage.php:515
spamPageWithContent( $match=false)
Show "your edit contains spam" page with your diff and text.
An error page which can definitely be safely rendered using the OutputPage.
static titleAttrib( $name, $options=null, array $msgParams=[])
Given the id of an interface element, constructs the appropriate title attribute from the system mess...
Definition Linker.php:2120
static accesskey( $name)
Given the id of an interface element, constructs the appropriate accesskey attribute from the system ...
Definition Linker.php:2168
static formatHiddenCategories( $hiddencats)
Returns HTML for the "hidden categories on this page" list.
Definition Linker.php:2085
static commentBlock( $comment, $title=null, $local=false, $wikiId=null, $useParentheses=true)
Wrap a comment in standard punctuation and formatting if it's non-empty, otherwise return empty strin...
Definition Linker.php:1584
const DELETED_USER
Definition LogPage.php:40
const DELETED_COMMENT
Definition LogPage.php:39
Exception representing a failure to serialize or unserialize a content object.
MediaWiki exception.
Exception thrown when an unregistered content model is requested.
Class for creating new log entries and inserting them into the database.
setPerformer(UserIdentity $performer)
Set the user that performed the action being logged.
A DatabaseBlock (unlike a SystemBlock) is stored in the database, may give rise to autoblocks and may...
Helper for displaying edit conflicts in text content models to users.
getEditFormHtmlAfterContent()
Content to go in the edit form after textbox1.
getEditConflictMainTextBox(array $customAttribs=[])
HTML to build the textbox1 on edit conflicts.
setTextboxes( $yourtext, $storedversion)
getEditFormHtmlBeforeContent()
Content to go in the edit form before textbox1.
Helps EditPage build textboxes.
PSR-3 logger instance factory.
MediaWikiServices is the service locator for the application scope of MediaWiki.
A service class for checking permissions To obtain an instance, use MediaWikiServices::getInstance()-...
Page revision base class.
A RevisionRecord representing an existing revision persisted in the revision table.
Service for looking up page revisions.
Value object representing a content slot associated with a page revision.
static plaintextParam( $plaintext)
Definition Message.php:1130
This is one of the Core classes and should be read at least once by any new developers.
addHTML( $text)
Append $text to the body HTML.
Show an error when a user tries to do something they do not have the necessary permissions for.
Variant of the Message class.
Show an error when the wiki is locked/read-only and the user tries to do something that requires writ...
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,...
getErrors()
Get the list of errors.
isOK()
Returns whether the operation completed.
fatal( $message,... $parameters)
Add an error and set OK to false, indicating that the operation as a whole was fatal.
isGood()
Returns whether the operation completed and didn't have any error or warnings.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:44
getWikiText( $shortContext=false, $longContext=false, $lang=null)
Get the error list as a wikitext formatted list.
Definition Status.php:189
Handles formatting for the "templates used on this page" lists.
Show an error when the user hits a rate limit.
Represents a title within MediaWiki.
Definition Title.php:42
setContentModel( $model)
Set a proposed content model for the page for permissions checking.
Definition Title.php:1103
Show an error when the user tries to do something whilst blocked.
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:60
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition User.php:541
static isIP( $name)
Does the string match an anonymous IP address?
Definition User.php:951
static getExpiryOptions(MessageLocalizer $msgLocalizer, $watchedItem)
Get options and default for a watchlist expiry select list.
static doWatchOrUnwatch( $watch, Title $title, User $user, string $expiry=null)
Watch or unwatch a page.
Class representing a MediaWiki article and history.
Definition WikiPage.php:51
getContent( $audience=RevisionRecord::FOR_PUBLIC, User $user=null)
Get the content of the current revision.
Definition WikiPage.php:802
getRedirectTarget()
If this page is a redirect, get its target.
isRedirect()
Tests if the article content represents a redirect.
Definition WikiPage.php:612
Type definition for expiry timestamps.
Definition ExpiryDef.php:17
const EDIT_FORCE_BOT
Definition Defines.php:146
const EDIT_UPDATE
Definition Defines.php:143
const NS_USER
Definition Defines.php:72
const CONTENT_MODEL_CSS
Definition Defines.php:227
const NS_FILE
Definition Defines.php:76
const NS_MEDIAWIKI
Definition Defines.php:78
const CONTENT_MODEL_JSON
Definition Defines.php:229
const NS_USER_TALK
Definition Defines.php:73
const EDIT_MINOR
Definition Defines.php:144
const CONTENT_MODEL_JAVASCRIPT
Definition Defines.php:226
const EDIT_AUTOSUMMARY
Definition Defines.php:148
const EDIT_NEW
Definition Defines.php:142
Interface for configuration instances.
Definition Config.php:30
get( $name)
Get a configuration variable such as "Sitename" or "UploadMaintenance.".
Base interface for content objects.
Definition Content.php:35
Interface for objects which can provide a MediaWiki context on request.
Serves as a common repository of constants for EditPage edit status results.
const AS_RATE_LIMITED
Status: rate limiter for action 'edit' was tripped.
const AS_NO_CHANGE_CONTENT_MODEL
Status: user tried to modify the content model, but is not allowed to do that ( User::isAllowed('edit...
const AS_READ_ONLY_PAGE_LOGGED
Status: this logged in user is not allowed to edit this page.
const AS_READ_ONLY_PAGE_ANON
Status: this anonymous user is not allowed to edit this page.
const AS_CONFLICT_DETECTED
Status: (non-resolvable) edit conflict.
const AS_UNICODE_NOT_SUPPORTED
Status: edit rejected because browser doesn't support Unicode.
const AS_ARTICLE_WAS_DELETED
Status: article was deleted while editing and wpRecreate == false or form was not posted.
const AS_TEXTBOX_EMPTY
Status: user tried to create a new section without content.
const AS_HOOK_ERROR_EXPECTED
Status: A hook function returned an error.
const AS_PARSE_ERROR
Status: can't parse content.
const AS_CONTENT_TOO_BIG
Status: Content too big (> $wgMaxArticleSize)
const AS_SPAM_ERROR
Status: summary contained spam according to one of the regexes in $wgSummarySpamRegex.
const AS_SUCCESS_UPDATE
Status: Article successfully updated.
const AS_IMAGE_REDIRECT_ANON
Status: anonymous user is not allowed to upload (User::isAllowed('upload') == false)
const AS_SELF_REDIRECT
Status: user tried to create self-redirect and wpIgnoreSelfRedirect is false.
const AS_SUCCESS_NEW_ARTICLE
Status: Article successfully created.
const AS_NO_CREATE_PERMISSION
Status: user tried to create this page, but is not allowed to do that.
const AS_IMAGE_REDIRECT_LOGGED
Status: logged in user is not allowed to upload (User::isAllowed('upload') == false)
const AS_MAX_ARTICLE_SIZE_EXCEEDED
Status: article is too big (> $wgMaxArticleSize), after merging in the new section.
const AS_END
Status: WikiPage::doEdit() was unsuccessful.
const AS_BLOCKED_PAGE_FOR_USER
Status: User is blocked from editing this page.
const AS_SUMMARY_NEEDED
Status: no edit summary given and the user has forceeditsummary set and the user is not editing in hi...
const AS_HOOK_ERROR
Status: Article update aborted by a hook function.
const AS_BLANK_ARTICLE
Status: user tried to create a blank page and wpIgnoreBlankArticle == false.
const AS_CHANGE_TAG_ERROR
Status: an error relating to change tagging.
const AS_CANNOT_USE_CUSTOM_MODEL
Status: when changing the content model is disallowed due to $wgContentHandlerUseDB being false.
const AS_READ_ONLY_PAGE
Status: wiki is in readonly mode (wfReadOnly() == true)
const DB_REPLICA
Definition defines.php:25
const DB_MASTER
Definition defines.php:29
$content
Definition router.php:76
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition router.php:42
if(!isset( $args[0])) $lang