MediaWiki REL1_34
EditPage.php
Go to the documentation of this file.
1<?php
29use Wikimedia\ScopedCallback;
30
46class EditPage {
50 const UNICODE_CHECK = 'ℳ𝒲β™₯π“Šπ“ƒπ’Ύπ’Έβ„΄π’Ήβ„―';
51
55 const AS_SUCCESS_UPDATE = 200;
56
61
65 const AS_HOOK_ERROR = 210;
66
71
76
80 const AS_CONTENT_TOO_BIG = 216;
81
86
91
95 const AS_READ_ONLY_PAGE = 220;
96
100 const AS_RATE_LIMITED = 221;
101
107
113
117 const AS_BLANK_ARTICLE = 224;
118
123
128 const AS_SUMMARY_NEEDED = 226;
129
133 const AS_TEXTBOX_EMPTY = 228;
134
139
143 const AS_END = 231;
144
148 const AS_SPAM_ERROR = 232;
149
154
159
165
170 const AS_SELF_REDIRECT = 236;
171
177
181 const AS_PARSE_ERROR = 240;
182
188
193
197 const EDITFORM_ID = 'editform';
198
203 const POST_EDIT_COOKIE_KEY_PREFIX = 'PostEditRevision';
204
219
224 public $mArticle;
226 private $page;
227
232 public $mTitle;
233
235 private $mContextTitle = null;
236
238 public $action = 'submit';
239
244 public $isConflict = false;
245
247 public $isNew = false;
248
251
253 public $formtype;
254
260
263
265 public $mTokenOk = false;
266
268 public $mTokenOkExceptSuffix = false;
269
271 public $mTriedSave = false;
272
274 public $incompleteForm = false;
275
277 public $tooBig = false;
278
280 public $missingComment = false;
281
283 public $missingSummary = false;
284
286 public $allowBlankSummary = false;
287
289 protected $blankArticle = false;
290
292 protected $allowBlankArticle = false;
293
295 protected $selfRedirect = false;
296
298 protected $allowSelfRedirect = false;
299
301 public $autoSumm = '';
302
304 public $hookError = '';
305
308
310 public $hasPresetSummary = false;
311
313 public $mBaseRevision = false;
314
316 public $mShowSummaryField = true;
317
318 # Form values
319
321 public $save = false;
322
324 public $preview = false;
325
327 public $diff = false;
328
330 public $minoredit = false;
331
333 public $watchthis = false;
334
336 public $recreate = false;
337
341 public $textbox1 = '';
342
344 public $textbox2 = '';
345
347 public $summary = '';
348
352 public $nosummary = false;
353
358 public $edittime = '';
359
371 private $editRevId = null;
372
374 public $section = '';
375
377 public $sectiontitle = '';
378
382 public $starttime = '';
383
389 public $oldid = 0;
390
396 public $parentRevId = 0;
397
399 public $editintro = '';
400
402 public $scrolltop = null;
403
405 public $bot = true;
406
409
411 public $contentFormat = null;
412
414 private $changeTags = null;
415
416 # Placeholders for text injection by hooks (must be HTML)
417 # extensions should take care to _append_ to the present value
418
420 public $editFormPageTop = '';
421 public $editFormTextTop = '';
428 public $mPreloadContent = null;
429
430 /* $didSave should be set to true whenever an article was successfully altered. */
431 public $didSave = false;
432 public $undidRev = 0;
433
434 public $suppressIntro = false;
435
437 protected $edit;
438
440 protected $contentLength = false;
441
445 private $enableApiEditOverride = false;
446
450 protected $context;
451
455 private $isOldRev = false;
456
461
468
473
477 public function __construct( Article $article ) {
478 $this->mArticle = $article;
479 $this->page = $article->getPage(); // model object
480 $this->mTitle = $article->getTitle();
481
482 // Make sure the local context is in sync with other member variables.
483 // Particularly make sure everything is using the same WikiPage instance.
484 // This should probably be the case in Article as well, but it's
485 // particularly important for EditPage, to make use of the in-place caching
486 // facility in WikiPage::prepareContentForEdit.
487 $this->context = new DerivativeContext( $article->getContext() );
488 $this->context->setWikiPage( $this->page );
489 $this->context->setTitle( $this->mTitle );
490
491 $this->contentModel = $this->mTitle->getContentModel();
492
493 $handler = ContentHandler::getForModelID( $this->contentModel );
494 $this->contentFormat = $handler->getDefaultFormat();
495 $this->editConflictHelperFactory = [ $this, 'newTextConflictHelper' ];
496 }
497
501 public function getArticle() {
502 return $this->mArticle;
503 }
504
509 public function getContext() {
510 return $this->context;
511 }
512
517 public function getTitle() {
518 return $this->mTitle;
519 }
520
526 public function setContextTitle( $title ) {
527 $this->mContextTitle = $title;
528 }
529
538 public function getContextTitle() {
539 if ( is_null( $this->mContextTitle ) ) {
540 wfDeprecated( __METHOD__ . ' called with no title set', '1.32' );
541 global $wgTitle;
542 return $wgTitle;
543 } else {
545 }
546 }
547
555 public function isSupportedContentModel( $modelId ) {
556 return $this->enableApiEditOverride === true ||
557 ContentHandler::getForModelID( $modelId )->supportsDirectEditing();
558 }
559
566 public function setApiEditOverride( $enableOverride ) {
567 $this->enableApiEditOverride = $enableOverride;
568 }
569
581 public function edit() {
582 // Allow extensions to modify/prevent this form or submission
583 if ( !Hooks::run( 'AlternateEdit', [ $this ] ) ) {
584 return;
585 }
586
587 wfDebug( __METHOD__ . ": enter\n" );
588
589 $request = $this->context->getRequest();
590 // If they used redlink=1 and the page exists, redirect to the main article
591 if ( $request->getBool( 'redlink' ) && $this->mTitle->exists() ) {
592 $this->context->getOutput()->redirect( $this->mTitle->getFullURL() );
593 return;
594 }
595
596 $this->importFormData( $request );
597 $this->firsttime = false;
598
599 if ( wfReadOnly() && $this->save ) {
600 // Force preview
601 $this->save = false;
602 $this->preview = true;
603 }
604
605 if ( $this->save ) {
606 $this->formtype = 'save';
607 } elseif ( $this->preview ) {
608 $this->formtype = 'preview';
609 } elseif ( $this->diff ) {
610 $this->formtype = 'diff';
611 } else { # First time through
612 $this->firsttime = true;
613 if ( $this->previewOnOpen() ) {
614 $this->formtype = 'preview';
615 } else {
616 $this->formtype = 'initial';
617 }
618 }
619
620 $permErrors = $this->getEditPermissionErrors( $this->save ? 'secure' : 'full' );
621 if ( $permErrors ) {
622 wfDebug( __METHOD__ . ": User can't edit\n" );
623
624 if ( $this->context->getUser()->getBlock() ) {
625 // Track block with a cookie if it doesn't exist already
626 MediaWikiServices::getInstance()->getBlockManager()
627 ->trackBlockWithCookie( $this->context->getUser() );
628
629 // Auto-block user's IP if the account was "hard" blocked
630 if ( !wfReadOnly() ) {
631 DeferredUpdates::addCallableUpdate( function () {
632 $this->context->getUser()->spreadAnyEditBlock();
633 } );
634 }
635 }
636 $this->displayPermissionsError( $permErrors );
637
638 return;
639 }
640
641 $revision = $this->mArticle->getRevisionFetched();
642 // Disallow editing revisions with content models different from the current one
643 // Undo edits being an exception in order to allow reverting content model changes.
644 if ( $revision
645 && $revision->getContentModel() !== $this->contentModel
646 ) {
647 $prevRev = null;
648 if ( $this->undidRev ) {
649 $undidRevObj = Revision::newFromId( $this->undidRev );
650 $prevRev = $undidRevObj ? $undidRevObj->getPrevious() : null;
651 }
652 if ( !$this->undidRev
653 || !$prevRev
654 || $prevRev->getContentModel() !== $this->contentModel
655 ) {
657 $this->getContentObject(),
658 $this->context->msg(
659 'contentmodelediterror',
660 $revision->getContentModel(),
661 $this->contentModel
662 )->plain()
663 );
664 return;
665 }
666 }
667
668 $this->isConflict = false;
669
670 # Show applicable editing introductions
671 if ( $this->formtype == 'initial' || $this->firsttime ) {
672 $this->showIntro();
673 }
674
675 # Attempt submission here. This will check for edit conflicts,
676 # and redundantly check for locked database, blocked IPs, etc.
677 # that edit() already checked just in case someone tries to sneak
678 # in the back door with a hand-edited submission URL.
679
680 if ( $this->formtype == 'save' ) {
681 $resultDetails = null;
682 $status = $this->attemptSave( $resultDetails );
683 if ( !$this->handleStatus( $status, $resultDetails ) ) {
684 return;
685 }
686 }
687
688 # First time through: get contents, set time for conflict
689 # checking, etc.
690 if ( $this->formtype == 'initial' || $this->firsttime ) {
691 if ( $this->initialiseForm() === false ) {
692 return;
693 }
694
695 if ( !$this->mTitle->getArticleID() ) {
696 Hooks::run( 'EditFormPreloadText', [ &$this->textbox1, &$this->mTitle ] );
697 } else {
698 Hooks::run( 'EditFormInitialText', [ $this ] );
699 }
700
701 }
702
703 $this->showEditForm();
704 }
705
710 protected function getEditPermissionErrors( $rigor = 'secure' ) {
711 $user = $this->context->getUser();
712 $permErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $user, $rigor );
713 # Can this title be created?
714 if ( !$this->mTitle->exists() ) {
715 $permErrors = array_merge(
716 $permErrors,
718 $this->mTitle->getUserPermissionsErrors( 'create', $user, $rigor ),
719 $permErrors
720 )
721 );
722 }
723 # Ignore some permissions errors when a user is just previewing/viewing diffs
724 $remove = [];
725 foreach ( $permErrors as $error ) {
726 if ( ( $this->preview || $this->diff )
727 && (
728 $error[0] == 'blockedtext' ||
729 $error[0] == 'autoblockedtext' ||
730 $error[0] == 'systemblockedtext'
731 )
732 ) {
733 $remove[] = $error;
734 }
735 }
736 $permErrors = wfArrayDiff2( $permErrors, $remove );
737
738 return $permErrors;
739 }
740
754 protected function displayPermissionsError( array $permErrors ) {
755 $out = $this->context->getOutput();
756 if ( $this->context->getRequest()->getBool( 'redlink' ) ) {
757 // The edit page was reached via a red link.
758 // Redirect to the article page and let them click the edit tab if
759 // they really want a permission error.
760 $out->redirect( $this->mTitle->getFullURL() );
761 return;
762 }
763
764 $content = $this->getContentObject();
765
766 # Use the normal message if there's nothing to display
767 if ( $this->firsttime && ( !$content || $content->isEmpty() ) ) {
768 $action = $this->mTitle->exists() ? 'edit' :
769 ( $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage' );
770 throw new PermissionsError( $action, $permErrors );
771 }
772
774 $content,
775 $out->formatPermissionsErrorMessage( $permErrors, 'edit' )
776 );
777 }
778
784 protected function displayViewSourcePage( Content $content, $errorMessage = '' ) {
785 $out = $this->context->getOutput();
786 Hooks::run( 'EditPage::showReadOnlyForm:initial', [ $this, &$out ] );
787
788 $out->setRobotPolicy( 'noindex,nofollow' );
789 $out->setPageTitle( $this->context->msg(
790 'viewsource-title',
791 $this->getContextTitle()->getPrefixedText()
792 ) );
793 $out->addBacklinkSubtitle( $this->getContextTitle() );
794 $out->addHTML( $this->editFormPageTop );
795 $out->addHTML( $this->editFormTextTop );
796
797 if ( $errorMessage !== '' ) {
798 $out->addWikiTextAsInterface( $errorMessage );
799 $out->addHTML( "<hr />\n" );
800 }
801
802 # If the user made changes, preserve them when showing the markup
803 # (This happens when a user is blocked during edit, for instance)
804 if ( !$this->firsttime ) {
805 $text = $this->textbox1;
806 $out->addWikiMsg( 'viewyourtext' );
807 } else {
808 try {
809 $text = $this->toEditText( $content );
810 } catch ( MWException $e ) {
811 # Serialize using the default format if the content model is not supported
812 # (e.g. for an old revision with a different model)
813 $text = $content->serialize();
814 }
815 $out->addWikiMsg( 'viewsourcetext' );
816 }
817
818 $out->addHTML( $this->editFormTextBeforeContent );
819 $this->showTextbox( $text, 'wpTextbox1', [ 'readonly' ] );
820 $out->addHTML( $this->editFormTextAfterContent );
821
822 $out->addHTML( $this->makeTemplatesOnThisPageList( $this->getTemplates() ) );
823
824 $out->addModules( 'mediawiki.action.edit.collapsibleFooter' );
825
826 $out->addHTML( $this->editFormTextBottom );
827 if ( $this->mTitle->exists() ) {
828 $out->returnToMain( null, $this->mTitle );
829 }
830 }
831
837 protected function previewOnOpen() {
838 $config = $this->context->getConfig();
839 $previewOnOpenNamespaces = $config->get( 'PreviewOnOpenNamespaces' );
840 $request = $this->context->getRequest();
841 if ( $config->get( 'RawHtml' ) ) {
842 // If raw HTML is enabled, disable preview on open
843 // since it has to be posted with a token for
844 // security reasons
845 return false;
846 }
847 if ( $request->getVal( 'preview' ) == 'yes' ) {
848 // Explicit override from request
849 return true;
850 } elseif ( $request->getVal( 'preview' ) == 'no' ) {
851 // Explicit override from request
852 return false;
853 } elseif ( $this->section == 'new' ) {
854 // Nothing *to* preview for new sections
855 return false;
856 } elseif ( ( $request->getCheck( 'preload' ) || $this->mTitle->exists() )
857 && $this->context->getUser()->getOption( 'previewonfirst' )
858 ) {
859 // Standard preference behavior
860 return true;
861 } elseif ( !$this->mTitle->exists()
862 && isset( $previewOnOpenNamespaces[$this->mTitle->getNamespace()] )
863 && $previewOnOpenNamespaces[$this->mTitle->getNamespace()]
864 ) {
865 // Categories are special
866 return true;
867 } else {
868 return false;
869 }
870 }
871
878 protected function isWrongCaseUserConfigPage() {
879 if ( $this->mTitle->isUserConfigPage() ) {
880 $name = $this->mTitle->getSkinFromConfigSubpage();
881 $skins = array_merge(
882 array_keys( Skin::getSkinNames() ),
883 [ 'common' ]
884 );
885 return !in_array( $name, $skins )
886 && in_array( strtolower( $name ), $skins );
887 } else {
888 return false;
889 }
890 }
891
899 protected function isSectionEditSupported() {
900 $contentHandler = ContentHandler::getForTitle( $this->mTitle );
901 return $contentHandler->supportsSections();
902 }
903
909 public function importFormData( &$request ) {
910 # Section edit can come from either the form or a link
911 $this->section = $request->getVal( 'wpSection', $request->getVal( 'section' ) );
912
913 if ( $this->section !== null && $this->section !== '' && !$this->isSectionEditSupported() ) {
914 throw new ErrorPageError( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
915 }
916
917 $this->isNew = !$this->mTitle->exists() || $this->section == 'new';
918
919 if ( $request->wasPosted() ) {
920 # These fields need to be checked for encoding.
921 # Also remove trailing whitespace, but don't remove _initial_
922 # whitespace from the text boxes. This may be significant formatting.
923 $this->textbox1 = rtrim( $request->getText( 'wpTextbox1' ) );
924 if ( !$request->getCheck( 'wpTextbox2' ) ) {
925 // Skip this if wpTextbox2 has input, it indicates that we came
926 // from a conflict page with raw page text, not a custom form
927 // modified by subclasses
928 $textbox1 = $this->importContentFormData( $request );
929 if ( $textbox1 !== null ) {
930 $this->textbox1 = $textbox1;
931 }
932 }
933
934 $this->unicodeCheck = $request->getText( 'wpUnicodeCheck' );
935
936 $this->summary = $request->getText( 'wpSummary' );
937
938 # If the summary consists of a heading, e.g. '==Foobar==', extract the title from the
939 # header syntax, e.g. 'Foobar'. This is mainly an issue when we are using wpSummary for
940 # section titles.
941 $this->summary = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->summary );
942
943 # Treat sectiontitle the same way as summary.
944 # Note that wpSectionTitle is not yet a part of the actual edit form, as wpSummary is
945 # currently doing double duty as both edit summary and section title. Right now this
946 # is just to allow API edits to work around this limitation, but this should be
947 # incorporated into the actual edit form when EditPage is rewritten (T20654, T28312).
948 $this->sectiontitle = $request->getText( 'wpSectionTitle' );
949 $this->sectiontitle = preg_replace( '/^\s*=+\s*(.*?)\s*=+\s*$/', '$1', $this->sectiontitle );
950
951 $this->edittime = $request->getVal( 'wpEdittime' );
952 $this->editRevId = $request->getIntOrNull( 'editRevId' );
953 $this->starttime = $request->getVal( 'wpStarttime' );
954
955 $undidRev = $request->getInt( 'wpUndidRevision' );
956 if ( $undidRev ) {
957 $this->undidRev = $undidRev;
958 }
959
960 $this->scrolltop = $request->getIntOrNull( 'wpScrolltop' );
961
962 if ( $this->textbox1 === '' && !$request->getCheck( 'wpTextbox1' ) ) {
963 // wpTextbox1 field is missing, possibly due to being "too big"
964 // according to some filter rules such as Suhosin's setting for
965 // suhosin.request.max_value_length (d'oh)
966 $this->incompleteForm = true;
967 } else {
968 // If we receive the last parameter of the request, we can fairly
969 // claim the POST request has not been truncated.
970 $this->incompleteForm = !$request->getVal( 'wpUltimateParam' );
971 }
972 if ( $this->incompleteForm ) {
973 # If the form is incomplete, force to preview.
974 wfDebug( __METHOD__ . ": Form data appears to be incomplete\n" );
975 wfDebug( "POST DATA: " . var_export( $request->getPostValues(), true ) . "\n" );
976 $this->preview = true;
977 } else {
978 $this->preview = $request->getCheck( 'wpPreview' );
979 $this->diff = $request->getCheck( 'wpDiff' );
980
981 // Remember whether a save was requested, so we can indicate
982 // if we forced preview due to session failure.
983 $this->mTriedSave = !$this->preview;
984
985 if ( $this->tokenOk( $request ) ) {
986 # Some browsers will not report any submit button
987 # if the user hits enter in the comment box.
988 # The unmarked state will be assumed to be a save,
989 # if the form seems otherwise complete.
990 wfDebug( __METHOD__ . ": Passed token check.\n" );
991 } elseif ( $this->diff ) {
992 # Failed token check, but only requested "Show Changes".
993 wfDebug( __METHOD__ . ": Failed token check; Show Changes requested.\n" );
994 } else {
995 # Page might be a hack attempt posted from
996 # an external site. Preview instead of saving.
997 wfDebug( __METHOD__ . ": Failed token check; forcing preview\n" );
998 $this->preview = true;
999 }
1000 }
1001 $this->save = !$this->preview && !$this->diff;
1002 if ( !preg_match( '/^\d{14}$/', $this->edittime ) ) {
1003 $this->edittime = null;
1004 }
1005
1006 if ( !preg_match( '/^\d{14}$/', $this->starttime ) ) {
1007 $this->starttime = null;
1008 }
1009
1010 $this->recreate = $request->getCheck( 'wpRecreate' );
1011
1012 $this->minoredit = $request->getCheck( 'wpMinoredit' );
1013 $this->watchthis = $request->getCheck( 'wpWatchthis' );
1014
1015 $user = $this->context->getUser();
1016 # Don't force edit summaries when a user is editing their own user or talk page
1017 if ( ( $this->mTitle->mNamespace == NS_USER || $this->mTitle->mNamespace == NS_USER_TALK )
1018 && $this->mTitle->getText() == $user->getName()
1019 ) {
1020 $this->allowBlankSummary = true;
1021 } else {
1022 $this->allowBlankSummary = $request->getBool( 'wpIgnoreBlankSummary' )
1023 || !$user->getOption( 'forceeditsummary' );
1024 }
1025
1026 $this->autoSumm = $request->getText( 'wpAutoSummary' );
1027
1028 $this->allowBlankArticle = $request->getBool( 'wpIgnoreBlankArticle' );
1029 $this->allowSelfRedirect = $request->getBool( 'wpIgnoreSelfRedirect' );
1030
1031 $changeTags = $request->getVal( 'wpChangeTags' );
1032 if ( is_null( $changeTags ) || $changeTags === '' ) {
1033 $this->changeTags = [];
1034 } else {
1035 $this->changeTags = array_filter( array_map( 'trim', explode( ',',
1036 $changeTags ) ) );
1037 }
1038 } else {
1039 # Not a posted form? Start with nothing.
1040 wfDebug( __METHOD__ . ": Not a posted form.\n" );
1041 $this->textbox1 = '';
1042 $this->summary = '';
1043 $this->sectiontitle = '';
1044 $this->edittime = '';
1045 $this->editRevId = null;
1046 $this->starttime = wfTimestampNow();
1047 $this->edit = false;
1048 $this->preview = false;
1049 $this->save = false;
1050 $this->diff = false;
1051 $this->minoredit = false;
1052 // Watch may be overridden by request parameters
1053 $this->watchthis = $request->getBool( 'watchthis', false );
1054 $this->recreate = false;
1055
1056 // When creating a new section, we can preload a section title by passing it as the
1057 // preloadtitle parameter in the URL (T15100)
1058 if ( $this->section == 'new' && $request->getVal( 'preloadtitle' ) ) {
1059 $this->sectiontitle = $request->getVal( 'preloadtitle' );
1060 // Once wpSummary isn't being use for setting section titles, we should delete this.
1061 $this->summary = $request->getVal( 'preloadtitle' );
1062 } elseif ( $this->section != 'new' && $request->getVal( 'summary' ) !== '' ) {
1063 $this->summary = $request->getText( 'summary' );
1064 if ( $this->summary !== '' ) {
1065 $this->hasPresetSummary = true;
1066 }
1067 }
1068
1069 if ( $request->getVal( 'minor' ) ) {
1070 $this->minoredit = true;
1071 }
1072 }
1073
1074 $this->oldid = $request->getInt( 'oldid' );
1075 $this->parentRevId = $request->getInt( 'parentRevId' );
1076
1077 $this->bot = $request->getBool( 'bot', true );
1078 $this->nosummary = $request->getBool( 'nosummary' );
1079
1080 // May be overridden by revision.
1081 $this->contentModel = $request->getText( 'model', $this->contentModel );
1082 // May be overridden by revision.
1083 $this->contentFormat = $request->getText( 'format', $this->contentFormat );
1084
1085 try {
1086 $handler = ContentHandler::getForModelID( $this->contentModel );
1087 } catch ( MWUnknownContentModelException $e ) {
1088 throw new ErrorPageError(
1089 'editpage-invalidcontentmodel-title',
1090 'editpage-invalidcontentmodel-text',
1091 [ wfEscapeWikiText( $this->contentModel ) ]
1092 );
1093 }
1094
1095 if ( !$handler->isSupportedFormat( $this->contentFormat ) ) {
1096 throw new ErrorPageError(
1097 'editpage-notsupportedcontentformat-title',
1098 'editpage-notsupportedcontentformat-text',
1099 [
1100 wfEscapeWikiText( $this->contentFormat ),
1101 wfEscapeWikiText( ContentHandler::getLocalizedName( $this->contentModel ) )
1102 ]
1103 );
1104 }
1105
1112 $this->editintro = $request->getText( 'editintro',
1113 // Custom edit intro for new sections
1114 $this->section === 'new' ? 'MediaWiki:addsection-editintro' : '' );
1115
1116 // Allow extensions to modify form data
1117 Hooks::run( 'EditPage::importFormData', [ $this, $request ] );
1118 }
1119
1129 protected function importContentFormData( &$request ) {
1130 return null; // Don't do anything, EditPage already extracted wpTextbox1
1131 }
1132
1138 public function initialiseForm() {
1139 $this->edittime = $this->page->getTimestamp();
1140 $this->editRevId = $this->page->getLatest();
1141
1142 $content = $this->getContentObject( false ); # TODO: track content object?!
1143 if ( $content === false ) {
1144 $out = $this->context->getOutput();
1145 if ( $out->getRedirect() === '' ) { // mcrundo hack redirects, don't override it
1146 $this->noSuchSectionPage();
1147 }
1148 return false;
1149 }
1150
1151 if ( !$this->isSupportedContentModel( $content->getModel() ) ) {
1152 $modelMsg = $this->getContext()->msg( 'content-model-' . $content->getModel() );
1153 $modelName = $modelMsg->exists() ? $modelMsg->text() : $content->getModel();
1154
1155 $out = $this->context->getOutput();
1156 $out->showErrorPage(
1157 'modeleditnotsupported-title',
1158 'modeleditnotsupported-text',
1159 [ $modelName ]
1160 );
1161 return false;
1162 }
1163
1164 $this->textbox1 = $this->toEditText( $content );
1165
1166 $user = $this->context->getUser();
1167 // activate checkboxes if user wants them to be always active
1168 # Sort out the "watch" checkbox
1169 if ( $user->getOption( 'watchdefault' ) ) {
1170 # Watch all edits
1171 $this->watchthis = true;
1172 } elseif ( $user->getOption( 'watchcreations' ) && !$this->mTitle->exists() ) {
1173 # Watch creations
1174 $this->watchthis = true;
1175 } elseif ( $user->isWatched( $this->mTitle ) ) {
1176 # Already watched
1177 $this->watchthis = true;
1178 }
1179 if ( $user->getOption( 'minordefault' ) && !$this->isNew ) {
1180 $this->minoredit = true;
1181 }
1182 if ( $this->textbox1 === false ) {
1183 return false;
1184 }
1185 return true;
1186 }
1187
1195 protected function getContentObject( $def_content = null ) {
1196 global $wgDisableAnonTalk;
1197
1198 $content = false;
1199
1200 $user = $this->context->getUser();
1201 $request = $this->context->getRequest();
1202 // For message page not locally set, use the i18n message.
1203 // For other non-existent articles, use preload text if any.
1204 if ( !$this->mTitle->exists() || $this->section == 'new' ) {
1205 if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && $this->section != 'new' ) {
1206 # If this is a system message, get the default text.
1207 $msg = $this->mTitle->getDefaultMessageText();
1208
1209 $content = $this->toEditContent( $msg );
1210 }
1211 if ( $content === false ) {
1212 # If requested, preload some text.
1213 $preload = $request->getVal( 'preload',
1214 // Custom preload text for new sections
1215 $this->section === 'new' ? 'MediaWiki:addsection-preload' : '' );
1216 $params = $request->getArray( 'preloadparams', [] );
1217
1218 $content = $this->getPreloadedContent( $preload, $params );
1219 }
1220 // For existing pages, get text based on "undo" or section parameters.
1221 } elseif ( $this->section != '' ) {
1222 // Get section edit text (returns $def_text for invalid sections)
1223 $orig = $this->getOriginalContent( $user );
1224 $content = $orig ? $orig->getSection( $this->section ) : null;
1225
1226 if ( !$content ) {
1227 $content = $def_content;
1228 }
1229 } else {
1230 $undoafter = $request->getInt( 'undoafter' );
1231 $undo = $request->getInt( 'undo' );
1232
1233 if ( $undo > 0 && $undoafter > 0 ) {
1234 $undorev = Revision::newFromId( $undo );
1235 $oldrev = Revision::newFromId( $undoafter );
1236 $undoMsg = null;
1237
1238 # Sanity check, make sure it's the right page,
1239 # the revisions exist and they were not deleted.
1240 # Otherwise, $content will be left as-is.
1241 if ( !is_null( $undorev ) && !is_null( $oldrev ) &&
1242 !$undorev->isDeleted( RevisionRecord::DELETED_TEXT ) &&
1243 !$oldrev->isDeleted( RevisionRecord::DELETED_TEXT )
1244 ) {
1245 if ( WikiPage::hasDifferencesOutsideMainSlot( $undorev, $oldrev )
1246 || !$this->isSupportedContentModel( $oldrev->getContentModel() )
1247 ) {
1248 // Hack for undo while EditPage can't handle multi-slot editing
1249 $this->context->getOutput()->redirect( $this->mTitle->getFullURL( [
1250 'action' => 'mcrundo',
1251 'undo' => $undo,
1252 'undoafter' => $undoafter,
1253 ] ) );
1254 return false;
1255 } else {
1256 $content = $this->page->getUndoContent( $undorev, $oldrev );
1257
1258 if ( $content === false ) {
1259 # Warn the user that something went wrong
1260 $undoMsg = 'failure';
1261 }
1262 }
1263
1264 if ( $undoMsg === null ) {
1265 $oldContent = $this->page->getContent( RevisionRecord::RAW );
1266 $popts = ParserOptions::newFromUserAndLang(
1267 $user, MediaWikiServices::getInstance()->getContentLanguage() );
1268 $newContent = $content->preSaveTransform( $this->mTitle, $user, $popts );
1269 if ( $newContent->getModel() !== $oldContent->getModel() ) {
1270 // The undo may change content
1271 // model if its reverting the top
1272 // edit. This can result in
1273 // mismatched content model/format.
1274 $this->contentModel = $newContent->getModel();
1275 $this->contentFormat = $oldrev->getContentFormat();
1276 }
1277
1278 if ( $newContent->equals( $oldContent ) ) {
1279 # Tell the user that the undo results in no change,
1280 # i.e. the revisions were already undone.
1281 $undoMsg = 'nochange';
1282 $content = false;
1283 } else {
1284 # Inform the user of our success and set an automatic edit summary
1285 $undoMsg = 'success';
1286
1287 # If we just undid one rev, use an autosummary
1288 $firstrev = $oldrev->getNext();
1289 if ( $firstrev && $firstrev->getId() == $undo ) {
1290 $userText = $undorev->getUserText();
1291 if ( $userText === '' ) {
1292 $undoSummary = $this->context->msg(
1293 'undo-summary-username-hidden',
1294 $undo
1295 )->inContentLanguage()->text();
1296 } else {
1297 $undoMessage = ( $undorev->getUser() === 0 && $wgDisableAnonTalk ) ?
1298 'undo-summary-anon' :
1299 'undo-summary';
1300 $undoSummary = $this->context->msg(
1301 $undoMessage,
1302 $undo,
1303 $userText
1304 )->inContentLanguage()->text();
1305 }
1306 if ( $this->summary === '' ) {
1307 $this->summary = $undoSummary;
1308 } else {
1309 $this->summary = $undoSummary . $this->context->msg( 'colon-separator' )
1310 ->inContentLanguage()->text() . $this->summary;
1311 }
1312 $this->undidRev = $undo;
1313 }
1314 $this->formtype = 'diff';
1315 }
1316 }
1317 } else {
1318 // Failed basic sanity checks.
1319 // Older revisions may have been removed since the link
1320 // was created, or we may simply have got bogus input.
1321 $undoMsg = 'norev';
1322 }
1323
1324 $out = $this->context->getOutput();
1325 // Messages: undo-success, undo-failure, undo-main-slot-only, undo-norev,
1326 // undo-nochange.
1327 $class = ( $undoMsg == 'success' ? '' : 'error ' ) . "mw-undo-{$undoMsg}";
1328 $this->editFormPageTop .= Html::rawElement(
1329 'div', [ 'class' => $class ],
1330 $out->parseAsInterface(
1331 $this->context->msg( 'undo-' . $undoMsg )->plain()
1332 )
1333 );
1334 }
1335
1336 if ( $content === false ) {
1337 // Hack for restoring old revisions while EditPage
1338 // can't handle multi-slot editing.
1339
1340 $curRevision = $this->page->getRevision();
1341 $oldRevision = $this->mArticle->getRevisionFetched();
1342
1343 if ( $curRevision
1344 && $oldRevision
1345 && $curRevision->getId() !== $oldRevision->getId()
1346 && ( WikiPage::hasDifferencesOutsideMainSlot( $oldRevision, $curRevision )
1347 || !$this->isSupportedContentModel( $oldRevision->getContentModel() ) )
1348 ) {
1349 $this->context->getOutput()->redirect(
1350 $this->mTitle->getFullURL(
1351 [
1352 'action' => 'mcrrestore',
1353 'restore' => $oldRevision->getId(),
1354 ]
1355 )
1356 );
1357
1358 return false;
1359 }
1360 }
1361
1362 if ( $content === false ) {
1363 $content = $this->getOriginalContent( $user );
1364 }
1365 }
1366
1367 return $content;
1368 }
1369
1385 private function getOriginalContent( User $user ) {
1386 if ( $this->section == 'new' ) {
1387 return $this->getCurrentContent();
1388 }
1389 $revision = $this->mArticle->getRevisionFetched();
1390 if ( $revision === null ) {
1391 $handler = ContentHandler::getForModelID( $this->contentModel );
1392 return $handler->makeEmptyContent();
1393 }
1394 $content = $revision->getContent( RevisionRecord::FOR_THIS_USER, $user );
1395 return $content;
1396 }
1397
1410 public function getParentRevId() {
1411 if ( $this->parentRevId ) {
1412 return $this->parentRevId;
1413 } else {
1414 return $this->mArticle->getRevIdFetched();
1415 }
1416 }
1417
1426 protected function getCurrentContent() {
1427 $rev = $this->page->getRevision();
1428 $content = $rev ? $rev->getContent( RevisionRecord::RAW ) : null;
1429
1430 if ( $content === false || $content === null ) {
1431 $handler = ContentHandler::getForModelID( $this->contentModel );
1432 return $handler->makeEmptyContent();
1433 } elseif ( !$this->undidRev ) {
1434 // Content models should always be the same since we error
1435 // out if they are different before this point (in ->edit()).
1436 // The exception being, during an undo, the current revision might
1437 // differ from the prior revision.
1438 $logger = LoggerFactory::getInstance( 'editpage' );
1439 if ( $this->contentModel !== $rev->getContentModel() ) {
1440 $logger->warning( "Overriding content model from current edit {prev} to {new}", [
1441 'prev' => $this->contentModel,
1442 'new' => $rev->getContentModel(),
1443 'title' => $this->getTitle()->getPrefixedDBkey(),
1444 'method' => __METHOD__
1445 ] );
1446 $this->contentModel = $rev->getContentModel();
1447 }
1448
1449 // Given that the content models should match, the current selected
1450 // format should be supported.
1451 if ( !$content->isSupportedFormat( $this->contentFormat ) ) {
1452 $logger->warning( "Current revision content format unsupported. Overriding {prev} to {new}", [
1453
1454 'prev' => $this->contentFormat,
1455 'new' => $rev->getContentFormat(),
1456 'title' => $this->getTitle()->getPrefixedDBkey(),
1457 'method' => __METHOD__
1458 ] );
1459 $this->contentFormat = $rev->getContentFormat();
1460 }
1461 }
1462 return $content;
1463 }
1464
1473 $this->mPreloadContent = $content;
1474 }
1475
1487 protected function getPreloadedContent( $preload, $params = [] ) {
1488 if ( !empty( $this->mPreloadContent ) ) {
1490 }
1491
1492 $handler = ContentHandler::getForModelID( $this->contentModel );
1493
1494 if ( $preload === '' ) {
1495 return $handler->makeEmptyContent();
1496 }
1497
1498 $user = $this->context->getUser();
1499 $title = Title::newFromText( $preload );
1500
1501 # Check for existence to avoid getting MediaWiki:Noarticletext
1502 if ( !$this->isPageExistingAndViewable( $title, $user ) ) {
1503 // TODO: somehow show a warning to the user!
1504 return $handler->makeEmptyContent();
1505 }
1506
1507 $page = WikiPage::factory( $title );
1508 if ( $page->isRedirect() ) {
1510 # Same as before
1511 if ( !$this->isPageExistingAndViewable( $title, $user ) ) {
1512 // TODO: somehow show a warning to the user!
1513 return $handler->makeEmptyContent();
1514 }
1515 $page = WikiPage::factory( $title );
1516 }
1517
1518 $parserOptions = ParserOptions::newFromUser( $user );
1519 $content = $page->getContent( RevisionRecord::RAW );
1520
1521 if ( !$content ) {
1522 // TODO: somehow show a warning to the user!
1523 return $handler->makeEmptyContent();
1524 }
1525
1526 if ( $content->getModel() !== $handler->getModelID() ) {
1527 $converted = $content->convert( $handler->getModelID() );
1528
1529 if ( !$converted ) {
1530 // TODO: somehow show a warning to the user!
1531 wfDebug( "Attempt to preload incompatible content: " .
1532 "can't convert " . $content->getModel() .
1533 " to " . $handler->getModelID() );
1534
1535 return $handler->makeEmptyContent();
1536 }
1537
1538 $content = $converted;
1539 }
1540
1541 return $content->preloadTransform( $title, $parserOptions, $params );
1542 }
1543
1553 private function isPageExistingAndViewable( $title, User $user ) {
1554 $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
1555
1556 return $title && $title->exists() && $permissionManager->userCan( 'read', $user, $title );
1557 }
1558
1566 public function tokenOk( &$request ) {
1567 $token = $request->getVal( 'wpEditToken' );
1568 $user = $this->context->getUser();
1569 $this->mTokenOk = $user->matchEditToken( $token );
1570 $this->mTokenOkExceptSuffix = $user->matchEditTokenNoSuffix( $token );
1571 return $this->mTokenOk;
1572 }
1573
1588 protected function setPostEditCookie( $statusValue ) {
1589 $revisionId = $this->page->getLatest();
1590 $postEditKey = self::POST_EDIT_COOKIE_KEY_PREFIX . $revisionId;
1591
1592 $val = 'saved';
1593 if ( $statusValue == self::AS_SUCCESS_NEW_ARTICLE ) {
1594 $val = 'created';
1595 } elseif ( $this->oldid ) {
1596 $val = 'restored';
1597 }
1598
1599 $response = $this->context->getRequest()->response();
1600 $response->setCookie( $postEditKey, $val, time() + self::POST_EDIT_COOKIE_DURATION );
1601 }
1602
1609 public function attemptSave( &$resultDetails = false ) {
1610 // TODO: MCR: treat $this->minoredit like $this->bot and check isAllowed( 'minoredit' )!
1611 // Also, add $this->autopatrol like $this->bot and check isAllowed( 'autopatrol' )!
1612 // This is needed since PageUpdater no longer checks these rights!
1613
1614 // Allow bots to exempt some edits from bot flagging
1615 $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
1616 $bot = $permissionManager->userHasRight( $this->context->getUser(), 'bot' ) && $this->bot;
1617 $status = $this->internalAttemptSave( $resultDetails, $bot );
1618
1619 Hooks::run( 'EditPage::attemptSave:after', [ $this, $status, $resultDetails ] );
1620
1621 return $status;
1622 }
1623
1627 private function incrementResolvedConflicts() {
1628 if ( $this->context->getRequest()->getText( 'mode' ) !== 'conflict' ) {
1629 return;
1630 }
1631
1632 $this->getEditConflictHelper()->incrementResolvedStats();
1633 }
1634
1644 private function handleStatus( Status $status, $resultDetails ) {
1649 if ( $status->value == self::AS_SUCCESS_UPDATE
1650 || $status->value == self::AS_SUCCESS_NEW_ARTICLE
1651 ) {
1653
1654 $this->didSave = true;
1655 if ( !$resultDetails['nullEdit'] ) {
1656 $this->setPostEditCookie( $status->value );
1657 }
1658 }
1659
1660 $out = $this->context->getOutput();
1661
1662 // "wpExtraQueryRedirect" is a hidden input to modify
1663 // after save URL and is not used by actual edit form
1664 $request = $this->context->getRequest();
1665 $extraQueryRedirect = $request->getVal( 'wpExtraQueryRedirect' );
1666
1667 switch ( $status->value ) {
1675 case self::AS_END:
1678 return true;
1679
1681 return false;
1682
1686 $out->wrapWikiTextAsInterface( 'error',
1687 $status->getWikiText( false, false, $this->context->getLanguage() )
1688 );
1689 return true;
1690
1692 $query = $resultDetails['redirect'] ? 'redirect=no' : '';
1693 if ( $extraQueryRedirect ) {
1694 if ( $query !== '' ) {
1695 $query .= '&';
1696 }
1697 $query .= $extraQueryRedirect;
1698 }
1699 $anchor = $resultDetails['sectionanchor'] ?? '';
1700 $out->redirect( $this->mTitle->getFullURL( $query ) . $anchor );
1701 return false;
1702
1704 $extraQuery = '';
1705 $sectionanchor = $resultDetails['sectionanchor'];
1706
1707 // Give extensions a chance to modify URL query on update
1708 Hooks::run(
1709 'ArticleUpdateBeforeRedirect',
1710 [ $this->mArticle, &$sectionanchor, &$extraQuery ]
1711 );
1712
1713 if ( $resultDetails['redirect'] ) {
1714 if ( $extraQuery !== '' ) {
1715 $extraQuery = '&' . $extraQuery;
1716 }
1717 $extraQuery = 'redirect=no' . $extraQuery;
1718 }
1719 if ( $extraQueryRedirect ) {
1720 if ( $extraQuery !== '' ) {
1721 $extraQuery .= '&';
1722 }
1723 $extraQuery .= $extraQueryRedirect;
1724 }
1725
1726 $out->redirect( $this->mTitle->getFullURL( $extraQuery ) . $sectionanchor );
1727 return false;
1728
1730 $this->spamPageWithContent( $resultDetails['spam'] );
1731 return false;
1732
1734 throw new UserBlockedError( $this->context->getUser()->getBlock() );
1735
1738 throw new PermissionsError( 'upload' );
1739
1742 throw new PermissionsError( 'edit' );
1743
1745 throw new ReadOnlyError;
1746
1748 throw new ThrottledError();
1749
1751 $permission = $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage';
1752 throw new PermissionsError( $permission );
1753
1755 throw new PermissionsError( 'editcontentmodel' );
1756
1757 default:
1758 // We don't recognize $status->value. The only way that can happen
1759 // is if an extension hook aborted from inside ArticleSave.
1760 // Render the status object into $this->hookError
1761 // FIXME this sucks, we should just use the Status object throughout
1762 $this->hookError = '<div class="error">' . "\n" .
1763 $status->getWikiText( false, false, $this->context->getLanguage() ) .
1764 '</div>';
1765 return true;
1766 }
1767 }
1768
1778 protected function runPostMergeFilters( Content $content, Status $status, User $user ) {
1779 // Run old style post-section-merge edit filter
1780 if ( $this->hookError != '' ) {
1781 # ...or the hook could be expecting us to produce an error
1782 $status->fatal( 'hookaborted' );
1783 $status->value = self::AS_HOOK_ERROR_EXPECTED;
1784 return false;
1785 }
1786
1787 // Run new style post-section-merge edit filter
1788 if ( !Hooks::run( 'EditFilterMergedContent',
1789 [ $this->context, $content, $status, $this->summary,
1790 $user, $this->minoredit ] )
1791 ) {
1792 # Error messages etc. could be handled within the hook...
1793 if ( $status->isGood() ) {
1794 $status->fatal( 'hookaborted' );
1795 // Not setting $this->hookError here is a hack to allow the hook
1796 // to cause a return to the edit page without $this->hookError
1797 // being set. This is used by ConfirmEdit to display a captcha
1798 // without any error message cruft.
1799 } else {
1800 $this->hookError = $this->formatStatusErrors( $status );
1801 }
1802 // Use the existing $status->value if the hook set it
1803 if ( !$status->value ) {
1804 $status->value = self::AS_HOOK_ERROR;
1805 }
1806 return false;
1807 } elseif ( !$status->isOK() ) {
1808 # ...or the hook could be expecting us to produce an error
1809 // FIXME this sucks, we should just use the Status object throughout
1810 if ( !$status->getErrors() ) {
1811 // Provide a fallback error message if none was set
1812 $status->fatal( 'hookaborted' );
1813 }
1814 $this->hookError = $this->formatStatusErrors( $status );
1815 $status->value = self::AS_HOOK_ERROR_EXPECTED;
1816 return false;
1817 }
1818
1819 return true;
1820 }
1821
1828 private function formatStatusErrors( Status $status ) {
1829 $errmsg = $status->getWikiText(
1830 'edit-error-short',
1831 'edit-error-long',
1832 $this->context->getLanguage()
1833 );
1834 return <<<ERROR
1835<div class="errorbox">
1836{$errmsg}
1837</div>
1838<br clear="all" />
1839ERROR;
1840 }
1841
1848 private function newSectionSummary( &$sectionanchor = null ) {
1849 if ( $this->sectiontitle !== '' ) {
1850 $sectionanchor = $this->guessSectionName( $this->sectiontitle );
1851 // If no edit summary was specified, create one automatically from the section
1852 // title and have it link to the new section. Otherwise, respect the summary as
1853 // passed.
1854 if ( $this->summary === '' ) {
1855 $cleanSectionTitle = MediaWikiServices::getInstance()->getParser()
1856 ->stripSectionName( $this->sectiontitle );
1857 return $this->context->msg( 'newsectionsummary' )
1858 ->plaintextParams( $cleanSectionTitle )->inContentLanguage()->text();
1859 }
1860 } elseif ( $this->summary !== '' ) {
1861 $sectionanchor = $this->guessSectionName( $this->summary );
1862 # This is a new section, so create a link to the new section
1863 # in the revision summary.
1864 $cleanSummary = MediaWikiServices::getInstance()->getParser()
1865 ->stripSectionName( $this->summary );
1866 return $this->context->msg( 'newsectionsummary' )
1867 ->plaintextParams( $cleanSummary )->inContentLanguage()->text();
1868 }
1869 return $this->summary;
1870 }
1871
1896 public function internalAttemptSave( &$result, $bot = false ) {
1897 $status = Status::newGood();
1898 $user = $this->context->getUser();
1899 $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
1900
1901 if ( !Hooks::run( 'EditPage::attemptSave', [ $this ] ) ) {
1902 wfDebug( "Hook 'EditPage::attemptSave' aborted article saving\n" );
1903 $status->fatal( 'hookaborted' );
1904 $status->value = self::AS_HOOK_ERROR;
1905 return $status;
1906 }
1907
1908 if ( $this->unicodeCheck !== self::UNICODE_CHECK ) {
1909 $status->fatal( 'unicode-support-fail' );
1910 $status->value = self::AS_UNICODE_NOT_SUPPORTED;
1911 return $status;
1912 }
1913
1914 $request = $this->context->getRequest();
1915 $spam = $request->getText( 'wpAntispam' );
1916 if ( $spam !== '' ) {
1917 wfDebugLog(
1918 'SimpleAntiSpam',
1919 $user->getName() .
1920 ' editing "' .
1921 $this->mTitle->getPrefixedText() .
1922 '" submitted bogus field "' .
1923 $spam .
1924 '"'
1925 );
1926 $status->fatal( 'spamprotectionmatch', false );
1927 $status->value = self::AS_SPAM_ERROR;
1928 return $status;
1929 }
1930
1931 try {
1932 # Construct Content object
1933 $textbox_content = $this->toEditContent( $this->textbox1 );
1934 } catch ( MWContentSerializationException $ex ) {
1935 $status->fatal(
1936 'content-failed-to-parse',
1937 $this->contentModel,
1938 $this->contentFormat,
1939 $ex->getMessage()
1940 );
1941 $status->value = self::AS_PARSE_ERROR;
1942 return $status;
1943 }
1944
1945 # Check image redirect
1946 if ( $this->mTitle->getNamespace() == NS_FILE &&
1947 $textbox_content->isRedirect() &&
1948 !$permissionManager->userHasRight( $user, 'upload' )
1949 ) {
1951 $status->setResult( false, $code );
1952
1953 return $status;
1954 }
1955
1956 # Check for spam
1957 $match = self::matchSummarySpamRegex( $this->summary );
1958 if ( $match === false && $this->section == 'new' ) {
1959 # $wgSpamRegex is enforced on this new heading/summary because, unlike
1960 # regular summaries, it is added to the actual wikitext.
1961 if ( $this->sectiontitle !== '' ) {
1962 # This branch is taken when the API is used with the 'sectiontitle' parameter.
1963 $match = self::matchSpamRegex( $this->sectiontitle );
1964 } else {
1965 # This branch is taken when the "Add Topic" user interface is used, or the API
1966 # is used with the 'summary' parameter.
1967 $match = self::matchSpamRegex( $this->summary );
1968 }
1969 }
1970 if ( $match === false ) {
1971 $match = self::matchSpamRegex( $this->textbox1 );
1972 }
1973 if ( $match !== false ) {
1974 $result['spam'] = $match;
1975 $ip = $request->getIP();
1976 $pdbk = $this->mTitle->getPrefixedDBkey();
1977 $match = str_replace( "\n", '', $match );
1978 wfDebugLog( 'SpamRegex', "$ip spam regex hit [[$pdbk]]: \"$match\"" );
1979 $status->fatal( 'spamprotectionmatch', $match );
1980 $status->value = self::AS_SPAM_ERROR;
1981 return $status;
1982 }
1983 if ( !Hooks::run(
1984 'EditFilter',
1985 [ $this, $this->textbox1, $this->section, &$this->hookError, $this->summary ] )
1986 ) {
1987 # Error messages etc. could be handled within the hook...
1988 $status->fatal( 'hookaborted' );
1989 $status->value = self::AS_HOOK_ERROR;
1990 return $status;
1991 } elseif ( $this->hookError != '' ) {
1992 # ...or the hook could be expecting us to produce an error
1993 $status->fatal( 'hookaborted' );
1994 $status->value = self::AS_HOOK_ERROR_EXPECTED;
1995 return $status;
1996 }
1997
1998 if ( $permissionManager->isBlockedFrom( $user, $this->mTitle ) ) {
1999 // Auto-block user's IP if the account was "hard" blocked
2000 if ( !wfReadOnly() ) {
2001 $user->spreadAnyEditBlock();
2002 }
2003 # Check block state against master, thus 'false'.
2004 $status->setResult( false, self::AS_BLOCKED_PAGE_FOR_USER );
2005 return $status;
2006 }
2007
2008 $this->contentLength = strlen( $this->textbox1 );
2009 $config = $this->context->getConfig();
2010 $maxArticleSize = $config->get( 'MaxArticleSize' );
2011 if ( $this->contentLength > $maxArticleSize * 1024 ) {
2012 // Error will be displayed by showEditForm()
2013 $this->tooBig = true;
2014 $status->setResult( false, self::AS_CONTENT_TOO_BIG );
2015 return $status;
2016 }
2017
2018 if ( !$permissionManager->userHasRight( $user, 'edit' ) ) {
2019 if ( $user->isAnon() ) {
2020 $status->setResult( false, self::AS_READ_ONLY_PAGE_ANON );
2021 return $status;
2022 } else {
2023 $status->fatal( 'readonlytext' );
2024 $status->value = self::AS_READ_ONLY_PAGE_LOGGED;
2025 return $status;
2026 }
2027 }
2028
2029 $changingContentModel = false;
2030 if ( $this->contentModel !== $this->mTitle->getContentModel() ) {
2031 if ( !$config->get( 'ContentHandlerUseDB' ) ) {
2032 $status->fatal( 'editpage-cannot-use-custom-model' );
2033 $status->value = self::AS_CANNOT_USE_CUSTOM_MODEL;
2034 return $status;
2035 } elseif ( !$permissionManager->userHasRight( $user, 'editcontentmodel' ) ) {
2036 $status->setResult( false, self::AS_NO_CHANGE_CONTENT_MODEL );
2037 return $status;
2038 }
2039 // Make sure the user can edit the page under the new content model too
2040 $titleWithNewContentModel = clone $this->mTitle;
2041 $titleWithNewContentModel->setContentModel( $this->contentModel );
2042
2043 $canEditModel = $permissionManager->userCan(
2044 'editcontentmodel',
2045 $user,
2046 $titleWithNewContentModel
2047 );
2048
2049 if (
2050 !$canEditModel
2051 || !$permissionManager->userCan( 'edit', $user, $titleWithNewContentModel )
2052 ) {
2053 $status->setResult( false, self::AS_NO_CHANGE_CONTENT_MODEL );
2054
2055 return $status;
2056 }
2057
2058 $changingContentModel = true;
2059 $oldContentModel = $this->mTitle->getContentModel();
2060 }
2061
2062 if ( $this->changeTags ) {
2063 $changeTagsStatus = ChangeTags::canAddTagsAccompanyingChange(
2064 $this->changeTags, $user );
2065 if ( !$changeTagsStatus->isOK() ) {
2066 $changeTagsStatus->value = self::AS_CHANGE_TAG_ERROR;
2067 return $changeTagsStatus;
2068 }
2069 }
2070
2071 if ( wfReadOnly() ) {
2072 $status->fatal( 'readonlytext' );
2073 $status->value = self::AS_READ_ONLY_PAGE;
2074 return $status;
2075 }
2076 if ( $user->pingLimiter() || $user->pingLimiter( 'linkpurge', 0 )
2077 || ( $changingContentModel && $user->pingLimiter( 'editcontentmodel' ) )
2078 ) {
2079 $status->fatal( 'actionthrottledtext' );
2080 $status->value = self::AS_RATE_LIMITED;
2081 return $status;
2082 }
2083
2084 # If the article has been deleted while editing, don't save it without
2085 # confirmation
2086 if ( $this->wasDeletedSinceLastEdit() && !$this->recreate ) {
2087 $status->setResult( false, self::AS_ARTICLE_WAS_DELETED );
2088 return $status;
2089 }
2090
2091 # Load the page data from the master. If anything changes in the meantime,
2092 # we detect it by using page_latest like a token in a 1 try compare-and-swap.
2093 $this->page->loadPageData( 'fromdbmaster' );
2094 $new = !$this->page->exists();
2095
2096 if ( $new ) {
2097 // Late check for create permission, just in case *PARANOIA*
2098 if ( !$permissionManager->userCan( 'create', $user, $this->mTitle ) ) {
2099 $status->fatal( 'nocreatetext' );
2100 $status->value = self::AS_NO_CREATE_PERMISSION;
2101 wfDebug( __METHOD__ . ": no create permission\n" );
2102 return $status;
2103 }
2104
2105 // Don't save a new page if it's blank or if it's a MediaWiki:
2106 // message with content equivalent to default (allow empty pages
2107 // in this case to disable messages, see T52124)
2108 $defaultMessageText = $this->mTitle->getDefaultMessageText();
2109 if ( $this->mTitle->getNamespace() === NS_MEDIAWIKI && $defaultMessageText !== false ) {
2110 $defaultText = $defaultMessageText;
2111 } else {
2112 $defaultText = '';
2113 }
2114
2115 if ( !$this->allowBlankArticle && $this->textbox1 === $defaultText ) {
2116 $this->blankArticle = true;
2117 $status->fatal( 'blankarticle' );
2118 $status->setResult( false, self::AS_BLANK_ARTICLE );
2119 return $status;
2120 }
2121
2122 if ( !$this->runPostMergeFilters( $textbox_content, $status, $user ) ) {
2123 return $status;
2124 }
2125
2126 $content = $textbox_content;
2127
2128 $result['sectionanchor'] = '';
2129 if ( $this->section == 'new' ) {
2130 if ( $this->sectiontitle !== '' ) {
2131 // Insert the section title above the content.
2132 $content = $content->addSectionHeader( $this->sectiontitle );
2133 } elseif ( $this->summary !== '' ) {
2134 // Insert the section title above the content.
2135 $content = $content->addSectionHeader( $this->summary );
2136 }
2137 $this->summary = $this->newSectionSummary( $result['sectionanchor'] );
2138 }
2139
2140 $status->value = self::AS_SUCCESS_NEW_ARTICLE;
2141
2142 } else { # not $new
2143
2144 # Article exists. Check for edit conflict.
2145
2146 $this->page->clear(); # Force reload of dates, etc.
2147 $timestamp = $this->page->getTimestamp();
2148 $latest = $this->page->getLatest();
2149
2150 wfDebug( "timestamp: {$timestamp}, edittime: {$this->edittime}\n" );
2151
2152 // An edit conflict is detected if the current revision is different from the
2153 // revision that was current when editing was initiated on the client.
2154 // This is checked based on the timestamp and revision ID.
2155 // TODO: the timestamp based check can probably go away now.
2156 if ( $timestamp != $this->edittime
2157 || ( $this->editRevId !== null && $this->editRevId != $latest )
2158 ) {
2159 $this->isConflict = true;
2160 if ( $this->section == 'new' ) {
2161 if ( $this->page->getUserText() == $user->getName() &&
2162 $this->page->getComment() == $this->newSectionSummary()
2163 ) {
2164 // Probably a duplicate submission of a new comment.
2165 // This can happen when CDN resends a request after
2166 // a timeout but the first one actually went through.
2167 wfDebug( __METHOD__
2168 . ": duplicate new section submission; trigger edit conflict!\n" );
2169 } else {
2170 // New comment; suppress conflict.
2171 $this->isConflict = false;
2172 wfDebug( __METHOD__ . ": conflict suppressed; new section\n" );
2173 }
2174 } elseif ( $this->section == ''
2176 DB_MASTER, $this->mTitle->getArticleID(),
2177 $user->getId(), $this->edittime
2178 )
2179 ) {
2180 # Suppress edit conflict with self, except for section edits where merging is required.
2181 wfDebug( __METHOD__ . ": Suppressing edit conflict, same user.\n" );
2182 $this->isConflict = false;
2183 }
2184 }
2185
2186 // If sectiontitle is set, use it, otherwise use the summary as the section title.
2187 if ( $this->sectiontitle !== '' ) {
2188 $sectionTitle = $this->sectiontitle;
2189 } else {
2190 $sectionTitle = $this->summary;
2191 }
2192
2193 $content = null;
2194
2195 if ( $this->isConflict ) {
2196 wfDebug( __METHOD__
2197 . ": conflict! getting section '{$this->section}' for time '{$this->edittime}'"
2198 . " (id '{$this->editRevId}') (article time '{$timestamp}')\n" );
2199 // @TODO: replaceSectionAtRev() with base ID (not prior current) for ?oldid=X case
2200 // ...or disable section editing for non-current revisions (not exposed anyway).
2201 if ( $this->editRevId !== null ) {
2202 $content = $this->page->replaceSectionAtRev(
2203 $this->section,
2204 $textbox_content,
2205 $sectionTitle,
2206 $this->editRevId
2207 );
2208 } else {
2209 $content = $this->page->replaceSectionContent(
2210 $this->section,
2211 $textbox_content,
2212 $sectionTitle,
2213 $this->edittime
2214 );
2215 }
2216 } else {
2217 wfDebug( __METHOD__ . ": getting section '{$this->section}'\n" );
2218 $content = $this->page->replaceSectionContent(
2219 $this->section,
2220 $textbox_content,
2221 $sectionTitle
2222 );
2223 }
2224
2225 if ( is_null( $content ) ) {
2226 wfDebug( __METHOD__ . ": activating conflict; section replace failed.\n" );
2227 $this->isConflict = true;
2228 $content = $textbox_content; // do not try to merge here!
2229 } elseif ( $this->isConflict ) {
2230 # Attempt merge
2231 if ( $this->mergeChangesIntoContent( $content ) ) {
2232 // Successful merge! Maybe we should tell the user the good news?
2233 $this->isConflict = false;
2234 wfDebug( __METHOD__ . ": Suppressing edit conflict, successful merge.\n" );
2235 } else {
2236 $this->section = '';
2237 $this->textbox1 = ContentHandler::getContentText( $content );
2238 wfDebug( __METHOD__ . ": Keeping edit conflict, failed merge.\n" );
2239 }
2240 }
2241
2242 if ( $this->isConflict ) {
2243 $status->setResult( false, self::AS_CONFLICT_DETECTED );
2244 return $status;
2245 }
2246
2247 if ( !$this->runPostMergeFilters( $content, $status, $user ) ) {
2248 return $status;
2249 }
2250
2251 if ( $this->section == 'new' ) {
2252 // Handle the user preference to force summaries here
2253 if ( !$this->allowBlankSummary && trim( $this->summary ) == '' ) {
2254 $this->missingSummary = true;
2255 $status->fatal( 'missingsummary' ); // or 'missingcommentheader' if $section == 'new'. Blegh
2256 $status->value = self::AS_SUMMARY_NEEDED;
2257 return $status;
2258 }
2259
2260 // Do not allow the user to post an empty comment
2261 if ( $this->textbox1 == '' ) {
2262 $this->missingComment = true;
2263 $status->fatal( 'missingcommenttext' );
2264 $status->value = self::AS_TEXTBOX_EMPTY;
2265 return $status;
2266 }
2267 } elseif ( !$this->allowBlankSummary
2268 && !$content->equals( $this->getOriginalContent( $user ) )
2269 && !$content->isRedirect()
2270 && md5( $this->summary ) == $this->autoSumm
2271 ) {
2272 $this->missingSummary = true;
2273 $status->fatal( 'missingsummary' );
2274 $status->value = self::AS_SUMMARY_NEEDED;
2275 return $status;
2276 }
2277
2278 # All's well
2279 $sectionanchor = '';
2280 if ( $this->section == 'new' ) {
2281 $this->summary = $this->newSectionSummary( $sectionanchor );
2282 } elseif ( $this->section != '' ) {
2283 # Try to get a section anchor from the section source, redirect
2284 # to edited section if header found.
2285 # XXX: Might be better to integrate this into Article::replaceSectionAtRev
2286 # for duplicate heading checking and maybe parsing.
2287 $hasmatch = preg_match( "/^ *([=]{1,6})(.*?)(\\1) *\\n/i", $this->textbox1, $matches );
2288 # We can't deal with anchors, includes, html etc in the header for now,
2289 # headline would need to be parsed to improve this.
2290 if ( $hasmatch && strlen( $matches[2] ) > 0 ) {
2291 $sectionanchor = $this->guessSectionName( $matches[2] );
2292 }
2293 }
2294 $result['sectionanchor'] = $sectionanchor;
2295
2296 // Save errors may fall down to the edit form, but we've now
2297 // merged the section into full text. Clear the section field
2298 // so that later submission of conflict forms won't try to
2299 // replace that into a duplicated mess.
2300 $this->textbox1 = $this->toEditText( $content );
2301 $this->section = '';
2302
2303 $status->value = self::AS_SUCCESS_UPDATE;
2304 }
2305
2306 if ( !$this->allowSelfRedirect
2307 && $content->isRedirect()
2308 && $content->getRedirectTarget()->equals( $this->getTitle() )
2309 ) {
2310 // If the page already redirects to itself, don't warn.
2311 $currentTarget = $this->getCurrentContent()->getRedirectTarget();
2312 if ( !$currentTarget || !$currentTarget->equals( $this->getTitle() ) ) {
2313 $this->selfRedirect = true;
2314 $status->fatal( 'selfredirect' );
2315 $status->value = self::AS_SELF_REDIRECT;
2316 return $status;
2317 }
2318 }
2319
2320 // Check for length errors again now that the section is merged in
2321 $this->contentLength = strlen( $this->toEditText( $content ) );
2322 if ( $this->contentLength > $maxArticleSize * 1024 ) {
2323 $this->tooBig = true;
2324 $status->setResult( false, self::AS_MAX_ARTICLE_SIZE_EXCEEDED );
2325 return $status;
2326 }
2327
2328 $flags = EDIT_AUTOSUMMARY |
2329 ( $new ? EDIT_NEW : EDIT_UPDATE ) |
2330 ( ( $this->minoredit && !$this->isNew ) ? EDIT_MINOR : 0 ) |
2331 ( $bot ? EDIT_FORCE_BOT : 0 );
2332
2333 $doEditStatus = $this->page->doEditContent(
2334 $content,
2335 $this->summary,
2336 $flags,
2337 false,
2338 $user,
2339 $content->getDefaultFormat(),
2342 );
2343
2344 if ( !$doEditStatus->isOK() ) {
2345 // Failure from doEdit()
2346 // Show the edit conflict page for certain recognized errors from doEdit(),
2347 // but don't show it for errors from extension hooks
2348 $errors = $doEditStatus->getErrorsArray();
2349 if ( in_array( $errors[0][0],
2350 [ 'edit-gone-missing', 'edit-conflict', 'edit-already-exists' ] )
2351 ) {
2352 $this->isConflict = true;
2353 // Destroys data doEdit() put in $status->value but who cares
2354 $doEditStatus->value = self::AS_END;
2355 }
2356 return $doEditStatus;
2357 }
2358
2359 $result['nullEdit'] = $doEditStatus->hasMessage( 'edit-no-change' );
2360 if ( $result['nullEdit'] ) {
2361 // We don't know if it was a null edit until now, so increment here
2362 $user->pingLimiter( 'linkpurge' );
2363 }
2364 $result['redirect'] = $content->isRedirect();
2365
2366 $this->updateWatchlist();
2367
2368 // If the content model changed, add a log entry
2369 if ( $changingContentModel ) {
2371 $user,
2372 $new ? false : $oldContentModel,
2375 );
2376 }
2377
2378 return $status;
2379 }
2380
2387 protected function addContentModelChangeLogEntry( User $user, $oldModel, $newModel, $reason ) {
2388 $new = $oldModel === false;
2389 $log = new ManualLogEntry( 'contentmodel', $new ? 'new' : 'change' );
2390 $log->setPerformer( $user );
2391 $log->setTarget( $this->mTitle );
2392 $log->setComment( $reason );
2393 $log->setParameters( [
2394 '4::oldmodel' => $oldModel,
2395 '5::newmodel' => $newModel
2396 ] );
2397 $logid = $log->insert();
2398 $log->publish( $logid );
2399 }
2400
2404 protected function updateWatchlist() {
2405 $user = $this->context->getUser();
2406 if ( !$user->isLoggedIn() ) {
2407 return;
2408 }
2409
2411 $watch = $this->watchthis;
2412 // Do this in its own transaction to reduce contention...
2413 DeferredUpdates::addCallableUpdate( function () use ( $user, $title, $watch ) {
2414 if ( $watch == $user->isWatched( $title, User::IGNORE_USER_RIGHTS ) ) {
2415 return; // nothing to change
2416 }
2417 WatchAction::doWatchOrUnwatch( $watch, $title, $user );
2418 } );
2419 }
2420
2432 private function mergeChangesIntoContent( &$editContent ) {
2433 $db = wfGetDB( DB_MASTER );
2434
2435 // This is the revision that was current at the time editing was initiated on the client,
2436 // even if the edit was based on an old revision.
2437 $baseRevision = $this->getBaseRevision();
2438 $baseContent = $baseRevision ? $baseRevision->getContent() : null;
2439
2440 if ( is_null( $baseContent ) ) {
2441 return false;
2442 }
2443
2444 // The current state, we want to merge updates into it
2445 $currentRevision = Revision::loadFromTitle( $db, $this->mTitle );
2446 $currentContent = $currentRevision ? $currentRevision->getContent() : null;
2447
2448 if ( is_null( $currentContent ) ) {
2449 return false;
2450 }
2451
2452 $handler = ContentHandler::getForModelID( $baseContent->getModel() );
2453
2454 $result = $handler->merge3( $baseContent, $editContent, $currentContent );
2455
2456 if ( $result ) {
2457 $editContent = $result;
2458 // Update parentRevId to what we just merged.
2459 $this->parentRevId = $currentRevision->getId();
2460 return true;
2461 }
2462
2463 return false;
2464 }
2465
2478 public function getBaseRevision() {
2479 if ( !$this->mBaseRevision ) {
2480 $db = wfGetDB( DB_MASTER );
2481 $this->mBaseRevision = $this->editRevId
2482 ? Revision::newFromId( $this->editRevId, Revision::READ_LATEST )
2483 : Revision::loadFromTimestamp( $db, $this->mTitle, $this->edittime );
2484 }
2485 return $this->mBaseRevision;
2486 }
2487
2495 public static function matchSpamRegex( $text ) {
2496 global $wgSpamRegex;
2497 // For back compatibility, $wgSpamRegex may be a single string or an array of regexes.
2498 $regexes = (array)$wgSpamRegex;
2499 return self::matchSpamRegexInternal( $text, $regexes );
2500 }
2501
2509 public static function matchSummarySpamRegex( $text ) {
2510 global $wgSummarySpamRegex;
2511 $regexes = (array)$wgSummarySpamRegex;
2512 return self::matchSpamRegexInternal( $text, $regexes );
2513 }
2514
2520 protected static function matchSpamRegexInternal( $text, $regexes ) {
2521 foreach ( $regexes as $regex ) {
2522 $matches = [];
2523 if ( preg_match( $regex, $text, $matches ) ) {
2524 return $matches[0];
2525 }
2526 }
2527 return false;
2528 }
2529
2530 public function setHeaders() {
2531 $out = $this->context->getOutput();
2532
2533 $out->addModules( 'mediawiki.action.edit' );
2534 $out->addModuleStyles( 'mediawiki.action.edit.styles' );
2535 $out->addModuleStyles( 'mediawiki.editfont.styles' );
2536
2537 $user = $this->context->getUser();
2538
2539 if ( $user->getOption( 'uselivepreview' ) ) {
2540 $out->addModules( 'mediawiki.action.edit.preview' );
2541 }
2542
2543 if ( $user->getOption( 'useeditwarning' ) ) {
2544 $out->addModules( 'mediawiki.action.edit.editWarning' );
2545 }
2546
2547 # Enabled article-related sidebar, toplinks, etc.
2548 $out->setArticleRelated( true );
2549
2550 $contextTitle = $this->getContextTitle();
2551 if ( $this->isConflict ) {
2552 $msg = 'editconflict';
2553 } elseif ( $contextTitle->exists() && $this->section != '' ) {
2554 $msg = $this->section == 'new' ? 'editingcomment' : 'editingsection';
2555 } else {
2556 $msg = $contextTitle->exists()
2557 || ( $contextTitle->getNamespace() == NS_MEDIAWIKI
2558 && $contextTitle->getDefaultMessageText() !== false
2559 )
2560 ? 'editing'
2561 : 'creating';
2562 }
2563
2564 # Use the title defined by DISPLAYTITLE magic word when present
2565 # NOTE: getDisplayTitle() returns HTML while getPrefixedText() returns plain text.
2566 # setPageTitle() treats the input as wikitext, which should be safe in either case.
2567 $displayTitle = isset( $this->mParserOutput ) ? $this->mParserOutput->getDisplayTitle() : false;
2568 if ( $displayTitle === false ) {
2569 $displayTitle = $contextTitle->getPrefixedText();
2570 } else {
2571 $out->setDisplayTitle( $displayTitle );
2572 }
2573 $out->setPageTitle( $this->context->msg( $msg, $displayTitle ) );
2574
2575 $config = $this->context->getConfig();
2576
2577 # Transmit the name of the message to JavaScript for live preview
2578 # Keep Resources.php/mediawiki.action.edit.preview in sync with the possible keys
2579 $out->addJsConfigVars( [
2580 'wgEditMessage' => $msg,
2581 'wgAjaxEditStash' => $config->get( 'AjaxEditStash' ),
2582 ] );
2583
2584 // Add whether to use 'save' or 'publish' messages to JavaScript for post-edit, other
2585 // editors, etc.
2586 $out->addJsConfigVars(
2587 'wgEditSubmitButtonLabelPublish',
2588 $config->get( 'EditSubmitButtonLabelPublish' )
2589 );
2590 }
2591
2595 protected function showIntro() {
2596 if ( $this->suppressIntro ) {
2597 return;
2598 }
2599
2600 $out = $this->context->getOutput();
2601 $namespace = $this->mTitle->getNamespace();
2602
2603 if ( $namespace == NS_MEDIAWIKI ) {
2604 # Show a warning if editing an interface message
2605 $out->wrapWikiMsg( "<div class='mw-editinginterface'>\n$1\n</div>", 'editinginterface' );
2606 # If this is a default message (but not css, json, or js),
2607 # show a hint that it is translatable on translatewiki.net
2608 if (
2609 !$this->mTitle->hasContentModel( CONTENT_MODEL_CSS )
2610 && !$this->mTitle->hasContentModel( CONTENT_MODEL_JSON )
2611 && !$this->mTitle->hasContentModel( CONTENT_MODEL_JAVASCRIPT )
2612 ) {
2613 $defaultMessageText = $this->mTitle->getDefaultMessageText();
2614 if ( $defaultMessageText !== false ) {
2615 $out->wrapWikiMsg( "<div class='mw-translateinterface'>\n$1\n</div>",
2616 'translateinterface' );
2617 }
2618 }
2619 } elseif ( $namespace == NS_FILE ) {
2620 # Show a hint to shared repo
2621 $file = MediaWikiServices::getInstance()->getRepoGroup()->findFile( $this->mTitle );
2622 if ( $file && !$file->isLocal() ) {
2623 $descUrl = $file->getDescriptionUrl();
2624 # there must be a description url to show a hint to shared repo
2625 if ( $descUrl ) {
2626 if ( !$this->mTitle->exists() ) {
2627 $out->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-create\">\n$1\n</div>", [
2628 'sharedupload-desc-create', $file->getRepo()->getDisplayName(), $descUrl
2629 ] );
2630 } else {
2631 $out->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-edit\">\n$1\n</div>", [
2632 'sharedupload-desc-edit', $file->getRepo()->getDisplayName(), $descUrl
2633 ] );
2634 }
2635 }
2636 }
2637 }
2638
2639 # Show a warning message when someone creates/edits a user (talk) page but the user does not exist
2640 # Show log extract when the user is currently blocked
2641 if ( $namespace == NS_USER || $namespace == NS_USER_TALK ) {
2642 $username = explode( '/', $this->mTitle->getText(), 2 )[0];
2643 $user = User::newFromName( $username, false /* allow IP users */ );
2644 $ip = User::isIP( $username );
2645 $block = DatabaseBlock::newFromTarget( $user, $user );
2646 if ( !( $user && $user->isLoggedIn() ) && !$ip ) { # User does not exist
2647 $out->wrapWikiMsg( "<div class=\"mw-userpage-userdoesnotexist error\">\n$1\n</div>",
2648 [ 'userpage-userdoesnotexist', wfEscapeWikiText( $username ) ] );
2649 } elseif (
2650 !is_null( $block ) &&
2651 $block->getType() != DatabaseBlock::TYPE_AUTO &&
2652 ( $block->isSitewide() || $user->isBlockedFrom( $this->mTitle ) )
2653 ) {
2654 // Show log extract if the user is sitewide blocked or is partially
2655 // blocked and not allowed to edit their user page or user talk page
2657 $out,
2658 'block',
2659 MediaWikiServices::getInstance()->getNamespaceInfo()->
2660 getCanonicalName( NS_USER ) . ':' . $block->getTarget(),
2661 '',
2662 [
2663 'lim' => 1,
2664 'showIfEmpty' => false,
2665 'msgKey' => [
2666 'blocked-notice-logextract',
2667 $user->getName() # Support GENDER in notice
2668 ]
2669 ]
2670 );
2671 }
2672 }
2673 # Try to add a custom edit intro, or use the standard one if this is not possible.
2674 if ( !$this->showCustomIntro() && !$this->mTitle->exists() ) {
2675 $helpLink = wfExpandUrl( Skin::makeInternalOrExternalUrl(
2676 $this->context->msg( 'helppage' )->inContentLanguage()->text()
2677 ) );
2678 if ( $this->context->getUser()->isLoggedIn() ) {
2679 $out->wrapWikiMsg(
2680 // Suppress the external link icon, consider the help url an internal one
2681 "<div class=\"mw-newarticletext plainlinks\">\n$1\n</div>",
2682 [
2683 'newarticletext',
2684 $helpLink
2685 ]
2686 );
2687 } else {
2688 $out->wrapWikiMsg(
2689 // Suppress the external link icon, consider the help url an internal one
2690 "<div class=\"mw-newarticletextanon plainlinks\">\n$1\n</div>",
2691 [
2692 'newarticletextanon',
2693 $helpLink
2694 ]
2695 );
2696 }
2697 }
2698 # Give a notice if the user is editing a deleted/moved page...
2699 if ( !$this->mTitle->exists() ) {
2700 $dbr = wfGetDB( DB_REPLICA );
2701
2702 LogEventsList::showLogExtract( $out, [ 'delete', 'move' ], $this->mTitle,
2703 '',
2704 [
2705 'lim' => 10,
2706 'conds' => [ 'log_action != ' . $dbr->addQuotes( 'revision' ) ],
2707 'showIfEmpty' => false,
2708 'msgKey' => [ 'recreate-moveddeleted-warn' ]
2709 ]
2710 );
2711 }
2712 }
2713
2719 protected function showCustomIntro() {
2720 if ( $this->editintro ) {
2721 $title = Title::newFromText( $this->editintro );
2722 if ( $this->isPageExistingAndViewable( $title, $this->context->getUser() ) ) {
2723 // Added using template syntax, to take <noinclude>'s into account.
2724 $this->context->getOutput()->addWikiTextAsContent(
2725 '<div class="mw-editintro">{{:' . $title->getFullText() . '}}</div>',
2726 /*linestart*/true,
2727 $this->mTitle
2728 );
2729 return true;
2730 }
2731 }
2732 return false;
2733 }
2734
2753 protected function toEditText( $content ) {
2754 if ( $content === null || $content === false || is_string( $content ) ) {
2755 return $content;
2756 }
2757
2758 if ( !$this->isSupportedContentModel( $content->getModel() ) ) {
2759 throw new MWException( 'This content model is not supported: ' . $content->getModel() );
2760 }
2761
2762 return $content->serialize( $this->contentFormat );
2763 }
2764
2781 protected function toEditContent( $text ) {
2782 if ( $text === false || $text === null ) {
2783 return $text;
2784 }
2785
2786 $content = ContentHandler::makeContent( $text, $this->getTitle(),
2787 $this->contentModel, $this->contentFormat );
2788
2789 if ( !$this->isSupportedContentModel( $content->getModel() ) ) {
2790 throw new MWException( 'This content model is not supported: ' . $content->getModel() );
2791 }
2792
2793 return $content;
2794 }
2795
2804 public function showEditForm( $formCallback = null ) {
2805 # need to parse the preview early so that we know which templates are used,
2806 # otherwise users with "show preview after edit box" will get a blank list
2807 # we parse this near the beginning so that setHeaders can do the title
2808 # setting work instead of leaving it in getPreviewText
2809 $previewOutput = '';
2810 if ( $this->formtype == 'preview' ) {
2811 $previewOutput = $this->getPreviewText();
2812 }
2813
2814 $out = $this->context->getOutput();
2815
2816 // Avoid PHP 7.1 warning of passing $this by reference
2817 $editPage = $this;
2818 Hooks::run( 'EditPage::showEditForm:initial', [ &$editPage, &$out ] );
2819
2820 $this->setHeaders();
2821
2822 $this->addTalkPageText();
2823 $this->addEditNotices();
2824
2825 if ( !$this->isConflict &&
2826 $this->section != '' &&
2827 !$this->isSectionEditSupported() ) {
2828 // We use $this->section to much before this and getVal('wgSection') directly in other places
2829 // at this point we can't reset $this->section to '' to fallback to non-section editing.
2830 // Someone is welcome to try refactoring though
2831 $out->showErrorPage( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
2832 return;
2833 }
2834
2835 $this->showHeader();
2836
2837 $out->addHTML( $this->editFormPageTop );
2838
2839 $user = $this->context->getUser();
2840 if ( $user->getOption( 'previewontop' ) ) {
2841 $this->displayPreviewArea( $previewOutput, true );
2842 }
2843
2844 $out->addHTML( $this->editFormTextTop );
2845
2846 if ( $this->wasDeletedSinceLastEdit() && $this->formtype !== 'save' ) {
2847 $out->wrapWikiMsg( "<div class='error mw-deleted-while-editing'>\n$1\n</div>",
2848 'deletedwhileediting' );
2849 }
2850
2851 // @todo add EditForm plugin interface and use it here!
2852 // search for textarea1 and textarea2, and allow EditForm to override all uses.
2853 $out->addHTML( Html::openElement(
2854 'form',
2855 [
2856 'class' => 'mw-editform',
2857 'id' => self::EDITFORM_ID,
2858 'name' => self::EDITFORM_ID,
2859 'method' => 'post',
2860 'action' => $this->getActionURL( $this->getContextTitle() ),
2861 'enctype' => 'multipart/form-data'
2862 ]
2863 ) );
2864
2865 if ( is_callable( $formCallback ) ) {
2866 wfWarn( 'The $formCallback parameter to ' . __METHOD__ . 'is deprecated' );
2867 call_user_func_array( $formCallback, [ &$out ] );
2868 }
2869
2870 // Add a check for Unicode support
2871 $out->addHTML( Html::hidden( 'wpUnicodeCheck', self::UNICODE_CHECK ) );
2872
2873 // Add an empty field to trip up spambots
2874 $out->addHTML(
2875 Xml::openElement( 'div', [ 'id' => 'antispam-container', 'style' => 'display: none;' ] )
2876 . Html::rawElement(
2877 'label',
2878 [ 'for' => 'wpAntispam' ],
2879 $this->context->msg( 'simpleantispam-label' )->parse()
2880 )
2881 . Xml::element(
2882 'input',
2883 [
2884 'type' => 'text',
2885 'name' => 'wpAntispam',
2886 'id' => 'wpAntispam',
2887 'value' => ''
2888 ]
2889 )
2890 . Xml::closeElement( 'div' )
2891 );
2892
2893 // Avoid PHP 7.1 warning of passing $this by reference
2894 $editPage = $this;
2895 Hooks::run( 'EditPage::showEditForm:fields', [ &$editPage, &$out ] );
2896
2897 // Put these up at the top to ensure they aren't lost on early form submission
2898 $this->showFormBeforeText();
2899
2900 if ( $this->wasDeletedSinceLastEdit() && $this->formtype == 'save' ) {
2901 $username = $this->lastDelete->user_name;
2902 $comment = CommentStore::getStore()
2903 ->getComment( 'log_comment', $this->lastDelete )->text;
2904
2905 // It is better to not parse the comment at all than to have templates expanded in the middle
2906 // TODO: can the checkLabel be moved outside of the div so that wrapWikiMsg could be used?
2907 $key = $comment === ''
2908 ? 'confirmrecreate-noreason'
2909 : 'confirmrecreate';
2910 $out->addHTML(
2911 '<div class="mw-confirm-recreate">' .
2912 $this->context->msg( $key, $username, "<nowiki>$comment</nowiki>" )->parse() .
2913 Xml::checkLabel( $this->context->msg( 'recreate' )->text(), 'wpRecreate', 'wpRecreate', false,
2914 [ 'title' => Linker::titleAttrib( 'recreate' ), 'tabindex' => 1, 'id' => 'wpRecreate' ]
2915 ) .
2916 '</div>'
2917 );
2918 }
2919
2920 # When the summary is hidden, also hide them on preview/show changes
2921 if ( $this->nosummary ) {
2922 $out->addHTML( Html::hidden( 'nosummary', true ) );
2923 }
2924
2925 # If a blank edit summary was previously provided, and the appropriate
2926 # user preference is active, pass a hidden tag as wpIgnoreBlankSummary. This will stop the
2927 # user being bounced back more than once in the event that a summary
2928 # is not required.
2929 # ####
2930 # For a bit more sophisticated detection of blank summaries, hash the
2931 # automatic one and pass that in the hidden field wpAutoSummary.
2932 if ( $this->missingSummary || ( $this->section == 'new' && $this->nosummary ) ) {
2933 $out->addHTML( Html::hidden( 'wpIgnoreBlankSummary', true ) );
2934 }
2935
2936 if ( $this->undidRev ) {
2937 $out->addHTML( Html::hidden( 'wpUndidRevision', $this->undidRev ) );
2938 }
2939
2940 if ( $this->selfRedirect ) {
2941 $out->addHTML( Html::hidden( 'wpIgnoreSelfRedirect', true ) );
2942 }
2943
2944 if ( $this->hasPresetSummary ) {
2945 // If a summary has been preset using &summary= we don't want to prompt for
2946 // a different summary. Only prompt for a summary if the summary is blanked.
2947 // (T19416)
2948 $this->autoSumm = md5( '' );
2949 }
2950
2951 $autosumm = $this->autoSumm !== '' ? $this->autoSumm : md5( $this->summary );
2952 $out->addHTML( Html::hidden( 'wpAutoSummary', $autosumm ) );
2953
2954 $out->addHTML( Html::hidden( 'oldid', $this->oldid ) );
2955 $out->addHTML( Html::hidden( 'parentRevId', $this->getParentRevId() ) );
2956
2957 $out->addHTML( Html::hidden( 'format', $this->contentFormat ) );
2958 $out->addHTML( Html::hidden( 'model', $this->contentModel ) );
2959
2960 $out->enableOOUI();
2961
2962 if ( $this->section == 'new' ) {
2963 $this->showSummaryInput( true, $this->summary );
2964 $out->addHTML( $this->getSummaryPreview( true, $this->summary ) );
2965 }
2966
2967 $out->addHTML( $this->editFormTextBeforeContent );
2968 if ( $this->isConflict ) {
2969 // In an edit conflict, we turn textbox2 into the user's text,
2970 // and textbox1 into the stored version
2971 $this->textbox2 = $this->textbox1;
2972
2973 $content = $this->getCurrentContent();
2974 $this->textbox1 = $this->toEditText( $content );
2975
2977 $editConflictHelper->setTextboxes( $this->textbox2, $this->textbox1 );
2978 $editConflictHelper->setContentModel( $this->contentModel );
2979 $editConflictHelper->setContentFormat( $this->contentFormat );
2981 }
2982
2983 if ( !$this->mTitle->isUserConfigPage() ) {
2984 $out->addHTML( self::getEditToolbar() );
2985 }
2986
2987 if ( $this->blankArticle ) {
2988 $out->addHTML( Html::hidden( 'wpIgnoreBlankArticle', true ) );
2989 }
2990
2991 if ( $this->isConflict ) {
2992 // In an edit conflict bypass the overridable content form method
2993 // and fallback to the raw wpTextbox1 since editconflicts can't be
2994 // resolved between page source edits and custom ui edits using the
2995 // custom edit ui.
2996 $conflictTextBoxAttribs = [];
2997 if ( $this->wasDeletedSinceLastEdit() ) {
2998 $conflictTextBoxAttribs['style'] = 'display:none;';
2999 } elseif ( $this->isOldRev ) {
3000 $conflictTextBoxAttribs['class'] = 'mw-textarea-oldrev';
3001 }
3002
3003 $out->addHTML( $editConflictHelper->getEditConflictMainTextBox( $conflictTextBoxAttribs ) );
3005 } else {
3006 $this->showContentForm();
3007 }
3008
3009 $out->addHTML( $this->editFormTextAfterContent );
3010
3011 $this->showStandardInputs();
3012
3013 $this->showFormAfterText();
3014
3015 $this->showTosSummary();
3016
3017 $this->showEditTools();
3018
3019 $out->addHTML( $this->editFormTextAfterTools . "\n" );
3020
3021 $out->addHTML( $this->makeTemplatesOnThisPageList( $this->getTemplates() ) );
3022
3023 $out->addHTML( Html::rawElement( 'div', [ 'class' => 'hiddencats' ],
3024 Linker::formatHiddenCategories( $this->page->getHiddenCategories() ) ) );
3025
3026 $out->addHTML( Html::rawElement( 'div', [ 'class' => 'limitreport' ],
3027 self::getPreviewLimitReport( $this->mParserOutput ) ) );
3028
3029 $out->addModules( 'mediawiki.action.edit.collapsibleFooter' );
3030
3031 if ( $this->isConflict ) {
3032 try {
3033 $this->showConflict();
3034 } catch ( MWContentSerializationException $ex ) {
3035 // this can't really happen, but be nice if it does.
3036 $msg = $this->context->msg(
3037 'content-failed-to-parse',
3038 $this->contentModel,
3039 $this->contentFormat,
3040 $ex->getMessage()
3041 );
3042 $out->wrapWikiTextAsInterface( 'error', $msg->plain() );
3043 }
3044 }
3045
3046 // Set a hidden field so JS knows what edit form mode we are in
3047 if ( $this->isConflict ) {
3048 $mode = 'conflict';
3049 } elseif ( $this->preview ) {
3050 $mode = 'preview';
3051 } elseif ( $this->diff ) {
3052 $mode = 'diff';
3053 } else {
3054 $mode = 'text';
3055 }
3056 $out->addHTML( Html::hidden( 'mode', $mode, [ 'id' => 'mw-edit-mode' ] ) );
3057
3058 // Marker for detecting truncated form data. This must be the last
3059 // parameter sent in order to be of use, so do not move me.
3060 $out->addHTML( Html::hidden( 'wpUltimateParam', true ) );
3061 $out->addHTML( $this->editFormTextBottom . "\n</form>\n" );
3062
3063 if ( !$user->getOption( 'previewontop' ) ) {
3064 $this->displayPreviewArea( $previewOutput, false );
3065 }
3066 }
3067
3075 public function makeTemplatesOnThisPageList( array $templates ) {
3076 $templateListFormatter = new TemplatesOnThisPageFormatter(
3077 $this->context, MediaWikiServices::getInstance()->getLinkRenderer()
3078 );
3079
3080 // preview if preview, else section if section, else false
3081 $type = false;
3082 if ( $this->preview ) {
3083 $type = 'preview';
3084 } elseif ( $this->section != '' ) {
3085 $type = 'section';
3086 }
3087
3088 return Html::rawElement( 'div', [ 'class' => 'templatesUsed' ],
3089 $templateListFormatter->format( $templates, $type )
3090 );
3091 }
3092
3099 public static function extractSectionTitle( $text ) {
3100 preg_match( "/^(=+)(.+)\\1\\s*(\n|$)/i", $text, $matches );
3101 if ( !empty( $matches[2] ) ) {
3102 return MediaWikiServices::getInstance()->getParser()
3103 ->stripSectionName( trim( $matches[2] ) );
3104 } else {
3105 return false;
3106 }
3107 }
3108
3109 protected function showHeader() {
3110 $out = $this->context->getOutput();
3111 $user = $this->context->getUser();
3112 if ( $this->isConflict ) {
3113 $this->addExplainConflictHeader( $out );
3114 $this->editRevId = $this->page->getLatest();
3115 } else {
3116 if ( $this->section != '' && $this->section != 'new' && !$this->summary &&
3117 !$this->preview && !$this->diff
3118 ) {
3119 $sectionTitle = self::extractSectionTitle( $this->textbox1 ); // FIXME: use Content object
3120 if ( $sectionTitle !== false ) {
3121 $this->summary = "/* $sectionTitle */ ";
3122 }
3123 }
3124
3125 $buttonLabel = $this->context->msg( $this->getSubmitButtonLabel() )->text();
3126
3127 if ( $this->missingComment ) {
3128 $out->wrapWikiMsg( "<div id='mw-missingcommenttext'>\n$1\n</div>", 'missingcommenttext' );
3129 }
3130
3131 if ( $this->missingSummary && $this->section != 'new' ) {
3132 $out->wrapWikiMsg(
3133 "<div id='mw-missingsummary'>\n$1\n</div>",
3134 [ 'missingsummary', $buttonLabel ]
3135 );
3136 }
3137
3138 if ( $this->missingSummary && $this->section == 'new' ) {
3139 $out->wrapWikiMsg(
3140 "<div id='mw-missingcommentheader'>\n$1\n</div>",
3141 [ 'missingcommentheader', $buttonLabel ]
3142 );
3143 }
3144
3145 if ( $this->blankArticle ) {
3146 $out->wrapWikiMsg(
3147 "<div id='mw-blankarticle'>\n$1\n</div>",
3148 [ 'blankarticle', $buttonLabel ]
3149 );
3150 }
3151
3152 if ( $this->selfRedirect ) {
3153 $out->wrapWikiMsg(
3154 "<div id='mw-selfredirect'>\n$1\n</div>",
3155 [ 'selfredirect', $buttonLabel ]
3156 );
3157 }
3158
3159 if ( $this->hookError !== '' ) {
3160 $out->addWikiTextAsInterface( $this->hookError );
3161 }
3162
3163 if ( $this->section != 'new' ) {
3164 $revision = $this->mArticle->getRevisionFetched();
3165 if ( $revision ) {
3166 // Let sysop know that this will make private content public if saved
3167
3168 if ( !$revision->userCan( RevisionRecord::DELETED_TEXT, $user ) ) {
3169 $out->wrapWikiMsg(
3170 "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
3171 'rev-deleted-text-permission'
3172 );
3173 } elseif ( $revision->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
3174 $out->wrapWikiMsg(
3175 "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
3176 'rev-deleted-text-view'
3177 );
3178 }
3179
3180 if ( !$revision->isCurrent() ) {
3181 $this->mArticle->setOldSubtitle( $revision->getId() );
3182 $out->wrapWikiMsg(
3183 Html::warningBox( "\n$1\n" ),
3184 'editingold'
3185 );
3186 $this->isOldRev = true;
3187 }
3188 } elseif ( $this->mTitle->exists() ) {
3189 // Something went wrong
3190
3191 $out->wrapWikiMsg( "<div class='errorbox'>\n$1\n</div>\n",
3192 [ 'missing-revision', $this->oldid ] );
3193 }
3194 }
3195 }
3196
3197 if ( wfReadOnly() ) {
3198 $out->wrapWikiMsg(
3199 "<div id=\"mw-read-only-warning\">\n$1\n</div>",
3200 [ 'readonlywarning', wfReadOnlyReason() ]
3201 );
3202 } elseif ( $user->isAnon() ) {
3203 if ( $this->formtype != 'preview' ) {
3204 $returntoquery = array_diff_key(
3205 $this->context->getRequest()->getValues(),
3206 [ 'title' => true, 'returnto' => true, 'returntoquery' => true ]
3207 );
3208 $out->wrapWikiMsg(
3209 "<div id='mw-anon-edit-warning' class='warningbox'>\n$1\n</div>",
3210 [ 'anoneditwarning',
3211 // Log-in link
3212 SpecialPage::getTitleFor( 'Userlogin' )->getFullURL( [
3213 'returnto' => $this->getTitle()->getPrefixedDBkey(),
3214 'returntoquery' => wfArrayToCgi( $returntoquery ),
3215 ] ),
3216 // Sign-up link
3217 SpecialPage::getTitleFor( 'CreateAccount' )->getFullURL( [
3218 'returnto' => $this->getTitle()->getPrefixedDBkey(),
3219 'returntoquery' => wfArrayToCgi( $returntoquery ),
3220 ] )
3221 ]
3222 );
3223 } else {
3224 $out->wrapWikiMsg( "<div id=\"mw-anon-preview-warning\" class=\"warningbox\">\n$1</div>",
3225 'anonpreviewwarning'
3226 );
3227 }
3228 } elseif ( $this->mTitle->isUserConfigPage() ) {
3229 # Check the skin exists
3230 if ( $this->isWrongCaseUserConfigPage() ) {
3231 $out->wrapWikiMsg(
3232 "<div class='error' id='mw-userinvalidconfigtitle'>\n$1\n</div>",
3233 [ 'userinvalidconfigtitle', $this->mTitle->getSkinFromConfigSubpage() ]
3234 );
3235 }
3236 if ( $this->getTitle()->isSubpageOf( $user->getUserPage() ) ) {
3237 $isUserCssConfig = $this->mTitle->isUserCssConfigPage();
3238 $isUserJsonConfig = $this->mTitle->isUserJsonConfigPage();
3239 $isUserJsConfig = $this->mTitle->isUserJsConfigPage();
3240
3241 $warning = $isUserCssConfig
3242 ? 'usercssispublic'
3243 : ( $isUserJsonConfig ? 'userjsonispublic' : 'userjsispublic' );
3244
3245 $out->wrapWikiMsg( '<div class="mw-userconfigpublic">$1</div>', $warning );
3246
3247 if ( $this->formtype !== 'preview' ) {
3248 $config = $this->context->getConfig();
3249 if ( $isUserCssConfig && $config->get( 'AllowUserCss' ) ) {
3250 $out->wrapWikiMsg(
3251 "<div id='mw-usercssyoucanpreview'>\n$1\n</div>",
3252 [ 'usercssyoucanpreview' ]
3253 );
3254 } elseif ( $isUserJsonConfig /* No comparable 'AllowUserJson' */ ) {
3255 $out->wrapWikiMsg(
3256 "<div id='mw-userjsonyoucanpreview'>\n$1\n</div>",
3257 [ 'userjsonyoucanpreview' ]
3258 );
3259 } elseif ( $isUserJsConfig && $config->get( 'AllowUserJs' ) ) {
3260 $out->wrapWikiMsg(
3261 "<div id='mw-userjsyoucanpreview'>\n$1\n</div>",
3262 [ 'userjsyoucanpreview' ]
3263 );
3264 }
3265 }
3266 }
3267 }
3268
3270
3271 $this->addLongPageWarningHeader();
3272
3273 # Add header copyright warning
3275 }
3276
3284 private function getSummaryInputAttributes( array $inputAttrs = null ) {
3285 // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
3286 // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
3287 // Unicode codepoints.
3288 return ( is_array( $inputAttrs ) ? $inputAttrs : [] ) + [
3289 'id' => 'wpSummary',
3290 'name' => 'wpSummary',
3291 'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT,
3292 'tabindex' => 1,
3293 'size' => 60,
3294 'spellcheck' => 'true',
3295 ];
3296 }
3297
3307 function getSummaryInputWidget( $summary = "", $labelText = null, $inputAttrs = null ) {
3308 $inputAttrs = OOUI\Element::configFromHtmlAttributes(
3309 $this->getSummaryInputAttributes( $inputAttrs )
3310 );
3311 $inputAttrs += [
3312 'title' => Linker::titleAttrib( 'summary' ),
3313 'accessKey' => Linker::accesskey( 'summary' ),
3314 ];
3315
3316 // For compatibility with old scripts and extensions, we want the legacy 'id' on the `<input>`
3317 $inputAttrs['inputId'] = $inputAttrs['id'];
3318 $inputAttrs['id'] = 'wpSummaryWidget';
3319
3320 return new OOUI\FieldLayout(
3321 new OOUI\TextInputWidget( [
3322 'value' => $summary,
3323 'infusable' => true,
3324 ] + $inputAttrs ),
3325 [
3326 'label' => new OOUI\HtmlSnippet( $labelText ),
3327 'align' => 'top',
3328 'id' => 'wpSummaryLabel',
3329 'classes' => [ $this->missingSummary ? 'mw-summarymissed' : 'mw-summary' ],
3330 ]
3331 );
3332 }
3333
3340 protected function showSummaryInput( $isSubjectPreview, $summary = "" ) {
3341 # Add a class if 'missingsummary' is triggered to allow styling of the summary line
3342 $summaryClass = $this->missingSummary ? 'mw-summarymissed' : 'mw-summary';
3343 if ( $isSubjectPreview ) {
3344 if ( $this->nosummary ) {
3345 return;
3346 }
3347 } elseif ( !$this->mShowSummaryField ) {
3348 return;
3349 }
3350
3351 $labelText = $this->context->msg( $isSubjectPreview ? 'subject' : 'summary' )->parse();
3352 $this->context->getOutput()->addHTML( $this->getSummaryInputWidget(
3353 $summary,
3354 $labelText,
3355 [ 'class' => $summaryClass ]
3356 ) );
3357 }
3358
3366 protected function getSummaryPreview( $isSubjectPreview, $summary = "" ) {
3367 // avoid spaces in preview, gets always trimmed on save
3368 $summary = trim( $summary );
3369 if ( !$summary || ( !$this->preview && !$this->diff ) ) {
3370 return "";
3371 }
3372
3373 if ( $isSubjectPreview ) {
3374 $summary = $this->context->msg( 'newsectionsummary' )
3375 ->rawParams( MediaWikiServices::getInstance()->getParser()
3376 ->stripSectionName( $summary ) )
3377 ->inContentLanguage()->text();
3378 }
3379
3380 $message = $isSubjectPreview ? 'subject-preview' : 'summary-preview';
3381
3382 $summary = $this->context->msg( $message )->parse()
3383 . Linker::commentBlock( $summary, $this->mTitle, $isSubjectPreview );
3384 return Xml::tags( 'div', [ 'class' => 'mw-summary-preview' ], $summary );
3385 }
3386
3387 protected function showFormBeforeText() {
3388 $out = $this->context->getOutput();
3389 $out->addHTML( Html::hidden( 'wpSection', $this->section ) );
3390 $out->addHTML( Html::hidden( 'wpStarttime', $this->starttime ) );
3391 $out->addHTML( Html::hidden( 'wpEdittime', $this->edittime ) );
3392 $out->addHTML( Html::hidden( 'editRevId', $this->editRevId ) );
3393 $out->addHTML( Html::hidden( 'wpScrolltop', $this->scrolltop, [ 'id' => 'wpScrolltop' ] ) );
3394 }
3395
3396 protected function showFormAfterText() {
3409 $this->context->getOutput()->addHTML(
3410 "\n" .
3411 Html::hidden( "wpEditToken", $this->context->getUser()->getEditToken() ) .
3412 "\n"
3413 );
3414 }
3415
3424 protected function showContentForm() {
3425 $this->showTextbox1();
3426 }
3427
3436 protected function showTextbox1( $customAttribs = null, $textoverride = null ) {
3437 if ( $this->wasDeletedSinceLastEdit() && $this->formtype == 'save' ) {
3438 $attribs = [ 'style' => 'display:none;' ];
3439 } else {
3440 $builder = new TextboxBuilder();
3441 $classes = $builder->getTextboxProtectionCSSClasses( $this->getTitle() );
3442
3443 # Is an old revision being edited?
3444 if ( $this->isOldRev ) {
3445 $classes[] = 'mw-textarea-oldrev';
3446 }
3447
3448 $attribs = [ 'tabindex' => 1 ];
3449
3450 if ( is_array( $customAttribs ) ) {
3451 $attribs += $customAttribs;
3452 }
3453
3454 $attribs = $builder->mergeClassesIntoAttributes( $classes, $attribs );
3455 }
3456
3457 $this->showTextbox(
3458 $textoverride ?? $this->textbox1,
3459 'wpTextbox1',
3460 $attribs
3461 );
3462 }
3463
3464 protected function showTextbox2() {
3465 $this->showTextbox( $this->textbox2, 'wpTextbox2', [ 'tabindex' => 6, 'readonly' ] );
3466 }
3467
3468 protected function showTextbox( $text, $name, $customAttribs = [] ) {
3469 $builder = new TextboxBuilder();
3470 $attribs = $builder->buildTextboxAttribs(
3471 $name,
3472 $customAttribs,
3473 $this->context->getUser(),
3474 $this->mTitle
3475 );
3476
3477 $this->context->getOutput()->addHTML(
3478 Html::textarea( $name, $builder->addNewLineAtEnd( $text ), $attribs )
3479 );
3480 }
3481
3482 protected function displayPreviewArea( $previewOutput, $isOnTop = false ) {
3483 $classes = [];
3484 if ( $isOnTop ) {
3485 $classes[] = 'ontop';
3486 }
3487
3488 $attribs = [ 'id' => 'wikiPreview', 'class' => implode( ' ', $classes ) ];
3489
3490 if ( $this->formtype != 'preview' ) {
3491 $attribs['style'] = 'display: none;';
3492 }
3493
3494 $out = $this->context->getOutput();
3495 $out->addHTML( Xml::openElement( 'div', $attribs ) );
3496
3497 if ( $this->formtype == 'preview' ) {
3498 $this->showPreview( $previewOutput );
3499 } else {
3500 // Empty content container for LivePreview
3501 $pageViewLang = $this->mTitle->getPageViewLanguage();
3502 $attribs = [ 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(),
3503 'class' => 'mw-content-' . $pageViewLang->getDir() ];
3504 $out->addHTML( Html::rawElement( 'div', $attribs ) );
3505 }
3506
3507 $out->addHTML( '</div>' );
3508
3509 if ( $this->formtype == 'diff' ) {
3510 try {
3511 $this->showDiff();
3512 } catch ( MWContentSerializationException $ex ) {
3513 $msg = $this->context->msg(
3514 'content-failed-to-parse',
3515 $this->contentModel,
3516 $this->contentFormat,
3517 $ex->getMessage()
3518 );
3519 $out->wrapWikiTextAsInterface( 'error', $msg->plain() );
3520 }
3521 }
3522 }
3523
3530 protected function showPreview( $text ) {
3531 if ( $this->mArticle instanceof CategoryPage ) {
3532 $this->mArticle->openShowCategory();
3533 }
3534 # This hook seems slightly odd here, but makes things more
3535 # consistent for extensions.
3536 $out = $this->context->getOutput();
3537 Hooks::run( 'OutputPageBeforeHTML', [ &$out, &$text ] );
3538 $out->addHTML( $text );
3539 if ( $this->mArticle instanceof CategoryPage ) {
3540 $this->mArticle->closeShowCategory();
3541 }
3542 }
3543
3551 public function showDiff() {
3552 $oldtitlemsg = 'currentrev';
3553 # if message does not exist, show diff against the preloaded default
3554 if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && !$this->mTitle->exists() ) {
3555 $oldtext = $this->mTitle->getDefaultMessageText();
3556 if ( $oldtext !== false ) {
3557 $oldtitlemsg = 'defaultmessagetext';
3558 $oldContent = $this->toEditContent( $oldtext );
3559 } else {
3560 $oldContent = null;
3561 }
3562 } else {
3563 $oldContent = $this->getCurrentContent();
3564 }
3565
3566 $textboxContent = $this->toEditContent( $this->textbox1 );
3567 if ( $this->editRevId !== null ) {
3568 $newContent = $this->page->replaceSectionAtRev(
3569 $this->section, $textboxContent, $this->summary, $this->editRevId
3570 );
3571 } else {
3572 $newContent = $this->page->replaceSectionContent(
3573 $this->section, $textboxContent, $this->summary, $this->edittime
3574 );
3575 }
3576
3577 if ( $newContent ) {
3578 Hooks::run( 'EditPageGetDiffContent', [ $this, &$newContent ] );
3579
3580 $user = $this->context->getUser();
3581 $popts = ParserOptions::newFromUserAndLang( $user,
3582 MediaWikiServices::getInstance()->getContentLanguage() );
3583 $newContent = $newContent->preSaveTransform( $this->mTitle, $user, $popts );
3584 }
3585
3586 if ( ( $oldContent && !$oldContent->isEmpty() ) || ( $newContent && !$newContent->isEmpty() ) ) {
3587 $oldtitle = $this->context->msg( $oldtitlemsg )->parse();
3588 $newtitle = $this->context->msg( 'yourtext' )->parse();
3589
3590 if ( !$oldContent ) {
3591 $oldContent = $newContent->getContentHandler()->makeEmptyContent();
3592 }
3593
3594 if ( !$newContent ) {
3595 $newContent = $oldContent->getContentHandler()->makeEmptyContent();
3596 }
3597
3598 $de = $oldContent->getContentHandler()->createDifferenceEngine( $this->context );
3599 $de->setContent( $oldContent, $newContent );
3600
3601 $difftext = $de->getDiff( $oldtitle, $newtitle );
3602 $de->showDiffStyle();
3603 } else {
3604 $difftext = '';
3605 }
3606
3607 $this->context->getOutput()->addHTML( '<div id="wikiDiff">' . $difftext . '</div>' );
3608 }
3609
3613 protected function showHeaderCopyrightWarning() {
3614 $msg = 'editpage-head-copy-warn';
3615 if ( !$this->context->msg( $msg )->isDisabled() ) {
3616 $this->context->getOutput()->wrapWikiMsg( "<div class='editpage-head-copywarn'>\n$1\n</div>",
3617 'editpage-head-copy-warn' );
3618 }
3619 }
3620
3629 protected function showTosSummary() {
3630 $msg = 'editpage-tos-summary';
3631 Hooks::run( 'EditPageTosSummary', [ $this->mTitle, &$msg ] );
3632 if ( !$this->context->msg( $msg )->isDisabled() ) {
3633 $out = $this->context->getOutput();
3634 $out->addHTML( '<div class="mw-tos-summary">' );
3635 $out->addWikiMsg( $msg );
3636 $out->addHTML( '</div>' );
3637 }
3638 }
3639
3644 protected function showEditTools() {
3645 $this->context->getOutput()->addHTML( '<div class="mw-editTools">' .
3646 $this->context->msg( 'edittools' )->inContentLanguage()->parse() .
3647 '</div>' );
3648 }
3649
3656 protected function getCopywarn() {
3657 return self::getCopyrightWarning( $this->mTitle );
3658 }
3659
3668 public static function getCopyrightWarning( $title, $format = 'plain', $langcode = null ) {
3669 global $wgRightsText;
3670 if ( $wgRightsText ) {
3671 $copywarnMsg = [ 'copyrightwarning',
3672 '[[' . wfMessage( 'copyrightpage' )->inContentLanguage()->text() . ']]',
3673 $wgRightsText ];
3674 } else {
3675 $copywarnMsg = [ 'copyrightwarning2',
3676 '[[' . wfMessage( 'copyrightpage' )->inContentLanguage()->text() . ']]' ];
3677 }
3678 // Allow for site and per-namespace customization of contribution/copyright notice.
3679 Hooks::run( 'EditPageCopyrightWarning', [ $title, &$copywarnMsg ] );
3680
3681 $msg = wfMessage( ...$copywarnMsg )->title( $title );
3682 if ( $langcode ) {
3683 $msg->inLanguage( $langcode );
3684 }
3685 return "<div id=\"editpage-copywarn\">\n" .
3686 $msg->$format() . "\n</div>";
3687 }
3688
3696 public static function getPreviewLimitReport( ParserOutput $output = null ) {
3697 global $wgLang;
3698
3699 if ( !$output || !$output->getLimitReportData() ) {
3700 return '';
3701 }
3702
3703 $limitReport = Html::rawElement( 'div', [ 'class' => 'mw-limitReportExplanation' ],
3704 wfMessage( 'limitreport-title' )->parseAsBlock()
3705 );
3706
3707 // Show/hide animation doesn't work correctly on a table, so wrap it in a div.
3708 $limitReport .= Html::openElement( 'div', [ 'class' => 'preview-limit-report-wrapper' ] );
3709
3710 $limitReport .= Html::openElement( 'table', [
3711 'class' => 'preview-limit-report wikitable'
3712 ] ) .
3713 Html::openElement( 'tbody' );
3714
3715 foreach ( $output->getLimitReportData() as $key => $value ) {
3716 if ( Hooks::run( 'ParserLimitReportFormat',
3717 [ $key, &$value, &$limitReport, true, true ]
3718 ) ) {
3719 $keyMsg = wfMessage( $key );
3720 $valueMsg = wfMessage( [ "$key-value-html", "$key-value" ] );
3721 if ( !$valueMsg->exists() ) {
3722 $valueMsg = new RawMessage( '$1' );
3723 }
3724 if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) {
3725 $limitReport .= Html::openElement( 'tr' ) .
3726 Html::rawElement( 'th', null, $keyMsg->parse() ) .
3727 Html::rawElement( 'td', null,
3728 $wgLang->formatNum( $valueMsg->params( $value )->parse() )
3729 ) .
3730 Html::closeElement( 'tr' );
3731 }
3732 }
3733 }
3734
3735 $limitReport .= Html::closeElement( 'tbody' ) .
3736 Html::closeElement( 'table' ) .
3737 Html::closeElement( 'div' );
3738
3739 return $limitReport;
3740 }
3741
3742 protected function showStandardInputs( &$tabindex = 2 ) {
3743 $out = $this->context->getOutput();
3744 $out->addHTML( "<div class='editOptions'>\n" );
3745
3746 if ( $this->section != 'new' ) {
3747 $this->showSummaryInput( false, $this->summary );
3748 $out->addHTML( $this->getSummaryPreview( false, $this->summary ) );
3749 }
3750
3751 $checkboxes = $this->getCheckboxesWidget(
3752 $tabindex,
3753 [ 'minor' => $this->minoredit, 'watch' => $this->watchthis ]
3754 );
3755 $checkboxesHTML = new OOUI\HorizontalLayout( [ 'items' => $checkboxes ] );
3756
3757 $out->addHTML( "<div class='editCheckboxes'>" . $checkboxesHTML . "</div>\n" );
3758
3759 // Show copyright warning.
3760 $out->addWikiTextAsInterface( $this->getCopywarn() );
3761 $out->addHTML( $this->editFormTextAfterWarn );
3762
3763 $out->addHTML( "<div class='editButtons'>\n" );
3764 $out->addHTML( implode( "\n", $this->getEditButtons( $tabindex ) ) . "\n" );
3765
3766 $cancel = $this->getCancelLink();
3767
3768 $message = $this->context->msg( 'edithelppage' )->inContentLanguage()->text();
3769 $edithelpurl = Skin::makeInternalOrExternalUrl( $message );
3770 $edithelp =
3771 Html::linkButton(
3772 $this->context->msg( 'edithelp' )->text(),
3773 [ 'target' => 'helpwindow', 'href' => $edithelpurl ],
3774 [ 'mw-ui-quiet' ]
3775 ) .
3776 $this->context->msg( 'word-separator' )->escaped() .
3777 $this->context->msg( 'newwindow' )->parse();
3778
3779 $out->addHTML( " <span class='cancelLink'>{$cancel}</span>\n" );
3780 $out->addHTML( " <span class='editHelp'>{$edithelp}</span>\n" );
3781 $out->addHTML( "</div><!-- editButtons -->\n" );
3782
3783 Hooks::run( 'EditPage::showStandardInputs:options', [ $this, $out, &$tabindex ] );
3784
3785 $out->addHTML( "</div><!-- editOptions -->\n" );
3786 }
3787
3792 protected function showConflict() {
3793 $out = $this->context->getOutput();
3794 // Avoid PHP 7.1 warning of passing $this by reference
3795 $editPage = $this;
3796 if ( Hooks::run( 'EditPageBeforeConflictDiff', [ &$editPage, &$out ] ) ) {
3797 $this->incrementConflictStats();
3798
3799 $this->getEditConflictHelper()->showEditFormTextAfterFooters();
3800 }
3801 }
3802
3803 protected function incrementConflictStats() {
3804 $this->getEditConflictHelper()->incrementConflictStats();
3805 }
3806
3810 public function getCancelLink() {
3811 $cancelParams = [];
3812 if ( !$this->isConflict && $this->oldid > 0 ) {
3813 $cancelParams['oldid'] = $this->oldid;
3814 } elseif ( $this->getContextTitle()->isRedirect() ) {
3815 $cancelParams['redirect'] = 'no';
3816 }
3817
3818 return new OOUI\ButtonWidget( [
3819 'id' => 'mw-editform-cancel',
3820 'href' => $this->getContextTitle()->getLinkURL( $cancelParams ),
3821 'label' => new OOUI\HtmlSnippet( $this->context->msg( 'cancel' )->parse() ),
3822 'framed' => false,
3823 'infusable' => true,
3824 'flags' => 'destructive',
3825 ] );
3826 }
3827
3837 protected function getActionURL( Title $title ) {
3838 return $title->getLocalURL( [ 'action' => $this->action ] );
3839 }
3840
3848 protected function wasDeletedSinceLastEdit() {
3849 if ( $this->deletedSinceEdit !== null ) {
3851 }
3852
3853 $this->deletedSinceEdit = false;
3854
3855 if ( !$this->mTitle->exists() && $this->mTitle->isDeletedQuick() ) {
3856 $this->lastDelete = $this->getLastDelete();
3857 if ( $this->lastDelete ) {
3858 $deleteTime = wfTimestamp( TS_MW, $this->lastDelete->log_timestamp );
3859 if ( $deleteTime > $this->starttime ) {
3860 $this->deletedSinceEdit = true;
3861 }
3862 }
3863 }
3864
3866 }
3867
3873 protected function getLastDelete() {
3874 $dbr = wfGetDB( DB_REPLICA );
3875 $commentQuery = CommentStore::getStore()->getJoin( 'log_comment' );
3876 $actorQuery = ActorMigration::newMigration()->getJoin( 'log_user' );
3877 $data = $dbr->selectRow(
3878 array_merge( [ 'logging' ], $commentQuery['tables'], $actorQuery['tables'], [ 'user' ] ),
3879 [
3880 'log_type',
3881 'log_action',
3882 'log_timestamp',
3883 'log_namespace',
3884 'log_title',
3885 'log_params',
3886 'log_deleted',
3887 'user_name'
3888 ] + $commentQuery['fields'] + $actorQuery['fields'],
3889 [
3890 'log_namespace' => $this->mTitle->getNamespace(),
3891 'log_title' => $this->mTitle->getDBkey(),
3892 'log_type' => 'delete',
3893 'log_action' => 'delete',
3894 ],
3895 __METHOD__,
3896 [ 'LIMIT' => 1, 'ORDER BY' => 'log_timestamp DESC' ],
3897 [
3898 'user' => [ 'JOIN', 'user_id=' . $actorQuery['fields']['log_user'] ],
3899 ] + $commentQuery['joins'] + $actorQuery['joins']
3900 );
3901 // Quick paranoid permission checks...
3902 if ( is_object( $data ) ) {
3903 if ( $data->log_deleted & LogPage::DELETED_USER ) {
3904 $data->user_name = $this->context->msg( 'rev-deleted-user' )->escaped();
3905 }
3906
3907 if ( $data->log_deleted & LogPage::DELETED_COMMENT ) {
3908 $data->log_comment_text = $this->context->msg( 'rev-deleted-comment' )->escaped();
3909 $data->log_comment_data = null;
3910 }
3911 }
3912
3913 return $data;
3914 }
3915
3921 public function getPreviewText() {
3922 $out = $this->context->getOutput();
3923 $config = $this->context->getConfig();
3924
3925 if ( $config->get( 'RawHtml' ) && !$this->mTokenOk ) {
3926 // Could be an offsite preview attempt. This is very unsafe if
3927 // HTML is enabled, as it could be an attack.
3928 $parsedNote = '';
3929 if ( $this->textbox1 !== '' ) {
3930 // Do not put big scary notice, if previewing the empty
3931 // string, which happens when you initially edit
3932 // a category page, due to automatic preview-on-open.
3933 $parsedNote = Html::rawElement( 'div', [ 'class' => 'previewnote' ],
3934 $out->parseAsInterface(
3935 $this->context->msg( 'session_fail_preview_html' )->plain()
3936 ) );
3937 }
3938 $this->incrementEditFailureStats( 'session_loss' );
3939 return $parsedNote;
3940 }
3941
3942 $note = '';
3943
3944 try {
3945 $content = $this->toEditContent( $this->textbox1 );
3946
3947 $previewHTML = '';
3948 if ( !Hooks::run(
3949 'AlternateEditPreview',
3950 [ $this, &$content, &$previewHTML, &$this->mParserOutput ] )
3951 ) {
3952 return $previewHTML;
3953 }
3954
3955 # provide a anchor link to the editform
3956 $continueEditing = '<span class="mw-continue-editing">' .
3957 '[[#' . self::EDITFORM_ID . '|' .
3958 $this->context->getLanguage()->getArrow() . ' ' .
3959 $this->context->msg( 'continue-editing' )->text() . ']]</span>';
3960 if ( $this->mTriedSave && !$this->mTokenOk ) {
3961 if ( $this->mTokenOkExceptSuffix ) {
3962 $note = $this->context->msg( 'token_suffix_mismatch' )->plain();
3963 $this->incrementEditFailureStats( 'bad_token' );
3964 } else {
3965 $note = $this->context->msg( 'session_fail_preview' )->plain();
3966 $this->incrementEditFailureStats( 'session_loss' );
3967 }
3968 } elseif ( $this->incompleteForm ) {
3969 $note = $this->context->msg( 'edit_form_incomplete' )->plain();
3970 if ( $this->mTriedSave ) {
3971 $this->incrementEditFailureStats( 'incomplete_form' );
3972 }
3973 } else {
3974 $note = $this->context->msg( 'previewnote' )->plain() . ' ' . $continueEditing;
3975 }
3976
3977 # don't parse non-wikitext pages, show message about preview
3978 if ( $this->mTitle->isUserConfigPage() || $this->mTitle->isSiteConfigPage() ) {
3979 if ( $this->mTitle->isUserConfigPage() ) {
3980 $level = 'user';
3981 } elseif ( $this->mTitle->isSiteConfigPage() ) {
3982 $level = 'site';
3983 } else {
3984 $level = false;
3985 }
3986
3987 if ( $content->getModel() == CONTENT_MODEL_CSS ) {
3988 $format = 'css';
3989 if ( $level === 'user' && !$config->get( 'AllowUserCss' ) ) {
3990 $format = false;
3991 }
3992 } elseif ( $content->getModel() == CONTENT_MODEL_JSON ) {
3993 $format = 'json';
3994 if ( $level === 'user' /* No comparable 'AllowUserJson' */ ) {
3995 $format = false;
3996 }
3997 } elseif ( $content->getModel() == CONTENT_MODEL_JAVASCRIPT ) {
3998 $format = 'js';
3999 if ( $level === 'user' && !$config->get( 'AllowUserJs' ) ) {
4000 $format = false;
4001 }
4002 } else {
4003 $format = false;
4004 }
4005
4006 # Used messages to make sure grep find them:
4007 # Messages: usercsspreview, userjsonpreview, userjspreview,
4008 # sitecsspreview, sitejsonpreview, sitejspreview
4009 if ( $level && $format ) {
4010 $note = "<div id='mw-{$level}{$format}preview'>" .
4011 $this->context->msg( "{$level}{$format}preview" )->plain() .
4012 ' ' . $continueEditing . "</div>";
4013 }
4014 }
4015
4016 # If we're adding a comment, we need to show the
4017 # summary as the headline
4018 if ( $this->section === "new" && $this->summary !== "" ) {
4019 $content = $content->addSectionHeader( $this->summary );
4020 }
4021
4022 $hook_args = [ $this, &$content ];
4023 Hooks::run( 'EditPageGetPreviewContent', $hook_args );
4024
4025 $parserResult = $this->doPreviewParse( $content );
4026 $parserOutput = $parserResult['parserOutput'];
4027 $previewHTML = $parserResult['html'];
4028 $this->mParserOutput = $parserOutput;
4029 $out->addParserOutputMetadata( $parserOutput );
4030 if ( $out->userCanPreview() ) {
4031 $out->addContentOverride( $this->getTitle(), $content );
4032 }
4033
4034 if ( count( $parserOutput->getWarnings() ) ) {
4035 $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
4036 }
4037
4038 } catch ( MWContentSerializationException $ex ) {
4039 $m = $this->context->msg(
4040 'content-failed-to-parse',
4041 $this->contentModel,
4042 $this->contentFormat,
4043 $ex->getMessage()
4044 );
4045 $note .= "\n\n" . $m->plain(); # gets parsed down below
4046 $previewHTML = '';
4047 }
4048
4049 if ( $this->isConflict ) {
4050 $conflict = Html::rawElement(
4051 'div', [ 'id' => 'mw-previewconflict', 'class' => 'warningbox' ],
4052 $this->context->msg( 'previewconflict' )->escaped()
4053 );
4054 } else {
4055 $conflict = '';
4056 }
4057
4058 $previewhead = Html::rawElement(
4059 'div', [ 'class' => 'previewnote' ],
4060 Html::rawElement(
4061 'h2', [ 'id' => 'mw-previewheader' ],
4062 $this->context->msg( 'preview' )->escaped()
4063 ) .
4064 Html::rawElement( 'div', [ 'class' => 'warningbox' ],
4065 $out->parseAsInterface( $note )
4066 ) . $conflict
4067 );
4068
4069 $pageViewLang = $this->mTitle->getPageViewLanguage();
4070 $attribs = [ 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(),
4071 'class' => 'mw-content-' . $pageViewLang->getDir() ];
4072 $previewHTML = Html::rawElement( 'div', $attribs, $previewHTML );
4073
4074 return $previewhead . $previewHTML . $this->previewTextAfterContent;
4075 }
4076
4077 private function incrementEditFailureStats( $failureType ) {
4078 $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
4079 $stats->increment( 'edit.failures.' . $failureType );
4080 }
4081
4086 protected function getPreviewParserOptions() {
4087 $parserOptions = $this->page->makeParserOptions( $this->context );
4088 $parserOptions->setIsPreview( true );
4089 $parserOptions->setIsSectionPreview( !is_null( $this->section ) && $this->section !== '' );
4090 $parserOptions->enableLimitReport();
4091
4092 // XXX: we could call $parserOptions->setCurrentRevisionCallback here to force the
4093 // current revision to be null during PST, until setupFakeRevision is called on
4094 // the ParserOptions. Currently, we rely on Parser::getRevisionObject() to ignore
4095 // existing revisions in preview mode.
4096
4097 return $parserOptions;
4098 }
4099
4109 protected function doPreviewParse( Content $content ) {
4110 $user = $this->context->getUser();
4111 $parserOptions = $this->getPreviewParserOptions();
4112
4113 // NOTE: preSaveTransform doesn't have a fake revision to operate on.
4114 // Parser::getRevisionObject() will return null in preview mode,
4115 // causing the context user to be used for {{subst:REVISIONUSER}}.
4116 // XXX: Alternatively, we could also call setupFakeRevision() a second time:
4117 // once before PST with $content, and then after PST with $pstContent.
4118 $pstContent = $content->preSaveTransform( $this->mTitle, $user, $parserOptions );
4119 $scopedCallback = $parserOptions->setupFakeRevision( $this->mTitle, $pstContent, $user );
4120 $parserOutput = $pstContent->getParserOutput( $this->mTitle, null, $parserOptions );
4121 ScopedCallback::consume( $scopedCallback );
4122 return [
4123 'parserOutput' => $parserOutput,
4124 'html' => $parserOutput->getText( [
4125 'enableSectionEditLinks' => false
4126 ] )
4127 ];
4128 }
4129
4133 public function getTemplates() {
4134 if ( $this->preview || $this->section != '' ) {
4135 $templates = [];
4136 if ( !isset( $this->mParserOutput ) ) {
4137 return $templates;
4138 }
4139 foreach ( $this->mParserOutput->getTemplates() as $ns => $template ) {
4140 foreach ( array_keys( $template ) as $dbk ) {
4141 $templates[] = Title::makeTitle( $ns, $dbk );
4142 }
4143 }
4144 return $templates;
4145 } else {
4146 return $this->mTitle->getTemplateLinksFrom();
4147 }
4148 }
4149
4155 public static function getEditToolbar() {
4156 $startingToolbar = '<div id="toolbar"></div>';
4157 $toolbar = $startingToolbar;
4158
4159 if ( !Hooks::run( 'EditPageBeforeEditToolbar', [ &$toolbar ] ) ) {
4160 return null;
4161 }
4162 // Don't add a pointless `<div>` to the page unless a hook caller populated it
4163 return ( $toolbar === $startingToolbar ) ? null : $toolbar;
4164 }
4165
4184 public function getCheckboxesDefinition( $checked ) {
4185 $checkboxes = [];
4186
4187 $user = $this->context->getUser();
4188 // don't show the minor edit checkbox if it's a new page or section
4189 $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
4190 if ( !$this->isNew && $permissionManager->userHasRight( $user, 'minoredit' ) ) {
4191 $checkboxes['wpMinoredit'] = [
4192 'id' => 'wpMinoredit',
4193 'label-message' => 'minoredit',
4194 // Uses messages: tooltip-minoredit, accesskey-minoredit
4195 'tooltip' => 'minoredit',
4196 'label-id' => 'mw-editpage-minoredit',
4197 'legacy-name' => 'minor',
4198 'default' => $checked['minor'],
4199 ];
4200 }
4201
4202 if ( $user->isLoggedIn() ) {
4203 $checkboxes['wpWatchthis'] = [
4204 'id' => 'wpWatchthis',
4205 'label-message' => 'watchthis',
4206 // Uses messages: tooltip-watch, accesskey-watch
4207 'tooltip' => 'watch',
4208 'label-id' => 'mw-editpage-watch',
4209 'legacy-name' => 'watch',
4210 'default' => $checked['watch'],
4211 ];
4212 }
4213
4214 $editPage = $this;
4215 Hooks::run( 'EditPageGetCheckboxesDefinition', [ $editPage, &$checkboxes ] );
4216
4217 return $checkboxes;
4218 }
4219
4230 public function getCheckboxesWidget( &$tabindex, $checked ) {
4231 $checkboxes = [];
4232 $checkboxesDef = $this->getCheckboxesDefinition( $checked );
4233
4234 foreach ( $checkboxesDef as $name => $options ) {
4235 $legacyName = $options['legacy-name'] ?? $name;
4236
4237 $title = null;
4238 $accesskey = null;
4239 if ( isset( $options['tooltip'] ) ) {
4240 $accesskey = $this->context->msg( "accesskey-{$options['tooltip']}" )->text();
4241 $title = Linker::titleAttrib( $options['tooltip'] );
4242 }
4243 if ( isset( $options['title-message'] ) ) {
4244 $title = $this->context->msg( $options['title-message'] )->text();
4245 }
4246
4247 $checkboxes[ $legacyName ] = new OOUI\FieldLayout(
4248 new OOUI\CheckboxInputWidget( [
4249 'tabIndex' => ++$tabindex,
4250 'accessKey' => $accesskey,
4251 'id' => $options['id'] . 'Widget',
4252 'inputId' => $options['id'],
4253 'name' => $name,
4254 'selected' => $options['default'],
4255 'infusable' => true,
4256 ] ),
4257 [
4258 'align' => 'inline',
4259 'label' => new OOUI\HtmlSnippet( $this->context->msg( $options['label-message'] )->parse() ),
4260 'title' => $title,
4261 'id' => $options['label-id'] ?? null,
4262 ]
4263 );
4264 }
4265
4266 return $checkboxes;
4267 }
4268
4275 protected function getSubmitButtonLabel() {
4276 $labelAsPublish =
4277 $this->context->getConfig()->get( 'EditSubmitButtonLabelPublish' );
4278
4279 // Can't use $this->isNew as that's also true if we're adding a new section to an extant page
4280 $newPage = !$this->mTitle->exists();
4281
4282 if ( $labelAsPublish ) {
4283 $buttonLabelKey = $newPage ? 'publishpage' : 'publishchanges';
4284 } else {
4285 $buttonLabelKey = $newPage ? 'savearticle' : 'savechanges';
4286 }
4287
4288 return $buttonLabelKey;
4289 }
4290
4299 public function getEditButtons( &$tabindex ) {
4300 $buttons = [];
4301
4302 $labelAsPublish =
4303 $this->context->getConfig()->get( 'EditSubmitButtonLabelPublish' );
4304
4305 $buttonLabel = $this->context->msg( $this->getSubmitButtonLabel() )->text();
4306 $buttonTooltip = $labelAsPublish ? 'publish' : 'save';
4307
4308 $buttons['save'] = new OOUI\ButtonInputWidget( [
4309 'name' => 'wpSave',
4310 'tabIndex' => ++$tabindex,
4311 'id' => 'wpSaveWidget',
4312 'inputId' => 'wpSave',
4313 // Support: IE 6 – Use <input>, otherwise it can't distinguish which button was clicked
4314 'useInputTag' => true,
4315 'flags' => [ 'progressive', 'primary' ],
4316 'label' => $buttonLabel,
4317 'infusable' => true,
4318 'type' => 'submit',
4319 // Messages used: tooltip-save, tooltip-publish
4320 'title' => Linker::titleAttrib( $buttonTooltip ),
4321 // Messages used: accesskey-save, accesskey-publish
4322 'accessKey' => Linker::accesskey( $buttonTooltip ),
4323 ] );
4324
4325 $buttons['preview'] = new OOUI\ButtonInputWidget( [
4326 'name' => 'wpPreview',
4327 'tabIndex' => ++$tabindex,
4328 'id' => 'wpPreviewWidget',
4329 'inputId' => 'wpPreview',
4330 // Support: IE 6 – Use <input>, otherwise it can't distinguish which button was clicked
4331 'useInputTag' => true,
4332 'label' => $this->context->msg( 'showpreview' )->text(),
4333 'infusable' => true,
4334 'type' => 'submit',
4335 // Message used: tooltip-preview
4336 'title' => Linker::titleAttrib( 'preview' ),
4337 // Message used: accesskey-preview
4338 'accessKey' => Linker::accesskey( 'preview' ),
4339 ] );
4340
4341 $buttons['diff'] = new OOUI\ButtonInputWidget( [
4342 'name' => 'wpDiff',
4343 'tabIndex' => ++$tabindex,
4344 'id' => 'wpDiffWidget',
4345 'inputId' => 'wpDiff',
4346 // Support: IE 6 – Use <input>, otherwise it can't distinguish which button was clicked
4347 'useInputTag' => true,
4348 'label' => $this->context->msg( 'showdiff' )->text(),
4349 'infusable' => true,
4350 'type' => 'submit',
4351 // Message used: tooltip-diff
4352 'title' => Linker::titleAttrib( 'diff' ),
4353 // Message used: accesskey-diff
4354 'accessKey' => Linker::accesskey( 'diff' ),
4355 ] );
4356
4357 // Avoid PHP 7.1 warning of passing $this by reference
4358 $editPage = $this;
4359 Hooks::run( 'EditPageBeforeEditButtons', [ &$editPage, &$buttons, &$tabindex ] );
4360
4361 return $buttons;
4362 }
4363
4368 public function noSuchSectionPage() {
4369 $out = $this->context->getOutput();
4370 $out->prepareErrorPage( $this->context->msg( 'nosuchsectiontitle' ) );
4371
4372 $res = $this->context->msg( 'nosuchsectiontext', $this->section )->parseAsBlock();
4373
4374 // Avoid PHP 7.1 warning of passing $this by reference
4375 $editPage = $this;
4376 Hooks::run( 'EditPageNoSuchSection', [ &$editPage, &$res ] );
4377 $out->addHTML( $res );
4378
4379 $out->returnToMain( false, $this->mTitle );
4380 }
4381
4387 public function spamPageWithContent( $match = false ) {
4388 $this->textbox2 = $this->textbox1;
4389
4390 if ( is_array( $match ) ) {
4391 $match = $this->context->getLanguage()->listToText( $match );
4392 }
4393 $out = $this->context->getOutput();
4394 $out->prepareErrorPage( $this->context->msg( 'spamprotectiontitle' ) );
4395
4396 $out->addHTML( '<div id="spamprotected">' );
4397 $out->addWikiMsg( 'spamprotectiontext' );
4398 if ( $match ) {
4399 $out->addWikiMsg( 'spamprotectionmatch', wfEscapeWikiText( $match ) );
4400 }
4401 $out->addHTML( '</div>' );
4402
4403 $out->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
4404 $this->showDiff();
4405
4406 $out->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
4407 $this->showTextbox2();
4408
4409 $out->addReturnTo( $this->getContextTitle(), [ 'action' => 'edit' ] );
4410 }
4411
4415 protected function addEditNotices() {
4416 $out = $this->context->getOutput();
4417 $editNotices = $this->mTitle->getEditNotices( $this->oldid );
4418 if ( count( $editNotices ) ) {
4419 $out->addHTML( implode( "\n", $editNotices ) );
4420 } else {
4421 $msg = $this->context->msg( 'editnotice-notext' );
4422 if ( !$msg->isDisabled() ) {
4423 $out->addHTML(
4424 '<div class="mw-editnotice-notext">'
4425 . $msg->parseAsBlock()
4426 . '</div>'
4427 );
4428 }
4429 }
4430 }
4431
4435 protected function addTalkPageText() {
4436 if ( $this->mTitle->isTalkPage() ) {
4437 $this->context->getOutput()->addWikiMsg( 'talkpagetext' );
4438 }
4439 }
4440
4444 protected function addLongPageWarningHeader() {
4445 if ( $this->contentLength === false ) {
4446 $this->contentLength = strlen( $this->textbox1 );
4447 }
4448
4449 $out = $this->context->getOutput();
4450 $lang = $this->context->getLanguage();
4451 $maxArticleSize = $this->context->getConfig()->get( 'MaxArticleSize' );
4452 if ( $this->tooBig || $this->contentLength > $maxArticleSize * 1024 ) {
4453 $out->wrapWikiMsg( "<div class='error' id='mw-edit-longpageerror'>\n$1\n</div>",
4454 [
4455 'longpageerror',
4456 $lang->formatNum( round( $this->contentLength / 1024, 3 ) ),
4457 $lang->formatNum( $maxArticleSize )
4458 ]
4459 );
4460 } elseif ( !$this->context->msg( 'longpage-hint' )->isDisabled() ) {
4461 $out->wrapWikiMsg( "<div id='mw-edit-longpage-hint'>\n$1\n</div>",
4462 [
4463 'longpage-hint',
4464 $lang->formatSize( strlen( $this->textbox1 ) ),
4465 strlen( $this->textbox1 )
4466 ]
4467 );
4468 }
4469 }
4470
4474 protected function addPageProtectionWarningHeaders() {
4475 $out = $this->context->getOutput();
4476 if ( $this->mTitle->isProtected( 'edit' ) &&
4477 MediaWikiServices::getInstance()->getPermissionManager()->getNamespaceRestrictionLevels(
4478 $this->getTitle()->getNamespace()
4479 ) !== [ '' ]
4480 ) {
4481 # Is the title semi-protected?
4482 if ( $this->mTitle->isSemiProtected() ) {
4483 $noticeMsg = 'semiprotectedpagewarning';
4484 } else {
4485 # Then it must be protected based on static groups (regular)
4486 $noticeMsg = 'protectedpagewarning';
4487 }
4488 LogEventsList::showLogExtract( $out, 'protect', $this->mTitle, '',
4489 [ 'lim' => 1, 'msgKey' => [ $noticeMsg ] ] );
4490 }
4491 if ( $this->mTitle->isCascadeProtected() ) {
4492 # Is this page under cascading protection from some source pages?
4494 list( $cascadeSources, /* $restrictions */ ) = $this->mTitle->getCascadeProtectionSources();
4495 $notice = "<div class='mw-cascadeprotectedwarning'>\n$1\n";
4496 $cascadeSourcesCount = count( $cascadeSources );
4497 if ( $cascadeSourcesCount > 0 ) {
4498 # Explain, and list the titles responsible
4499 foreach ( $cascadeSources as $page ) {
4500 $notice .= '* [[:' . $page->getPrefixedText() . "]]\n";
4501 }
4502 }
4503 $notice .= '</div>';
4504 $out->wrapWikiMsg( $notice, [ 'cascadeprotectedwarning', $cascadeSourcesCount ] );
4505 }
4506 if ( !$this->mTitle->exists() && $this->mTitle->getRestrictions( 'create' ) ) {
4507 LogEventsList::showLogExtract( $out, 'protect', $this->mTitle, '',
4508 [ 'lim' => 1,
4509 'showIfEmpty' => false,
4510 'msgKey' => [ 'titleprotectedwarning' ],
4511 'wrap' => "<div class=\"mw-titleprotectedwarning\">\n$1</div>" ] );
4512 }
4513 }
4514
4519 protected function addExplainConflictHeader( OutputPage $out ) {
4520 $out->addHTML(
4521 $this->getEditConflictHelper()->getExplainHeader()
4522 );
4523 }
4524
4532 protected function buildTextboxAttribs( $name, array $customAttribs, User $user ) {
4533 return ( new TextboxBuilder() )->buildTextboxAttribs(
4534 $name, $customAttribs, $user, $this->mTitle
4535 );
4536 }
4537
4543 protected function addNewLineAtEnd( $wikitext ) {
4544 return ( new TextboxBuilder() )->addNewLineAtEnd( $wikitext );
4545 }
4546
4557 private function guessSectionName( $text ) {
4558 // Detect Microsoft browsers
4559 $userAgent = $this->context->getRequest()->getHeader( 'User-Agent' );
4560 $parser = MediaWikiServices::getInstance()->getParser();
4561 if ( $userAgent && preg_match( '/MSIE|Edge/', $userAgent ) ) {
4562 // ...and redirect them to legacy encoding, if available
4563 return $parser->guessLegacySectionNameFromWikiText( $text );
4564 }
4565 // Meanwhile, real browsers get real anchors
4566 $name = $parser->guessSectionNameFromWikiText( $text );
4567 // With one little caveat: per T216029, fragments in HTTP redirects need to be urlencoded,
4568 // otherwise Chrome double-escapes the rest of the URL.
4569 return '#' . urlencode( mb_substr( $name, 1 ) );
4570 }
4571
4578 public function setEditConflictHelperFactory( callable $factory ) {
4579 $this->editConflictHelperFactory = $factory;
4580 $this->editConflictHelper = null;
4581 }
4582
4586 private function getEditConflictHelper() {
4587 if ( !$this->editConflictHelper ) {
4588 $this->editConflictHelper = call_user_func(
4589 $this->editConflictHelperFactory,
4590 $this->getSubmitButtonLabel()
4591 );
4592 }
4593
4595 }
4596
4601 private function newTextConflictHelper( $submitButtonLabel ) {
4602 return new TextConflictHelper(
4603 $this->getTitle(),
4604 $this->getContext()->getOutput(),
4605 MediaWikiServices::getInstance()->getStatsdDataFactory(),
4606 $submitButtonLabel
4607 );
4608 }
4609}
$wgDisableAnonTalk
Disable links to talk pages of anonymous users (IPs) in listings on special pages like page history,...
$wgSummarySpamRegex
Same as the above except for edit summaries.
$wgRightsText
If either $wgRightsUrl or $wgRightsPage is specified then this variable gives the text for the link.
$wgSpamRegex
Edits matching these regular expressions in body text will be recognised as spam and rejected automat...
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)
Throws a warning that $function is deprecated.
$wgLang
Definition Setup.php:880
if(! $wgRequest->checkUrlExtension()) if(isset( $_SERVER['PATH_INFO']) && $_SERVER['PATH_INFO'] !='') $wgTitle
Definition api.php:58
Class for viewing MediaWiki article and history.
Definition Article.php:38
getContext()
Gets the context this Article is executed in.
Definition Article.php:2267
getTitle()
Get the title object of the article.
Definition Article.php:221
getPage()
Get the WikiPage object of this instance.
Definition Article.php:231
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:46
getPreviewParserOptions()
Get parser options for a preview.
string $sectiontitle
Definition EditPage.php:377
getActionURL(Title $title)
Returns the URL to use in the form's action attribute.
getEditConflictHelper()
bool stdClass $lastDelete
Definition EditPage.php:262
showTextbox( $text, $name, $customAttribs=[])
string $hookError
Definition EditPage.php:304
attemptSave(&$resultDetails=false)
Attempt submission.
$editFormTextAfterTools
Definition EditPage.php:424
isSectionEditSupported()
Returns whether section editing is supported for the current page.
Definition EditPage.php:899
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:226
updateWatchlist()
Register the change of watch status.
const AS_SELF_REDIRECT
Status: user tried to create self-redirect (redirect to the same article) and wpIgnoreSelfRedirect ==...
Definition EditPage.php:170
getOriginalContent(User $user)
Get the content of the wanted revision, without section extraction.
getCancelLink()
null string $contentFormat
Definition EditPage.php:411
bool $allowBlankSummary
Definition EditPage.php:286
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:754
bool $firsttime
True the first time the edit form is rendered, false after re-rendering with diff,...
Definition EditPage.php:259
const AS_CANNOT_USE_CUSTOM_MODEL
Status: when changing the content model is disallowed due to $wgContentHandlerUseDB being false.
Definition EditPage.php:187
runPostMergeFilters(Content $content, Status $status, User $user)
Run hooks that can filter edits just before they get saved.
bool $bot
Definition EditPage.php:405
showPreview( $text)
Append preview output to OutputPage.
const AS_UNICODE_NOT_SUPPORTED
Status: edit rejected because browser doesn't support Unicode.
Definition EditPage.php:192
const AS_HOOK_ERROR_EXPECTED
Status: A hook function returned an error.
Definition EditPage.php:70
addTalkPageText()
const AS_READ_ONLY_PAGE_ANON
Status: this anonymous user is not allowed to edit this page.
Definition EditPage.php:85
string $textbox2
Definition EditPage.php:344
bool $tooBig
Definition EditPage.php:277
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:555
$editFormTextAfterContent
Definition EditPage.php:426
$editFormTextBottom
Definition EditPage.php:425
int $editRevId
Revision ID of the latest revision of the page when editing was initiated on the client.
Definition EditPage.php:371
$editFormTextBeforeContent
Definition EditPage.php:422
string $contentModel
Definition EditPage.php:408
bool $deletedSinceEdit
Definition EditPage.php:250
__construct(Article $article)
Definition EditPage.php:477
getEditPermissionErrors( $rigor='secure')
Definition EditPage.php:710
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:389
const POST_EDIT_COOKIE_KEY_PREFIX
Prefix of key for cookie used to pass post-edit state.
Definition EditPage.php:203
const AS_MAX_ARTICLE_SIZE_EXCEEDED
Status: article is too big (> $wgMaxArticleSize), after merging in the new section.
Definition EditPage.php:138
const AS_CONFLICT_DETECTED
Status: (non-resolvable) edit conflict.
Definition EditPage.php:122
addExplainConflictHeader(OutputPage $out)
$editFormTextAfterWarn
Definition EditPage.php:423
bool $mTokenOk
Definition EditPage.php:265
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()
const AS_SPAM_ERROR
Status: summary contained spam according to one of the regexes in $wgSummarySpamRegex.
Definition EditPage.php:148
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:460
static getEditToolbar()
Allow extensions to provide a toolbar.
const AS_READ_ONLY_PAGE_LOGGED
Status: this logged in user is not allowed to edit this page.
Definition EditPage.php:90
bool $mShowSummaryField
Definition EditPage.php:316
showFormAfterText()
bool $recreate
Definition EditPage.php:336
callable $editConflictHelperFactory
Factory function to create an edit conflict helper.
Definition EditPage.php:467
int $parentRevId
Revision ID the edit is based on, adjusted when an edit conflict is resolved.
Definition EditPage.php:396
isWrongCaseUserConfigPage()
Checks whether the user entered a skin name in uppercase, e.g.
Definition EditPage.php:878
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:310
const AS_BLANK_ARTICLE
Status: user tried to create a blank page and wpIgnoreBlankArticle == false.
Definition EditPage.php:117
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:909
static extractSectionTitle( $text)
Extract the section title from current section text, if any.
bool $missingSummary
Definition EditPage.php:283
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:232
Revision bool null $mBaseRevision
A revision object corresponding to $this->editRevId.
Definition EditPage.php:313
bool int $contentLength
Definition EditPage.php:440
const AS_PARSE_ERROR
Status: can't parse content.
Definition EditPage.php:181
bool $blankArticle
Definition EditPage.php:289
noSuchSectionPage()
Creates a basic error page which informs the user that they have attempted to edit a nonexistent sect...
bool $mTriedSave
Definition EditPage.php:271
incrementResolvedConflicts()
Log when a page was successfully saved after the edit conflict view.
showConflict()
Show an edit conflict.
guessSectionName( $text)
Turns section name wikitext into anchors for use in HTTP redirects.
const AS_READ_ONLY_PAGE
Status: wiki is in readonly mode (wfReadOnly() == true)
Definition EditPage.php:95
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:374
ParserOutput $mParserOutput
Definition EditPage.php:307
previewOnOpen()
Should we show a preview when the edit form is first shown?
Definition EditPage.php:837
displayPreviewArea( $previewOutput, $isOnTop=false)
doPreviewParse(Content $content)
Parse the page for a preview.
string $formtype
Definition EditPage.php:253
bool $allowBlankArticle
Definition EditPage.php:292
setApiEditOverride( $enableOverride)
Allow editing of content that supports API direct editing, but not general direct editing.
Definition EditPage.php:566
internalAttemptSave(&$result, $bot=false)
Attempt submission (no UI)
string $summary
Definition EditPage.php:347
getSubmitButtonLabel()
Get the message key of the label for the button to save the page.
getSummaryInputWidget( $summary="", $labelText=null, $inputAttrs=null)
Builds a standard summary input with a label.
bool $save
Definition EditPage.php:321
const AS_CONTENT_TOO_BIG
Status: Content too big (> $wgMaxArticleSize)
Definition EditPage.php:80
bool $diff
Definition EditPage.php:327
showStandardInputs(&$tabindex=2)
getCurrentContent()
Get the current content of the page.
bool $selfRedirect
Definition EditPage.php:295
string $textbox1
Page content input field.
Definition EditPage.php:341
addContentModelChangeLogEntry(User $user, $oldModel, $newModel, $reason)
const EDITFORM_ID
HTML id and name for the beginning of the edit form.
Definition EditPage.php:197
const AS_CHANGE_TAG_ERROR
Status: an error relating to change tagging.
Definition EditPage.php:176
string $editFormPageTop
Before even the preview.
Definition EditPage.php:420
const AS_BLOCKED_PAGE_FOR_USER
Status: User is blocked from editing this page.
Definition EditPage.php:75
const AS_NO_CHANGE_CONTENT_MODEL
Status: user tried to modify the content model, but is not allowed to do that ( User::isAllowed('edit...
Definition EditPage.php:164
getCopywarn()
Get the copyright warning.
const POST_EDIT_COOKIE_DURATION
Duration of PostEdit cookie, in seconds.
Definition EditPage.php:218
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:538
toEditText( $content)
Gets an editable textual representation of $content.
null Title $mContextTitle
Definition EditPage.php:235
string $autoSumm
Definition EditPage.php:301
bool $isOldRev
Whether an old revision is edited.
Definition EditPage.php:455
const AS_IMAGE_REDIRECT_LOGGED
Status: logged in user is not allowed to upload (User::isAllowed('upload') == false)
Definition EditPage.php:158
const AS_END
Status: WikiPage::doEdit() was unsuccessful.
Definition EditPage.php:143
const AS_ARTICLE_WAS_DELETED
Status: article was deleted while editing and param wpRecreate == false or form was not posted.
Definition EditPage.php:106
const AS_TEXTBOX_EMPTY
Status: user tried to create a new section without content.
Definition EditPage.php:133
bool $mTokenOkExceptSuffix
Definition EditPage.php:268
newSectionSummary(&$sectionanchor=null)
Return the summary to be used for a new section.
const AS_SUCCESS_UPDATE
Status: Article successfully updated.
Definition EditPage.php:55
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:382
bool $isNew
New page or new section.
Definition EditPage.php:247
showIntro()
Show all applicable editing introductions.
Article $mArticle
Definition EditPage.php:224
bool $allowSelfRedirect
Definition EditPage.php:298
const AS_IMAGE_REDIRECT_ANON
Status: anonymous user is not allowed to upload (User::isAllowed('upload') == false)
Definition EditPage.php:153
getContentObject( $def_content=null)
bool $isConflict
Whether an edit conflict needs to be resolved.
Definition EditPage.php:244
null $scrolltop
Definition EditPage.php:402
getLastDelete()
Get the last log record of this page being deleted, if ever.
string $action
Definition EditPage.php:238
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:581
string $editintro
Definition EditPage.php:399
newTextConflictHelper( $submitButtonLabel)
bool $enableApiEditOverride
Set in ApiEditPage, based on ContentHandler::allowsDirectApiEditing.
Definition EditPage.php:445
displayViewSourcePage(Content $content, $errorMessage='')
Display a read-only View Source page.
Definition EditPage.php:784
bool $watchthis
Definition EditPage.php:333
addPageProtectionWarningHeaders()
addLongPageWarningHeader()
const AS_NO_CREATE_PERMISSION
Status: user tried to create this page, but is not allowed to do that ( Title->userCan('create') == f...
Definition EditPage.php:112
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:330
$previewTextAfterContent
Definition EditPage.php:427
getSummaryPreview( $isSubjectPreview, $summary="")
bool $nosummary
If true, hide the summary field.
Definition EditPage.php:352
bool $incompleteForm
Definition EditPage.php:274
const AS_SUCCESS_NEW_ARTICLE
Status: Article successfully created.
Definition EditPage.php:60
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:280
string $edittime
Timestamp of the latest revision of the page when editing was initiated on the client.
Definition EditPage.php:358
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:50
getPreviewText()
Get the rendered text for previewing.
formatStatusErrors(Status $status)
Wrap status errors in an errorbox for increased visibility.
showContentForm()
Subpage overridable method for printing the form for page content editing By default this simply outp...
bool $edit
Definition EditPage.php:437
incrementConflictStats()
IContextSource $context
Definition EditPage.php:450
bool $preview
Definition EditPage.php:324
getPreloadedContent( $preload, $params=[])
Get the contents to be preloaded into the box, either set by an earlier setPreloadText() or by loadin...
static matchSpamRegexInternal( $text, $regexes)
TextConflictHelper null $editConflictHelper
Definition EditPage.php:472
showEditForm( $formCallback=null)
Send the edit form and related headers to OutputPage.
null array $changeTags
Definition EditPage.php:414
const AS_RATE_LIMITED
Status: rate limiter for action 'edit' was tripped.
Definition EditPage.php:100
const AS_HOOK_ERROR
Status: Article update aborted by a hook function.
Definition EditPage.php:65
setContextTitle( $title)
Set the context Title object.
Definition EditPage.php:526
spamPageWithContent( $match=false)
Show "your edit contains spam" page with your diff and text.
const AS_SUMMARY_NEEDED
Status: no edit summary given and the user has forceeditsummary set and the user is not editing in hi...
Definition EditPage.php:128
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:2026
static accesskey( $name)
Given the id of an interface element, constructs the appropriate accesskey attribute from the system ...
Definition Linker.php:2074
static formatHiddenCategories( $hiddencats)
Returns HTML for the "hidden categories on this page" list.
Definition Linker.php:1991
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:1547
static showLogExtract(&$out, $types=[], $page='', $user='', $param=[])
Show log extract.
const DELETED_USER
Definition LogPage.php:36
const DELETED_COMMENT
Definition LogPage.php:35
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.
Page revision base class.
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 loadFromTitle( $db, $title, $id=0)
Load either the current, or a specified, revision that's attached to a given page.
Definition Revision.php:278
static loadFromTimestamp( $db, $title, $timestamp)
Load the revision for the given title with the given timestamp.
Definition Revision.php:296
static userWasLastToEdit( $db, $pageId, $userId, $since)
Check if no edits were made by other users since the time a user started editing the page.
static newFromId( $id, $flags=0)
Load a page revision from a given revision ID number.
Definition Revision.php:119
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:40
getWikiText( $shortContext=false, $longContext=false, $lang=null)
Get the error list as a wikitext formatted list.
Definition Status.php:176
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:1099
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:51
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition User.php:518
const IGNORE_USER_RIGHTS
Definition User.php:83
static isIP( $name)
Does the string match an anonymous IP address?
Definition User.php:933
static doWatchOrUnwatch( $watch, Title $title, User $user)
Watch or unwatch a page.
Class representing a MediaWiki article and history.
Definition WikiPage.php:47
getContent( $audience=RevisionRecord::FOR_PUBLIC, User $user=null)
Get the content of the current revision.
Definition WikiPage.php:820
getRedirectTarget()
If this page is a redirect, get its target.
Definition WikiPage.php:999
isRedirect()
Tests if the article content represents a redirect.
Definition WikiPage.php:634
const EDIT_FORCE_BOT
Definition Defines.php:145
const EDIT_UPDATE
Definition Defines.php:142
const NS_USER
Definition Defines.php:71
const CONTENT_MODEL_CSS
Definition Defines.php:226
const NS_FILE
Definition Defines.php:75
const NS_MEDIAWIKI
Definition Defines.php:77
const CONTENT_MODEL_JSON
Definition Defines.php:228
const NS_USER_TALK
Definition Defines.php:72
const EDIT_MINOR
Definition Defines.php:143
const CONTENT_MODEL_JAVASCRIPT
Definition Defines.php:225
const EDIT_AUTOSUMMARY
Definition Defines.php:147
const EDIT_NEW
Definition Defines.php:141
Base interface for content objects.
Definition Content.php:34
Interface for objects which can provide a MediaWiki context on request.
const DB_REPLICA
Definition defines.php:25
const DB_MASTER
Definition defines.php:26
$content
Definition router.php:78
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